Skip to content

Commit 0b985e0

Browse files
committed
feat: github oauth
1 parent 9dcf394 commit 0b985e0

File tree

9 files changed

+104
-40
lines changed

9 files changed

+104
-40
lines changed

.env.example

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
1-
UNDB_DB_TURSO_URL=libsql://undb-project-nichenqin.turso.io
2-
UNDB_DB_TURSO_AUTH_TOKEN=
1+
UNDB_DB_TURSO_URL=libsql://127.0.0.1:8080?tls=0
2+
UNDB_DB_TURSO_AUTH_TOKEN=
3+
4+
GITHUB_CLIENT_ID=
5+
GITHUB_CLIENT_SECRET=

apps/backend/src/app.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { swagger } from "@elysiajs/swagger"
1111
import { trpc } from "@elysiajs/trpc"
1212
import { executionContext } from "@undb/context/server"
1313
import { container } from "@undb/di"
14+
import { env } from "@undb/env"
1415
import { Graphql } from "@undb/graphql"
1516
import { createLogger } from "@undb/logger"
1617
import { dbMigrate } from "@undb/persistence"
@@ -135,7 +136,7 @@ export const app = new Elysia()
135136
if (!user) {
136137
return context.redirect(`/login?redirect=${context.path}`, 301)
137138
}
138-
if (!user.emailVerified && user.email) {
139+
if (env.UNDB_VERIFY_EMAIL && !user.emailVerified && user.email) {
139140
return context.redirect(`/verify-email?redirect=${context.path}`, 301)
140141
}
141142
},

apps/backend/src/modules/auth/auth.ts

Lines changed: 71 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ export class Auth {
191191
return ctx.redirect("/login")
192192
}
193193

194-
if (!user.emailVerified && user.email) {
194+
if (env.UNDB_VERIFY_EMAIL && !user.emailVerified && user.email) {
195195
return ctx.redirect(`/verify-email`, 301)
196196
}
197197

@@ -271,17 +271,19 @@ export class Auth {
271271
await this.spaceMemberService.createMember(userId, space.id.value, "owner")
272272
ctx.cookie[SPACE_ID_COOKIE_NAME].set({ value: space.id.value })
273273

274-
const verificationCode = await this.#generateEmailVerificationCode(userId, email)
275-
await this.mailService.send({
276-
template: "verify-email",
277-
data: {
278-
username: username!,
279-
code: verificationCode,
280-
action_url: new URL(`/api/email-verification`, env.UNDB_BASE_URL).toString(),
281-
},
282-
subject: "Verify your email - undb",
283-
to: email,
284-
})
274+
if (env.UNDB_VERIFY_EMAIL) {
275+
const verificationCode = await this.#generateEmailVerificationCode(userId, email)
276+
await this.mailService.send({
277+
template: "verify-email",
278+
data: {
279+
username: username!,
280+
code: verificationCode,
281+
action_url: new URL(`/api/email-verification`, env.UNDB_BASE_URL).toString(),
282+
},
283+
subject: "Verify your email - undb",
284+
to: email,
285+
})
286+
}
285287
})
286288
}
287289

@@ -423,7 +425,7 @@ export class Auth {
423425
)
424426
.get("/login/github", async (ctx) => {
425427
const state = generateState()
426-
const url = await github.createAuthorizationURL(state)
428+
const url = await github.createAuthorizationURL(state, { scopes: ["user:email"] })
427429
return new Response(null, {
428430
status: 302,
429431
headers: {
@@ -483,6 +485,56 @@ export class Auth {
483485
})
484486
}
485487

488+
const emailsResponse = await fetch("https://api.github.com/user/emails", {
489+
headers: {
490+
Authorization: `Bearer ${tokens.accessToken}`,
491+
},
492+
})
493+
const emails: GithubEmail[] = await emailsResponse.json()
494+
495+
const primaryEmail = emails.find((email) => email.primary) ?? null
496+
if (!primaryEmail) {
497+
return new Response("No primary email address", {
498+
status: 400,
499+
})
500+
}
501+
if (!primaryEmail.verified) {
502+
return new Response("Unverified email", {
503+
status: 400,
504+
})
505+
}
506+
507+
const existingGithubUser = await this.queryBuilder
508+
.selectFrom("undb_user")
509+
.selectAll()
510+
.where("undb_user.email", "=", primaryEmail.email)
511+
.executeTakeFirst()
512+
513+
if (existingGithubUser) {
514+
const spaceId = ctx.cookie[SPACE_ID_COOKIE_NAME].value
515+
if (!spaceId) {
516+
await this.spaceService.setSpaceContext(setContextValue, { userId: existingGithubUser.id })
517+
}
518+
519+
await this.queryBuilder
520+
.insertInto("undb_oauth_account")
521+
.values({
522+
provider_id: "github",
523+
provider_user_id: githubUserResult.id.toString(),
524+
user_id: existingGithubUser.id,
525+
})
526+
.execute()
527+
528+
const session = await lucia.createSession(existingGithubUser.id, {})
529+
const sessionCookie = lucia.createSessionCookie(session.id)
530+
return new Response(null, {
531+
status: 302,
532+
headers: {
533+
Location: "/",
534+
"Set-Cookie": sessionCookie.serialize(),
535+
},
536+
})
537+
}
486538
const userId = generateIdFromEntropySize(10) // 16 characters long
487539
await withTransaction(this.queryBuilder)(async () => {
488540
const tx = getCurrentTransaction()
@@ -561,3 +613,9 @@ interface GitHubUserResult {
561613
id: number
562614
login: string // username
563615
}
616+
617+
interface GithubEmail {
618+
email: string
619+
primary: boolean
620+
verified: boolean
621+
}
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 16 deletions
Loading

apps/frontend/src/routes/(auth)/login/+page.svelte

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { Input } from "$lib/components/ui/input/index.js"
55
import { Label } from "$lib/components/ui/label/index.js"
66
import Logo from "$lib/images/logo.svg"
7+
import Github from "$lib/images/github.svg"
78
import { createMutation } from "@tanstack/svelte-query"
89
import { z } from "@undb/zod"
910
import { defaults, superForm } from "sveltekit-superforms"
@@ -12,7 +13,6 @@
1213
import { toast } from "svelte-sonner"
1314
import { Button } from "$lib/components/ui/button"
1415
import { Separator } from "$lib/components/ui/separator"
15-
import { GithubIcon } from "lucide-svelte"
1616
1717
const schema = z.object({
1818
email: z.string().email(),
@@ -30,10 +30,6 @@
3030
toast.error(error.message)
3131
await goto("/signup")
3232
},
33-
onSettled(data, error, variables, context) {
34-
console.log(data)
35-
console.log(error)
36-
},
3733
})
3834
3935
const form = superForm(
@@ -112,9 +108,9 @@
112108
<a href="/signup" class="underline"> Sign up </a>
113109
</div>
114110
<Separator class="my-6" />
115-
<div>
111+
<div class="space-y-2">
116112
<Button href="/login/github" variant="secondary" class="w-full">
117-
<GithubIcon class="mr-2 h-5 w-5" />
113+
<img class="mr-2 h-5 w-5" src={Github} alt="github" />
118114
Login with Github
119115
</Button>
120116
</div>

apps/frontend/src/routes/(auth)/signup/+page.svelte

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
import { Input } from "$lib/components/ui/input/index.js"
77
import { Label } from "$lib/components/ui/label/index.js"
88
import Logo from "$lib/images/logo.svg"
9+
import Github from "$lib/images/github.svg"
910
import { createMutation } from "@tanstack/svelte-query"
1011
import { z } from "@undb/zod"
1112
import { defaults, superForm } from "sveltekit-superforms"
1213
import { zodClient } from "sveltekit-superforms/adapters"
1314
import * as Form from "$lib/components/ui/form"
1415
import { toast } from "svelte-sonner"
16+
import { Separator } from "$lib/components/ui/separator"
1517
1618
const schema = z.object({
1719
email: z.string().email(),
@@ -159,6 +161,13 @@
159161
Already have an account?
160162
<a href="/login" class="underline"> Sign in </a>
161163
</div>
164+
<Separator class="my-6" />
165+
<div class="space-y-2">
166+
<Button href="/login/github" variant="secondary" class="w-full">
167+
<img class="mr-2 h-5 w-5" src={Github} alt="github" />
168+
Login with Github
169+
</Button>
170+
</div>
162171
</Card.Content>
163172
</Card.Root>
164173
</form>

apps/frontend/src/routes/(auth)/verify-email/+page.svelte

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
<script lang="ts">
2+
import { goto } from "$app/navigation"
23
import Button from "$lib/components/ui/button/button.svelte"
34
import { OTPInput, OTPRoot } from "@jimmyverburgt/svelte-input-otp"
45
import { createMutation } from "@tanstack/svelte-query"
56
import { LoaderCircleIcon } from "lucide-svelte"
67
import Minus from "lucide-svelte/icons/minus"
7-
import { defaults, superForm } from "sveltekit-superforms"
88
99
let otpref: any
1010
@@ -21,6 +21,9 @@
2121
body: JSON.stringify({ code: value }),
2222
}),
2323
mutationKey: ["verify-email"],
24+
async onSuccess(data, variables, context) {
25+
await goto("/")
26+
},
2427
})
2528
</script>
2629

packages/env/src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ export const env = createEnv({
2121
UNDB_MAIL_HOST: z.string().optional(),
2222
UNDB_MAIL_PORT: z.string().optional(),
2323
UNDB_MAIL_DEFAULT_FROM: z.string().optional(),
24+
UNDB_VERIFY_EMAIL: z
25+
.string()
26+
.optional()
27+
.default("false")
28+
.refine((v) => v === "true" || v === "false", {
29+
message: "UNDB_VERIFY_EMAIL must be a boolean",
30+
})
31+
.transform((v) => v === "true"),
2432

2533
GITHUB_CLIENT_ID: z.string().optional(),
2634
GITHUB_CLIENT_SECRET: z.string().optional(),

0 commit comments

Comments
 (0)