store token_expires_at in DB

This commit is contained in:
CaptOrb 2025-08-28 18:05:40 +01:00
parent 6b7e6a5c01
commit 0f52823461
5 changed files with 222 additions and 167 deletions

View File

@ -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;

View File

@ -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");

View File

@ -7,7 +7,10 @@ export interface Forge {
exchangeCodeForToken(
code: string,
codeVerifier: string,
): Promise<{ accessToken: string }>;
): Promise<{
accessToken: string;
accessTokenExpiresAt?: Date;
}>;
getUserInfo(accessToken: string): Promise<ForgeUser>;

View File

@ -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<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;
}
/**
* 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<operationId extends keyof Operations> = Operations[operationId]["context"];
export type OperationResponse<operationId extends keyof Operations> = Operations[operationId]["response"];
export type HandlerResponse<ResponseBody, ResponseModel = Record<string, any>> = ResponseModel & { _t?: ResponseBody };
export type OperationHandlerResponse<operationId extends keyof Operations> = HandlerResponse<OperationResponse<operationId>>;
export type OperationHandler<operationId extends keyof Operations, HandlerArgs extends unknown[] = unknown[]> = (...params: [OperationContext<operationId>, ...HandlerArgs]) => Promise<OperationHandlerResponse<operationId>>;
export type OperationContext<operationId extends keyof Operations> =
Operations[operationId]["context"];
export type OperationResponse<operationId extends keyof Operations> =
Operations[operationId]["response"];
export type HandlerResponse<
ResponseBody,
ResponseModel = Record<string, any>,
> = ResponseModel & { _t?: ResponseBody };
export type OperationHandlerResponse<operationId extends keyof Operations> =
HandlerResponse<OperationResponse<operationId>>;
export type OperationHandler<
operationId extends keyof Operations,
HandlerArgs extends unknown[] = unknown[],
> = (
...params: [OperationContext<operationId>, ...HandlerArgs]
) => Promise<OperationHandlerResponse<operationId>>;
export type Error = Components.Schemas.Error;
export type Repository = Components.Schemas.Repository;

View File

@ -66,16 +66,18 @@ export const getOrCreateUser = async (
): Promise<User> => {
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 (