diff --git a/.env.example b/.env.example index 1ab4b4dab..0d8ebdff3 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,5 @@ -UNDB_DB_TURSO_URL=libsql://undb-project-nichenqin.turso.io -UNDB_DB_TURSO_AUTH_TOKEN= \ No newline at end of file +UNDB_DB_TURSO_URL=libsql://127.0.0.1:8080?tls=0 +UNDB_DB_TURSO_AUTH_TOKEN= + +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= \ No newline at end of file diff --git a/apps/backend/src/app.ts b/apps/backend/src/app.ts index b5bc6107c..a26c4a6eb 100644 --- a/apps/backend/src/app.ts +++ b/apps/backend/src/app.ts @@ -11,6 +11,7 @@ import { swagger } from "@elysiajs/swagger" import { trpc } from "@elysiajs/trpc" import { executionContext } from "@undb/context/server" import { container } from "@undb/di" +import { env } from "@undb/env" import { Graphql } from "@undb/graphql" import { createLogger } from "@undb/logger" import { dbMigrate } from "@undb/persistence" @@ -135,7 +136,7 @@ export const app = new Elysia() if (!user) { return context.redirect(`/login?redirect=${context.path}`, 301) } - if (!user.emailVerified && user.email) { + if (env.UNDB_VERIFY_EMAIL && !user.emailVerified && user.email) { return context.redirect(`/verify-email?redirect=${context.path}`, 301) } }, diff --git a/apps/backend/src/modules/auth/auth.ts b/apps/backend/src/modules/auth/auth.ts index 8dc7f4ba0..bb73fc508 100644 --- a/apps/backend/src/modules/auth/auth.ts +++ b/apps/backend/src/modules/auth/auth.ts @@ -191,7 +191,7 @@ export class Auth { return ctx.redirect("/login") } - if (!user.emailVerified && user.email) { + if (env.UNDB_VERIFY_EMAIL && !user.emailVerified && user.email) { return ctx.redirect(`/verify-email`, 301) } @@ -271,17 +271,19 @@ export class Auth { await this.spaceMemberService.createMember(userId, space.id.value, "owner") ctx.cookie[SPACE_ID_COOKIE_NAME].set({ value: space.id.value }) - const verificationCode = await this.#generateEmailVerificationCode(userId, email) - await this.mailService.send({ - template: "verify-email", - data: { - username: username!, - code: verificationCode, - action_url: new URL(`/api/email-verification`, env.UNDB_BASE_URL).toString(), - }, - subject: "Verify your email - undb", - to: email, - }) + if (env.UNDB_VERIFY_EMAIL) { + const verificationCode = await this.#generateEmailVerificationCode(userId, email) + await this.mailService.send({ + template: "verify-email", + data: { + username: username!, + code: verificationCode, + action_url: new URL(`/api/email-verification`, env.UNDB_BASE_URL).toString(), + }, + subject: "Verify your email - undb", + to: email, + }) + } }) } @@ -423,7 +425,7 @@ export class Auth { ) .get("/login/github", async (ctx) => { const state = generateState() - const url = await github.createAuthorizationURL(state) + const url = await github.createAuthorizationURL(state, { scopes: ["user:email"] }) return new Response(null, { status: 302, headers: { @@ -483,6 +485,56 @@ export class Auth { }) } + const emailsResponse = await fetch("https://api.github.com/user/emails", { + headers: { + Authorization: `Bearer ${tokens.accessToken}`, + }, + }) + const emails: GithubEmail[] = await emailsResponse.json() + + const primaryEmail = emails.find((email) => email.primary) ?? null + if (!primaryEmail) { + return new Response("No primary email address", { + status: 400, + }) + } + if (!primaryEmail.verified) { + return new Response("Unverified email", { + status: 400, + }) + } + + const existingGithubUser = await this.queryBuilder + .selectFrom("undb_user") + .selectAll() + .where("undb_user.email", "=", primaryEmail.email) + .executeTakeFirst() + + if (existingGithubUser) { + const spaceId = ctx.cookie[SPACE_ID_COOKIE_NAME].value + if (!spaceId) { + await this.spaceService.setSpaceContext(setContextValue, { userId: existingGithubUser.id }) + } + + await this.queryBuilder + .insertInto("undb_oauth_account") + .values({ + provider_id: "github", + provider_user_id: githubUserResult.id.toString(), + user_id: existingGithubUser.id, + }) + .execute() + + const session = await lucia.createSession(existingGithubUser.id, {}) + const sessionCookie = lucia.createSessionCookie(session.id) + return new Response(null, { + status: 302, + headers: { + Location: "/", + "Set-Cookie": sessionCookie.serialize(), + }, + }) + } const userId = generateIdFromEntropySize(10) // 16 characters long await withTransaction(this.queryBuilder)(async () => { const tx = getCurrentTransaction() @@ -561,3 +613,9 @@ interface GitHubUserResult { id: number login: string // username } + +interface GithubEmail { + email: string + primary: boolean + verified: boolean +} diff --git a/apps/frontend/src/lib/images/Google.svg b/apps/frontend/src/lib/images/Google.svg new file mode 100644 index 000000000..2eaf91554 --- /dev/null +++ b/apps/frontend/src/lib/images/Google.svg @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/apps/frontend/src/lib/images/github.svg b/apps/frontend/src/lib/images/github.svg index bc5d249d3..538ec5bf2 100644 --- a/apps/frontend/src/lib/images/github.svg +++ b/apps/frontend/src/lib/images/github.svg @@ -1,16 +1 @@ - - - - \ No newline at end of file +GitHub \ No newline at end of file diff --git a/apps/frontend/src/routes/(auth)/login/+page.svelte b/apps/frontend/src/routes/(auth)/login/+page.svelte index a87323e9c..35894afb4 100644 --- a/apps/frontend/src/routes/(auth)/login/+page.svelte +++ b/apps/frontend/src/routes/(auth)/login/+page.svelte @@ -4,6 +4,7 @@ import { Input } from "$lib/components/ui/input/index.js" import { Label } from "$lib/components/ui/label/index.js" import Logo from "$lib/images/logo.svg" + import Github from "$lib/images/github.svg" import { createMutation } from "@tanstack/svelte-query" import { z } from "@undb/zod" import { defaults, superForm } from "sveltekit-superforms" @@ -12,7 +13,6 @@ import { toast } from "svelte-sonner" import { Button } from "$lib/components/ui/button" import { Separator } from "$lib/components/ui/separator" - import { GithubIcon } from "lucide-svelte" const schema = z.object({ email: z.string().email(), @@ -30,10 +30,6 @@ toast.error(error.message) await goto("/signup") }, - onSettled(data, error, variables, context) { - console.log(data) - console.log(error) - }, }) const form = superForm( @@ -112,9 +108,9 @@ Sign up -
+
diff --git a/apps/frontend/src/routes/(auth)/signup/+page.svelte b/apps/frontend/src/routes/(auth)/signup/+page.svelte index 2876f0cc1..686c08638 100644 --- a/apps/frontend/src/routes/(auth)/signup/+page.svelte +++ b/apps/frontend/src/routes/(auth)/signup/+page.svelte @@ -6,12 +6,14 @@ import { Input } from "$lib/components/ui/input/index.js" import { Label } from "$lib/components/ui/label/index.js" import Logo from "$lib/images/logo.svg" + import Github from "$lib/images/github.svg" import { createMutation } from "@tanstack/svelte-query" import { z } from "@undb/zod" import { defaults, superForm } from "sveltekit-superforms" import { zodClient } from "sveltekit-superforms/adapters" import * as Form from "$lib/components/ui/form" import { toast } from "svelte-sonner" + import { Separator } from "$lib/components/ui/separator" const schema = z.object({ email: z.string().email(), @@ -159,6 +161,13 @@ Already have an account? Sign in
+ +
+ +
diff --git a/apps/frontend/src/routes/(auth)/verify-email/+page.svelte b/apps/frontend/src/routes/(auth)/verify-email/+page.svelte index 8830675e3..46aa15095 100644 --- a/apps/frontend/src/routes/(auth)/verify-email/+page.svelte +++ b/apps/frontend/src/routes/(auth)/verify-email/+page.svelte @@ -1,10 +1,10 @@ diff --git a/packages/env/src/index.ts b/packages/env/src/index.ts index 4a9e3e2f1..7274a4cf1 100644 --- a/packages/env/src/index.ts +++ b/packages/env/src/index.ts @@ -21,6 +21,14 @@ export const env = createEnv({ UNDB_MAIL_HOST: z.string().optional(), UNDB_MAIL_PORT: z.string().optional(), UNDB_MAIL_DEFAULT_FROM: z.string().optional(), + UNDB_VERIFY_EMAIL: z + .string() + .optional() + .default("false") + .refine((v) => v === "true" || v === "false", { + message: "UNDB_VERIFY_EMAIL must be a boolean", + }) + .transform((v) => v === "true"), GITHUB_CLIENT_ID: z.string().optional(), GITHUB_CLIENT_SECRET: z.string().optional(),