OIDC + SSO

This commit is contained in:
KingKDot
2026-07-08 20:34:25 -05:00
parent 97e9bf8f3d
commit faac5b6029
20 changed files with 1885 additions and 11 deletions
+1
View File
@@ -5,6 +5,7 @@ import { db, dbPath } from "./connection";
import "./schema";
export * from "./repositories/chat";
export * from "./repositories/oidc";
export * from "./repositories/revoked-tokens";
export * from "./repositories/sessions";
export * from "./repositories/shared-files";
+106
View File
@@ -0,0 +1,106 @@
import { db } from "../connection";
import "../schema";
export type OidcAuthState = {
state: string;
nonce: string;
codeVerifier: string;
returnTo: string | null;
createdAt: number;
expiresAt: number;
};
export type OidcIdentityRecord = {
issuer: string;
subject: string;
userId: number;
email: string | null;
username: string | null;
createdAt: number;
lastLogin: number;
};
export function saveOidcAuthState(record: OidcAuthState): void {
db.run(
`INSERT INTO oidc_auth_states (state, nonce, code_verifier, return_to, created_at, expires_at)
VALUES (?, ?, ?, ?, ?, ?)`,
record.state,
record.nonce,
record.codeVerifier,
record.returnTo,
record.createdAt,
record.expiresAt,
);
}
export function takeOidcAuthState(state: string): OidcAuthState | null {
const row = db.query<any>(`SELECT * FROM oidc_auth_states WHERE state=?`).get(state);
if (!row) return null;
db.run(`DELETE FROM oidc_auth_states WHERE state=?`, state);
return {
state: row.state,
nonce: row.nonce,
codeVerifier: row.code_verifier,
returnTo: row.return_to,
createdAt: row.created_at,
expiresAt: row.expires_at,
};
}
export function pruneExpiredOidcAuthStates(now = Date.now()): number {
const result = db.run(`DELETE FROM oidc_auth_states WHERE expires_at <= ?`, now);
return result.changes;
}
export function getOidcIdentity(issuer: string, subject: string): OidcIdentityRecord | null {
const row = db.query<any>(
`SELECT * FROM oidc_identities WHERE issuer=? AND subject=?`,
).get(issuer, subject);
return row ? mapIdentity(row) : null;
}
export function getOidcIdentityByEmail(issuer: string, email: string): OidcIdentityRecord | null {
const row = db.query<any>(
`SELECT * FROM oidc_identities WHERE issuer=? AND LOWER(email)=LOWER(?) LIMIT 1`,
).get(issuer, email);
return row ? mapIdentity(row) : null;
}
export function upsertOidcIdentity(input: {
issuer: string;
subject: string;
userId: number;
email?: string | null;
username?: string | null;
now?: number;
}): void {
const now = input.now ?? Date.now();
db.run(
`INSERT INTO oidc_identities (issuer, subject, user_id, email, username, created_at, last_login)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(issuer, subject) DO UPDATE SET
user_id=excluded.user_id,
email=excluded.email,
username=excluded.username,
last_login=excluded.last_login`,
input.issuer,
input.subject,
input.userId,
input.email || null,
input.username || null,
now,
now,
);
}
function mapIdentity(row: any): OidcIdentityRecord {
return {
issuer: row.issuer,
subject: row.subject,
userId: row.user_id,
email: row.email || null,
username: row.username || null,
createdAt: row.created_at,
lastLogin: row.last_login,
};
}
+28
View File
@@ -302,6 +302,34 @@ db.run(`CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);`);
db.run(`CREATE INDEX IF NOT EXISTS idx_sessions_token_hash ON sessions(token_hash);`);
db.run(`CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);`);
db.run(`
CREATE TABLE IF NOT EXISTS oidc_auth_states (
state TEXT PRIMARY KEY,
nonce TEXT NOT NULL,
code_verifier TEXT NOT NULL,
return_to TEXT,
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL
);
`);
db.run(`CREATE INDEX IF NOT EXISTS idx_oidc_auth_states_expires_at ON oidc_auth_states(expires_at);`);
db.run(`
CREATE TABLE IF NOT EXISTS oidc_identities (
issuer TEXT NOT NULL,
subject TEXT NOT NULL,
user_id INTEGER NOT NULL,
email TEXT,
username TEXT,
created_at INTEGER NOT NULL,
last_login INTEGER NOT NULL,
PRIMARY KEY (issuer, subject),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
`);
db.run(`CREATE INDEX IF NOT EXISTS idx_oidc_identities_user_id ON oidc_identities(user_id);`);
db.run(`CREATE INDEX IF NOT EXISTS idx_oidc_identities_email ON oidc_identities(email);`);
db.run(`
CREATE TABLE IF NOT EXISTS builds (
id TEXT PRIMARY KEY,