From cba72e553dced0fb88fefe7512d5dbfebf2c7222 Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Wed, 12 Jun 2024 09:13:00 +0200 Subject: [PATCH 1/2] fix: correctly handle oauth tokens --- server/api/auth/callback.get.ts | 8 +- .../migrations/0010_previous_steve_rogers.sql | 1 + server/db/migrations/meta/0010_snapshot.json | 385 ++++++++++++++++++ server/db/migrations/meta/_journal.json | 7 + server/forges/github.ts | 27 +- server/forges/gitlab.ts | 8 +- server/forges/index.ts | 6 +- server/forges/types.ts | 2 +- server/schemas/index.ts | 2 +- server/utils/auth.ts | 2 +- 10 files changed, 430 insertions(+), 18 deletions(-) create mode 100644 server/db/migrations/0010_previous_steve_rogers.sql create mode 100644 server/db/migrations/meta/0010_snapshot.json diff --git a/server/api/auth/callback.get.ts b/server/api/auth/callback.get.ts index f9bc6c3..b427b92 100644 --- a/server/api/auth/callback.get.ts +++ b/server/api/auth/callback.get.ts @@ -70,7 +70,7 @@ export default defineEventHandler(async (event) => { forgeId: forgeModel.id, remoteUserId: oauthUser.remoteUserId, accessToken: tokens.accessToken, - accessTokenExpiresIn: tokens.accessTokenExpiresIn, + accessTokenExpiresAt: tokens.accessTokenExpiresAt, refreshToken: tokens.refreshToken, }) .onConflictDoUpdate({ @@ -78,7 +78,7 @@ export default defineEventHandler(async (event) => { set: { remoteUserId: oauthUser.remoteUserId, accessToken: tokens.accessToken, - accessTokenExpiresIn: tokens.accessTokenExpiresIn, + accessTokenExpiresAt: tokens.accessTokenExpiresAt, refreshToken: tokens.refreshToken, }, }) @@ -118,7 +118,7 @@ export default defineEventHandler(async (event) => { .update(userForgesSchema) .set({ accessToken: tokens.accessToken, - accessTokenExpiresIn: tokens.accessTokenExpiresIn, + accessTokenExpiresAt: tokens.accessTokenExpiresAt, refreshToken: tokens.refreshToken, }) .where(eq(userForgesSchema.id, userForge.id)) @@ -145,7 +145,7 @@ export default defineEventHandler(async (event) => { forgeId: forgeModel.id, remoteUserId: oauthUser.remoteUserId, accessToken: tokens.accessToken, - accessTokenExpiresIn: tokens.accessTokenExpiresIn, + accessTokenExpiresAt: tokens.accessTokenExpiresAt, refreshToken: tokens.refreshToken, }) .run(); diff --git a/server/db/migrations/0010_previous_steve_rogers.sql b/server/db/migrations/0010_previous_steve_rogers.sql new file mode 100644 index 0000000..24a3909 --- /dev/null +++ b/server/db/migrations/0010_previous_steve_rogers.sql @@ -0,0 +1 @@ +ALTER TABLE `userForges` RENAME COLUMN `accessTokenExpiresIn` TO `accessTokenExpiresAt`; \ No newline at end of file diff --git a/server/db/migrations/meta/0010_snapshot.json b/server/db/migrations/meta/0010_snapshot.json new file mode 100644 index 0000000..82a6440 --- /dev/null +++ b/server/db/migrations/meta/0010_snapshot.json @@ -0,0 +1,385 @@ +{ + "version": "5", + "dialect": "sqlite", + "id": "ae1b2432-1ea5-4e87-a8de-5be27bd8f6d3", + "prevId": "22633665-a7ff-443e-8204-6807f0ef38a8", + "tables": { + "chatMessages": { + "name": "chatMessages", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "chatId": { + "name": "chatId", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "from": { + "name": "from", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "chats": { + "name": "chats", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repoId": { + "name": "repoId", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "forges": { + "name": "forges", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner": { + "name": "owner", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allowLogin": { + "name": "allowLogin", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "clientId": { + "name": "clientId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "clientSecret": { + "name": "clientSecret", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "forges_host_unique": { + "name": "forges_host_unique", + "columns": [ + "host" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "repos": { + "name": "repos", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "forgeId": { + "name": "forgeId", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "remoteId": { + "name": "remoteId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cloneUrl": { + "name": "cloneUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "defaultBranch": { + "name": "defaultBranch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lastFetch": { + "name": "lastFetch", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatarUrl": { + "name": "avatarUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "uniqueForgeRemoteId": { + "name": "uniqueForgeRemoteId", + "columns": [ + "forgeId", + "remoteId" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "userForges": { + "name": "userForges", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "forgeId": { + "name": "forgeId", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "remoteUserId": { + "name": "remoteUserId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accessTokenExpiresAt": { + "name": "accessTokenExpiresAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "uniqueUserIdForgeId": { + "name": "uniqueUserIdForgeId", + "columns": [ + "userId", + "forgeId" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "userRepos": { + "name": "userRepos", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repoId": { + "name": "repoId", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatarUrl": { + "name": "avatarUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": { + "\"userForges\".\"accessTokenExpiresIn\"": "\"userForges\".\"accessTokenExpiresAt\"" + } + } +} \ No newline at end of file diff --git a/server/db/migrations/meta/_journal.json b/server/db/migrations/meta/_journal.json index 42da9bd..d5013ce 100644 --- a/server/db/migrations/meta/_journal.json +++ b/server/db/migrations/meta/_journal.json @@ -71,6 +71,13 @@ "when": 1713355631391, "tag": "0009_clean_storm", "breakpoints": true + }, + { + "idx": 10, + "version": "5", + "when": 1718176183453, + "tag": "0010_previous_steve_rogers", + "breakpoints": true } ] } \ No newline at end of file diff --git a/server/forges/github.ts b/server/forges/github.ts index f0c6c3d..0a28aa7 100644 --- a/server/forges/github.ts +++ b/server/forges/github.ts @@ -62,14 +62,23 @@ export class Github implements Forge { }&scope=public_repo&state=${state}&scope=${scopes.join('%20')}`; } - public async getUserInfo(token: string): Promise { - const client = this.getClient(token); + public async getUserInfo(accessToken: string): Promise { + const client = this.getClient(accessToken); const githubUser = await client.request('GET /user'); + let email = githubUser.data.email; + // if no public email, check the private ones + if (!email) { + const emails = await client.request('GET /user/emails'); + const primaryEmail = emails.data.find((email: any) => email.primary); + // Still no email + email = primaryEmail?.email ?? null; + } + return { - name: githubUser.data.name, + name: githubUser.data.name || githubUser.data.login, avatarUrl: githubUser.data.avatar_url, - email: githubUser.data.email, + email, remoteUserId: githubUser.data.id.toString(), }; } @@ -95,9 +104,11 @@ export class Github implements Forge { throw new Error('Error getting access token'); } + const now = Math.floor(Date.now() / 1000); + return { accessToken: response.access_token, - accessTokenExpiresIn: response.expires_in || -1, // We use -1 as github access_tokens issued by oauth apps don't expire + accessTokenExpiresAt: response.expires_in ? now + response.expires_in : -1, // We use -1 as github access_tokens issued by oauth apps don't expire refreshToken: response.refresh_token || null, // Use null as oauth apps don't return refresh tokens }; } @@ -117,10 +128,12 @@ export class Github implements Forge { throw new Error('Error refreshing access token'); } + const now = Math.floor(Date.now() / 1000); + return { accessToken: response.access_token, - accessTokenExpiresIn: response.expires_in, - refreshToken: null, // TODO: we use an empty string for now as github access_tokens don't expire + accessTokenExpiresAt: response.expires_in ? now + response.expires_in : -1, + refreshToken, }; } diff --git a/server/forges/gitlab.ts b/server/forges/gitlab.ts index 3e25ae3..d11ae57 100644 --- a/server/forges/gitlab.ts +++ b/server/forges/gitlab.ts @@ -72,9 +72,11 @@ export class Gitlab implements Forge { throw new Error('Error getting access token'); } + const now = Math.floor(Date.now() / 1000); + return { accessToken: response.access_token, - accessTokenExpiresIn: response.expires_in, + accessTokenExpiresAt: now + response.expires_in, refreshToken: response.refresh_token, }; } @@ -95,9 +97,11 @@ export class Gitlab implements Forge { throw new Error('Error refreshing access token'); } + const now = Math.floor(Date.now() / 1000); + return { accessToken: response.access_token, - accessTokenExpiresIn: response.expires_in, + accessTokenExpiresAt: now + response.expires_in, refreshToken: response.refresh_token, }; } diff --git a/server/forges/index.ts b/server/forges/index.ts index 8cd4d52..6623fec 100644 --- a/server/forges/index.ts +++ b/server/forges/index.ts @@ -25,11 +25,13 @@ export class ForgeApi { private async refreshTokenIfNeeded(user: UserWithTokens): Promise { // skip refreshing tokens if they don't expire (e.g. Github) - if (user.tokens.accessTokenExpiresIn === -1) { + if (user.tokens.accessTokenExpiresAt === -1) { return user.tokens; } - if (user.tokens.accessTokenExpiresIn * 1000 > Date.now()) { + const now = Math.floor(Date.now() / 1000); + + if (user.tokens.accessTokenExpiresAt > now) { return user.tokens; } diff --git a/server/forges/types.ts b/server/forges/types.ts index 87f1ad5..03d5605 100644 --- a/server/forges/types.ts +++ b/server/forges/types.ts @@ -1,7 +1,7 @@ import { H3Event } from 'h3'; import { type User } from '~/server/schemas'; -export type Tokens = { accessToken: string; accessTokenExpiresIn: number; refreshToken: string | null }; +export type Tokens = { accessToken: string; accessTokenExpiresAt: number; refreshToken: string | null }; export type Credentials = { username: string; diff --git a/server/schemas/index.ts b/server/schemas/index.ts index ea99f73..c496231 100644 --- a/server/schemas/index.ts +++ b/server/schemas/index.ts @@ -28,7 +28,7 @@ export const userForgesSchema = sqliteTable( forgeId: integer('forgeId').notNull(), remoteUserId: text('remoteUserId').notNull(), accessToken: text('accessToken').notNull(), - accessTokenExpiresIn: integer('accessTokenExpiresIn').notNull(), + accessTokenExpiresAt: integer('accessTokenExpiresAt').notNull(), refreshToken: text('refreshToken'), }, (t) => ({ diff --git a/server/utils/auth.ts b/server/utils/auth.ts index 6c1faae..8f28cd2 100644 --- a/server/utils/auth.ts +++ b/server/utils/auth.ts @@ -57,7 +57,7 @@ export async function getUserForgeAPI(user: User, forgeId: number) { const tokens = { accessToken: userForge.accessToken, refreshToken: userForge.refreshToken, - accessTokenExpiresIn: userForge.accessTokenExpiresIn, + accessTokenExpiresAt: userForge.accessTokenExpiresAt, }; const forge = getForgeFromDB(forgeModel); From e07414ff10bb08187d824237f3a239bfda5caabe Mon Sep 17 00:00:00 2001 From: Anbraten <6918444+anbraten@users.noreply.github.com> Date: Wed, 12 Jun 2024 09:14:23 +0200 Subject: [PATCH 2/2] rm unused --- server/utils/auth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/utils/auth.ts b/server/utils/auth.ts index 8f28cd2..5b74f8a 100644 --- a/server/utils/auth.ts +++ b/server/utils/auth.ts @@ -1,4 +1,4 @@ -import type { H3Event, SessionConfig } from 'h3'; +import type { H3Event } from 'h3'; import { type User, forgeSchema, repoSchema, userForgesSchema, userReposSchema, userSchema } from '../schemas'; import { and, eq } from 'drizzle-orm'; import { getForgeFromDB, ForgeApi } from '~/server/forges';