Skip to content

Commit

Permalink
Admin requests (#85)
Browse files Browse the repository at this point in the history
* Admin requests

* Changes

* working

* cerrrar

* get

* throw error

* nit
  • Loading branch information
Juanito98 authored Sep 18, 2024
1 parent eadff06 commit 28e8c22
Show file tree
Hide file tree
Showing 10 changed files with 624 additions and 13 deletions.
413 changes: 410 additions & 3 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
"@next/mdx": "^14.0.4",
"@prisma/client": "^5.19.1",
"@reduxjs/toolkit": "^1.9.5",
"@rjsf/core": "^5.21.1",
"@rjsf/utils": "^5.21.1",
"@rjsf/validator-ajv8": "^5.21.1",
"@sinclair/typebox": "^0.32.4",
"@types/mdx": "^2.0.10",
"@types/node": "20.5.8",
Expand Down
9 changes: 9 additions & 0 deletions src/components/admin/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { SendEmailRequestSchema } from "@/types/admin.schema";
import { TObject, Type } from "@sinclair/typebox";

export const APIS: {
[key: string]: ["GET" | "POST", TObject];
} = {
"/api/admin/sendEmail": ["POST", SendEmailRequestSchema],
"/api/admin/exportParticipants": ["GET", Type.Object({})],
};
103 changes: 103 additions & 0 deletions src/components/admin/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { useState } from "react";
import { APIS } from "./client";
import Form from "@rjsf/core";
import validator from "@rjsf/validator-ajv8";
import { RJSFSchema } from "@rjsf/utils";

function APIForm({ endpoint }: { endpoint: string }): JSX.Element {
const [loading, setLoading] = useState(false);
const [response, setResponse] = useState<string | null>(null);
const [method, requestSchema] = APIS[endpoint];

return (
<div>
<Form
schema={requestSchema as RJSFSchema}
validator={validator}
disabled={loading}
onSubmit={async (data, ev) => {
ev.preventDefault();
setLoading(true);
setResponse(null);
const formData = data.formData;
if (!formData) {
return;
}

let res: Response | null = null;
if (method === "POST") {
res = await fetch(endpoint, {
method,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formData),
});
} else {
const query = new URLSearchParams(formData);
res = await fetch(`${endpoint}?${query.toString()}`, {
method,
});
}
setLoading(false);
const json = await res.json();
setResponse(JSON.stringify(json, null, 2));
}}
/>
{response && (
<div>
<div className="mt-6 rounded-md border border-gray-300 bg-gray-100 p-4">
<pre className="whitespace-pre-wrap break-words">{response}</pre>
</div>
<button
onClick={(ev) => {
ev.preventDefault();
setResponse(null);
}}
>
Cerrar
</button>
</div>
)}
</div>
);
}

export default function Admin(): JSX.Element {
const [endpoint, setEndpoint] = useState<string>();

return (
<div className="mx-auto max-w-3xl px-2 pt-4">
<div>
<label
htmlFor="endpoint"
className="block font-medium leading-6 text-gray-900"
>
API endpoint
</label>
<select
id="endpoint"
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:leading-6"
value={endpoint}
onChange={(ev) => {
ev.preventDefault();
setEndpoint(
ev.currentTarget.value in APIS
? ev.currentTarget.value
: undefined,
);
}}
>
<option value={""}></option>
{Object.keys(APIS).map((name) => (
<option value={name} key={name}>
{name}
</option>
))}
</select>
</div>

{endpoint && <APIForm endpoint={endpoint}></APIForm>}
</div>
);
}
1 change: 1 addition & 0 deletions src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import "@/styles/globals.css";
import "@/styles/react-calendar.css";
import "@/styles/rjsf.css";
import { Provider } from "jotai";
import { SessionProvider } from "next-auth/react";
import type { AppProps } from "next/app";
Expand Down
5 changes: 5 additions & 0 deletions src/pages/admin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Admin from "@/components/admin";

export default function AdminPage(): JSX.Element {
return <Admin />;
}
26 changes: 16 additions & 10 deletions src/pages/api/admin/exportParticipants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,22 @@ async function exportParticipantsHandler(
return res.status(403).json({ message: "Forbidden" });
}
const ofmi = await findMostRecentOfmi();
const spreadsheetId = await exportParticipants({
userAuthId,
ofmi,
spreadsheetName: registrationSpreadsheetsPath(ofmi.edition),
});
return res.status(200).json({
success: true,
spreadsheetId,
spreadsheetUrl: spreadsheetURL(spreadsheetId),
});
try {
const spreadsheetId = await exportParticipants({
userAuthId,
ofmi,
spreadsheetName: registrationSpreadsheetsPath(ofmi.edition),
});
return res.status(200).json({
success: true,
spreadsheetId,
spreadsheetUrl: spreadsheetURL(spreadsheetId),
});
} catch (e) {
return res.status(500).json({
message: `Internal Server Error. ${e}`,
});
}
}

export default async function handle(
Expand Down
51 changes: 51 additions & 0 deletions src/pages/api/admin/sendEmail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { emailer } from "@/lib/emailer";
import { OFMI_EMAIL_SMTP_USER_KEY } from "@/lib/emailer/template";
import { getSecretOrError } from "@/lib/secret";
import { parseValueError } from "@/lib/typebox";
import {
SendEmailRequestSchema,
SendEmailResponse,
} from "@/types/admin.schema";
import { BadRequestError } from "@/types/errors";
import { Value } from "@sinclair/typebox/value";
import type { NextApiRequest, NextApiResponse } from "next/types";

async function sendEmailHandler(
req: NextApiRequest,
res: NextApiResponse<SendEmailResponse | BadRequestError>,
): Promise<void> {
const { body } = req;
console.log(body);
if (!Value.Check(SendEmailRequestSchema, body)) {
const firstError = Value.Errors(SendEmailRequestSchema, body).First();
console.log(firstError);
return res.status(400).json({
message: `${firstError ? parseValueError(firstError) : "Invalid request body."}`,
});
}
const { email, subject, content } = body;

await emailer.sendEmail({
from: getSecretOrError(OFMI_EMAIL_SMTP_USER_KEY),
to: email,
subject,
text: subject,
html: content,
});

return res.status(200).json({
success: true,
});
}

export default async function handle(
req: NextApiRequest,
res: NextApiResponse<SendEmailResponse | BadRequestError>,
): Promise<void> {
if (req.method === "POST") {
// register to OFMI
await sendEmailHandler(req, res);
} else {
return res.status(405).json({ message: "Method Not allowed" });
}
}
12 changes: 12 additions & 0 deletions src/styles/rjsf.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.rjsf .form-group {
display: flex;
flex-direction: column;
margin-top: 12px;
}

.rjsf .btn {
margin-top: 12px;
padding: 8px;
border: solid 2px black;
border-radius: 24px;
}
14 changes: 14 additions & 0 deletions src/types/admin.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Static, Type } from "@sinclair/typebox";

export type SendEmailResponse = Static<typeof SendEmailResponseSchema>;
export const SendEmailResponseSchema = Type.Object({
success: Type.Boolean(),
});

export type SendEmailRequest = Static<typeof SendEmailRequestSchema>;
export const SendEmailRequestSchema = Type.Object({
email: Type.String(),
subject: Type.String({ minLength: 1 }),
// Html of the email content
content: Type.String({ minLength: 1 }),
});

0 comments on commit 28e8c22

Please sign in to comment.