Administrator 8117958bd6 feat: add user center with RBAC, OAuth2 multi-mode and collapsible sidebar
- 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)
2026-03-19 17:19:57 +08:00

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))
}