@jhits/plugin-users 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Plugin Users - Auth API Handler
3
+ * NextAuth API route handler for App Router
4
+ *
5
+ * This is a core plugin that provides authentication functionality.
6
+ * authOptions should be passed from the client app (typically from @jhits/dashboard/lib/auth)
7
+ */
8
+ import type { NextAuthOptions } from 'next-auth';
9
+ /**
10
+ * Initialize the auth handler with authOptions
11
+ * This should be called by the client app before handling auth requests
12
+ */
13
+ export declare function initAuthHandler(authOptions: NextAuthOptions): void;
14
+ export declare function GET(req: any, context?: {
15
+ params?: Promise<{
16
+ nextauth?: string[];
17
+ }>;
18
+ }): Promise<any>;
19
+ export declare function POST(req: any, context?: {
20
+ params?: Promise<{
21
+ nextauth?: string[];
22
+ }>;
23
+ }): Promise<any>;
24
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/api/auth.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAKjD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,eAAe,QAE3D;AAiBD,wBAAsB,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAC;QAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAA;CAAE,gBAsB1F;AAED,wBAAsB,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAC;QAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAA;CAAE,gBAoB3F"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Plugin Users - Auth API Handler
3
+ * NextAuth API route handler for App Router
4
+ *
5
+ * This is a core plugin that provides authentication functionality.
6
+ * authOptions should be passed from the client app (typically from @jhits/dashboard/lib/auth)
7
+ */
8
+ import NextAuth from 'next-auth';
9
+ // Store authOptions - will be initialized by the client app
10
+ let authOptionsInstance = null;
11
+ /**
12
+ * Initialize the auth handler with authOptions
13
+ * This should be called by the client app before handling auth requests
14
+ */
15
+ export function initAuthHandler(authOptions) {
16
+ authOptionsInstance = authOptions;
17
+ }
18
+ /**
19
+ * Get the NextAuth handler
20
+ * Throws an error if authOptions haven't been initialized
21
+ */
22
+ function getHandler() {
23
+ if (!authOptionsInstance) {
24
+ throw new Error('Auth handler not initialized. Call initAuthHandler() with authOptions from @jhits/dashboard/lib/auth');
25
+ }
26
+ return NextAuth(authOptionsInstance);
27
+ }
28
+ // Export handlers - NextAuth handler works for both GET and POST
29
+ // NextAuth expects the route to be /api/auth/[...nextauth] with path segments in context.params.nextauth
30
+ export async function GET(req, context) {
31
+ const handler = getHandler();
32
+ // Use context if provided, otherwise extract from URL
33
+ let nextauthPath = [];
34
+ if (context?.params) {
35
+ const resolvedParams = await context.params;
36
+ nextauthPath = resolvedParams.nextauth || [];
37
+ }
38
+ else {
39
+ // Fallback: extract from URL
40
+ const url = new URL(req.url);
41
+ const pathSegments = url.pathname.split('/').filter(Boolean);
42
+ // Path: /api/auth/session -> segments: ['api', 'auth', 'session'] -> nextauth: ['session']
43
+ nextauthPath = pathSegments.length > 2 ? pathSegments.slice(2) : [];
44
+ }
45
+ // Create context with nextauth parameter (NextAuth expects this)
46
+ const nextauthContext = {
47
+ params: Promise.resolve({ nextauth: nextauthPath })
48
+ };
49
+ return handler(req, nextauthContext);
50
+ }
51
+ export async function POST(req, context) {
52
+ const handler = getHandler();
53
+ // Use context if provided, otherwise extract from URL
54
+ let nextauthPath = [];
55
+ if (context?.params) {
56
+ const resolvedParams = await context.params;
57
+ nextauthPath = resolvedParams.nextauth || [];
58
+ }
59
+ else {
60
+ // Fallback: extract from URL
61
+ const url = new URL(req.url);
62
+ const pathSegments = url.pathname.split('/').filter(Boolean);
63
+ nextauthPath = pathSegments.length > 2 ? pathSegments.slice(2) : [];
64
+ }
65
+ const nextauthContext = {
66
+ params: Promise.resolve({ nextauth: nextauthPath })
67
+ };
68
+ return handler(req, nextauthContext);
69
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Plugin Users API Exports
3
+ */
4
+ export { GET, POST, initAuthHandler } from './auth';
5
+ export { handleUsersApi } from './router';
6
+ export type { UsersApiConfig } from './users';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAC;AAGpD,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Plugin Users API Exports
3
+ */
4
+ // Auth exports
5
+ export { GET, POST, initAuthHandler } from './auth';
6
+ // Users API exports
7
+ export { handleUsersApi } from './router';
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Plugin Users API Router
3
+ * Centralized API handler for all users plugin routes
4
+ *
5
+ * This router handles requests to:
6
+ * - /api/auth/[...nextauth] (NextAuth routes)
7
+ * - /api/users (user management routes)
8
+ * - /api/plugin-users/* (plugin-specific routes)
9
+ */
10
+ import { NextRequest, NextResponse } from 'next/server';
11
+ import { UsersApiConfig } from './users';
12
+ /**
13
+ * Handle users API requests
14
+ * Routes requests to appropriate handlers based on path
15
+ * Handles both NextAuth routes (/api/auth) and user management routes (/api/users)
16
+ */
17
+ export declare function handleUsersApi(req: NextRequest, path: string[], config: UsersApiConfig): Promise<NextResponse>;
18
+ //# sourceMappingURL=router.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/api/router.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAIzC;;;;GAIG;AACH,wBAAsB,cAAc,CAChC,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,MAAM,EAAE,EACd,MAAM,EAAE,cAAc,GACvB,OAAO,CAAC,YAAY,CAAC,CAwEvB"}
@@ -0,0 +1,80 @@
1
+ 'use server';
2
+ /**
3
+ * Plugin Users API Router
4
+ * Centralized API handler for all users plugin routes
5
+ *
6
+ * This router handles requests to:
7
+ * - /api/auth/[...nextauth] (NextAuth routes)
8
+ * - /api/users (user management routes)
9
+ * - /api/plugin-users/* (plugin-specific routes)
10
+ */
11
+ import { NextResponse } from 'next/server';
12
+ import { GET_USERS, POST_USERS, PATCH_USER, DELETE_USER } from './users';
13
+ import { GET as AuthGET, POST as AuthPOST, initAuthHandler } from './auth';
14
+ /**
15
+ * Handle users API requests
16
+ * Routes requests to appropriate handlers based on path
17
+ * Handles both NextAuth routes (/api/auth) and user management routes (/api/users)
18
+ */
19
+ export async function handleUsersApi(req, path, config) {
20
+ const method = req.method;
21
+ const route = path && path.length > 0 ? path[0] : '';
22
+ try {
23
+ // Initialize auth handler if authOptions are provided
24
+ if (config.authOptions) {
25
+ initAuthHandler(config.authOptions);
26
+ }
27
+ // Route: /api/auth/[...nextauth] - NextAuth routes
28
+ // When routed from /api/auth, the path contains the nextauth segments
29
+ // Path structure: /api/auth/session -> path: ['session']
30
+ // Path structure: /api/auth/signin -> path: ['signin']
31
+ // Path structure: /api/auth -> path: [] (empty)
32
+ // We handle this by checking if route is empty (root /api/auth) or if it's a known NextAuth route
33
+ const isNextAuthRoute = route === '' ||
34
+ ['session', 'signin', 'signout', 'callback', 'csrf', 'providers', 'error'].includes(route);
35
+ if (isNextAuthRoute) {
36
+ // This is a NextAuth route - use the path as nextauth segments
37
+ // For /api/auth, path is [] -> nextauthPath is []
38
+ // For /api/auth/session, path is ['session'] -> nextauthPath is ['session']
39
+ const nextauthPath = path;
40
+ // Create NextAuth context
41
+ const nextauthContext = {
42
+ params: Promise.resolve({ nextauth: nextauthPath })
43
+ };
44
+ if (method === 'GET') {
45
+ return await AuthGET(req, nextauthContext);
46
+ }
47
+ if (method === 'POST') {
48
+ return await AuthPOST(req, nextauthContext);
49
+ }
50
+ }
51
+ // Route: /api/users or /api/plugin-users/users (user management)
52
+ if (route === 'users') {
53
+ if (path.length === 1) {
54
+ // /api/users or /api/plugin-users/users
55
+ if (method === 'GET') {
56
+ return await GET_USERS(req, config);
57
+ }
58
+ if (method === 'POST') {
59
+ return await POST_USERS(req, config);
60
+ }
61
+ }
62
+ else if (path.length === 2) {
63
+ // /api/users/[id] or /api/plugin-users/users/[id]
64
+ const userId = path[1];
65
+ if (method === 'PATCH') {
66
+ return await PATCH_USER(req, userId, config);
67
+ }
68
+ if (method === 'DELETE') {
69
+ return await DELETE_USER(req, userId, config);
70
+ }
71
+ }
72
+ }
73
+ // Method not allowed
74
+ return NextResponse.json({ error: `Method ${method} not allowed for route: ${route || '/'}` }, { status: 405 });
75
+ }
76
+ catch (error) {
77
+ console.error('[UsersApiRouter] Error:', error);
78
+ return NextResponse.json({ error: error.message || 'Internal server error' }, { status: 500 });
79
+ }
80
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Plugin Users - Users API Handler
3
+ * Handles user management API routes
4
+ */
5
+ import { NextRequest, NextResponse } from 'next/server';
6
+ export interface UsersApiConfig {
7
+ /** MongoDB client promise */
8
+ getDb: () => Promise<{
9
+ db: () => any;
10
+ }>;
11
+ /** Collection name (default: 'users') */
12
+ collectionName?: string;
13
+ /** NextAuth options for authentication routes */
14
+ authOptions?: any;
15
+ }
16
+ /**
17
+ * GET /api/plugin-users/users - List all users
18
+ */
19
+ export declare function GET_USERS(req: NextRequest, config: UsersApiConfig): Promise<NextResponse>;
20
+ /**
21
+ * POST /api/plugin-users/users - Create new user
22
+ */
23
+ export declare function POST_USERS(req: NextRequest, config: UsersApiConfig): Promise<NextResponse>;
24
+ /**
25
+ * PATCH /api/plugin-users/users/[id] - Update user
26
+ */
27
+ export declare function PATCH_USER(req: NextRequest, userId: string, config: UsersApiConfig): Promise<NextResponse>;
28
+ /**
29
+ * DELETE /api/plugin-users/users/[id] - Delete user
30
+ */
31
+ export declare function DELETE_USER(req: NextRequest, userId: string, config: UsersApiConfig): Promise<NextResponse>;
32
+ //# sourceMappingURL=users.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../src/api/users.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAIxD,MAAM,WAAW,cAAc;IAC3B,6BAA6B;IAC7B,KAAK,EAAE,MAAM,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,GAAG,CAAA;KAAE,CAAC,CAAC;IACxC,yCAAyC;IACzC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iDAAiD;IACjD,WAAW,CAAC,EAAE,GAAG,CAAC;CACrB;AAED;;GAEG;AACH,wBAAsB,SAAS,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC,CAmB/F;AAED;;GAEG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC,CAiDhG;AAED;;GAEG;AACH,wBAAsB,UAAU,CAC5B,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,cAAc,GACvB,OAAO,CAAC,YAAY,CAAC,CAiEvB;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC7B,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,cAAc,GACvB,OAAO,CAAC,YAAY,CAAC,CA6BvB"}
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Plugin Users - Users API Handler
3
+ * Handles user management API routes
4
+ */
5
+ import { NextResponse } from 'next/server';
6
+ import { ObjectId } from 'mongodb';
7
+ import bcrypt from 'bcryptjs';
8
+ /**
9
+ * GET /api/plugin-users/users - List all users
10
+ */
11
+ export async function GET_USERS(req, config) {
12
+ try {
13
+ const dbConnection = await config.getDb();
14
+ const db = dbConnection.db();
15
+ const users = db.collection(config.collectionName || 'users');
16
+ // Exclude passwords for security
17
+ const userList = await users
18
+ .find({}, { projection: { password: 0 } })
19
+ .toArray();
20
+ return NextResponse.json(userList);
21
+ }
22
+ catch (err) {
23
+ console.error('[UsersAPI] GET error:', err);
24
+ return NextResponse.json({ error: 'Failed to fetch users', detail: err.message }, { status: 500 });
25
+ }
26
+ }
27
+ /**
28
+ * POST /api/plugin-users/users - Create new user
29
+ */
30
+ export async function POST_USERS(req, config) {
31
+ try {
32
+ const { email, name, role, password } = await req.json();
33
+ const dbConnection = await config.getDb();
34
+ const db = dbConnection.db();
35
+ const users = db.collection(config.collectionName || 'users');
36
+ // Validation
37
+ if (!password || password.length < 6) {
38
+ return NextResponse.json({ error: 'Password too short (minimum 6 characters)' }, { status: 400 });
39
+ }
40
+ const exists = await users.findOne({ email });
41
+ if (exists) {
42
+ return NextResponse.json({ error: 'User already exists' }, { status: 400 });
43
+ }
44
+ // Hash the password
45
+ const hashedPassword = await bcrypt.hash(password, 12);
46
+ // Save to DB
47
+ const result = await users.insertOne({
48
+ email,
49
+ name,
50
+ role: role || 'editor',
51
+ password: hashedPassword,
52
+ createdAt: new Date(),
53
+ });
54
+ return NextResponse.json({
55
+ _id: result.insertedId,
56
+ email,
57
+ name,
58
+ role: role || 'editor',
59
+ createdAt: new Date()
60
+ }, { status: 201 });
61
+ }
62
+ catch (err) {
63
+ console.error('[UsersAPI] POST error:', err);
64
+ return NextResponse.json({ error: 'Failed to create user', detail: err.message }, { status: 500 });
65
+ }
66
+ }
67
+ /**
68
+ * PATCH /api/plugin-users/users/[id] - Update user
69
+ */
70
+ export async function PATCH_USER(req, userId, config) {
71
+ try {
72
+ const { role, name, password, currentPassword } = await req.json();
73
+ const dbConnection = await config.getDb();
74
+ const db = dbConnection.db();
75
+ const users = db.collection(config.collectionName || 'users');
76
+ // Prepare the update object
77
+ const updateData = { updatedAt: new Date() };
78
+ if (role)
79
+ updateData.role = role;
80
+ if (name)
81
+ updateData.name = name;
82
+ // Handle Password Update Logic
83
+ if (password) {
84
+ // Find the user first to verify identity
85
+ const user = await users.findOne({ _id: new ObjectId(userId) });
86
+ if (!user) {
87
+ return NextResponse.json({ error: 'User not found' }, { status: 404 });
88
+ }
89
+ // Safety Check: Verify current password before allowing change
90
+ if (!currentPassword) {
91
+ return NextResponse.json({ error: 'Current password is required' }, { status: 400 });
92
+ }
93
+ const isCorrect = await bcrypt.compare(currentPassword, user.password);
94
+ if (!isCorrect) {
95
+ return NextResponse.json({ error: 'Current password is incorrect' }, { status: 401 });
96
+ }
97
+ // Hash the NEW password
98
+ if (password.length < 6) {
99
+ return NextResponse.json({ error: 'New password too short (minimum 6 characters)' }, { status: 400 });
100
+ }
101
+ updateData.password = await bcrypt.hash(password, 12);
102
+ }
103
+ // Execute the Update
104
+ const result = await users.updateOne({ _id: new ObjectId(userId) }, { $set: updateData });
105
+ if (result.matchedCount === 0) {
106
+ return NextResponse.json({ error: 'User not found' }, { status: 404 });
107
+ }
108
+ return NextResponse.json({ message: 'Update successful' });
109
+ }
110
+ catch (err) {
111
+ console.error('[UsersAPI] PATCH error:', err);
112
+ return NextResponse.json({ error: 'Update failed', detail: err.message }, { status: 500 });
113
+ }
114
+ }
115
+ /**
116
+ * DELETE /api/plugin-users/users/[id] - Delete user
117
+ */
118
+ export async function DELETE_USER(req, userId, config) {
119
+ try {
120
+ const dbConnection = await config.getDb();
121
+ const db = dbConnection.db();
122
+ const users = db.collection(config.collectionName || 'users');
123
+ // Safety check: Prevent deleting developer account
124
+ const user = await users.findOne({ _id: new ObjectId(userId) });
125
+ if (user?.role === 'dev') {
126
+ return NextResponse.json({ error: 'Cannot delete developer account' }, { status: 403 });
127
+ }
128
+ const result = await users.deleteOne({ _id: new ObjectId(userId) });
129
+ if (result.deletedCount === 0) {
130
+ return NextResponse.json({ error: 'User not found' }, { status: 404 });
131
+ }
132
+ return NextResponse.json({ message: 'User deleted' });
133
+ }
134
+ catch (err) {
135
+ console.error('[UsersAPI] DELETE error:', err);
136
+ return NextResponse.json({ error: 'Delete failed', detail: err.message }, { status: 500 });
137
+ }
138
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Plugin Users - Server-Only API Exports
3
+ * This file is only imported in server-side code (API routes)
4
+ *
5
+ * IMPORTANT: This file uses Node.js modules and should NEVER
6
+ * be imported in client-side code. Only use in server-side API routes.
7
+ */
8
+ export * from './api';
9
+ //# sourceMappingURL=api-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-server.d.ts","sourceRoot":"","sources":["../src/api-server.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,cAAc,OAAO,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Plugin Users - Server-Only API Exports
3
+ * This file is only imported in server-side code (API routes)
4
+ *
5
+ * IMPORTANT: This file uses Node.js modules and should NEVER
6
+ * be imported in client-side code. Only use in server-side API routes.
7
+ */
8
+ // Re-export everything from the API index
9
+ export * from './api';
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+ export declare const Index: React.FC<any>;
3
+ export default Index;
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC,GAAG,CAQ/B,CAAC;AAEF,eAAe,KAAK,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import UserManagement from './views/UserManagement';
4
+ // User Management Plugin - Always enabled by default
5
+ export const Index = ({ subPath = [], siteId, locale: dashboardLocale = 'en' }) => {
6
+ return (_jsx("div", { className: "w-full h-full flex flex-col overflow-hidden", children: _jsx("div", { className: "flex-1 overflow-y-auto", children: _jsx(UserManagement, { locale: dashboardLocale }) }) }));
7
+ };
8
+ export default Index;
@@ -0,0 +1,12 @@
1
+ import 'server-only';
2
+ /**
3
+ * Plugin Users - Server-Only Entry Point
4
+ * This file exports only server-side API handlers
5
+ * Used by the dynamic plugin router via @jhits/plugin-users/server
6
+ *
7
+ * Note: This file is server-only (no 'use server' needed - that's only for Server Actions)
8
+ */
9
+ export { handleUsersApi as handleApi } from './api/router';
10
+ export { handleUsersApi } from './api/router';
11
+ export type { UsersApiConfig } from './api/users';
12
+ //# sourceMappingURL=index.server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.server.d.ts","sourceRoot":"","sources":["../src/index.server.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAErB;;;;;;GAMG;AAEH,OAAO,EAAE,cAAc,IAAI,SAAS,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,10 @@
1
+ import 'server-only';
2
+ /**
3
+ * Plugin Users - Server-Only Entry Point
4
+ * This file exports only server-side API handlers
5
+ * Used by the dynamic plugin router via @jhits/plugin-users/server
6
+ *
7
+ * Note: This file is server-only (no 'use server' needed - that's only for Server Actions)
8
+ */
9
+ export { handleUsersApi as handleApi } from './api/router';
10
+ export { handleUsersApi } from './api/router'; // Keep original export for backward compatibility
@@ -0,0 +1,4 @@
1
+ export default function UserManagement({ locale }: {
2
+ locale?: string;
3
+ }): import("react/jsx-runtime").JSX.Element;
4
+ //# sourceMappingURL=UserManagement.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UserManagement.d.ts","sourceRoot":"","sources":["../../src/views/UserManagement.tsx"],"names":[],"mappings":"AAgBA,MAAM,CAAC,OAAO,UAAU,cAAc,CAAC,EAAE,MAAa,EAAE,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,2CA0T5E"}
@@ -0,0 +1,94 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState, useEffect } from "react";
4
+ import { UserPlus, Shield, Trash2, Search, Loader2, X, Eye, EyeOff, Copy, Check, Calendar } from "lucide-react";
5
+ export default function UserManagement({ locale = 'en' }) {
6
+ const [users, setUsers] = useState([]);
7
+ const [loading, setLoading] = useState(true);
8
+ const [searchTerm, setSearchTerm] = useState("");
9
+ // Modal State
10
+ const [isModalOpen, setIsModalOpen] = useState(false);
11
+ const [isCreating, setIsCreating] = useState(false);
12
+ const [showPassword, setShowPassword] = useState(false);
13
+ const [copied, setCopied] = useState(false);
14
+ // Form State
15
+ const [formData, setFormData] = useState({
16
+ name: "",
17
+ email: "",
18
+ password: "",
19
+ role: "editor"
20
+ });
21
+ const fetchUsers = async () => {
22
+ try {
23
+ setLoading(true);
24
+ const res = await fetch('/api/users');
25
+ const data = await res.json();
26
+ if (Array.isArray(data))
27
+ setUsers(data);
28
+ }
29
+ catch (err) {
30
+ console.error("Failed to load users", err);
31
+ }
32
+ finally {
33
+ setLoading(false);
34
+ }
35
+ };
36
+ useEffect(() => { fetchUsers(); }, []);
37
+ const generateTempPassword = () => {
38
+ const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%";
39
+ let retVal = "";
40
+ for (let i = 0, n = charset.length; i < 10; ++i) {
41
+ retVal += charset.charAt(Math.floor(Math.random() * n));
42
+ }
43
+ setFormData({ ...formData, password: retVal });
44
+ };
45
+ const handleCreateUser = async (e) => {
46
+ e.preventDefault();
47
+ setIsCreating(true);
48
+ try {
49
+ const res = await fetch('/api/users', {
50
+ method: 'POST',
51
+ headers: { 'Content-Type': 'application/json' },
52
+ body: JSON.stringify(formData)
53
+ });
54
+ if (res.ok) {
55
+ const newUser = await res.json();
56
+ setUsers([...users, newUser]);
57
+ setIsModalOpen(false);
58
+ setFormData({ name: "", email: "", password: "", role: "editor" });
59
+ }
60
+ else {
61
+ const err = await res.json();
62
+ alert(err.error || "Failed to create user");
63
+ }
64
+ }
65
+ catch (err) {
66
+ alert("Network error while creating user");
67
+ }
68
+ finally {
69
+ setIsCreating(false);
70
+ }
71
+ };
72
+ const copyToClipboard = () => {
73
+ navigator.clipboard.writeText(formData.password);
74
+ setCopied(true);
75
+ setTimeout(() => setCopied(false), 2000);
76
+ };
77
+ const handleDelete = async (userId, userRole) => {
78
+ if (userRole === 'dev')
79
+ return alert("Cannot delete developer account.");
80
+ if (!confirm("Are you sure you want to delete this user?"))
81
+ return;
82
+ try {
83
+ const res = await fetch(`/api/users/${userId}`, { method: 'DELETE' });
84
+ if (res.ok)
85
+ setUsers(users.filter(u => u._id !== userId));
86
+ }
87
+ catch (err) {
88
+ alert("Error deleting user.");
89
+ }
90
+ };
91
+ const filteredUsers = users.filter(user => user.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
92
+ user.email?.toLowerCase().includes(searchTerm.toLowerCase()));
93
+ return (_jsxs("div", { className: "h-full w-full rounded-[2.5rem] bg-dashboard-card dark:bg-neutral-900 p-8 overflow-y-auto", children: [_jsxs("div", { className: "flex flex-col md:flex-row md:items-center justify-between gap-6 mb-8", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-3xl font-black text-dashboard-text uppercase tracking-tighter mb-2", children: "User Management" }), _jsx("p", { className: "text-sm text-dashboard-text-secondary", children: "Manage who has access to the dashboard." })] }), _jsxs("button", { onClick: () => { setIsModalOpen(true); generateTempPassword(); }, className: "inline-flex items-center gap-2 px-6 py-3 bg-primary text-white rounded-full text-[10px] font-black uppercase tracking-widest hover:bg-primary/90 transition-all shadow-lg shadow-primary/20", children: [_jsx(UserPlus, { size: 16 }), "New User"] })] }), _jsxs("div", { className: "bg-dashboard-bg dark:bg-neutral-800/50 rounded-2xl sm:rounded-[2rem] border border-dashboard-border overflow-hidden", children: [_jsxs("div", { className: "p-4 sm:p-6 border-b border-dashboard-border flex items-center justify-between", children: [_jsxs("div", { className: "relative w-full sm:w-72", children: [_jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 text-dashboard-text-secondary", size: 16 }), _jsx("input", { type: "text", placeholder: "Search users...", value: searchTerm, onChange: (e) => setSearchTerm(e.target.value), className: "w-full pl-10 pr-4 py-2.5 bg-dashboard-card border border-dashboard-border rounded-lg text-sm outline-none focus:ring-2 focus:ring-primary/20 transition-all text-dashboard-text placeholder:text-dashboard-text-secondary" })] }), loading && _jsx(Loader2, { className: "animate-spin text-primary ml-4", size: 20 })] }), _jsx("div", { className: "hidden md:block overflow-x-auto", children: _jsxs("table", { className: "w-full text-left border-collapse", children: [_jsx("thead", { children: _jsxs("tr", { className: "text-[11px] font-bold text-dashboard-text-secondary uppercase tracking-widest border-b border-dashboard-border", children: [_jsx("th", { className: "px-8 py-4", children: "User" }), _jsx("th", { className: "px-8 py-4", children: "Role" }), _jsx("th", { className: "px-8 py-4", children: "Since" }), _jsx("th", { className: "px-8 py-4 text-right", children: "Actions" })] }) }), _jsx("tbody", { className: "divide-y divide-dashboard-border", children: filteredUsers.map((user) => (_jsxs("tr", { className: "group hover:bg-dashboard-card transition-colors", children: [_jsx("td", { className: "px-8 py-5", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "w-10 h-10 rounded-full bg-primary/10 text-primary flex items-center justify-center font-bold border border-primary/20", children: user.name?.[0] || 'U' }), _jsxs("div", { children: [_jsx("p", { className: "font-bold text-dashboard-text text-sm", children: user.name }), _jsx("p", { className: "text-xs text-dashboard-text-secondary", children: user.email })] })] }) }), _jsx("td", { className: "px-8 py-5", children: _jsxs("span", { className: `inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider ${user.role === 'dev' ? 'bg-primary text-white' : 'bg-dashboard-bg text-dashboard-text'}`, children: [_jsx(Shield, { size: 10 }), " ", user.role] }) }), _jsx("td", { className: "px-8 py-5 text-sm text-dashboard-text-secondary", children: user.createdAt ? new Date(user.createdAt).toLocaleDateString() : '-' }), _jsx("td", { className: "px-8 py-5 text-right", children: _jsx("button", { onClick: () => handleDelete(user._id, user.role), className: "p-2 text-dashboard-text-secondary hover:text-red-500 transition-colors disabled:opacity-0", disabled: user.role === 'dev', children: _jsx(Trash2, { size: 16 }) }) })] }, user._id))) })] }) }), _jsx("div", { className: "md:hidden divide-y divide-dashboard-border", children: filteredUsers.map((user) => (_jsxs("div", { className: "p-5 space-y-4", children: [_jsxs("div", { className: "flex justify-between items-start", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "w-10 h-10 rounded-full bg-primary/10 text-primary flex items-center justify-center font-bold border border-primary/20", children: user.name?.[0] || 'U' }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "font-bold text-dashboard-text text-sm truncate", children: user.name }), _jsx("p", { className: "text-[11px] text-dashboard-text-secondary truncate", children: user.email })] })] }), _jsx("button", { onClick: () => handleDelete(user._id, user.role), className: `p-2 rounded-lg bg-red-50 dark:bg-red-950/20 text-red-400 ${user.role === 'dev' ? 'hidden' : 'block'}`, children: _jsx(Trash2, { size: 16 }) })] }), _jsxs("div", { className: "flex items-center justify-between pt-2 border-t border-dashboard-border", children: [_jsxs("span", { className: `inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[9px] font-black uppercase tracking-wider ${user.role === 'dev' ? 'bg-primary text-white' : 'bg-dashboard-bg text-dashboard-text'}`, children: [_jsx(Shield, { size: 10 }), " ", user.role] }), _jsxs("div", { className: "flex items-center gap-1.5 text-[10px] text-dashboard-text-secondary font-medium", children: [_jsx(Calendar, { size: 12 }), user.createdAt ? new Date(user.createdAt).toLocaleDateString() : '-'] })] })] }, user._id))) }), filteredUsers.length === 0 && !loading && (_jsx("div", { className: "p-12 text-center text-dashboard-text-secondary text-sm", children: "No users found matching your search." }))] }), isModalOpen && (_jsx("div", { className: "fixed inset-0 z-50 flex items-end sm:items-center justify-center p-0 sm:p-4 bg-black/40 backdrop-blur-sm animate-in fade-in duration-200", children: _jsxs("div", { className: "bg-dashboard-card w-full max-w-md rounded-t-[2.5rem] sm:rounded-[2.5rem] shadow-2xl overflow-hidden animate-in slide-in-from-bottom sm:zoom-in-95 duration-300 max-h-[90vh] overflow-y-auto border border-dashboard-border", children: [_jsxs("div", { className: "p-6 sm:p-8 bg-primary text-white flex justify-between items-center sticky top-0 z-10", children: [_jsx("h2", { className: "text-xl sm:text-2xl font-black uppercase tracking-tighter", children: "New User" }), _jsx("button", { onClick: () => setIsModalOpen(false), className: "hover:rotate-90 transition-transform p-2", children: _jsx(X, { size: 24 }) })] }), _jsxs("form", { onSubmit: handleCreateUser, className: "p-6 sm:p-8 space-y-5", children: [_jsxs("div", { className: "space-y-2", children: [_jsx("label", { className: "text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest ml-1", children: "Name" }), _jsx("input", { required: true, className: "w-full px-5 py-3 bg-dashboard-bg border border-dashboard-border rounded-2xl focus:ring-2 focus:ring-primary outline-none text-sm transition-all text-dashboard-text", value: formData.name, onChange: e => setFormData({ ...formData, name: e.target.value }), placeholder: "e.g. John Doe" })] }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { className: "text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest ml-1", children: "Email Address" }), _jsx("input", { required: true, type: "email", className: "w-full px-5 py-3 bg-dashboard-bg border border-dashboard-border rounded-2xl focus:ring-2 focus:ring-primary outline-none text-sm transition-all text-dashboard-text", value: formData.email, onChange: e => setFormData({ ...formData, email: e.target.value }), placeholder: "email@example.com" })] }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { className: "text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest ml-1", children: "Temporary Password" }), _jsxs("div", { className: "relative", children: [_jsx("input", { required: true, type: showPassword ? "text" : "password", className: "w-full px-5 py-3 bg-dashboard-bg border border-dashboard-border rounded-2xl focus:ring-2 focus:ring-primary outline-none text-sm transition-all font-mono text-dashboard-text", value: formData.password, onChange: e => setFormData({ ...formData, password: e.target.value }) }), _jsxs("div", { className: "absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-1", children: [_jsx("button", { type: "button", onClick: () => setShowPassword(!showPassword), className: "p-1.5 text-dashboard-text-secondary hover:text-primary", children: showPassword ? _jsx(EyeOff, { size: 16 }) : _jsx(Eye, { size: 16 }) }), _jsx("button", { type: "button", onClick: copyToClipboard, className: "p-1.5 text-dashboard-text-secondary hover:text-primary", children: copied ? _jsx(Check, { size: 16, className: "text-emerald-500" }) : _jsx(Copy, { size: 16 }) })] })] }), _jsx("button", { type: "button", onClick: generateTempPassword, className: "text-[10px] text-primary font-bold uppercase tracking-wider hover:underline ml-1", children: "Generate new password" })] }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { className: "text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest ml-1", children: "Role" }), _jsxs("div", { className: "relative", children: [_jsxs("select", { className: "w-full px-5 py-3 bg-dashboard-bg border border-dashboard-border rounded-2xl focus:ring-2 focus:ring-primary outline-none text-sm appearance-none cursor-pointer text-dashboard-text", value: formData.role, onChange: e => setFormData({ ...formData, role: e.target.value }), children: [_jsx("option", { value: "editor", children: "Editor (Content only)" }), _jsx("option", { value: "dev", children: "Developer" }), _jsx("option", { value: "admin", children: "Admin (All settings)" })] }), _jsx("div", { className: "absolute right-4 top-1/2 -translate-y-1/2 pointer-events-none text-dashboard-text-secondary", children: _jsx(Shield, { size: 16 }) })] })] }), _jsx("button", { type: "submit", disabled: isCreating, className: "w-full py-4 bg-primary text-white rounded-full text-[10px] font-black uppercase tracking-widest shadow-lg shadow-primary/20 hover:bg-primary/90 transition-all mt-4 flex items-center justify-center gap-2", children: isCreating ? _jsx(Loader2, { className: "animate-spin", size: 20 }) : "Add User" })] })] }) }))] }));
94
+ }
package/package.json CHANGED
@@ -1,34 +1,37 @@
1
1
  {
2
2
  "name": "@jhits/plugin-users",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "User management and authentication plugin for the JHITS ecosystem",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
8
- "main": "./src/index.tsx",
9
- "types": "./src/index.tsx",
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
10
  "exports": {
11
11
  ".": {
12
- "types": "./src/index.tsx",
13
- "default": "./src/index.tsx"
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
14
  },
15
15
  "./server": {
16
- "types": "./src/index.server.ts",
17
- "default": "./src/index.server.ts"
16
+ "types": "./dist/index.server.d.ts",
17
+ "default": "./dist/index.server.js"
18
18
  }
19
19
  },
20
+ "scripts": {
21
+ "build": "tsc"
22
+ },
20
23
  "dependencies": {
21
24
  "@jhits/plugin-core": "^0.0.2",
22
25
  "bcrypt": "^6.0.0",
23
26
  "bcryptjs": "^3.0.3",
24
- "framer-motion": "^12.27.5",
25
- "lucide-react": "^0.562.0",
26
- "mongodb": "^7.0.0",
27
+ "framer-motion": "^12.34.0",
28
+ "lucide-react": "^0.564.0",
29
+ "mongodb": "^7.1.0",
27
30
  "next-auth": "^4.24.13",
28
31
  "@types/bcrypt": "^6.0.0",
29
32
  "@types/bcryptjs": "^3.0.0",
30
- "@types/node": "^25.0.9",
31
- "@types/react": "^19.2.9",
33
+ "@types/node": "^25.2.3",
34
+ "@types/react": "^19.2.14",
32
35
  "@types/react-dom": "^19.2.3"
33
36
  },
34
37
  "peerDependencies": {
@@ -38,16 +41,17 @@
38
41
  },
39
42
  "devDependencies": {
40
43
  "@tailwindcss/postcss": "^4.1.18",
41
- "eslint": "^9.39.2",
42
- "eslint-config-next": "16.1.4",
43
- "next": "16.1.4",
44
- "react": "19.2.3",
45
- "react-dom": "19.2.3",
44
+ "eslint": "^10.0.0",
45
+ "eslint-config-next": "16.1.6",
46
+ "next": "16.1.6",
47
+ "react": "19.2.4",
48
+ "react-dom": "19.2.4",
46
49
  "tailwindcss": "^4.1.18",
47
50
  "typescript": "^5.9.3"
48
51
  },
49
52
  "files": [
53
+ "dist",
50
54
  "src",
51
55
  "package.json"
52
56
  ]
53
- }
57
+ }
@@ -105,32 +105,38 @@ export default function UserManagement({ locale = 'en' }: { locale?: string }) {
105
105
  );
106
106
 
107
107
  return (
108
- <div className="p-4 rounded-[2.5rem] sm:p-8 space-y-6 sm:space-y-8 animate-in fade-in duration-500 max-w-[1400px] mx-auto">
109
- {/* Header */}
110
- <div className="flex flex-col sm:flex-row sm:justify-between sm:items-end gap-4">
108
+ <div className="h-full w-full rounded-[2.5rem] bg-dashboard-card dark:bg-neutral-900 p-8 overflow-y-auto">
109
+ {/* Header Section */}
110
+ <div className="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-8">
111
111
  <div>
112
- <h1 className="text-2xl sm:text-3xl font-pp uppercase font-bold text-neutral-900 dark:text-neutral-100">User Management</h1>
113
- <p className="text-neutral-500 dark:text-neutral-400 text-xs sm:text-sm font-medium">Manage who has access to the dashboard.</p>
112
+ <h1 className="text-3xl font-black text-dashboard-text uppercase tracking-tighter mb-2">
113
+ User Management
114
+ </h1>
115
+ <p className="text-sm text-dashboard-text-secondary">
116
+ Manage who has access to the dashboard.
117
+ </p>
114
118
  </div>
119
+
115
120
  <button
116
121
  onClick={() => { setIsModalOpen(true); generateTempPassword(); }}
117
- className="flex items-center justify-center gap-2 px-5 py-3 bg-primary text-white rounded-xl font-bold hover:bg-primary/90 transition-all shadow-lg text-sm sm:text-base"
122
+ className="inline-flex items-center gap-2 px-6 py-3 bg-primary text-white rounded-full text-[10px] font-black uppercase tracking-widest hover:bg-primary/90 transition-all shadow-lg shadow-primary/20"
118
123
  >
119
- <UserPlus size={18} /> New User
124
+ <UserPlus size={16} />
125
+ New User
120
126
  </button>
121
127
  </div>
122
128
 
123
129
  {/* Toolbar */}
124
- <div className="bg-dashboard-card rounded-2xl sm:rounded-[2rem] border border-dashboard-border shadow-sm overflow-hidden">
125
- <div className="p-4 sm:p-6 border-b border-dashboard-border flex items-center justify-between bg-dashboard-bg">
130
+ <div className="bg-dashboard-bg dark:bg-neutral-800/50 rounded-2xl sm:rounded-[2rem] border border-dashboard-border overflow-hidden">
131
+ <div className="p-4 sm:p-6 border-b border-dashboard-border flex items-center justify-between">
126
132
  <div className="relative w-full sm:w-72">
127
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 text-neutral-400" size={16} />
133
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 text-dashboard-text-secondary" size={16} />
128
134
  <input
129
135
  type="text"
130
136
  placeholder="Search users..."
131
137
  value={searchTerm}
132
138
  onChange={(e) => setSearchTerm(e.target.value)}
133
- className="w-full pl-10 pr-4 py-2 bg-dashboard-card border border-dashboard-border rounded-lg text-sm outline-none focus:ring-2 focus:ring-primary/20 transition-all text-dashboard-text"
139
+ className="w-full pl-10 pr-4 py-2.5 bg-dashboard-card border border-dashboard-border rounded-lg text-sm outline-none focus:ring-2 focus:ring-primary/20 transition-all text-dashboard-text placeholder:text-dashboard-text-secondary"
134
140
  />
135
141
  </div>
136
142
  {loading && <Loader2 className="animate-spin text-primary ml-4" size={20} />}
@@ -140,16 +146,16 @@ export default function UserManagement({ locale = 'en' }: { locale?: string }) {
140
146
  <div className="hidden md:block overflow-x-auto">
141
147
  <table className="w-full text-left border-collapse">
142
148
  <thead>
143
- <tr className="text-[11px] font-bold text-neutral-400 uppercase tracking-widest border-b border-dashboard-border">
149
+ <tr className="text-[11px] font-bold text-dashboard-text-secondary uppercase tracking-widest border-b border-dashboard-border">
144
150
  <th className="px-8 py-4">User</th>
145
151
  <th className="px-8 py-4">Role</th>
146
152
  <th className="px-8 py-4">Since</th>
147
153
  <th className="px-8 py-4 text-right">Actions</th>
148
154
  </tr>
149
155
  </thead>
150
- <tbody className="divide-y divide-neutral-100 dark:divide-neutral-800">
156
+ <tbody className="divide-y divide-dashboard-border">
151
157
  {filteredUsers.map((user) => (
152
- <tr key={user._id} className="group hover:bg-dashboard-bg transition-colors">
158
+ <tr key={user._id} className="group hover:bg-dashboard-card transition-colors">
153
159
  <td className="px-8 py-5">
154
160
  <div className="flex items-center gap-3">
155
161
  <div className="w-10 h-10 rounded-full bg-primary/10 text-primary flex items-center justify-center font-bold border border-primary/20">
@@ -157,7 +163,7 @@ export default function UserManagement({ locale = 'en' }: { locale?: string }) {
157
163
  </div>
158
164
  <div>
159
165
  <p className="font-bold text-dashboard-text text-sm">{user.name}</p>
160
- <p className="text-xs text-neutral-400">{user.email}</p>
166
+ <p className="text-xs text-dashboard-text-secondary">{user.email}</p>
161
167
  </div>
162
168
  </div>
163
169
  </td>
@@ -167,13 +173,13 @@ export default function UserManagement({ locale = 'en' }: { locale?: string }) {
167
173
  <Shield size={10} /> {user.role}
168
174
  </span>
169
175
  </td>
170
- <td className="px-8 py-5 text-sm text-neutral-500 dark:text-neutral-400">
176
+ <td className="px-8 py-5 text-sm text-dashboard-text-secondary">
171
177
  {user.createdAt ? new Date(user.createdAt).toLocaleDateString() : '-'}
172
178
  </td>
173
179
  <td className="px-8 py-5 text-right">
174
180
  <button
175
181
  onClick={() => handleDelete(user._id, user.role)}
176
- className="p-2 text-neutral-300 dark:text-neutral-600 hover:text-red-500 transition-colors disabled:opacity-0"
182
+ className="p-2 text-dashboard-text-secondary hover:text-red-500 transition-colors disabled:opacity-0"
177
183
  disabled={user.role === 'dev'}
178
184
  >
179
185
  <Trash2 size={16} />
@@ -186,7 +192,7 @@ export default function UserManagement({ locale = 'en' }: { locale?: string }) {
186
192
  </div>
187
193
 
188
194
  {/* Mobile Card List */}
189
- <div className="md:hidden divide-y divide-neutral-100 dark:divide-neutral-800">
195
+ <div className="md:hidden divide-y divide-dashboard-border">
190
196
  {filteredUsers.map((user) => (
191
197
  <div key={user._id} className="p-5 space-y-4">
192
198
  <div className="flex justify-between items-start">
@@ -195,8 +201,8 @@ export default function UserManagement({ locale = 'en' }: { locale?: string }) {
195
201
  {user.name?.[0] || 'U'}
196
202
  </div>
197
203
  <div className="min-w-0">
198
- <p className="font-bold text-neutral-900 dark:text-neutral-100 text-sm truncate">{user.name}</p>
199
- <p className="text-[11px] text-neutral-400 truncate">{user.email}</p>
204
+ <p className="font-bold text-dashboard-text text-sm truncate">{user.name}</p>
205
+ <p className="text-[11px] text-dashboard-text-secondary truncate">{user.email}</p>
200
206
  </div>
201
207
  </div>
202
208
  <button
@@ -207,12 +213,12 @@ export default function UserManagement({ locale = 'en' }: { locale?: string }) {
207
213
  </button>
208
214
  </div>
209
215
 
210
- <div className="flex items-center justify-between pt-2 border-t border-neutral-100 dark:border-neutral-800">
211
- <span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[9px] font-black uppercase tracking-wider ${user.role === 'dev' ? 'bg-primary text-white' : 'bg-neutral-100 dark:bg-neutral-800 text-neutral-700 dark:text-neutral-300'
216
+ <div className="flex items-center justify-between pt-2 border-t border-dashboard-border">
217
+ <span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[9px] font-black uppercase tracking-wider ${user.role === 'dev' ? 'bg-primary text-white' : 'bg-dashboard-bg text-dashboard-text'
212
218
  }`}>
213
219
  <Shield size={10} /> {user.role}
214
220
  </span>
215
- <div className="flex items-center gap-1.5 text-[10px] text-neutral-400 font-medium">
221
+ <div className="flex items-center gap-1.5 text-[10px] text-dashboard-text-secondary font-medium">
216
222
  <Calendar size={12} />
217
223
  {user.createdAt ? new Date(user.createdAt).toLocaleDateString() : '-'}
218
224
  </div>
@@ -222,7 +228,7 @@ export default function UserManagement({ locale = 'en' }: { locale?: string }) {
222
228
  </div>
223
229
 
224
230
  {filteredUsers.length === 0 && !loading && (
225
- <div className="p-12 text-center text-neutral-400 text-sm">
231
+ <div className="p-12 text-center text-dashboard-text-secondary text-sm">
226
232
  No users found matching your search.
227
233
  </div>
228
234
  )}
@@ -230,10 +236,10 @@ export default function UserManagement({ locale = 'en' }: { locale?: string }) {
230
236
 
231
237
  {/* Create User Modal */}
232
238
  {isModalOpen && (
233
- <div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center p-0 sm:p-4 bg-black/20 backdrop-blur-sm animate-in fade-in duration-200">
234
- <div className="bg-white dark:bg-neutral-900 w-full max-w-md rounded-t-[2.5rem] sm:rounded-[2.5rem] shadow-2xl overflow-hidden animate-in slide-in-from-bottom sm:zoom-in-95 duration-300 max-h-[90vh] overflow-y-auto">
239
+ <div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center p-0 sm:p-4 bg-black/40 backdrop-blur-sm animate-in fade-in duration-200">
240
+ <div className="bg-dashboard-card w-full max-w-md rounded-t-[2.5rem] sm:rounded-[2.5rem] shadow-2xl overflow-hidden animate-in slide-in-from-bottom sm:zoom-in-95 duration-300 max-h-[90vh] overflow-y-auto border border-dashboard-border">
235
241
  <div className="p-6 sm:p-8 bg-primary text-white flex justify-between items-center sticky top-0 z-10">
236
- <h2 className="text-xl sm:text-2xl font-pp uppercase font-bold">New User</h2>
242
+ <h2 className="text-xl sm:text-2xl font-black uppercase tracking-tighter">New User</h2>
237
243
  <button onClick={() => setIsModalOpen(false)} className="hover:rotate-90 transition-transform p-2">
238
244
  <X size={24} />
239
245
  </button>
@@ -241,10 +247,10 @@ export default function UserManagement({ locale = 'en' }: { locale?: string }) {
241
247
 
242
248
  <form onSubmit={handleCreateUser} className="p-6 sm:p-8 space-y-5">
243
249
  <div className="space-y-2">
244
- <label className="text-xs font-bold text-neutral-500 dark:text-neutral-400 uppercase tracking-widest ml-1">Name</label>
250
+ <label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest ml-1">Name</label>
245
251
  <input
246
252
  required
247
- className="w-full px-5 py-3 bg-dashboard-bg border-none rounded-2xl focus:ring-2 focus:ring-primary outline-none text-sm transition-all text-dashboard-text"
253
+ className="w-full px-5 py-3 bg-dashboard-bg border border-dashboard-border rounded-2xl focus:ring-2 focus:ring-primary outline-none text-sm transition-all text-dashboard-text"
248
254
  value={formData.name}
249
255
  onChange={e => setFormData({ ...formData, name: e.target.value })}
250
256
  placeholder="e.g. John Doe"
@@ -252,11 +258,11 @@ export default function UserManagement({ locale = 'en' }: { locale?: string }) {
252
258
  </div>
253
259
 
254
260
  <div className="space-y-2">
255
- <label className="text-xs font-bold text-neutral-500 dark:text-neutral-400 uppercase tracking-widest ml-1">Email Address</label>
261
+ <label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest ml-1">Email Address</label>
256
262
  <input
257
263
  required
258
264
  type="email"
259
- className="w-full px-5 py-3 bg-dashboard-bg border-none rounded-2xl focus:ring-2 focus:ring-primary outline-none text-sm transition-all text-dashboard-text"
265
+ className="w-full px-5 py-3 bg-dashboard-bg border border-dashboard-border rounded-2xl focus:ring-2 focus:ring-primary outline-none text-sm transition-all text-dashboard-text"
260
266
  value={formData.email}
261
267
  onChange={e => setFormData({ ...formData, email: e.target.value })}
262
268
  placeholder="email@example.com"
@@ -264,20 +270,20 @@ export default function UserManagement({ locale = 'en' }: { locale?: string }) {
264
270
  </div>
265
271
 
266
272
  <div className="space-y-2">
267
- <label className="text-xs font-bold text-neutral-500 dark:text-neutral-400 uppercase tracking-widest ml-1">Temporary Password</label>
273
+ <label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest ml-1">Temporary Password</label>
268
274
  <div className="relative">
269
275
  <input
270
276
  required
271
277
  type={showPassword ? "text" : "password"}
272
- className="w-full px-5 py-3 bg-neutral-100 dark:bg-neutral-800 border-none rounded-2xl focus:ring-2 focus:ring-primary outline-none text-sm transition-all font-mono text-neutral-900 dark:text-neutral-100"
278
+ className="w-full px-5 py-3 bg-dashboard-bg border border-dashboard-border rounded-2xl focus:ring-2 focus:ring-primary outline-none text-sm transition-all font-mono text-dashboard-text"
273
279
  value={formData.password}
274
280
  onChange={e => setFormData({ ...formData, password: e.target.value })}
275
281
  />
276
282
  <div className="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-1">
277
- <button type="button" onClick={() => setShowPassword(!showPassword)} className="p-1.5 text-neutral-400 hover:text-primary">
283
+ <button type="button" onClick={() => setShowPassword(!showPassword)} className="p-1.5 text-dashboard-text-secondary hover:text-primary">
278
284
  {showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
279
285
  </button>
280
- <button type="button" onClick={copyToClipboard} className="p-1.5 text-neutral-400 hover:text-primary">
286
+ <button type="button" onClick={copyToClipboard} className="p-1.5 text-dashboard-text-secondary hover:text-primary">
281
287
  {copied ? <Check size={16} className="text-emerald-500" /> : <Copy size={16} />}
282
288
  </button>
283
289
  </div>
@@ -292,10 +298,10 @@ export default function UserManagement({ locale = 'en' }: { locale?: string }) {
292
298
  </div>
293
299
 
294
300
  <div className="space-y-2">
295
- <label className="text-xs font-bold text-neutral-500 dark:text-neutral-400 uppercase tracking-widest ml-1">Role</label>
301
+ <label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest ml-1">Role</label>
296
302
  <div className="relative">
297
303
  <select
298
- className="w-full px-5 py-3 bg-neutral-100 dark:bg-neutral-800 border-none rounded-2xl focus:ring-2 focus:ring-primary outline-none text-sm appearance-none cursor-pointer text-neutral-900 dark:text-neutral-100"
304
+ className="w-full px-5 py-3 bg-dashboard-bg border border-dashboard-border rounded-2xl focus:ring-2 focus:ring-primary outline-none text-sm appearance-none cursor-pointer text-dashboard-text"
299
305
  value={formData.role}
300
306
  onChange={e => setFormData({ ...formData, role: e.target.value as any })}
301
307
  >
@@ -303,7 +309,7 @@ export default function UserManagement({ locale = 'en' }: { locale?: string }) {
303
309
  <option value="dev">Developer</option>
304
310
  <option value="admin">Admin (All settings)</option>
305
311
  </select>
306
- <div className="absolute right-4 top-1/2 -translate-y-1/2 pointer-events-none text-neutral-400">
312
+ <div className="absolute right-4 top-1/2 -translate-y-1/2 pointer-events-none text-dashboard-text-secondary">
307
313
  <Shield size={16} />
308
314
  </div>
309
315
  </div>
@@ -312,7 +318,7 @@ export default function UserManagement({ locale = 'en' }: { locale?: string }) {
312
318
  <button
313
319
  type="submit"
314
320
  disabled={isCreating}
315
- className="w-full py-4 bg-primary text-white rounded-2xl font-bold shadow-lg shadow-primary/20 hover:bg-primary/90 transition-all mt-4 flex items-center justify-center gap-2"
321
+ className="w-full py-4 bg-primary text-white rounded-full text-[10px] font-black uppercase tracking-widest shadow-lg shadow-primary/20 hover:bg-primary/90 transition-all mt-4 flex items-center justify-center gap-2"
316
322
  >
317
323
  {isCreating ? <Loader2 className="animate-spin" size={20} /> : "Add User"}
318
324
  </button>