From 327561d9c83fd51ccd927dda784e33ee35ac2bcf Mon Sep 17 00:00:00 2001 From: thormengkheang Date: Thu, 14 Mar 2024 14:37:26 +0700 Subject: [PATCH 1/5] add email column --- src/db/schema.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/db/schema.ts b/src/db/schema.ts index 6009ca7c..ce49fa81 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -14,6 +14,7 @@ export const user = sqliteTable("user", { createdAt: text("created_at") .notNull() .default(sql`CURRENT_TIMESTAMP`), + email: text("email"), }); export const user_session = sqliteTable( From 95187aa9a054b608f34fc9b87bb47768e0796b75 Mon Sep 17 00:00:00 2001 From: thormengkheang Date: Thu, 14 Mar 2024 14:37:36 +0700 Subject: [PATCH 2/5] db migration --- drizzle/0001_crazy_eternals.sql | 1 + drizzle/meta/0001_snapshot.json | 186 ++++++++++++++++++++++++++++++++ drizzle/meta/_journal.json | 7 ++ 3 files changed, 194 insertions(+) create mode 100644 drizzle/0001_crazy_eternals.sql create mode 100644 drizzle/meta/0001_snapshot.json diff --git a/drizzle/0001_crazy_eternals.sql b/drizzle/0001_crazy_eternals.sql new file mode 100644 index 00000000..6127014f --- /dev/null +++ b/drizzle/0001_crazy_eternals.sql @@ -0,0 +1 @@ +ALTER TABLE user ADD `email` text; \ No newline at end of file diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 00000000..f220ab07 --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,186 @@ +{ + "version": "5", + "dialect": "sqlite", + "id": "1752cbef-1ce6-4863-bcef-5ffad31ac535", + "prevId": "842060cb-8a3f-4721-aeb8-909365440ca3", + "tables": { + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "picture": { + "name": "picture", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user_auth": { + "name": "user_auth", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "user_auth_provider_provider_id_unique": { + "name": "user_auth_provider_provider_id_unique", + "columns": [ + "provider", + "provider_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user_session": { + "name": "user_session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth_id": { + "name": "auth_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "blob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_session_expire_idx": { + "name": "user_session_expire_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + }, + "user_session_auth_id_idx": { + "name": "user_session_auth_id_idx", + "columns": [ + "auth_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_session_user_id_user_id_fk": { + "name": "user_session_user_id_user_id_fk", + "tableFrom": "user_session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index cda09071..9ccec03c 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1710049140295, "tag": "0000_solid_scream", "breakpoints": true + }, + { + "idx": 1, + "version": "5", + "when": 1710401496821, + "tag": "0001_crazy_eternals", + "breakpoints": true } ] } \ No newline at end of file From 444f3bb8b57ed43e0a5c21a96f4c7d9c637d88bf Mon Sep 17 00:00:00 2001 From: thormengkheang Date: Thu, 14 Mar 2024 14:38:23 +0700 Subject: [PATCH 3/5] save gh email to user table --- src/app/login/github/callback/route.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/login/github/callback/route.ts b/src/app/login/github/callback/route.ts index 9e3620e2..1adc0306 100644 --- a/src/app/login/github/callback/route.ts +++ b/src/app/login/github/callback/route.ts @@ -61,7 +61,9 @@ export async function GET(request: Request): Promise { const userId = generateId(15); const authId = generateId(15); - await db.insert(user).values({ id: userId, name: githubUser.login }); + await db + .insert(user) + .values({ id: userId, name: githubUser.login, email: githubUser.email }); await db.insert(user_oauth).values({ id: authId, provider: "GITHUB", @@ -106,4 +108,5 @@ export async function GET(request: Request): Promise { interface GitHubUser { id: string; login: string; + email: string; } From 64cbe6b5a821e2fc9f2c927d65b7d05373dbaed0 Mon Sep 17 00:00:00 2001 From: thormengkheang Date: Thu, 14 Mar 2024 17:10:06 +0700 Subject: [PATCH 4/5] add user email scope --- src/app/login/github/route.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/login/github/route.ts b/src/app/login/github/route.ts index 6f79210a..b867f19e 100644 --- a/src/app/login/github/route.ts +++ b/src/app/login/github/route.ts @@ -4,7 +4,9 @@ import { cookies } from "next/headers"; export async function GET(): Promise { const state = generateState(); - const url = await github.createAuthorizationURL(state); + const url = await github.createAuthorizationURL(state, { + scopes: ["user:email"], + }); cookies().set("github_oauth_state", state, { path: "/", From 10ba4a9611ad4737ef36024e8e90555300452a5c Mon Sep 17 00:00:00 2001 From: thormengkheang Date: Thu, 14 Mar 2024 17:10:45 +0700 Subject: [PATCH 5/5] fetch email when is not public --- src/app/login/github/callback/route.ts | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/app/login/github/callback/route.ts b/src/app/login/github/callback/route.ts index 1adc0306..699796cf 100644 --- a/src/app/login/github/callback/route.ts +++ b/src/app/login/github/callback/route.ts @@ -10,6 +10,7 @@ export async function GET(request: Request): Promise { const code = url.searchParams.get("code"); const state = url.searchParams.get("state"); const headerStore = headers(); + const GITHUB_API_URL = "https://api.github.com"; const storedState = cookies().get("github_oauth_state")?.value ?? null; if (!code || !state || !storedState || state !== storedState) { @@ -19,14 +20,25 @@ export async function GET(request: Request): Promise { } try { - const tokens = await github.validateAuthorizationCode(code); - const githubUserResponse = await fetch("https://api.github.com/user", { + const token = await github.validateAuthorizationCode(code); + const githubUserResponse = await fetch(`${GITHUB_API_URL}/user`, { headers: { - Authorization: `Bearer ${tokens.accessToken}`, + Authorization: `Bearer ${token.accessToken}`, }, }); const githubUser: GitHubUser = await githubUserResponse.json(); + if (githubUser.email === null) { + const resp = await fetch(`${GITHUB_API_URL}/user/emails`, { + headers: { + Authorization: `Bearer ${token.accessToken}`, + }, + }); + const githubEmails: GitHubEmail[] = await resp.json(); + githubUser.email = + githubEmails.find((email) => email.primary)?.email || null; + } + // Replace this with your own DB client. const existingUser = await db.query.user_oauth.findFirst({ where: (field, op) => @@ -108,5 +120,12 @@ export async function GET(request: Request): Promise { interface GitHubUser { id: string; login: string; + email: string | null; +} + +interface GitHubEmail { email: string; + primary: boolean; + verified: boolean; + visibility: "public" | "private"; }