Created
April 28, 2024 10:47
-
-
Save oezguerisbert/99a4f21259f89a0d38c1542ba8915c10 to your computer and use it in GitHub Desktop.
Auth Stuff with Lucia + SST + SolidStart
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { Lucia, TimeSpan } from "lucia"; | |
| import { luciaAdapter } from "@/core/src/drizzle/sql"; | |
| import type { SessionSelect, UserSelect } from "@/core/src/drizzle/sql/schema"; | |
| export const lucia = new Lucia(luciaAdapter, { | |
| sessionExpiresIn: new TimeSpan(2, "w"), | |
| sessionCookie: { | |
| attributes: { | |
| // set to `true` when using HTTPS | |
| secure: import.meta.env.PROD, | |
| }, | |
| }, | |
| getUserAttributes: (attributes) => { | |
| return { | |
| username: attributes.name, | |
| email: attributes.email, | |
| }; | |
| }, | |
| getSessionAttributes(databaseSessionAttributes) { | |
| return { | |
| access_token: databaseSessionAttributes.access_token, | |
| createdAt: databaseSessionAttributes.createdAt, | |
| // addiional information | |
| // organization_id: databaseSessionAttributes.organization_id, | |
| }; | |
| }, | |
| }); | |
| declare module "lucia" { | |
| interface Register { | |
| Lucia: typeof lucia; | |
| DatabaseUserAttributes: DatabaseUserAttributes; | |
| DatabaseSessionAttributes: DatabaseSessionAttributes; | |
| } | |
| } | |
| type DatabaseUserAttributes = Omit<UserSelect, "id">; | |
| type DatabaseSessionAttributes = Omit<SessionSelect, "id" | "userID" | "expiresAt" | "userId" | "updatedAt">; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { Organization } from "@/core/src/entities/organizations"; | |
| import { User } from "@/core/src/entities/users"; | |
| import { cache, redirect } from "@solidjs/router"; | |
| import { getCookie, getEvent } from "vinxi/http"; | |
| import { lucia } from "."; | |
| export const getAuthenticatedUser = cache(async () => { | |
| "use server"; | |
| const event = getEvent()!; | |
| if (!event.context.session) { | |
| return null; | |
| } | |
| const { id } = event.context.session; | |
| const { user } = await lucia.validateSession(id); | |
| return user; | |
| }, "user"); | |
| export type UserSession = { | |
| id: string | null; | |
| token: string | null; | |
| expiresAt: Date | null; | |
| user: Awaited<ReturnType<typeof User.findById>> | null; | |
| organization: Awaited<ReturnType<typeof Organization.findById>> | null; | |
| createdAt: Date | null; | |
| }; | |
| export const getAuthenticatedSession = cache(async () => { | |
| "use server"; | |
| let userSession = { | |
| id: null, | |
| token: null, | |
| expiresAt: null, | |
| user: null, | |
| organization: null, | |
| createdAt: null, | |
| } as UserSession; | |
| const event = getEvent()!; | |
| const sessionId = getCookie(event, lucia.sessionCookieName) ?? null; | |
| if (!sessionId) { | |
| // throw redirect("/auth/login"); | |
| return userSession; | |
| } | |
| const { session } = await lucia.validateSession(sessionId); | |
| if (!session) { | |
| // throw redirect("/auth/login"); | |
| // console.error("invalid session"); | |
| return userSession; | |
| } | |
| userSession.id = session.id; | |
| if (session.userId) userSession.user = await User.findById(session.userId); | |
| if (session.createdAt) userSession.createdAt = session.createdAt; | |
| // additional information | |
| if (session.organization_id) userSession.organization = await Organization.findById(session.organization_id); | |
| return userSession; | |
| }, "session"); | |
| export const getAuthenticatedSessions = cache(async () => { | |
| "use server"; | |
| const event = getEvent()!; | |
| if (!event.context.user) { | |
| return redirect("/auth/login"); | |
| } | |
| const { id } = event.context.user; | |
| const sessions = await lucia.getUserSessions(id); | |
| return sessions; | |
| }, "sessions"); | |
| export const getCurrentOrganization = cache(async () => { | |
| "use server"; | |
| const event = getEvent()!; | |
| if (!event.context.session) { | |
| return redirect("/auth/login"); | |
| } | |
| const { id } = event.context.session; | |
| const { user, session } = await lucia.validateSession(id); | |
| if (!user || !session) { | |
| throw redirect("/auth/login"); | |
| } | |
| if (!session.organization_id) { | |
| throw redirect("/setup/organization"); | |
| } | |
| const org = Organization.findById(session.organization_id); | |
| if (!org) { | |
| throw redirect("/setup/organization"); | |
| } | |
| return org; | |
| }, "current-organization"); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { lucia } from "@/lib/auth"; | |
| import type { APIEvent } from "@solidjs/start/server"; | |
| import { appendHeader, sendRedirect } from "vinxi/http"; | |
| export async function GET(e: APIEvent) { | |
| const event = e.nativeEvent; | |
| const url = new URL(e.request.url); | |
| const code = url.searchParams.get("code"); | |
| const client_id = url.searchParams.get("client_id"); | |
| if (!client_id) { | |
| return sendRedirect(event, "/auth/error?error=missing_client_id", 303); | |
| } | |
| if (!code) { | |
| return sendRedirect(event, "/auth/error?error=missing_code", 303); | |
| } | |
| // console.log({ code }); | |
| const body = new URLSearchParams({ | |
| grant_type: "authorization_code", | |
| client_id, | |
| code, | |
| redirect_uri: `${url.origin}${url.pathname}?client_id=${client_id}`, | |
| }); | |
| const token = await fetch(`${import.meta.env.VITE_AUTH_URL}/token`, { | |
| method: "POST", | |
| body, | |
| }).then((r) => r.json()); | |
| if (!token.access_token) { | |
| return sendRedirect(event, "/auth/error?error=missing_access_token", 303); | |
| } | |
| const { id, organization_id } = await fetch(new URL("/session", import.meta.env.VITE_API_URL), { | |
| headers: { | |
| Authorization: `Bearer ${token.access_token}`, | |
| }, | |
| }).then((r) => r.json()); | |
| if (!id) { | |
| return sendRedirect(event, "/auth/error?error=missing_user", 303); | |
| } | |
| const session = await lucia.createSession(id, { | |
| access_token: token.access_token, | |
| organization_id, | |
| createdAt: new Date(), | |
| }); | |
| appendHeader(event, "Set-Cookie", lucia.createSessionCookie(session.id).serialize()); | |
| event.context.session = session; | |
| // everything works fine, redirect to dashboard | |
| return sendRedirect(event, "/dashboard", 303); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { Button, buttonVariants } from "@/components/ui/button"; | |
| import { Logo } from "@/components/ui/custom/logo"; | |
| import { TextField, TextFieldInput } from "@/components/ui/textfield"; | |
| import { cn } from "@/lib/utils"; | |
| import { A, useParams, useSearchParams } from "@solidjs/router"; | |
| import { Show, createEffect } from "solid-js"; | |
| import { For, createSignal } from "solid-js"; | |
| export default function ConfirmCodePage() { | |
| const [submitting, setSubmitting] = createSignal<boolean>(); | |
| const [searchParams] = useSearchParams(); | |
| const isInvalidOrMissingEmail = () => searchParams.error && searchParams.error !== "invalid_or_missing_email"; | |
| function submit() { | |
| setSubmitting(true); | |
| const code = [...document.querySelectorAll("[data-element=code]")] | |
| .map((el) => (el as HTMLInputElement).value) | |
| .join(""); | |
| location.href = | |
| import.meta.env.VITE_AUTH_URL + | |
| "/callback?" + | |
| new URLSearchParams({ | |
| code, | |
| client_id: "email", | |
| }).toString(); | |
| } | |
| function inputs() { | |
| return [...document.querySelectorAll<HTMLInputElement>("[data-element=code]")]; | |
| } | |
| createEffect(() => { | |
| const code = searchParams.code; | |
| if (code) { | |
| // set the code in the inputs | |
| const _inputs = inputs(); | |
| _inputs.forEach((input, index) => { | |
| input.value = code[index]; | |
| }); | |
| submit(); | |
| } | |
| }); | |
| return ( | |
| <div class="container h-screen flex flex-col items-center justify-center px-10"> | |
| <div class="w-full h-[650px] -mt-60"> | |
| <div class="w-full relative flex h-full flex-col items-center justify-center lg:px-0"> | |
| <div class="mx-auto flex w-max flex-col justify-center space-y-6 p-4 py-20 border rounded-md border-neutral-200 dark:border-neutral-800 shadow-md gap-10"> | |
| <div class="flex flex-row gap-4 items-center w-full justify-center text-lg font-bold"> | |
| <Logo /> Login | |
| </div> | |
| <Show | |
| when={!isInvalidOrMissingEmail()} | |
| fallback={ | |
| <div class="flex flex-col gap-8 items-center w-full justify-center px-10"> | |
| <span class="text-sm text-muted-foreground">Please provide a valid email address</span> | |
| <A class={cn(buttonVariants({ variant: "default", size: "sm" }))} href="/auth/login"> | |
| <span>Try again</span> | |
| </A> | |
| </div> | |
| } | |
| > | |
| <div class="flex flex-col gap-8 items-center w-full justify-center"> | |
| <form | |
| class="flex flex-col gap-8 items-center w-full justify-center px-4" | |
| action={import.meta.env.VITE_AUTH_URL + "/authorize?provider=email"} | |
| method="get" | |
| onSubmit={async (e) => { | |
| setSubmitting(true); | |
| e.preventDefault(); | |
| const form = e.currentTarget; | |
| form.submit(); | |
| }} | |
| > | |
| <div class="flex flex-row gap-4 items-center w-full justify-center"> | |
| <For each={Array(6).fill(0)}> | |
| {() => ( | |
| <TextField> | |
| <TextFieldInput | |
| data-element="code" | |
| class="w-10 text-center" | |
| maxLength={1} | |
| inputmode="numeric" | |
| disabled={submitting()} | |
| type="text" | |
| onPaste={(e) => { | |
| const code = e.clipboardData?.getData("text/plain")?.trim(); | |
| if (!code) return; | |
| const i = inputs(); | |
| if (code.length !== i.length) return; | |
| i.forEach((item, index) => { | |
| item.value = code[index]; | |
| }); | |
| e.preventDefault(); | |
| submit(); | |
| }} | |
| onFocus={(e) => { | |
| e.currentTarget.select(); | |
| }} | |
| onKeyDown={(e) => { | |
| if (!e.currentTarget.value && e.key === "Backspace") { | |
| e.preventDefault(); | |
| const previous = | |
| e.currentTarget.parentNode?.parentNode?.previousSibling?.firstChild?.firstChild; | |
| if (previous instanceof HTMLInputElement) { | |
| previous.focus(); | |
| } | |
| return; | |
| } | |
| }} | |
| onInput={(e) => { | |
| const all = inputs(); | |
| const index = all.indexOf(e.currentTarget); | |
| if (!e.currentTarget.value) { | |
| const previous = all[index - 1]; | |
| if (previous) { | |
| previous.focus(); | |
| } | |
| return; | |
| } | |
| const next = all[index + 1]; | |
| if (next) { | |
| next.focus(); | |
| next.select(); | |
| return; | |
| } | |
| if (!next) submit(); | |
| }} | |
| ></TextFieldInput> | |
| </TextField> | |
| )} | |
| </For> | |
| </div> | |
| <Button | |
| variant="default" | |
| size="lg" | |
| type="submit" | |
| class={cn("w-full", { | |
| "opacity-50 cursor-not-allowed": submitting(), | |
| })} | |
| aria-busy={submitting()} | |
| aria-label="Continue with Email" | |
| disabled={submitting()} | |
| > | |
| <span>Confirm</span> | |
| </Button> | |
| </form> | |
| <div class="px-8 text-center text-sm text-muted-foreground gap-2 flex flex-col"> | |
| <span>By continuing, you agree to our</span> | |
| <div class=""> | |
| <A href="/terms-of-service" class="underline underline-offset-4 hover:text-primary"> | |
| Terms of Service | |
| </A>{" "} | |
| and{" "} | |
| <A href="/privacy" class="underline underline-offset-4 hover:text-primary"> | |
| Privacy Policy | |
| </A> | |
| . | |
| </div> | |
| </div> | |
| </div> | |
| </Show> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { As } from "@kobalte/core"; | |
| import { A, useSearchParams } from "@solidjs/router"; | |
| import { Button, buttonVariants } from "../../components/ui/button"; | |
| import { cn } from "../../lib/utils"; | |
| export default function LoginErrorPage() { | |
| const [sp] = useSearchParams(); | |
| const error = sp.error || "unknown"; | |
| return ( | |
| <div class="w-full h-screen flex flex-col items-center justify-center"> | |
| <div class="w-full h-[650px] -mt-60"> | |
| <div class="w-full relative flex h-full flex-col items-center justify-center lg:px-0"> | |
| <div class="mx-auto flex w-max flex-col justify-center space-y-6 p-4 py-20 border rounded-md border-neutral-200 dark:border-neutral-800 shadow-md gap-10"> | |
| <div class="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]"> | |
| <div class="flex flex-col space-y-2 text-center"> | |
| <h1 class="text-2xl font-semibold tracking-tight">Upps some error occured</h1> | |
| <p class="text-sm text-muted-foreground"> | |
| {error === "invalid_code" && "The code is invalid"} | |
| {error === "missing_access_token" && "The access token is missing"} | |
| {error === "missing_user" && "The user is missing"} | |
| {error === "unknown" && "An unknown error occured"} | |
| </p> | |
| </div> | |
| <A | |
| class={cn( | |
| buttonVariants({ | |
| variant: "default", | |
| size: "lg", | |
| }) | |
| )} | |
| aria-label="Go to the login page" | |
| href="/auth/login" | |
| > | |
| <span>Login again</span> | |
| </A> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { Button } from "@/components/ui/button"; | |
| import { Logo } from "@/components/ui/custom/logo"; | |
| import { TextField, TextFieldInput, TextFieldLabel } from "@/components/ui/textfield"; | |
| import { cn } from "@/lib/utils"; | |
| import { As } from "@kobalte/core"; | |
| import { A, useNavigate } from "@solidjs/router"; | |
| import type { SVGAttributes } from "lucide-solid/dist/types/types"; | |
| import { For, JSX, createSignal } from "solid-js"; | |
| import { toast } from "solid-sonner"; | |
| const generateAuthUrl = (provider: string) => { | |
| const url = new URL("/authorize", import.meta.env.VITE_AUTH_URL); | |
| url.searchParams.set("provider", provider); | |
| url.searchParams.set("response_type", "code"); | |
| url.searchParams.set("client_id", provider); | |
| url.searchParams.set( | |
| "redirect_uri", | |
| (import.meta.env.NODE_ENV === "production" ? "https://<your-domain>" : "http://localhost:3000") + | |
| "/api/auth/callback" | |
| ); | |
| return url.toString(); | |
| }; | |
| const logins = { | |
| google: generateAuthUrl("google"), | |
| } as const; | |
| export type Logins = keyof typeof logins; | |
| const logos: Record<Logins, (props: SVGAttributes) => JSX.Element> = { | |
| google: (props: SVGAttributes) => ( | |
| <svg {...props} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> | |
| <path | |
| d="M12 13.9V10.18H21.36C21.5 10.81 21.61 11.4 21.61 12.23C21.61 17.94 17.78 22 12.01 22C6.48 22 2 17.52 2 12C2 6.48 6.48 2 12 2C14.7 2 16.96 2.99 18.69 4.61L15.85 7.37C15.13 6.69 13.88 5.88 12 5.88C8.69 5.88 5.99 8.63 5.99 12C5.99 15.37 8.69 18.12 12 18.12C15.83 18.12 17.24 15.47 17.5 13.9H12Z" | |
| fill="currentColor" | |
| ></path> | |
| </svg> | |
| ), | |
| }; | |
| export default function LoginPage() { | |
| const [email, setEmail] = createSignal<string>(""); | |
| const [submitting, setSubmitting] = createSignal<boolean>(); | |
| return ( | |
| <div class="container h-[calc(100vh-49px)] flex flex-col items-center justify-center px-10"> | |
| <div class="w-full h-[650px] -mt-60 border border-neutral-200 dark:border-neutral-800 rounded-lg overflow-clip"> | |
| <div class="w-full relative flex h-full flex-col items-center justify-center md:grid lg:max-w-none lg:grid-cols-2 lg:px-0"> | |
| <div class="relative hidden h-full flex-col bg-muted p-10 dark:border-r lg:flex"> | |
| <div class="absolute inset-0 bg-neutral-100 dark:bg-neutral-900" /> | |
| <div class="relative z-20 flex items-center text-lg font-medium gap-2"> | |
| <Logo /> | |
| Portal | |
| </div> | |
| <div class="relative z-20 mt-auto"> | |
| </div> | |
| </div> | |
| <div class="p-8 w-full"> | |
| <div class="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]"> | |
| <div class="relative z-20 flex lg:hidden items-center text-lg font-medium gap-2 justify-center"> | |
| <Logo /> | |
| Portal | |
| </div> | |
| <div class="flex flex-col space-y-4 text-center"> | |
| <h1 class="text-2xl font-semibold tracking-tight">Create an account</h1> | |
| <p class="text-sm text-muted-foreground">Enter your email below to create your account</p> | |
| </div> | |
| <div class="flex flex-col gap-4 items-center w-full"> | |
| <TextField class="w-full" name="email" value={email()}> | |
| <TextFieldInput | |
| type="email" | |
| name="email" | |
| placeholder="name@example.com" | |
| autoCapitalize="none" | |
| autocomplete="email" | |
| autocorrect="off" | |
| class="w-full" | |
| disabled={submitting()} | |
| onInput={(e) => setEmail(e.currentTarget.value)} | |
| /> | |
| </TextField> | |
| <Button | |
| variant="default" | |
| size="lg" | |
| class={cn("w-full", { | |
| "opacity-50 cursor-not-allowed": submitting(), | |
| })} | |
| aria-busy={submitting()} | |
| aria-label="Continue with Email" | |
| disabled={submitting()} | |
| onClick={async () => { | |
| const _e = email(); | |
| setSubmitting(true); | |
| window.location.href = | |
| import.meta.env.VITE_AUTH_URL + | |
| "/authorize?" + | |
| new URLSearchParams({ | |
| client_id: "email", | |
| redirect_uri: import.meta.env.VITE_APP_URL + "/api/auth/callback?client_id=email", | |
| response_type: "code", | |
| provider: "email", | |
| email: _e, | |
| }); | |
| }} | |
| > | |
| <span>Continue with Email</span> | |
| </Button> | |
| <span class="text-muted-foreground text-sm">We'll send a pin code to your email</span> | |
| </div> | |
| <div class="relative"> | |
| <div class="absolute inset-0 flex items-center"> | |
| <span class="w-full border-t" /> | |
| </div> | |
| <div class="relative flex justify-center text-xs uppercase"> | |
| <span class="bg-background px-2 text-muted-foreground">Or continue with</span> | |
| </div> | |
| </div> | |
| <div class="flex flex-col gap-4 items-center w-full"> | |
| <For each={Object.entries(logins) as [Logins, string][]}> | |
| {([provider, url]) => { | |
| const L = logos[provider]; | |
| return ( | |
| <Button asChild variant="default" size="lg" class="!w-full"> | |
| <As | |
| component={A} | |
| href={url} | |
| class="flex items-center justify-center w-max text-sm font-medium gap-4 capitalize" | |
| > | |
| <L class="h-5 w-5" /> | |
| <span>{provider}</span> | |
| </As> | |
| </Button> | |
| ); | |
| }} | |
| </For> | |
| </div> | |
| <div class="px-8 text-center text-sm text-muted-foreground gap-4 flex flex-col"> | |
| <span>By continuing, you agree to our</span> | |
| <div class=""> | |
| <A href="/terms-of-service" class="underline underline-offset-4 hover:text-primary"> | |
| Terms of Service | |
| </A>{" "} | |
| and{" "} | |
| <A href="/privacy" class="underline underline-offset-4 hover:text-primary"> | |
| Privacy Policy | |
| </A> | |
| . | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { Context } from "sst/context/context2.js"; | |
| import { z } from "zod"; | |
| export const PublicActor = z.object({ | |
| type: z.literal("public"), | |
| properties: z.object({}), | |
| }); | |
| export type PublicActor = z.infer<typeof PublicActor>; | |
| export const AccountActor = z.object({ | |
| type: z.literal("account"), | |
| properties: z.object({ | |
| accountID: z.string().cuid2(), | |
| email: z.string().nonempty(), | |
| }), | |
| }); | |
| export type AccountActor = z.infer<typeof AccountActor>; | |
| export const UserActor = z.object({ | |
| type: z.literal("user"), | |
| properties: z.object({ | |
| userID: z.string().uuid(), | |
| workspaceID: z.string().uuid(), | |
| }), | |
| }); | |
| export type UserActor = z.infer<typeof UserActor>; | |
| export const SystemActor = z.object({ | |
| type: z.literal("system"), | |
| properties: z.object({ | |
| workspaceID: z.string().uuid(), | |
| }), | |
| }); | |
| export type SystemActor = z.infer<typeof SystemActor>; | |
| export const Actor = z.discriminatedUnion("type", [UserActor, AccountActor, PublicActor, SystemActor]); | |
| export type Actor = z.infer<typeof Actor>; | |
| const ActorContext = Context.create<Actor>("actor"); | |
| export const useActor = ActorContext.use; | |
| export const withActor = ActorContext.with; | |
| export function assertActor<T extends Actor["type"]>(type: T) { | |
| const actor = useActor(); | |
| if (actor.type !== type) { | |
| throw new Error(`Expected actor type ${type}, got ${actor.type}`); | |
| } | |
| return actor as Extract<Actor, { type: T }>; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { User } from "@/core/entities/users"; | |
| import { ApiHandler } from "sst/node/api"; | |
| import { Config } from "sst/node/config"; | |
| import { AuthHandler, CodeAdapter, GoogleAdapter } from "sst/node/future/auth"; | |
| import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2"; | |
| import { error, getUser, json, sessions } from "./utils"; | |
| import { z } from "zod"; | |
| import { withActor } from "@/core/actor"; | |
| import Nodemailer from "nodemailer"; | |
| export const handler = AuthHandler({ | |
| sessions, | |
| providers: { | |
| google: GoogleAdapter({ | |
| clientID: Config.GOOGLE_CLIENT_ID, | |
| mode: "oidc", | |
| }), | |
| email: CodeAdapter({ | |
| async onCodeRequest(code, claims) { | |
| console.log("code request", code, claims); | |
| return withActor( | |
| { | |
| type: "public", | |
| properties: {}, | |
| }, | |
| async () => { | |
| console.log("sending email to", claims); | |
| console.log("code", code); | |
| const email = z.string().email().safeParse(claims.email); | |
| if (!email.success) { | |
| console.log("invalid email, aborting", claims); | |
| return { | |
| statusCode: 302, | |
| headers: { | |
| Location: process.env.AUTH_FRONTEND_URL + "/auth/email?error=invalid_or_missing_email", | |
| }, | |
| }; | |
| } | |
| // TODO!: implement a better way to verify the email, and botspam. | |
| const ok = true; | |
| if (!ok) | |
| return { | |
| statusCode: 302, | |
| headers: { | |
| Location: process.env.AUTH_FRONTEND_URL + "/auth/email?error=invalid_code", | |
| }, | |
| }; | |
| const nodemailer = Nodemailer.createTransport({ | |
| host: Config.EMAIL_HOST, | |
| port: z.coerce.number().parse(Config.EMAIL_PORT), | |
| secure: process.env.IS_LOCAL ? false : true, | |
| auth: { | |
| user: Config.EMAIL_USERNAME, | |
| pass: Config.EMAIL_PASSWORD, | |
| }, | |
| }); | |
| // send via mail, you can use whatever api you want for it | |
| const info = await nodemailer | |
| .sendMail({ | |
| from: Config.EMAIL_FROM, | |
| to: claims.email, | |
| subject: "Your Pin Code: " + code, | |
| text: "Your pin code is " + code, | |
| html: "Your pin code is <strong>" + code + "</strong>", | |
| }) | |
| .catch((err) => { | |
| console.error("error sending email", err); | |
| return null; | |
| }); | |
| if (!info) { | |
| return { | |
| statusCode: 302, | |
| headers: { | |
| Location: process.env.AUTH_FRONTEND_URL + "/auth/email?error=sending_email_failed", | |
| }, | |
| }; | |
| } else { | |
| console.log("send message info", info); | |
| return { | |
| statusCode: 302, | |
| headers: { | |
| Location: process.env.AUTH_FRONTEND_URL + "/auth/email", | |
| }, | |
| }; | |
| } | |
| } | |
| ); | |
| }, | |
| async onCodeInvalid() { | |
| return { | |
| statusCode: 302, | |
| headers: { | |
| Location: process.env.AUTH_FRONTEND_URL + "/auth/error?error=invalid_code", | |
| }, | |
| }; | |
| }, | |
| }), | |
| }, | |
| callbacks: { | |
| error: async (e) => { | |
| console.log("upps error: ", e); | |
| return { | |
| statusCode: 302, | |
| headers: { | |
| Location: process.env.AUTH_FRONTEND_URL + "/auth/error?error=unknown", | |
| }, | |
| }; | |
| }, | |
| auth: { | |
| async allowClient(clientID, redirect) { | |
| const clients = ["solid", "google", "email"]; | |
| if (!clients.includes(clientID)) { | |
| return false; | |
| } | |
| return true; | |
| }, | |
| async error(error) { | |
| console.log("auth-error", error); | |
| return { | |
| statusCode: 302, | |
| headers: { | |
| Location: process.env.AUTH_FRONTEND_URL + "/auth/error?error=unknown", | |
| }, | |
| }; | |
| }, | |
| async success(input, response) { | |
| if (input.provider === "google") { | |
| const claims = input.tokenset.claims(); | |
| const email = claims.email; | |
| const name = claims.preferred_username ?? claims.name; | |
| if (!email || !name) { | |
| console.error("No email or name found in tokenset", input.tokenset); | |
| return response.http({ | |
| statusCode: 400, | |
| body: "No email found in tokenset", | |
| }); | |
| } | |
| let user_ = await User.findByEmail(email); | |
| if (!user_) { | |
| user_ = await User.create({ email, name }); | |
| } | |
| await User.update({ id: user_.id, deletedAt: null }); | |
| return response.session({ | |
| type: "user", | |
| properties: { | |
| id: user_.id, | |
| email: user_.email, | |
| }, | |
| }); | |
| } | |
| if (input.provider === "email") { | |
| const claims = input.claims; | |
| const email = claims.email; | |
| if (!email) { | |
| console.error("No email or name found in claims", input.claims); | |
| return response.http({ | |
| statusCode: 400, | |
| body: "No email found in claims", | |
| }); | |
| } | |
| let user_ = await User.findByEmail(email); | |
| if (!user_) { | |
| user_ = await User.create({ email, name: email }); | |
| } | |
| await User.update({ id: user_.id, deletedAt: null }); | |
| return response.session({ | |
| type: "user", | |
| properties: { | |
| id: user_.id, | |
| email: user_.email, | |
| }, | |
| }); | |
| } | |
| throw new Error("Unknown provider"); | |
| }, | |
| }, | |
| }, | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment