feat: implement account settings and API tokens management
- Added SettingsComponent for user profile management, including display name and password change functionality. - Introduced TokensComponent for managing API tokens, including creation and revocation. - Created LayoutComponent for consistent application layout with navigation and user information. - Established main application structure in index.html and main.ts. - Integrated Tailwind CSS for styling and responsive design. - Configured TypeScript settings for strict type checking and module resolution.
This commit is contained in:
260
ts/api/handlers/user.api.ts
Normal file
260
ts/api/handlers/user.api.ts
Normal file
@@ -0,0 +1,260 @@
|
||||
/**
|
||||
* User API handlers
|
||||
*/
|
||||
|
||||
import type { IApiContext, IApiResponse } from '../router.ts';
|
||||
import { PermissionService } from '../../services/permission.service.ts';
|
||||
import { AuthService } from '../../services/auth.service.ts';
|
||||
import { User } from '../../models/user.ts';
|
||||
|
||||
export class UserApi {
|
||||
private permissionService: PermissionService;
|
||||
|
||||
constructor(permissionService: PermissionService) {
|
||||
this.permissionService = permissionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/users
|
||||
*/
|
||||
public async list(ctx: IApiContext): Promise<IApiResponse> {
|
||||
if (!ctx.actor?.userId) {
|
||||
return { status: 401, body: { error: 'Authentication required' } };
|
||||
}
|
||||
|
||||
// Only system admins can list all users
|
||||
if (!ctx.actor.user?.isSystemAdmin) {
|
||||
return { status: 403, body: { error: 'System admin access required' } };
|
||||
}
|
||||
|
||||
try {
|
||||
const users = await User.getInstances({});
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
users: users.map((u) => ({
|
||||
id: u.id,
|
||||
email: u.email,
|
||||
username: u.username,
|
||||
displayName: u.displayName,
|
||||
isSystemAdmin: u.isSystemAdmin,
|
||||
isActive: u.isActive,
|
||||
createdAt: u.createdAt,
|
||||
lastLoginAt: u.lastLoginAt,
|
||||
})),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[UserApi] List error:', error);
|
||||
return { status: 500, body: { error: 'Failed to list users' } };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/users/:id
|
||||
*/
|
||||
public async get(ctx: IApiContext): Promise<IApiResponse> {
|
||||
if (!ctx.actor?.userId) {
|
||||
return { status: 401, body: { error: 'Authentication required' } };
|
||||
}
|
||||
|
||||
const { id } = ctx.params;
|
||||
|
||||
// Users can view their own profile, admins can view any
|
||||
if (id !== ctx.actor.userId && !ctx.actor.user?.isSystemAdmin) {
|
||||
return { status: 403, body: { error: 'Access denied' } };
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await User.findById(id);
|
||||
if (!user) {
|
||||
return { status: 404, body: { error: 'User not found' } };
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
displayName: user.displayName,
|
||||
avatarUrl: user.avatarUrl,
|
||||
isSystemAdmin: user.isSystemAdmin,
|
||||
isActive: user.isActive,
|
||||
createdAt: user.createdAt,
|
||||
lastLoginAt: user.lastLoginAt,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[UserApi] Get error:', error);
|
||||
return { status: 500, body: { error: 'Failed to get user' } };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/users
|
||||
*/
|
||||
public async create(ctx: IApiContext): Promise<IApiResponse> {
|
||||
if (!ctx.actor?.userId) {
|
||||
return { status: 401, body: { error: 'Authentication required' } };
|
||||
}
|
||||
|
||||
// Only system admins can create users
|
||||
if (!ctx.actor.user?.isSystemAdmin) {
|
||||
return { status: 403, body: { error: 'System admin access required' } };
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await ctx.request.json();
|
||||
const { email, username, password, displayName, isSystemAdmin } = body;
|
||||
|
||||
if (!email || !username || !password) {
|
||||
return {
|
||||
status: 400,
|
||||
body: { error: 'Email, username, and password are required' },
|
||||
};
|
||||
}
|
||||
|
||||
// Check if email already exists
|
||||
const existing = await User.findByEmail(email);
|
||||
if (existing) {
|
||||
return { status: 409, body: { error: 'Email already in use' } };
|
||||
}
|
||||
|
||||
// Check if username already exists
|
||||
const existingUsername = await User.findByUsername(username);
|
||||
if (existingUsername) {
|
||||
return { status: 409, body: { error: 'Username already in use' } };
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const passwordHash = await AuthService.hashPassword(password);
|
||||
|
||||
// Create user
|
||||
const user = new User();
|
||||
user.id = await User.getNewId();
|
||||
user.email = email;
|
||||
user.username = username;
|
||||
user.passwordHash = passwordHash;
|
||||
user.displayName = displayName || username;
|
||||
user.isSystemAdmin = isSystemAdmin || false;
|
||||
user.isActive = true;
|
||||
user.createdAt = new Date();
|
||||
|
||||
await user.save();
|
||||
|
||||
return {
|
||||
status: 201,
|
||||
body: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
displayName: user.displayName,
|
||||
isSystemAdmin: user.isSystemAdmin,
|
||||
createdAt: user.createdAt,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[UserApi] Create error:', error);
|
||||
return { status: 500, body: { error: 'Failed to create user' } };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /api/v1/users/:id
|
||||
*/
|
||||
public async update(ctx: IApiContext): Promise<IApiResponse> {
|
||||
if (!ctx.actor?.userId) {
|
||||
return { status: 401, body: { error: 'Authentication required' } };
|
||||
}
|
||||
|
||||
const { id } = ctx.params;
|
||||
|
||||
// Users can update their own profile, admins can update any
|
||||
if (id !== ctx.actor.userId && !ctx.actor.user?.isSystemAdmin) {
|
||||
return { status: 403, body: { error: 'Access denied' } };
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await User.findById(id);
|
||||
if (!user) {
|
||||
return { status: 404, body: { error: 'User not found' } };
|
||||
}
|
||||
|
||||
const body = await ctx.request.json();
|
||||
const { displayName, avatarUrl, password, isActive, isSystemAdmin } = body;
|
||||
|
||||
if (displayName !== undefined) user.displayName = displayName;
|
||||
if (avatarUrl !== undefined) user.avatarUrl = avatarUrl;
|
||||
|
||||
// Only admins can change these
|
||||
if (ctx.actor.user?.isSystemAdmin) {
|
||||
if (isActive !== undefined) user.isActive = isActive;
|
||||
if (isSystemAdmin !== undefined) user.isSystemAdmin = isSystemAdmin;
|
||||
}
|
||||
|
||||
// Password change
|
||||
if (password) {
|
||||
user.passwordHash = await AuthService.hashPassword(password);
|
||||
}
|
||||
|
||||
await user.save();
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
displayName: user.displayName,
|
||||
avatarUrl: user.avatarUrl,
|
||||
isSystemAdmin: user.isSystemAdmin,
|
||||
isActive: user.isActive,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[UserApi] Update error:', error);
|
||||
return { status: 500, body: { error: 'Failed to update user' } };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/v1/users/:id
|
||||
*/
|
||||
public async delete(ctx: IApiContext): Promise<IApiResponse> {
|
||||
if (!ctx.actor?.userId) {
|
||||
return { status: 401, body: { error: 'Authentication required' } };
|
||||
}
|
||||
|
||||
// Only system admins can delete users
|
||||
if (!ctx.actor.user?.isSystemAdmin) {
|
||||
return { status: 403, body: { error: 'System admin access required' } };
|
||||
}
|
||||
|
||||
const { id } = ctx.params;
|
||||
|
||||
// Cannot delete yourself
|
||||
if (id === ctx.actor.userId) {
|
||||
return { status: 400, body: { error: 'Cannot delete your own account' } };
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await User.findById(id);
|
||||
if (!user) {
|
||||
return { status: 404, body: { error: 'User not found' } };
|
||||
}
|
||||
|
||||
// Soft delete - deactivate instead of removing
|
||||
user.isActive = false;
|
||||
await user.save();
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: { message: 'User deactivated successfully' },
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[UserApi] Delete error:', error);
|
||||
return { status: 500, body: { error: 'Failed to delete user' } };
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user