From 0f52823461085fe104f6828befb944a19f99b7a0 Mon Sep 17 00:00:00 2001 From: CaptOrb Date: Thu, 28 Aug 2025 18:05:40 +0100 Subject: [PATCH] store token_expires_at in DB --- server/src/routes/auth.ts | 7 +- server/src/services/forges/gitea.ts | 8 +- server/src/types/forge.ts | 5 +- server/src/types/openapi.d.ts | 347 ++++++++++++++++------------ server/src/types/user.ts | 22 +- 5 files changed, 222 insertions(+), 167 deletions(-) diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 986f775..dd36016 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -47,10 +47,8 @@ authRouter.get("/callback", async (req: Request, res: Response) => { try { const forge = createForge(forgeType); - const { accessToken } = await forge.exchangeCodeForToken( - code, - codeVerifier, - ); + const { accessToken, accessTokenExpiresAt } = + await forge.exchangeCodeForToken(code, codeVerifier); const forgeUser = await forge.getUserInfo(accessToken); @@ -58,6 +56,7 @@ authRouter.get("/callback", async (req: Request, res: Response) => { forgeType, forgeUser.id.toString(), accessToken, + accessTokenExpiresAt, ); req.session.userId = internalUser.user_id; diff --git a/server/src/services/forges/gitea.ts b/server/src/services/forges/gitea.ts index 48b4d30..f487303 100644 --- a/server/src/services/forges/gitea.ts +++ b/server/src/services/forges/gitea.ts @@ -33,13 +33,17 @@ export class GiteaForge implements Forge { async exchangeCodeForToken( code: string, codeVerifier: string, - ): Promise<{ accessToken: string }> { + ): Promise<{ accessToken: string; accessTokenExpiresAt?: Date }> { try { const tokens = await this.gitea.validateAuthorizationCode( code, codeVerifier, ); - return { accessToken: tokens.accessToken() }; + + return { + accessToken: tokens.accessToken(), + accessTokenExpiresAt: tokens.accessTokenExpiresAt(), + }; } catch (error) { console.error("Failed to exchange code for token:", error); throw new Error("Failed to exchange authorisation code for token"); diff --git a/server/src/types/forge.ts b/server/src/types/forge.ts index f0e9795..c4a827a 100644 --- a/server/src/types/forge.ts +++ b/server/src/types/forge.ts @@ -7,7 +7,10 @@ export interface Forge { exchangeCodeForToken( code: string, codeVerifier: string, - ): Promise<{ accessToken: string }>; + ): Promise<{ + accessToken: string; + accessTokenExpiresAt?: Date; + }>; getUserInfo(accessToken: string): Promise; diff --git a/server/src/types/openapi.d.ts b/server/src/types/openapi.d.ts index ce57708..a364d7d 100644 --- a/server/src/types/openapi.d.ts +++ b/server/src/types/openapi.d.ts @@ -1,162 +1,209 @@ -import type { - Context, - UnknownParams, -} from 'openapi-backend'; +import type { Context, UnknownParams } from "openapi-backend"; declare namespace Components { - namespace Schemas { - export interface Error { - /** - * Error message - * example: - * Repository not found - */ - error: string; - /** - * Error code for programmatic handling - * example: - * REPO_NOT_FOUND - */ - code: string; - /** - * Additional error details - */ - details?: { - [name: string]: any; - }; - } - export interface Repository { - id?: number; - /** - * example: - * molci - */ - name?: string; - /** - * example: - * gitea - */ - forge?: "gitea"; - } - export interface RepositoryConfig { - repo?: Repository; - configured_at?: string; // date-time - webhook_id?: string; - } - export interface RepositoryConfigInput { - settings: { - [name: string]: any; - }; - } - } + namespace Schemas { + export interface Error { + /** + * Error message + * example: + * Repository not found + */ + error: string; + /** + * Error code for programmatic handling + * example: + * REPO_NOT_FOUND + */ + code: string; + /** + * Additional error details + */ + details?: { + [name: string]: any; + }; + } + export interface Repository { + id?: number; + /** + * example: + * molci + */ + name?: string; + /** + * example: + * gitea + */ + forge?: "gitea"; + } + export interface RepositoryConfig { + repo?: Repository; + configured_at?: string; // date-time + webhook_id?: string; + } + export interface RepositoryConfigInput { + settings: { + [name: string]: any; + }; + } + } } declare namespace Paths { - namespace ConfigureRepo { - namespace Parameters { - export type Id = number; - } - export interface PathParameters { - id: Parameters.Id; - } - export type RequestBody = Components.Schemas.RepositoryConfigInput; - namespace Responses { - export type $200 = Components.Schemas.RepositoryConfig; - export type $400 = Components.Schemas.Error; - export type $401 = Components.Schemas.Error; - export type $404 = Components.Schemas.Error; - export type $500 = Components.Schemas.Error; - } - } - namespace GetRepo { - namespace Parameters { - export type Id = string; - } - export interface PathParameters { - id: Parameters.Id; - } - namespace Responses { - export type $200 = Components.Schemas.RepositoryConfig; - export type $400 = Components.Schemas.Error; - export type $401 = Components.Schemas.Error; - export type $404 = Components.Schemas.Error; - export type $500 = Components.Schemas.Error; - } - } - namespace ListAvailableRepos { - namespace Responses { - export type $200 = Components.Schemas.Repository[]; - export type $401 = Components.Schemas.Error; - export type $403 = Components.Schemas.Error; - export type $500 = Components.Schemas.Error; - } - } - namespace ListConfiguredRepos { - namespace Responses { - export type $200 = Components.Schemas.RepositoryConfig[]; - export type $401 = Components.Schemas.Error; - export type $500 = Components.Schemas.Error; - } - } + namespace ConfigureRepo { + namespace Parameters { + export type Id = number; + } + export interface PathParameters { + id: Parameters.Id; + } + export type RequestBody = Components.Schemas.RepositoryConfigInput; + namespace Responses { + export type $200 = Components.Schemas.RepositoryConfig; + export type $400 = Components.Schemas.Error; + export type $401 = Components.Schemas.Error; + export type $404 = Components.Schemas.Error; + export type $500 = Components.Schemas.Error; + } + } + namespace GetRepo { + namespace Parameters { + export type Id = string; + } + export interface PathParameters { + id: Parameters.Id; + } + namespace Responses { + export type $200 = Components.Schemas.RepositoryConfig; + export type $400 = Components.Schemas.Error; + export type $401 = Components.Schemas.Error; + export type $404 = Components.Schemas.Error; + export type $500 = Components.Schemas.Error; + } + } + namespace ListAvailableRepos { + namespace Responses { + export type $200 = Components.Schemas.Repository[]; + export type $401 = Components.Schemas.Error; + export type $403 = Components.Schemas.Error; + export type $500 = Components.Schemas.Error; + } + } + namespace ListConfiguredRepos { + namespace Responses { + export type $200 = Components.Schemas.RepositoryConfig[]; + export type $401 = Components.Schemas.Error; + export type $500 = Components.Schemas.Error; + } + } } - export interface Operations { - /** - * GET /repos/available - */ - ['listAvailableRepos']: { - requestBody: any; - params: UnknownParams; - query: UnknownParams; - headers: UnknownParams; - cookies: UnknownParams; - context: Context; - response: Paths.ListAvailableRepos.Responses.$200 | Paths.ListAvailableRepos.Responses.$401 | Paths.ListAvailableRepos.Responses.$403 | Paths.ListAvailableRepos.Responses.$500; - } - /** - * GET /repos/configured - */ - ['listConfiguredRepos']: { - requestBody: any; - params: UnknownParams; - query: UnknownParams; - headers: UnknownParams; - cookies: UnknownParams; - context: Context; - response: Paths.ListConfiguredRepos.Responses.$200 | Paths.ListConfiguredRepos.Responses.$401 | Paths.ListConfiguredRepos.Responses.$500; - } - /** - * GET /repo/{id} - */ - ['getRepo']: { - requestBody: any; - params: Paths.GetRepo.PathParameters; - query: UnknownParams; - headers: UnknownParams; - cookies: UnknownParams; - context: Context; - response: Paths.GetRepo.Responses.$200 | Paths.GetRepo.Responses.$400 | Paths.GetRepo.Responses.$401 | Paths.GetRepo.Responses.$404 | Paths.GetRepo.Responses.$500; - } - /** - * PUT /repo/{id} - */ - ['configureRepo']: { - requestBody: Paths.ConfigureRepo.RequestBody; - params: Paths.ConfigureRepo.PathParameters; - query: UnknownParams; - headers: UnknownParams; - cookies: UnknownParams; - context: Context; - response: Paths.ConfigureRepo.Responses.$200 | Paths.ConfigureRepo.Responses.$400 | Paths.ConfigureRepo.Responses.$401 | Paths.ConfigureRepo.Responses.$404 | Paths.ConfigureRepo.Responses.$500; - } + /** + * GET /repos/available + */ + ["listAvailableRepos"]: { + requestBody: any; + params: UnknownParams; + query: UnknownParams; + headers: UnknownParams; + cookies: UnknownParams; + context: Context< + any, + UnknownParams, + UnknownParams, + UnknownParams, + UnknownParams + >; + response: + | Paths.ListAvailableRepos.Responses.$200 + | Paths.ListAvailableRepos.Responses.$401 + | Paths.ListAvailableRepos.Responses.$403 + | Paths.ListAvailableRepos.Responses.$500; + }; + /** + * GET /repos/configured + */ + ["listConfiguredRepos"]: { + requestBody: any; + params: UnknownParams; + query: UnknownParams; + headers: UnknownParams; + cookies: UnknownParams; + context: Context< + any, + UnknownParams, + UnknownParams, + UnknownParams, + UnknownParams + >; + response: + | Paths.ListConfiguredRepos.Responses.$200 + | Paths.ListConfiguredRepos.Responses.$401 + | Paths.ListConfiguredRepos.Responses.$500; + }; + /** + * GET /repo/{id} + */ + ["getRepo"]: { + requestBody: any; + params: Paths.GetRepo.PathParameters; + query: UnknownParams; + headers: UnknownParams; + cookies: UnknownParams; + context: Context< + any, + Paths.GetRepo.PathParameters, + UnknownParams, + UnknownParams, + UnknownParams + >; + response: + | Paths.GetRepo.Responses.$200 + | Paths.GetRepo.Responses.$400 + | Paths.GetRepo.Responses.$401 + | Paths.GetRepo.Responses.$404 + | Paths.GetRepo.Responses.$500; + }; + /** + * PUT /repo/{id} + */ + ["configureRepo"]: { + requestBody: Paths.ConfigureRepo.RequestBody; + params: Paths.ConfigureRepo.PathParameters; + query: UnknownParams; + headers: UnknownParams; + cookies: UnknownParams; + context: Context< + Paths.ConfigureRepo.RequestBody, + Paths.ConfigureRepo.PathParameters, + UnknownParams, + UnknownParams, + UnknownParams + >; + response: + | Paths.ConfigureRepo.Responses.$200 + | Paths.ConfigureRepo.Responses.$400 + | Paths.ConfigureRepo.Responses.$401 + | Paths.ConfigureRepo.Responses.$404 + | Paths.ConfigureRepo.Responses.$500; + }; } -export type OperationContext = Operations[operationId]["context"]; -export type OperationResponse = Operations[operationId]["response"]; -export type HandlerResponse> = ResponseModel & { _t?: ResponseBody }; -export type OperationHandlerResponse = HandlerResponse>; -export type OperationHandler = (...params: [OperationContext, ...HandlerArgs]) => Promise>; - +export type OperationContext = + Operations[operationId]["context"]; +export type OperationResponse = + Operations[operationId]["response"]; +export type HandlerResponse< + ResponseBody, + ResponseModel = Record, +> = ResponseModel & { _t?: ResponseBody }; +export type OperationHandlerResponse = + HandlerResponse>; +export type OperationHandler< + operationId extends keyof Operations, + HandlerArgs extends unknown[] = unknown[], +> = ( + ...params: [OperationContext, ...HandlerArgs] +) => Promise>; export type Error = Components.Schemas.Error; export type Repository = Components.Schemas.Repository; diff --git a/server/src/types/user.ts b/server/src/types/user.ts index 0566924..fa5f0f2 100644 --- a/server/src/types/user.ts +++ b/server/src/types/user.ts @@ -66,16 +66,18 @@ export const getOrCreateUser = async ( ): Promise => { const forgeId = resolveForgeId(forgeType); - let user = await findUserByForge(forgeId, forgeUserId); - if (!user) { - user = await insertUser( - forgeId, - forgeUserId, - access_token, - token_expires_at, - ); - } - return user; + const result = await pool.query( + `INSERT INTO users (forge_id, forge_user_id, access_token, token_expires_at) + VALUES ($1, $2, $3, $4) + ON CONFLICT (forge_id, forge_user_id) + DO UPDATE SET + access_token = EXCLUDED.access_token, + token_expires_at = EXCLUDED.token_expires_at + RETURNING *`, + [forgeId, forgeUserId, access_token, token_expires_at], + ); + + return result.rows[0]; }; export const getAccessToken = async (