Compare commits
2 Commits
ed433902d9
...
2ae47232cb
| Author | SHA1 | Date | |
|---|---|---|---|
|
2ae47232cb
|
|||
|
8b846bffbe
|
73
CLAUDE.md
73
CLAUDE.md
@@ -553,3 +553,76 @@ const params = new URLSearchParams({
|
|||||||
**QSO Management**:
|
**QSO Management**:
|
||||||
- Fixed DELETE /api/qsos/all to handle foreign key constraints
|
- Fixed DELETE /api/qsos/all to handle foreign key constraints
|
||||||
- Added cache invalidation after QSO deletion
|
- Added cache invalidation after QSO deletion
|
||||||
|
|
||||||
|
### Admin System and User Roles
|
||||||
|
|
||||||
|
The application supports three user roles with different permission levels:
|
||||||
|
|
||||||
|
**User Roles**:
|
||||||
|
- **Regular User**: View own QSOs, sync from LoTW/DCL, track award progress
|
||||||
|
- **Admin**: All user permissions + view system stats + manage users + impersonate regular users
|
||||||
|
- **Super Admin**: All admin permissions + promote/demote admins + impersonate admins
|
||||||
|
|
||||||
|
**Database Schema** (`src/backend/db/schema/index.js`):
|
||||||
|
- `isAdmin`: Boolean flag for admin users (default: false)
|
||||||
|
- `isSuperAdmin`: Boolean flag for super-admin users (default: false)
|
||||||
|
|
||||||
|
**Admin Service** (`src/backend/services/admin.service.js`):
|
||||||
|
- `isAdmin(userId)`: Check if user is admin
|
||||||
|
- `isSuperAdmin(userId)`: Check if user is super-admin
|
||||||
|
- `changeUserRole(adminId, targetUserId, newRole)`: Change user role ('user', 'admin', 'super-admin')
|
||||||
|
- `impersonateUser(adminId, targetUserId)`: Start impersonating a user
|
||||||
|
- `verifyImpersonation(token)`: Verify impersonation token validity
|
||||||
|
- `stopImpersonation(adminId, targetUserId)`: Stop impersonation
|
||||||
|
- `logAdminAction(adminId, actionType, targetUserId, details)`: Log admin actions
|
||||||
|
|
||||||
|
**Security Rules**:
|
||||||
|
1. Only super-admins can promote/demote super-admins
|
||||||
|
2. Regular admins cannot promote users to super-admin
|
||||||
|
3. Super-admins cannot demote themselves (prevents lockout)
|
||||||
|
4. Cannot demote the last super-admin
|
||||||
|
5. Regular admins can only impersonate regular users
|
||||||
|
6. Super-admins can impersonate any user (including other admins)
|
||||||
|
|
||||||
|
**Backend API Routes** (`src/backend/index.js`):
|
||||||
|
- `POST /api/admin/users/:userId/role`: Change user role
|
||||||
|
- Body: `{ "role": "user" | "admin" | "super-admin" }`
|
||||||
|
- `POST /api/admin/impersonate/:userId`: Start impersonating
|
||||||
|
- `POST /api/admin/impersonate/stop`: Stop impersonating
|
||||||
|
- `GET /api/admin/impersonation/status`: Check impersonation status
|
||||||
|
- `GET /api/admin/stats`: System statistics
|
||||||
|
- `GET /api/admin/users`: List all users
|
||||||
|
- `GET /api/admin/actions`: Admin action log
|
||||||
|
- `DELETE /api/admin/users/:userId`: Delete user
|
||||||
|
|
||||||
|
**JWT Token Claims**:
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
userId: number,
|
||||||
|
email: string,
|
||||||
|
callsign: string,
|
||||||
|
isAdmin: boolean,
|
||||||
|
isSuperAdmin: boolean, // Super-admin flag
|
||||||
|
impersonatedBy: number, // Present when impersonating
|
||||||
|
exp: number
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Frontend Admin Page** (`src/frontend/src/routes/admin/+page.svelte`):
|
||||||
|
- System statistics dashboard
|
||||||
|
- User management with filtering (all, super-admin, admin, user)
|
||||||
|
- Role change modal (user → admin → super-admin)
|
||||||
|
- Impersonate button (enabled for super-admins targeting admins)
|
||||||
|
- Admin action log viewing
|
||||||
|
|
||||||
|
**To create the first super-admin**:
|
||||||
|
1. Register a user account
|
||||||
|
2. Access database: `sqlite3 src/backend/award.db`
|
||||||
|
3. Run: `UPDATE users SET is_super_admin = 1 WHERE email = 'your@email.com';`
|
||||||
|
4. Log out and log back in to get updated JWT token
|
||||||
|
|
||||||
|
**To promote via admin interface**:
|
||||||
|
1. Log in as existing super-admin
|
||||||
|
2. Navigate to `/admin`
|
||||||
|
3. Find user in Users tab
|
||||||
|
4. Click "Promote" and select "Super Admin"
|
||||||
|
|||||||
49
README.md
49
README.md
@@ -277,6 +277,52 @@ The application will be available at:
|
|||||||
### Health
|
### Health
|
||||||
- `GET /api/health` - Health check endpoint
|
- `GET /api/health` - Health check endpoint
|
||||||
|
|
||||||
|
### Admin API (Admin Only)
|
||||||
|
|
||||||
|
All admin endpoints require authentication and admin privileges.
|
||||||
|
|
||||||
|
- `GET /api/admin/stats` - Get system-wide statistics
|
||||||
|
- `GET /api/admin/users` - Get all users with statistics
|
||||||
|
- `GET /api/admin/users/:userId` - Get detailed information about a specific user
|
||||||
|
- `POST /api/admin/users/:userId/role` - Update user role (`user`, `admin`, `super-admin`)
|
||||||
|
- `DELETE /api/admin/users/:userId` - Delete a user
|
||||||
|
- `POST /api/admin/impersonate/:userId` - Start impersonating a user
|
||||||
|
- `POST /api/admin/impersonate/stop` - Stop impersonating and return to admin account
|
||||||
|
- `GET /api/admin/impersonation/status` - Get current impersonation status
|
||||||
|
- `GET /api/admin/actions` - Get admin actions log
|
||||||
|
- `GET /api/admin/actions/my` - Get current admin's action log
|
||||||
|
|
||||||
|
### User Roles and Permissions
|
||||||
|
|
||||||
|
The application supports three user roles with different permission levels:
|
||||||
|
|
||||||
|
**Regular User**
|
||||||
|
- View own QSOs
|
||||||
|
- Sync from LoTW and DCL
|
||||||
|
- Track award progress
|
||||||
|
- Manage own credentials
|
||||||
|
|
||||||
|
**Admin**
|
||||||
|
- All user permissions
|
||||||
|
- View system statistics
|
||||||
|
- View all users
|
||||||
|
- Promote/demote regular users to/from admin
|
||||||
|
- Delete regular users
|
||||||
|
- Impersonate regular users (for support)
|
||||||
|
- View admin action log
|
||||||
|
|
||||||
|
**Super Admin**
|
||||||
|
- All admin permissions
|
||||||
|
- Promote/demote admins to/from super-admin
|
||||||
|
- Impersonate other admins (for support)
|
||||||
|
- Full access to all admin functions
|
||||||
|
|
||||||
|
**Security Rules:**
|
||||||
|
- Only super-admins can promote or demote super-admins
|
||||||
|
- Regular admins cannot promote users to super-admin
|
||||||
|
- Super-admins cannot demote themselves
|
||||||
|
- Cannot demote the last super-admin
|
||||||
|
|
||||||
## Database Schema
|
## Database Schema
|
||||||
|
|
||||||
### Users Table
|
### Users Table
|
||||||
@@ -289,6 +335,9 @@ CREATE TABLE users (
|
|||||||
lotwUsername TEXT,
|
lotwUsername TEXT,
|
||||||
lotwPassword TEXT,
|
lotwPassword TEXT,
|
||||||
dclApiKey TEXT, -- DCL API key (for future use)
|
dclApiKey TEXT, -- DCL API key (for future use)
|
||||||
|
isAdmin INTEGER DEFAULT 0 NOT NULL,
|
||||||
|
isSuperAdmin INTEGER DEFAULT 0 NOT NULL,
|
||||||
|
lastSeen INTEGER,
|
||||||
createdAt TEXT NOT NULL,
|
createdAt TEXT NOT NULL,
|
||||||
updatedAt TEXT NOT NULL
|
updatedAt TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -276,6 +276,9 @@ award/
|
|||||||
callsign: text (not null)
|
callsign: text (not null)
|
||||||
lotwUsername: text (nullable)
|
lotwUsername: text (nullable)
|
||||||
lotwPassword: text (nullable, encrypted)
|
lotwPassword: text (nullable, encrypted)
|
||||||
|
isAdmin: boolean (default: false)
|
||||||
|
isSuperAdmin: boolean (default: false)
|
||||||
|
lastSeen: timestamp (nullable)
|
||||||
createdAt: timestamp
|
createdAt: timestamp
|
||||||
updatedAt: timestamp
|
updatedAt: timestamp
|
||||||
}
|
}
|
||||||
@@ -1034,7 +1037,197 @@ When adding new awards or modifying the award system:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Resources
|
## Admin System
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
|
||||||
|
The admin system provides user management, role-based access control, and account impersonation capabilities for support and administrative purposes.
|
||||||
|
|
||||||
|
### User Roles
|
||||||
|
|
||||||
|
The application supports three user roles with increasing permissions:
|
||||||
|
|
||||||
|
#### Regular User
|
||||||
|
- View own QSOs and statistics
|
||||||
|
- Sync from LoTW and DCL
|
||||||
|
- Track award progress
|
||||||
|
- Manage own credentials (LoTW, DCL)
|
||||||
|
|
||||||
|
#### Admin
|
||||||
|
- All user permissions
|
||||||
|
- View system-wide statistics
|
||||||
|
- View all users and their activity
|
||||||
|
- Promote/demote regular users to/from admin role
|
||||||
|
- Delete regular users
|
||||||
|
- Impersonate regular users (for support)
|
||||||
|
- View admin action log
|
||||||
|
|
||||||
|
#### Super Admin
|
||||||
|
- All admin permissions
|
||||||
|
- Promote/demote admins to/from super-admin role
|
||||||
|
- Impersonate other admins (for support)
|
||||||
|
- Cannot be demoted by regular admins
|
||||||
|
- Protected from accidental lockout
|
||||||
|
|
||||||
|
### Security Rules
|
||||||
|
|
||||||
|
**Role Change Restrictions:**
|
||||||
|
- Only super-admins can promote or demote super-admins
|
||||||
|
- Regular admins cannot promote users to super-admin
|
||||||
|
- Super-admins cannot demote themselves
|
||||||
|
- Cannot demote the last super-admin (prevents lockout)
|
||||||
|
|
||||||
|
**Impersonation Restrictions:**
|
||||||
|
- Regular admins can only impersonate regular users
|
||||||
|
- Super-admins can impersonate any user (including other admins)
|
||||||
|
- All impersonation actions are logged to audit trail
|
||||||
|
- Impersonation tokens expire after 1 hour
|
||||||
|
|
||||||
|
### Admin API Endpoints
|
||||||
|
|
||||||
|
**Statistics and Monitoring:**
|
||||||
|
- `GET /api/admin/stats` - System-wide statistics (users, QSOs, jobs)
|
||||||
|
- `GET /api/admin/users` - List all users with statistics
|
||||||
|
- `GET /api/admin/users/:userId` - Get detailed user information
|
||||||
|
- `GET /api/admin/actions` - View admin action log
|
||||||
|
- `GET /api/admin/actions/my` - View current admin's actions
|
||||||
|
|
||||||
|
**User Management:**
|
||||||
|
- `POST /api/admin/users/:userId/role` - Change user role
|
||||||
|
- Body: `{ "role": "user" | "admin" | "super-admin" }`
|
||||||
|
- `DELETE /api/admin/users/:userId` - Delete a user
|
||||||
|
|
||||||
|
**Impersonation:**
|
||||||
|
- `POST /api/admin/impersonate/:userId` - Start impersonating a user
|
||||||
|
- `POST /api/admin/impersonate/stop` - Stop impersonation
|
||||||
|
- `GET /api/admin/impersonation/status` - Check impersonation status
|
||||||
|
|
||||||
|
### Admin Service
|
||||||
|
|
||||||
|
**File:** `src/backend/services/admin.service.js`
|
||||||
|
|
||||||
|
**Key Functions:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Check user permissions
|
||||||
|
await isAdmin(userId)
|
||||||
|
await isSuperAdmin(userId)
|
||||||
|
|
||||||
|
// Role management
|
||||||
|
await changeUserRole(adminId, targetUserId, newRole)
|
||||||
|
|
||||||
|
// Impersonation
|
||||||
|
await impersonateUser(adminId, targetUserId)
|
||||||
|
await verifyImpersonation(impersonationToken)
|
||||||
|
await stopImpersonation(adminId, targetUserId)
|
||||||
|
|
||||||
|
// Audit logging
|
||||||
|
await logAdminAction(adminId, actionType, targetUserId, details)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Audit Logging
|
||||||
|
|
||||||
|
All admin actions are logged to the `admin_actions` table for audit purposes:
|
||||||
|
|
||||||
|
**Action Types:**
|
||||||
|
- `impersonate_start` - Started impersonating a user
|
||||||
|
- `impersonate_stop` - Stopped impersonation
|
||||||
|
- `role_change` - Changed user role
|
||||||
|
- `user_delete` - Deleted a user
|
||||||
|
|
||||||
|
**Log Entry Structure:**
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
id: integer,
|
||||||
|
adminId: integer,
|
||||||
|
actionType: string,
|
||||||
|
targetUserId: integer (nullable),
|
||||||
|
details: string (JSON),
|
||||||
|
createdAt: timestamp
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend Admin Interface
|
||||||
|
|
||||||
|
**Route:** `/admin` (admin only)
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- **Overview Tab:** System statistics dashboard
|
||||||
|
- **Users Tab:** User management with filtering
|
||||||
|
- **Awards Tab:** Award definition management
|
||||||
|
- **Action Log Tab:** Audit trail of admin actions
|
||||||
|
|
||||||
|
**User Management Actions:**
|
||||||
|
- **Impersonate** - Switch to user account (disabled for admins unless super-admin)
|
||||||
|
- **Promote/Demote** - Change user role
|
||||||
|
- **Delete** - Remove user and all associated data
|
||||||
|
|
||||||
|
### JWT Token Claims
|
||||||
|
|
||||||
|
Admin tokens include additional claims:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
userId: number,
|
||||||
|
email: string,
|
||||||
|
callsign: string,
|
||||||
|
isAdmin: boolean,
|
||||||
|
isSuperAdmin: boolean, // New: Super-admin flag
|
||||||
|
exp: number
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impersonation Token:**
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
userId: number, // Target user ID
|
||||||
|
email: string,
|
||||||
|
callsign: string,
|
||||||
|
isAdmin: boolean,
|
||||||
|
isSuperAdmin: boolean,
|
||||||
|
impersonatedBy: number, // Admin ID who started impersonation
|
||||||
|
exp: number // 1 hour expiration
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
**To create the first super-admin:**
|
||||||
|
|
||||||
|
1. Register a user account normally
|
||||||
|
2. Access the database directly:
|
||||||
|
```bash
|
||||||
|
sqlite3 src/backend/award.db
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Update the user to super-admin:
|
||||||
|
```sql
|
||||||
|
UPDATE users SET is_super_admin = 1 WHERE email = 'your@email.com';
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Log out and log back in to get the updated JWT token
|
||||||
|
|
||||||
|
**To promote users via the admin interface:**
|
||||||
|
1. Log in as a super-admin
|
||||||
|
2. Navigate to `/admin`
|
||||||
|
3. Find the user in the Users tab
|
||||||
|
4. Click "Promote" and select "Super Admin"
|
||||||
|
|
||||||
|
### Production Deployment
|
||||||
|
|
||||||
|
After pulling the latest code:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Apply database migration (adds is_super_admin column)
|
||||||
|
sqlite3 src/backend/award.db "ALTER TABLE users ADD COLUMN is_super_admin INTEGER DEFAULT 0 NOT NULL;"
|
||||||
|
|
||||||
|
# Restart backend
|
||||||
|
pm2 restart award-backend
|
||||||
|
|
||||||
|
# Promote a user to super-admin via database or existing admin interface
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
- [ARRL LoTW](https://lotw.arrl.org/)
|
- [ARRL LoTW](https://lotw.arrl.org/)
|
||||||
- [DARC Community Logbook (DCL)](https://dcl.darc.de/)
|
- [DARC Community Logbook (DCL)](https://dcl.darc.de/)
|
||||||
|
|||||||
@@ -631,7 +631,7 @@
|
|||||||
<span class="summary-label">Confirmed:</span>
|
<span class="summary-label">Confirmed:</span>
|
||||||
<span class="summary-value">{filteredEntities.filter((e) => e.confirmed).length}</span>
|
<span class="summary-value">{filteredEntities.filter((e) => e.confirmed).length}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-card" style="background-color: #fff3cd; border-color: #ffc107;">
|
<div class="summary-card points">
|
||||||
<span class="summary-label">Points:</span>
|
<span class="summary-label">Points:</span>
|
||||||
<span class="summary-value">{earnedPoints}</span>
|
<span class="summary-value">{earnedPoints}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -640,7 +640,7 @@
|
|||||||
<span class="summary-label">Needed:</span>
|
<span class="summary-label">Needed:</span>
|
||||||
<span class="summary-value">{neededPoints}</span>
|
<span class="summary-value">{neededPoints}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-card" style="background-color: #e3f2fd; border-color: #2196f3;">
|
<div class="summary-card target">
|
||||||
<span class="summary-label">Target:</span>
|
<span class="summary-label">Target:</span>
|
||||||
<span class="summary-value">{targetPoints}</span>
|
<span class="summary-value">{targetPoints}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -665,7 +665,7 @@
|
|||||||
<span class="summary-label">Needed:</span>
|
<span class="summary-label">Needed:</span>
|
||||||
<span class="summary-value">{neededCount}</span>
|
<span class="summary-value">{neededCount}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-card" style="background-color: #e3f2fd; border-color: #2196f3;">
|
<div class="summary-card target">
|
||||||
<span class="summary-label">Target:</span>
|
<span class="summary-label">Target:</span>
|
||||||
<span class="summary-value">{targetCount}</span>
|
<span class="summary-value">{targetCount}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1141,6 +1141,24 @@
|
|||||||
background-color: var(--color-warning-bg);
|
background-color: var(--color-warning-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.summary-card.points {
|
||||||
|
border-color: #ff9800;
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .summary-card.points {
|
||||||
|
background-color: rgba(255, 152, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-card.target {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .summary-card.target {
|
||||||
|
background-color: rgba(33, 150, 243, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
.summary-label {
|
.summary-label {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user