- Add user management with roles and permissions (RBAC) - Implement OAuth2 service provider supporting 4 grant types: authorization_code, password, client_credentials, refresh_token - Add JWT authentication with 7-day expiry - Add admin API for users, roles and OAuth clients management - Add CLI tool for user management (scripts/user-cli.js) - Add collapsible sidebar layout with login dialog - Add user management page and OAuth client management page - Add server middleware for auth token verification - Add seed script for initial data (admin/admin123)
267 lines
8.3 KiB
TypeScript
267 lines
8.3 KiB
TypeScript
import db from '../../db'
|
|
import { v4 as uuidv4 } from 'uuid'
|
|
import bcrypt from 'bcryptjs'
|
|
import type { OAuthClient, OAuthToken, OAuthCode } from '../modules/auth/types'
|
|
|
|
const ACCESS_TOKEN_EXPIRY = 7 * 24 * 60 * 60 * 1000
|
|
const REFRESH_TOKEN_EXPIRY = 30 * 24 * 60 * 60 * 1000
|
|
const CODE_EXPIRY = 10 * 60 * 1000
|
|
|
|
export function getClients(): OAuthClient[] {
|
|
const stmt = db.prepare('SELECT * FROM oauth_clients ORDER BY created_at DESC')
|
|
const rows = stmt.all() as any[]
|
|
return rows.map(row => ({
|
|
...row,
|
|
redirect_uris: JSON.parse(row.redirect_uris || '[]'),
|
|
allowed_scopes: JSON.parse(row.allowed_scopes || '[]'),
|
|
grant_types: JSON.parse(row.grant_types || '[]')
|
|
}))
|
|
}
|
|
|
|
export function getClientById(clientId: string): OAuthClient | undefined {
|
|
const stmt = db.prepare('SELECT * FROM oauth_clients WHERE client_id = ?')
|
|
const row = stmt.get(clientId) as any
|
|
if (!row) return undefined
|
|
return {
|
|
...row,
|
|
redirect_uris: JSON.parse(row.redirect_uris || '[]'),
|
|
allowed_scopes: JSON.parse(row.allowed_scopes || '[]'),
|
|
grant_types: JSON.parse(row.grant_types || '[]')
|
|
}
|
|
}
|
|
|
|
export function createClient(
|
|
clientName: string,
|
|
redirectUris: string[],
|
|
allowedScopes: string[],
|
|
grantTypes: string[],
|
|
platform = 'web'
|
|
): { client: OAuthClient; clientSecret: string } {
|
|
const clientId = uuidv4().replace(/-/g, '')
|
|
const clientSecret = uuidv4()
|
|
const clientSecretHash = bcrypt.hashSync(clientSecret, 10)
|
|
|
|
const stmt = db.prepare(`
|
|
INSERT INTO oauth_clients
|
|
(client_id, client_secret_hash, client_name, redirect_uris, allowed_scopes, grant_types, platform)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
`)
|
|
stmt.run(
|
|
clientId,
|
|
clientSecretHash,
|
|
clientName,
|
|
JSON.stringify(redirectUris),
|
|
JSON.stringify(allowedScopes),
|
|
JSON.stringify(grantTypes),
|
|
platform
|
|
)
|
|
|
|
return {
|
|
client: {
|
|
id: 0,
|
|
client_id: clientId,
|
|
client_name: clientName,
|
|
redirect_uris: redirectUris,
|
|
allowed_scopes: allowedScopes,
|
|
grant_types: grantTypes,
|
|
platform,
|
|
is_active: 1
|
|
},
|
|
clientSecret
|
|
}
|
|
}
|
|
|
|
export function verifyClientSecret(client: OAuthClient, clientSecret: string): boolean {
|
|
return bcrypt.compareSync(clientSecret, client.client_secret_hash || '')
|
|
}
|
|
|
|
export function updateClient(clientId: string, data: Partial<OAuthClient>): OAuthClient | undefined {
|
|
const updates: string[] = []
|
|
const params: any[] = []
|
|
|
|
if (data.client_name !== undefined) {
|
|
updates.push('client_name = ?')
|
|
params.push(data.client_name)
|
|
}
|
|
if (data.redirect_uris !== undefined) {
|
|
updates.push('redirect_uris = ?')
|
|
params.push(JSON.stringify(data.redirect_uris))
|
|
}
|
|
if (data.allowed_scopes !== undefined) {
|
|
updates.push('allowed_scopes = ?')
|
|
params.push(JSON.stringify(data.allowed_scopes))
|
|
}
|
|
if (data.grant_types !== undefined) {
|
|
updates.push('grant_types = ?')
|
|
params.push(JSON.stringify(data.grant_types))
|
|
}
|
|
if (data.platform !== undefined) {
|
|
updates.push('platform = ?')
|
|
params.push(data.platform)
|
|
}
|
|
if (data.is_active !== undefined) {
|
|
updates.push('is_active = ?')
|
|
params.push(data.is_active)
|
|
}
|
|
|
|
if (updates.length === 0) return getClientById(clientId)
|
|
|
|
updates.push('updated_at = CURRENT_TIMESTAMP')
|
|
params.push(clientId)
|
|
|
|
const stmt = db.prepare(`UPDATE oauth_clients SET ${updates.join(', ')} WHERE client_id = ?`)
|
|
stmt.run(...params)
|
|
return getClientById(clientId)
|
|
}
|
|
|
|
export function deleteClient(clientId: string): boolean {
|
|
const stmt = db.prepare('DELETE FROM oauth_clients WHERE client_id = ?')
|
|
const result = stmt.run(clientId)
|
|
return result.changes > 0
|
|
}
|
|
|
|
export function createToken(
|
|
userId: number,
|
|
clientId: string,
|
|
scope?: string
|
|
): { accessToken: string; refreshToken: string; expiresAt: Date } {
|
|
const accessToken = uuidv4().replace(/-/g, '')
|
|
const refreshToken = uuidv4().replace(/-/g, '')
|
|
const expiresAt = new Date(Date.now() + ACCESS_TOKEN_EXPIRY)
|
|
|
|
const stmt = db.prepare(`
|
|
INSERT INTO oauth_tokens
|
|
(user_id, client_id, access_token, refresh_token, scope, expires_at)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
`)
|
|
stmt.run(userId, clientId, accessToken, refreshToken, scope || null, expiresAt.toISOString())
|
|
|
|
return { accessToken, refreshToken, expiresAt }
|
|
}
|
|
|
|
export function createTokenWithoutRefresh(
|
|
userId: number,
|
|
clientId: string,
|
|
scope?: string
|
|
): { accessToken: string; expiresAt: Date } {
|
|
const accessToken = uuidv4().replace(/-/g, '')
|
|
const expiresAt = new Date(Date.now() + ACCESS_TOKEN_EXPIRY)
|
|
|
|
const stmt = db.prepare(`
|
|
INSERT INTO oauth_tokens
|
|
(user_id, client_id, access_token, scope, expires_at)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
`)
|
|
stmt.run(userId, clientId, accessToken, scope || null, expiresAt.toISOString())
|
|
|
|
return { accessToken, expiresAt }
|
|
}
|
|
|
|
export function createClientCredentialsToken(
|
|
clientId: string,
|
|
scope?: string
|
|
): { accessToken: string; expiresAt: Date } {
|
|
const accessToken = uuidv4().replace(/-/g, '')
|
|
const expiresAt = new Date(Date.now() + ACCESS_TOKEN_EXPIRY)
|
|
|
|
const stmt = db.prepare(`
|
|
INSERT INTO oauth_tokens
|
|
(client_id, access_token, scope, expires_at)
|
|
VALUES (?, ?, ?, ?)
|
|
`)
|
|
stmt.run(clientId, accessToken, scope || null, expiresAt.toISOString())
|
|
|
|
return { accessToken, expiresAt }
|
|
}
|
|
|
|
export function getTokenByAccessToken(accessToken: string): OAuthToken | undefined {
|
|
const stmt = db.prepare('SELECT * FROM oauth_tokens WHERE access_token = ? AND revoked = 0')
|
|
const row = stmt.get(accessToken) as any
|
|
if (!row) return undefined
|
|
return {
|
|
...row,
|
|
refresh_token: row.refresh_token || undefined,
|
|
scope: row.scope || undefined
|
|
}
|
|
}
|
|
|
|
export function getTokenByRefreshToken(refreshToken: string): OAuthToken | undefined {
|
|
const stmt = db.prepare('SELECT * FROM oauth_tokens WHERE refresh_token = ? AND revoked = 0')
|
|
const row = stmt.get(refreshToken) as any
|
|
if (!row) return undefined
|
|
return {
|
|
...row,
|
|
scope: row.scope || undefined
|
|
}
|
|
}
|
|
|
|
export function refreshAccessToken(refreshToken: string): { accessToken: string; refreshToken: string; expiresAt: Date } | null {
|
|
const oldToken = getTokenByRefreshToken(refreshToken)
|
|
if (!oldToken) return null
|
|
|
|
revokeToken(refreshToken, 'refresh')
|
|
return createToken(oldToken.user_id!, oldToken.client_id, oldToken.scope || undefined)
|
|
}
|
|
|
|
export function revokeToken(token: string, type: 'access' | 'refresh' = 'access'): boolean {
|
|
const column = type === 'access' ? 'access_token' : 'refresh_token'
|
|
const stmt = db.prepare(`UPDATE oauth_tokens SET revoked = 1 WHERE ${column} = ?`)
|
|
const result = stmt.run(token)
|
|
return result.changes > 0
|
|
}
|
|
|
|
export function revokeAllUserTokens(userId: number): number {
|
|
const stmt = db.prepare('UPDATE oauth_tokens SET revoked = 1 WHERE user_id = ?')
|
|
const result = stmt.run(userId)
|
|
return result.changes
|
|
}
|
|
|
|
export function createAuthorizationCode(
|
|
userId: number,
|
|
clientId: string,
|
|
redirectUri: string,
|
|
scope?: string,
|
|
codeChallenge?: string,
|
|
codeChallengeMethod?: string
|
|
): string {
|
|
const code = uuidv4().replace(/-/g, '')
|
|
const expiresAt = new Date(Date.now() + CODE_EXPIRY)
|
|
|
|
const stmt = db.prepare(`
|
|
INSERT INTO oauth_codes
|
|
(code, user_id, client_id, redirect_uri, scope, code_challenge, code_challenge_method, expires_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
`)
|
|
stmt.run(code, userId, clientId, redirectUri, scope || null, codeChallenge || null, codeChallengeMethod || null, expiresAt.toISOString())
|
|
|
|
return code
|
|
}
|
|
|
|
export function getAuthorizationCode(code: string): OAuthCode | undefined {
|
|
const stmt = db.prepare('SELECT * FROM oauth_codes WHERE code = ? AND used = 0 AND expires_at > datetime("now")')
|
|
const row = stmt.get(code) as any
|
|
if (!row) return undefined
|
|
return {
|
|
...row,
|
|
scope: row.scope || undefined,
|
|
code_challenge: row.code_challenge || undefined,
|
|
code_challenge_method: row.code_challenge_method || undefined
|
|
}
|
|
}
|
|
|
|
export function useAuthorizationCode(code: string): boolean {
|
|
const stmt = db.prepare('UPDATE oauth_codes SET used = 1 WHERE code = ?')
|
|
const result = stmt.run(code)
|
|
return result.changes > 0
|
|
}
|
|
|
|
export function validateRedirectUri(client: OAuthClient, redirectUri: string): boolean {
|
|
return client.redirect_uris.includes(redirectUri)
|
|
}
|
|
|
|
export function validateScope(client: OAuthClient, scope: string): boolean {
|
|
if (!scope) return true
|
|
const requestedScopes = scope.split(' ')
|
|
return requestedScopes.every(s => client.allowed_scopes.includes(s))
|
|
}
|