feat(Users): add api_key support for user creation and updates
This commit is contained in:
parent
801527613c
commit
aa408841a5
8 changed files with 91 additions and 20 deletions
|
|
@ -99,7 +99,7 @@ export const api = {
|
|||
users: {
|
||||
getAll: (): Promise<User[]> => fetchJson<User[]>(`${API_BASE}/admin/users`),
|
||||
getById: (id: number): Promise<User> => fetchJson<User>(`${API_BASE}/admin/users/${id}`),
|
||||
create: (data: { name: string; email?: string; detail_logging?: boolean }): Promise<User> =>
|
||||
create: (data: { name: string; email?: string; api_key?: string; detail_logging?: boolean }): Promise<User> =>
|
||||
fetchJson<User>(`${API_BASE}/admin/users`, { method: 'POST', body: JSON.stringify(data) }),
|
||||
update: (id: number, data: Partial<User>): Promise<User> =>
|
||||
fetchJson<User>(`${API_BASE}/admin/users/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ type NoticeTone = 'success' | 'warning' | 'danger' | 'info';
|
|||
interface UserFormState {
|
||||
name: string;
|
||||
email: string;
|
||||
api_key: string;
|
||||
is_active: boolean;
|
||||
detail_logging: boolean;
|
||||
}
|
||||
|
|
@ -42,6 +43,7 @@ interface UserFormState {
|
|||
const emptyForm = (): UserFormState => ({
|
||||
name: '',
|
||||
email: '',
|
||||
api_key: '',
|
||||
is_active: true,
|
||||
detail_logging: false,
|
||||
});
|
||||
|
|
@ -124,6 +126,7 @@ export const Users: Component = () => {
|
|||
setForm({
|
||||
name: user.name,
|
||||
email: user.email ?? '',
|
||||
api_key: user.api_key,
|
||||
is_active: user.is_active,
|
||||
detail_logging: user.detail_logging,
|
||||
});
|
||||
|
|
@ -145,6 +148,7 @@ export const Users: Component = () => {
|
|||
await api.users.update(editingUser()!.id, {
|
||||
name: current.name.trim(),
|
||||
email: current.email.trim() || undefined,
|
||||
api_key: current.api_key.trim() || undefined,
|
||||
is_active: current.is_active,
|
||||
detail_logging: current.detail_logging,
|
||||
});
|
||||
|
|
@ -153,6 +157,7 @@ export const Users: Component = () => {
|
|||
await api.users.create({
|
||||
name: current.name.trim(),
|
||||
email: current.email.trim() || undefined,
|
||||
api_key: current.api_key.trim() || undefined,
|
||||
detail_logging: current.detail_logging,
|
||||
});
|
||||
setNotice({ tone: 'success', message: 'User created.' });
|
||||
|
|
@ -496,6 +501,17 @@ export const Users: Component = () => {
|
|||
placeholder="ops@example.com"
|
||||
onInput={(event) => setForm((current) => ({ ...current, email: event.currentTarget.value }))}
|
||||
/>
|
||||
<TextField
|
||||
label="API Key"
|
||||
value={form().api_key}
|
||||
placeholder="Leave blank to auto-generate"
|
||||
description={
|
||||
editingUser()
|
||||
? 'Set a replacement key for migrations or leave blank to keep the current key.'
|
||||
: 'Optional. Paste a legacy key to preserve it during migration, or leave blank to auto-generate.'
|
||||
}
|
||||
onInput={(event) => setForm((current) => ({ ...current, api_key: event.currentTarget.value }))}
|
||||
/>
|
||||
<Show when={editingUser()}>
|
||||
<Checkbox
|
||||
label="User is active"
|
||||
|
|
|
|||
|
|
@ -52,9 +52,9 @@
|
|||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/admin/users` | 전체 사용자 목록 |
|
||||
| POST | `/admin/users` | 사용자 생성 (API 키 자동 발급) |
|
||||
| POST | `/admin/users` | 사용자 생성 (`api_key` 생략 시 자동 발급, 지정 시 수동 등록) |
|
||||
| GET | `/admin/users/:id` | 사용자 조회 |
|
||||
| PUT | `/admin/users/:id` | 사용자 수정 (name, email, is_active, detail_logging) |
|
||||
| PUT | `/admin/users/:id` | 사용자 수정 (name, email, api_key, is_active, detail_logging) |
|
||||
| DELETE | `/admin/users/:id` | 사용자 삭제 |
|
||||
| POST | `/admin/users/:id/regenerate-api-key` | API 키 재발급 |
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ client/src/
|
|||
index.ts # TypeScript 타입 정의
|
||||
routes/
|
||||
Dashboard.tsx # 운영 요약, 최근 요청, 관리자 토큰 관리
|
||||
Users.tsx # 사용자 CRUD / 권한 매핑 관리
|
||||
Users.tsx # 사용자 CRUD / API 키 수동 지정 / 권한 매핑 관리
|
||||
Backends.tsx # 백엔드 CRUD
|
||||
Models.tsx # 모델 캐시/리라이트 규칙 관리
|
||||
Analytics.tsx # 분석 화면
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export class UserModel {
|
|||
}
|
||||
|
||||
static create(data: CreateUserData): User {
|
||||
const apiKey = `sk-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
||||
const apiKey = data.api_key ?? generateApiKey();
|
||||
const timestamp = getUtcTimestamp();
|
||||
const detailLogging = data.detail_logging ?? false;
|
||||
const stmt = getDb().prepare(
|
||||
|
|
@ -60,6 +60,10 @@ export class UserModel {
|
|||
updates.push('email = ?');
|
||||
values.push(data.email);
|
||||
}
|
||||
if (data.api_key !== undefined) {
|
||||
updates.push('api_key = ?');
|
||||
values.push(data.api_key);
|
||||
}
|
||||
if (data.is_active !== undefined) {
|
||||
updates.push('is_active = ?');
|
||||
values.push(data.is_active ? 1 : 0);
|
||||
|
|
|
|||
|
|
@ -34,25 +34,29 @@ router.get('/users', (req: Request, res: Response) => {
|
|||
});
|
||||
|
||||
router.post('/users', (req: Request, res: Response) => {
|
||||
const { name, email, detail_logging } = req.body as CreateUserData;
|
||||
const { name, email, api_key, detail_logging } = req.body as CreateUserData;
|
||||
|
||||
if (!name) {
|
||||
if (!name?.trim()) {
|
||||
res.status(400).json({ error: 'Name is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const user = UserModel.create({
|
||||
name,
|
||||
email,
|
||||
detail_logging,
|
||||
});
|
||||
try {
|
||||
const user = UserModel.create({
|
||||
name: name.trim(),
|
||||
email: email?.trim() || undefined,
|
||||
api_key: api_key?.trim() || undefined,
|
||||
detail_logging,
|
||||
});
|
||||
|
||||
const updatedUser = UserModel.regenerateApiKey(user.id);
|
||||
if (updatedUser) {
|
||||
user.api_key = updatedUser;
|
||||
res.status(201).json(user);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes('UNIQUE')) {
|
||||
res.status(409).json({ error: 'API key already exists' });
|
||||
return;
|
||||
}
|
||||
res.status(500).json({ error: 'Failed to create user' });
|
||||
}
|
||||
|
||||
res.status(201).json(user);
|
||||
});
|
||||
|
||||
router.get('/users/:id', (req: Request, res: Response) => {
|
||||
|
|
@ -76,10 +80,30 @@ router.put('/users/:id', (req: Request, res: Response) => {
|
|||
return;
|
||||
}
|
||||
|
||||
const { name, email, is_active, detail_logging } = req.body as UpdateUserData;
|
||||
const updatedUser = UserModel.update(id, { name, email, is_active, detail_logging });
|
||||
const { name, email, api_key, is_active, detail_logging } = req.body as UpdateUserData;
|
||||
|
||||
res.json(updatedUser);
|
||||
if (typeof name === 'string' && !name.trim()) {
|
||||
res.status(400).json({ error: 'Name cannot be empty' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedUser = UserModel.update(id, {
|
||||
name: typeof name === 'string' ? name.trim() : undefined,
|
||||
email: typeof email === 'string' ? email.trim() || undefined : undefined,
|
||||
api_key: typeof api_key === 'string' ? api_key.trim() || undefined : undefined,
|
||||
is_active,
|
||||
detail_logging,
|
||||
});
|
||||
|
||||
res.json(updatedUser);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes('UNIQUE')) {
|
||||
res.status(409).json({ error: 'API key already exists' });
|
||||
return;
|
||||
}
|
||||
res.status(500).json({ error: 'Failed to update user' });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/users/:id', (req: Request, res: Response) => {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,22 @@ describe('Admin API - User Management', () => {
|
|||
expect(response.body.api_key).toMatch(/^sk-/);
|
||||
});
|
||||
|
||||
it('should create a user with a manually supplied api key', async () => {
|
||||
const userData = { name: 'Migrated User', api_key: 'legacy-user-key-001' };
|
||||
const response = await admin.post('/admin/users').send(userData);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body.api_key).toBe(userData.api_key);
|
||||
});
|
||||
|
||||
it('should return 409 if a manually supplied api key already exists', async () => {
|
||||
await admin.post('/admin/users').send({ name: 'Duplicate Key Source', api_key: 'legacy-duplicate-key' });
|
||||
const response = await admin.post('/admin/users').send({ name: 'Duplicate Key Target', api_key: 'legacy-duplicate-key' });
|
||||
|
||||
expect(response.status).toBe(409);
|
||||
expect(response.body.error).toBe('API key already exists');
|
||||
});
|
||||
|
||||
it('should return 400 if name is missing', async () => {
|
||||
const response = await admin.post('/admin/users').send({ email: 'test@example.com' });
|
||||
|
||||
|
|
@ -86,6 +102,15 @@ describe('Admin API - User Management', () => {
|
|||
expect(response.body.email).toBe('updated@example.com');
|
||||
});
|
||||
|
||||
it('should update user api key manually', async () => {
|
||||
const response = await admin
|
||||
.put(`/admin/users/${userId}`)
|
||||
.send({ api_key: 'legacy-updated-key-001' });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.api_key).toBe('legacy-updated-key-001');
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent user', async () => {
|
||||
const response = await admin.put('/admin/users/99999').send({ name: 'Test' });
|
||||
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ export interface Permission {
|
|||
export interface CreateUserData {
|
||||
name: string;
|
||||
email?: string;
|
||||
api_key?: string;
|
||||
detail_logging?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -118,6 +119,7 @@ export interface CreatePermissionData {
|
|||
export interface UpdateUserData {
|
||||
name?: string;
|
||||
email?: string;
|
||||
api_key?: string;
|
||||
is_active?: boolean;
|
||||
detail_logging?: boolean;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue