feat: implement user authentication system

- Add SessionManager for managing user sessions (singleton pattern)
- Add BIPUsersDAO for database authentication
- Add LoginDialog component for username/password login
- Add UserSelectionDialog component for admin user selection
- Support silent login by computer name
- Implement main page with navigation to Extractor and Cleaner

Database:
- Table: dbo_BIPUsers
- Fields: UserName, Password, UserType, ComputerNmae

UI Flow:
1. Silent login on startup via computer name
2. Show login dialog if silent login fails
3. Display main page with user info and navigation
4. Support logout and re-login
This commit is contained in:
Misaka
2026-03-01 17:46:15 +08:00
parent 450eb41f96
commit 829851e3ca
12 changed files with 1671 additions and 112 deletions

View File

@@ -0,0 +1,223 @@
/**
* IPC handlers for User Authentication
*
* Provides APIs for the renderer process to:
* - Login with username and password
* - Silent login by computer name
* - Logout
* - Get current user info
* - Get all users (for admin user selection)
* - Switch user (admin only)
*/
import { ipcMain } from 'electron'
import { SessionManager } from '../services/user/session-manager'
import type { UserInfo } from '../types/user.types'
/**
* Login request
*/
export interface LoginRequest {
username: string
password: string
}
/**
* Login response
*/
export interface LoginResponse {
success: boolean
userInfo?: UserInfo
error?: string
}
/**
* Silent login response
*/
export interface SilentLoginResponse {
success: boolean
userInfo?: UserInfo
requiresUserSelection?: boolean // True if admin needs to select a user
error?: string
}
/**
* User selection response
*/
export interface UserSelectionResponse {
success: boolean
userInfo?: UserInfo
error?: string
}
/**
* Current user response
*/
export interface CurrentUserResponse {
isAuthenticated: boolean
userInfo?: UserInfo
}
/**
* Register IPC handlers for user authentication
*/
export function registerAuthHandlers(): void {
const sessionManager = SessionManager.getInstance()
/**
* Get computer name
*/
ipcMain.handle('auth:getComputerName', async (): Promise<string> => {
const os = await import('os')
return os.hostname()
})
/**
* Silent login by computer name
*/
ipcMain.handle(
'auth:silentLogin',
async (): Promise<SilentLoginResponse> => {
try {
const success = await sessionManager.loginByComputerName()
const userInfo = sessionManager.getUserInfo()
if (success && userInfo) {
// Check if admin needs user selection
const requiresUserSelection = userInfo.userType === 'Admin'
return {
success: true,
userInfo,
requiresUserSelection
}
}
return {
success: false,
requiresUserSelection: false
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
return {
success: false,
error: `无感登录失败:${message}`
}
}
}
)
/**
* Login with username and password
*/
ipcMain.handle(
'auth:login',
async (_event, request: LoginRequest): Promise<LoginResponse> => {
try {
const { username, password } = request
if (!username || !password) {
return {
success: false,
error: '请输入用户名和密码'
}
}
const success = await sessionManager.login(username, password)
const userInfo = sessionManager.getUserInfo()
if (success && userInfo) {
return {
success: true,
userInfo
}
}
return {
success: false,
error: '用户名或密码错误'
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
return {
success: false,
error: `登录失败:${message}`
}
}
}
)
/**
* Logout
*/
ipcMain.handle('auth:logout', async (): Promise<void> => {
sessionManager.logout()
})
/**
* Get current user
*/
ipcMain.handle(
'auth:getCurrentUser',
async (): Promise<CurrentUserResponse> => {
const isAuthenticated = sessionManager.isAuthenticated()
const userInfo = sessionManager.getUserInfo()
return {
isAuthenticated,
userInfo: userInfo ?? undefined
}
}
)
/**
* Get all users (for admin user selection)
*/
ipcMain.handle(
'auth:getAllUsers',
async (): Promise<UserInfo[]> => {
return await sessionManager.getAllUsers()
}
)
/**
* Switch user (admin only)
*/
ipcMain.handle(
'auth:switchUser',
async (_event, userInfo: UserInfo): Promise<UserSelectionResponse> => {
try {
const success = sessionManager.switchUser(userInfo)
if (success) {
const newUser = sessionManager.getUserInfo()
return {
success: true,
userInfo: newUser ?? undefined
}
}
return {
success: false,
error: '用户切换失败'
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
return {
success: false,
error: `用户切换失败:${message}`
}
}
}
)
/**
* Check if current user is admin
*/
ipcMain.handle(
'auth:isAdmin',
async (): Promise<boolean> => {
return sessionManager.isAdmin()
}
)
}

View File

@@ -8,6 +8,7 @@ import { registerExtractorHandlers } from './extractor-handler'
import { registerCleanerHandlers } from './cleaner-handler'
import { registerDatabaseHandlers } from './database-handler'
import { registerResolverHandlers } from './resolver-handler'
import { registerAuthHandlers } from './auth-handler'
/**
* Register all IPC handlers
@@ -18,4 +19,5 @@ export function registerIpcHandlers(): void {
registerCleanerHandlers()
registerDatabaseHandlers()
registerResolverHandlers()
registerAuthHandlers()
}

View File

@@ -0,0 +1,298 @@
/**
* BIPUsers DAO - Data access object for user authentication and management
*
* Mirrors the Python BIPUsersDAO functionality:
* - Authenticate users by username and password
* - Authenticate by computer name (silent login)
* - Get all users for admin user selection
* - Create, update, delete users
*/
import { MySqlService } from '../database/mysql'
import type { UserInfo } from '../../types/user.types'
/**
* Database configuration for BIPUsers table
*/
export const BIP_USERS_CONFIG = {
/** Table name in SQL Server: [dbo].[BIPUsers] */
TABLE_NAME_SQLSERVER: '[dbo].[BIPUsers]',
/** Table name in MySQL: dbo_BIPUsers */
TABLE_NAME_MYSQL: 'dbo_BIPUsers',
/** Column names */
COLUMNS: {
ID: 'ID',
USERNAME: 'UserName',
USER_TYPE: 'UserType',
PASSWORD: 'Password',
COMPUTER_NAME: 'ComputerNmae', // Note: typo in database schema
CREATE_TIME: 'CreateTime'
}
} as const
/**
* BIPUsers DAO Class
*/
export class BIPUsersDAO {
private mysqlService: MySqlService | null = null
/**
* Get MySQL service instance
*/
private async getMySqlService(): Promise<MySqlService> {
if (this.mysqlService && this.mysqlService.isConnected()) {
return this.mysqlService
}
this.mysqlService = new MySqlService({
host: process.env.DB_MYSQL_HOST || 'localhost',
port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10),
user: process.env.DB_USERNAME || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || ''
})
await this.mysqlService.connect()
return this.mysqlService
}
/**
* Authenticate a user with username and password
* @param username - The username to authenticate
* @param password - The password to verify
* @returns User info if authentication successful, null otherwise
*/
async authenticate(username: string, password: string): Promise<UserInfo | null> {
try {
const mysqlService = await this.getMySqlService()
const sql = `
SELECT ID, UserName, UserType
FROM ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL}
WHERE UserName = ? AND Password = ?
`
const result = await mysqlService.query(sql, [username, password])
if (result.rows.length > 0) {
const row = result.rows[0]
return {
id: row.ID as number,
username: row.UserName as string,
userType: row.UserType as 'Admin' | 'User' | 'Guest'
}
}
return null
} catch (error) {
console.error('[BIPUsersDAO] Authenticate error:', error)
return null
}
}
/**
* Authenticate a user using computer name (silent login)
* @param computerName - The computer name to authenticate
* @returns User info if authentication successful, null otherwise
*/
async authenticateByComputerName(computerName: string): Promise<UserInfo | null> {
try {
const mysqlService = await this.getMySqlService()
const sql = `
SELECT ID, UserName, UserType
FROM ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL}
WHERE ComputerNmae = ?
`
const result = await mysqlService.query(sql, [computerName])
if (result.rows.length > 0) {
const row = result.rows[0]
return {
id: row.ID as number,
username: row.UserName as string,
userType: row.UserType as 'Admin' | 'User' | 'Guest'
}
}
return null
} catch (error) {
console.error('[BIPUsersDAO] Authenticate by computer name error:', error)
return null
}
}
/**
* Get all users from the database
* @returns List of user information
*/
async getAllUsers(): Promise<UserInfo[]> {
try {
const mysqlService = await this.getMySqlService()
const sql = `
SELECT ID, UserName, UserType, CreateTime
FROM ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL}
ORDER BY UserName
`
const result = await mysqlService.query(sql)
return result.rows.map(row => ({
id: row.ID as number,
username: row.UserName as string,
userType: row.UserType as 'Admin' | 'User' | 'Guest',
createTime: row.CreateTime as Date | undefined
}))
} catch (error) {
console.error('[BIPUsersDAO] Get all users error:', error)
return []
}
}
/**
* Create a new user
* @param username - The username (must be unique)
* @param password - The password
* @param userType - User type ('Admin', 'User', or 'Guest')
* @param computerName - Optional computer name for silent login
* @returns True if successful
*/
async createUser(
username: string,
password: string,
userType: string,
computerName: string = ''
): Promise<boolean> {
try {
const mysqlService = await this.getMySqlService()
let sql: string
let params: any[]
if (computerName) {
sql = `
INSERT INTO ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL}
(UserName, Password, UserType, ComputerNmae)
VALUES (?, ?, ?, ?)
`
params = [username, password, userType, computerName]
} else {
sql = `
INSERT INTO ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL}
(UserName, Password, UserType)
VALUES (?, ?, ?)
`
params = [username, password, userType]
}
await mysqlService.query(sql, params)
return true
} catch (error) {
console.error('[BIPUsersDAO] Create user error:', error)
return false
}
}
/**
* Update a user's type
* @param username - The username to update
* @param userType - New user type
* @returns True if successful
*/
async updateUserType(username: string, userType: string): Promise<boolean> {
try {
const mysqlService = await this.getMySqlService()
const sql = `
UPDATE ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL}
SET UserType = ?
WHERE UserName = ?
`
await mysqlService.query(sql, [userType, username])
return true
} catch (error) {
console.error('[BIPUsersDAO] Update user type error:', error)
return false
}
}
/**
* Update a user's password
* @param username - The username to update
* @param newPassword - The new password
* @returns True if successful
*/
async updatePassword(username: string, newPassword: string): Promise<boolean> {
try {
const mysqlService = await this.getMySqlService()
const sql = `
UPDATE ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL}
SET Password = ?
WHERE UserName = ?
`
await mysqlService.query(sql, [newPassword, username])
return true
} catch (error) {
console.error('[BIPUsersDAO] Update password error:', error)
return false
}
}
/**
* Delete a user
* @param username - The username to delete
* @returns True if successful
*/
async deleteUser(username: string): Promise<boolean> {
try {
const mysqlService = await this.getMySqlService()
const sql = `
DELETE FROM ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL}
WHERE UserName = ?
`
await mysqlService.query(sql, [username])
return true
} catch (error) {
console.error('[BIPUsersDAO] Delete user error:', error)
return false
}
}
/**
* Check if a username already exists
* @param username - The username to check
* @returns True if username exists
*/
async userExists(username: string): Promise<boolean> {
try {
const mysqlService = await this.getMySqlService()
const sql = `
SELECT COUNT(*) as count
FROM ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL}
WHERE UserName = ?
`
const result = await mysqlService.query(sql, [username])
return result.rows.length > 0 && (result.rows[0].count as number) > 0
} catch (error) {
console.error('[BIPUsersDAO] User exists error:', error)
return false
}
}
/**
* Disconnect from database
*/
async disconnect(): Promise<void> {
if (this.mysqlService) {
await this.mysqlService.disconnect()
this.mysqlService = null
}
}
}

View File

@@ -0,0 +1,185 @@
/**
* User Session Manager - Singleton pattern for managing authenticated user session
*
* Mimics the Python SessionManager functionality:
* - Singleton pattern to maintain user state throughout application lifecycle
* - Support for username/password authentication
* - Support for silent login by computer name
* - Admin user can switch to other users
*/
import type { UserInfo } from '../../types/user.types'
/**
* Session Manager Class
*/
export class SessionManager {
private static instance: SessionManager | null = null
private currentUser: UserInfo | null = null
private originalAdminUser: UserInfo | null = null
private initialized: boolean = false
private constructor() {
if (this.initialized) {
return
}
this.initialized = true
}
/**
* Get the singleton instance
*/
public static getInstance(): SessionManager {
if (SessionManager.instance === null) {
SessionManager.instance = new SessionManager()
}
return SessionManager.instance
}
/**
* Authenticate and login a user
* @param username - The username to authenticate
* @param password - The password to verify
* @returns True if login successful, false otherwise
*/
public async login(username: string, password: string): Promise<boolean> {
try {
const { BIPUsersDAO } = await import('./bip-users-dao')
const dao = new BIPUsersDAO()
const userInfo = await dao.authenticate(username, password)
if (userInfo) {
this.currentUser = {
id: userInfo.id,
username: userInfo.username,
userType: userInfo.userType
}
return true
}
return false
} catch (error) {
console.error('[SessionManager] Login error:', error)
return false
}
}
/**
* Attempt silent login using computer name
* @returns True if login successful, false otherwise
*/
public async loginByComputerName(): Promise<boolean> {
try {
const { BIPUsersDAO } = await import('./bip-users-dao')
const dao = new BIPUsersDAO()
const computerName = require('os').hostname()
const userInfo = await dao.authenticateByComputerName(computerName)
if (userInfo) {
this.currentUser = {
id: userInfo.id,
username: userInfo.username,
userType: userInfo.userType
}
return true
}
return false
} catch (error) {
console.error('[SessionManager] Silent login error:', error)
return false
}
}
/**
* Logout the current user and clear session
*/
public logout(): void {
this.currentUser = null
this.originalAdminUser = null
}
/**
* Check if a user is currently authenticated
*/
public isAuthenticated(): boolean {
return this.currentUser !== null
}
/**
* Check if the current user is an admin
*/
public isAdmin(): boolean {
return this.currentUser?.userType === 'Admin'
}
/**
* Check if the current user is a guest
*/
public isGuest(): boolean {
return this.currentUser?.userType === 'Guest'
}
/**
* Get the current username
*/
public getUsername(): string | null {
return this.currentUser?.username ?? null
}
/**
* Get the current user type
*/
public getUserType(): string | null {
return this.currentUser?.userType ?? null
}
/**
* Get all current user information
*/
public getUserInfo(): UserInfo | null {
return this.currentUser
}
/**
* Switch to a different user (Admin only feature)
* @param userInfo - User info to switch to
* @returns True if switch successful
*/
public switchUser(userInfo: UserInfo): boolean {
if (!this.currentUser) {
return false
}
// Store original admin user for reference
if (!this.originalAdminUser) {
this.originalAdminUser = { ...this.currentUser }
}
this.currentUser = {
id: userInfo.id,
username: userInfo.username,
userType: userInfo.userType
}
return true
}
/**
* Get the original Admin user before any user switch
*/
public getOriginalAdmin(): UserInfo | null {
return this.originalAdminUser
}
/**
* Get all users from database (for Admin user selection)
*/
public async getAllUsers(): Promise<UserInfo[]> {
try {
const { BIPUsersDAO } = await import('./bip-users-dao')
const dao = new BIPUsersDAO()
return await dao.getAllUsers()
} catch (error) {
console.error('[SessionManager] Get all users error:', error)
return []
}
}
}

View File

@@ -0,0 +1,36 @@
/**
* User types and interfaces
*/
/**
* User type enumeration
*/
export type UserType = 'Admin' | 'User' | 'Guest'
/**
* User information interface
*/
export interface UserInfo {
/** User ID */
id: number
/** Username */
username: string
/** User type */
userType: UserType
/** Create time */
createTime?: Date
}
/**
* User session interface
*/
export interface UserSession {
/** Current authenticated user */
user: UserInfo | null
/** Session token (optional for future use) */
token?: string
/** Login timestamp */
loginTime: Date
/** Whether session is active */
isActive: boolean
}