@lovelybunch/api 1.0.56 → 1.0.57
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.
- package/dist/lib/auth/auth-manager.d.ts +122 -0
- package/dist/lib/auth/auth-manager.js +411 -0
- package/dist/middleware/auth.d.ts +31 -0
- package/dist/middleware/auth.js +142 -0
- package/dist/routes/api/v1/api-keys/index.d.ts +1 -0
- package/dist/routes/api/v1/api-keys/index.js +1 -0
- package/dist/routes/api/v1/api-keys/route.d.ts +3 -0
- package/dist/routes/api/v1/api-keys/route.js +120 -0
- package/dist/routes/api/v1/auth/index.d.ts +1 -0
- package/dist/routes/api/v1/auth/index.js +1 -0
- package/dist/routes/api/v1/auth/route.d.ts +3 -0
- package/dist/routes/api/v1/auth/route.js +230 -0
- package/dist/routes/api/v1/auth-settings/index.d.ts +1 -0
- package/dist/routes/api/v1/auth-settings/index.js +1 -0
- package/dist/routes/api/v1/auth-settings/route.d.ts +3 -0
- package/dist/routes/api/v1/auth-settings/route.js +249 -0
- package/dist/routes/api/v1/context/knowledge/[filename]/route.js +1 -1
- package/dist/routes/api/v1/context/knowledge/route.js +2 -2
- package/dist/server-with-static.js +9 -0
- package/dist/server.js +9 -0
- package/package.json +11 -4
- package/static/assets/index-CRg4lVi6.js +779 -0
- package/static/assets/index-VqhUTak4.css +33 -0
- package/static/index.html +2 -2
- package/static/assets/index-BvXDHRet.css +0 -33
- package/static/assets/index-NCb27WQQ.js +0 -747
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { AuthConfig, LocalAuthUser, AuthSession, ApiKey, UserRole } from '@lovelybunch/types';
|
|
2
|
+
export declare class AuthManager {
|
|
3
|
+
private authConfigPath;
|
|
4
|
+
private authConfig;
|
|
5
|
+
constructor(dataPath: string);
|
|
6
|
+
/**
|
|
7
|
+
* Check if auth is enabled
|
|
8
|
+
*/
|
|
9
|
+
isAuthEnabled(): Promise<boolean>;
|
|
10
|
+
/**
|
|
11
|
+
* Load auth config from file
|
|
12
|
+
*/
|
|
13
|
+
loadAuthConfig(): Promise<AuthConfig>;
|
|
14
|
+
/**
|
|
15
|
+
* Save auth config to file
|
|
16
|
+
*/
|
|
17
|
+
saveAuthConfig(config: AuthConfig): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Initialize auth config with defaults
|
|
20
|
+
*/
|
|
21
|
+
initializeAuthConfig(adminEmail: string, adminName: string): Promise<AuthConfig>;
|
|
22
|
+
/**
|
|
23
|
+
* Find user by email
|
|
24
|
+
*/
|
|
25
|
+
findUserByEmail(email: string): Promise<LocalAuthUser | null>;
|
|
26
|
+
/**
|
|
27
|
+
* Find user by ID
|
|
28
|
+
*/
|
|
29
|
+
findUserById(userId: string): Promise<LocalAuthUser | null>;
|
|
30
|
+
/**
|
|
31
|
+
* Check if email is whitelisted
|
|
32
|
+
*/
|
|
33
|
+
isEmailWhitelisted(email: string): Promise<boolean>;
|
|
34
|
+
/**
|
|
35
|
+
* Register a new user (must be whitelisted)
|
|
36
|
+
*/
|
|
37
|
+
registerUser(email: string, password: string, name: string): Promise<LocalAuthUser>;
|
|
38
|
+
/**
|
|
39
|
+
* Verify user credentials
|
|
40
|
+
*/
|
|
41
|
+
verifyCredentials(email: string, password: string): Promise<LocalAuthUser | null>;
|
|
42
|
+
/**
|
|
43
|
+
* Change user password
|
|
44
|
+
*/
|
|
45
|
+
changePassword(userId: string, currentPassword: string, newPassword: string): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Admin reset password (no current password required)
|
|
48
|
+
*/
|
|
49
|
+
adminResetPassword(userId: string, newPassword: string): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Update user's last login time
|
|
52
|
+
*/
|
|
53
|
+
updateUserLastLogin(userId: string): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Add a new whitelisted user
|
|
56
|
+
*/
|
|
57
|
+
addWhitelistedUser(email: string, name: string, role?: UserRole): Promise<LocalAuthUser>;
|
|
58
|
+
/**
|
|
59
|
+
* Remove a user
|
|
60
|
+
*/
|
|
61
|
+
removeUser(userId: string): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Update user role
|
|
64
|
+
*/
|
|
65
|
+
updateUserRole(userId: string, role: UserRole): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Generate JWT token
|
|
68
|
+
*/
|
|
69
|
+
generateToken(user: LocalAuthUser, provider?: string): Promise<string>;
|
|
70
|
+
/**
|
|
71
|
+
* Verify JWT token
|
|
72
|
+
*/
|
|
73
|
+
verifyToken(token: string): Promise<AuthSession | null>;
|
|
74
|
+
/**
|
|
75
|
+
* Create API key
|
|
76
|
+
*/
|
|
77
|
+
createApiKey(name: string, createdBy: string, scopes: string[], expiresIn?: string): Promise<{
|
|
78
|
+
apiKey: ApiKey;
|
|
79
|
+
rawKey: string;
|
|
80
|
+
}>;
|
|
81
|
+
/**
|
|
82
|
+
* Verify API key
|
|
83
|
+
*/
|
|
84
|
+
verifyApiKey(rawKey: string): Promise<ApiKey | null>;
|
|
85
|
+
/**
|
|
86
|
+
* Update API key last used time
|
|
87
|
+
*/
|
|
88
|
+
updateApiKeyLastUsed(apiKeyId: string): Promise<void>;
|
|
89
|
+
/**
|
|
90
|
+
* Delete API key
|
|
91
|
+
*/
|
|
92
|
+
deleteApiKey(apiKeyId: string): Promise<void>;
|
|
93
|
+
/**
|
|
94
|
+
* List all API keys (without raw keys)
|
|
95
|
+
*/
|
|
96
|
+
listApiKeys(): Promise<ApiKey[]>;
|
|
97
|
+
/**
|
|
98
|
+
* Generate a secure random ID
|
|
99
|
+
*/
|
|
100
|
+
private generateId;
|
|
101
|
+
/**
|
|
102
|
+
* Generate a secure random secret
|
|
103
|
+
*/
|
|
104
|
+
private generateSecret;
|
|
105
|
+
/**
|
|
106
|
+
* Generate a secure API key
|
|
107
|
+
*/
|
|
108
|
+
private generateApiKey;
|
|
109
|
+
/**
|
|
110
|
+
* Parse expiry string to seconds
|
|
111
|
+
*/
|
|
112
|
+
private parseExpiry;
|
|
113
|
+
/**
|
|
114
|
+
* Calculate expiry date from string
|
|
115
|
+
*/
|
|
116
|
+
private calculateExpiry;
|
|
117
|
+
/**
|
|
118
|
+
* Clear cached config (useful for testing)
|
|
119
|
+
*/
|
|
120
|
+
clearCache(): void;
|
|
121
|
+
}
|
|
122
|
+
export declare function getAuthManager(dataPath?: string): AuthManager;
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import bcrypt from 'bcrypt';
|
|
4
|
+
import jwt from 'jsonwebtoken';
|
|
5
|
+
import crypto from 'crypto';
|
|
6
|
+
const SALT_ROUNDS = 10;
|
|
7
|
+
const DEFAULT_SESSION_EXPIRY = '7d';
|
|
8
|
+
const DEFAULT_COOKIE_NAME = 'nut-session';
|
|
9
|
+
export class AuthManager {
|
|
10
|
+
authConfigPath;
|
|
11
|
+
authConfig = null;
|
|
12
|
+
constructor(dataPath) {
|
|
13
|
+
this.authConfigPath = path.join(dataPath, '.nut', 'auth.json');
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Check if auth is enabled
|
|
17
|
+
*/
|
|
18
|
+
async isAuthEnabled() {
|
|
19
|
+
try {
|
|
20
|
+
const config = await this.loadAuthConfig();
|
|
21
|
+
return config.enabled;
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
// If config doesn't exist or can't be loaded, auth is disabled
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Load auth config from file
|
|
30
|
+
*/
|
|
31
|
+
async loadAuthConfig() {
|
|
32
|
+
if (this.authConfig) {
|
|
33
|
+
return this.authConfig;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const content = await fs.readFile(this.authConfigPath, 'utf-8');
|
|
37
|
+
this.authConfig = JSON.parse(content);
|
|
38
|
+
return this.authConfig;
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
throw new Error('Auth config not found or invalid');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Save auth config to file
|
|
46
|
+
*/
|
|
47
|
+
async saveAuthConfig(config) {
|
|
48
|
+
await fs.writeFile(this.authConfigPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
49
|
+
this.authConfig = config;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Initialize auth config with defaults
|
|
53
|
+
*/
|
|
54
|
+
async initializeAuthConfig(adminEmail, adminName) {
|
|
55
|
+
const config = {
|
|
56
|
+
version: '1.0',
|
|
57
|
+
enabled: false, // Start disabled, admin must enable
|
|
58
|
+
allowRegistration: true,
|
|
59
|
+
providers: {
|
|
60
|
+
local: {
|
|
61
|
+
enabled: true,
|
|
62
|
+
users: [
|
|
63
|
+
{
|
|
64
|
+
id: this.generateId(),
|
|
65
|
+
email: adminEmail,
|
|
66
|
+
name: adminName,
|
|
67
|
+
role: 'admin',
|
|
68
|
+
registered: false,
|
|
69
|
+
createdAt: new Date(),
|
|
70
|
+
updatedAt: new Date(),
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
oauth: {},
|
|
75
|
+
},
|
|
76
|
+
session: {
|
|
77
|
+
secret: this.generateSecret(),
|
|
78
|
+
expiresIn: DEFAULT_SESSION_EXPIRY,
|
|
79
|
+
cookieName: DEFAULT_COOKIE_NAME,
|
|
80
|
+
secure: process.env.NODE_ENV === 'production',
|
|
81
|
+
},
|
|
82
|
+
apiKeys: [],
|
|
83
|
+
};
|
|
84
|
+
await this.saveAuthConfig(config);
|
|
85
|
+
return config;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Find user by email
|
|
89
|
+
*/
|
|
90
|
+
async findUserByEmail(email) {
|
|
91
|
+
const config = await this.loadAuthConfig();
|
|
92
|
+
const user = config.providers.local.users.find((u) => u.email.toLowerCase() === email.toLowerCase());
|
|
93
|
+
return user || null;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Find user by ID
|
|
97
|
+
*/
|
|
98
|
+
async findUserById(userId) {
|
|
99
|
+
const config = await this.loadAuthConfig();
|
|
100
|
+
const user = config.providers.local.users.find((u) => u.id === userId);
|
|
101
|
+
return user || null;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Check if email is whitelisted
|
|
105
|
+
*/
|
|
106
|
+
async isEmailWhitelisted(email) {
|
|
107
|
+
const config = await this.loadAuthConfig();
|
|
108
|
+
return config.providers.local.users.some((u) => u.email.toLowerCase() === email.toLowerCase());
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Register a new user (must be whitelisted)
|
|
112
|
+
*/
|
|
113
|
+
async registerUser(email, password, name) {
|
|
114
|
+
const config = await this.loadAuthConfig();
|
|
115
|
+
if (!config.allowRegistration) {
|
|
116
|
+
throw new Error('Registration is disabled');
|
|
117
|
+
}
|
|
118
|
+
// Find the whitelisted user entry
|
|
119
|
+
const userIndex = config.providers.local.users.findIndex((u) => u.email.toLowerCase() === email.toLowerCase());
|
|
120
|
+
if (userIndex === -1) {
|
|
121
|
+
throw new Error('Email not whitelisted');
|
|
122
|
+
}
|
|
123
|
+
const user = config.providers.local.users[userIndex];
|
|
124
|
+
if (user.registered) {
|
|
125
|
+
throw new Error('User already registered');
|
|
126
|
+
}
|
|
127
|
+
// Hash password
|
|
128
|
+
const passwordHash = await bcrypt.hash(password, SALT_ROUNDS);
|
|
129
|
+
// Update user
|
|
130
|
+
user.name = name;
|
|
131
|
+
user.passwordHash = passwordHash;
|
|
132
|
+
user.registered = true;
|
|
133
|
+
user.updatedAt = new Date();
|
|
134
|
+
await this.saveAuthConfig(config);
|
|
135
|
+
return user;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Verify user credentials
|
|
139
|
+
*/
|
|
140
|
+
async verifyCredentials(email, password) {
|
|
141
|
+
const user = await this.findUserByEmail(email);
|
|
142
|
+
if (!user || !user.registered || !user.passwordHash) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
const isValid = await bcrypt.compare(password, user.passwordHash);
|
|
146
|
+
if (!isValid) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
// Update last login
|
|
150
|
+
await this.updateUserLastLogin(user.id);
|
|
151
|
+
return user;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Change user password
|
|
155
|
+
*/
|
|
156
|
+
async changePassword(userId, currentPassword, newPassword) {
|
|
157
|
+
const config = await this.loadAuthConfig();
|
|
158
|
+
const userIndex = config.providers.local.users.findIndex((u) => u.id === userId);
|
|
159
|
+
if (userIndex === -1) {
|
|
160
|
+
throw new Error('User not found');
|
|
161
|
+
}
|
|
162
|
+
const user = config.providers.local.users[userIndex];
|
|
163
|
+
if (!user.passwordHash) {
|
|
164
|
+
throw new Error('User has no password set');
|
|
165
|
+
}
|
|
166
|
+
// Verify current password
|
|
167
|
+
const isValid = await bcrypt.compare(currentPassword, user.passwordHash);
|
|
168
|
+
if (!isValid) {
|
|
169
|
+
throw new Error('Current password is incorrect');
|
|
170
|
+
}
|
|
171
|
+
// Hash and set new password
|
|
172
|
+
user.passwordHash = await bcrypt.hash(newPassword, SALT_ROUNDS);
|
|
173
|
+
user.updatedAt = new Date();
|
|
174
|
+
await this.saveAuthConfig(config);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Admin reset password (no current password required)
|
|
178
|
+
*/
|
|
179
|
+
async adminResetPassword(userId, newPassword) {
|
|
180
|
+
const config = await this.loadAuthConfig();
|
|
181
|
+
const userIndex = config.providers.local.users.findIndex((u) => u.id === userId);
|
|
182
|
+
if (userIndex === -1) {
|
|
183
|
+
throw new Error('User not found');
|
|
184
|
+
}
|
|
185
|
+
const user = config.providers.local.users[userIndex];
|
|
186
|
+
user.passwordHash = await bcrypt.hash(newPassword, SALT_ROUNDS);
|
|
187
|
+
user.updatedAt = new Date();
|
|
188
|
+
await this.saveAuthConfig(config);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Update user's last login time
|
|
192
|
+
*/
|
|
193
|
+
async updateUserLastLogin(userId) {
|
|
194
|
+
const config = await this.loadAuthConfig();
|
|
195
|
+
const user = config.providers.local.users.find((u) => u.id === userId);
|
|
196
|
+
if (user) {
|
|
197
|
+
user.lastLoginAt = new Date();
|
|
198
|
+
await this.saveAuthConfig(config);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Add a new whitelisted user
|
|
203
|
+
*/
|
|
204
|
+
async addWhitelistedUser(email, name, role = 'viewer') {
|
|
205
|
+
const config = await this.loadAuthConfig();
|
|
206
|
+
// Check if user already exists
|
|
207
|
+
const existingUser = config.providers.local.users.find((u) => u.email.toLowerCase() === email.toLowerCase());
|
|
208
|
+
if (existingUser) {
|
|
209
|
+
throw new Error('User already exists');
|
|
210
|
+
}
|
|
211
|
+
const newUser = {
|
|
212
|
+
id: this.generateId(),
|
|
213
|
+
email,
|
|
214
|
+
name,
|
|
215
|
+
role,
|
|
216
|
+
registered: false,
|
|
217
|
+
createdAt: new Date(),
|
|
218
|
+
updatedAt: new Date(),
|
|
219
|
+
};
|
|
220
|
+
config.providers.local.users.push(newUser);
|
|
221
|
+
await this.saveAuthConfig(config);
|
|
222
|
+
return newUser;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Remove a user
|
|
226
|
+
*/
|
|
227
|
+
async removeUser(userId) {
|
|
228
|
+
const config = await this.loadAuthConfig();
|
|
229
|
+
const userIndex = config.providers.local.users.findIndex((u) => u.id === userId);
|
|
230
|
+
if (userIndex === -1) {
|
|
231
|
+
throw new Error('User not found');
|
|
232
|
+
}
|
|
233
|
+
config.providers.local.users.splice(userIndex, 1);
|
|
234
|
+
await this.saveAuthConfig(config);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Update user role
|
|
238
|
+
*/
|
|
239
|
+
async updateUserRole(userId, role) {
|
|
240
|
+
const config = await this.loadAuthConfig();
|
|
241
|
+
const user = config.providers.local.users.find((u) => u.id === userId);
|
|
242
|
+
if (!user) {
|
|
243
|
+
throw new Error('User not found');
|
|
244
|
+
}
|
|
245
|
+
user.role = role;
|
|
246
|
+
user.updatedAt = new Date();
|
|
247
|
+
await this.saveAuthConfig(config);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Generate JWT token
|
|
251
|
+
*/
|
|
252
|
+
async generateToken(user, provider = 'local') {
|
|
253
|
+
const config = await this.loadAuthConfig();
|
|
254
|
+
const payload = {
|
|
255
|
+
userId: user.id,
|
|
256
|
+
email: user.email,
|
|
257
|
+
name: user.name,
|
|
258
|
+
role: user.role,
|
|
259
|
+
provider: provider,
|
|
260
|
+
iat: Math.floor(Date.now() / 1000),
|
|
261
|
+
exp: Math.floor(Date.now() / 1000) + this.parseExpiry(config.session.expiresIn),
|
|
262
|
+
};
|
|
263
|
+
return jwt.sign(payload, config.session.secret);
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Verify JWT token
|
|
267
|
+
*/
|
|
268
|
+
async verifyToken(token) {
|
|
269
|
+
try {
|
|
270
|
+
const config = await this.loadAuthConfig();
|
|
271
|
+
const decoded = jwt.verify(token, config.session.secret);
|
|
272
|
+
return decoded;
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Create API key
|
|
280
|
+
*/
|
|
281
|
+
async createApiKey(name, createdBy, scopes, expiresIn) {
|
|
282
|
+
const config = await this.loadAuthConfig();
|
|
283
|
+
const rawKey = this.generateApiKey();
|
|
284
|
+
const hashedKey = await bcrypt.hash(rawKey, SALT_ROUNDS);
|
|
285
|
+
const apiKey = {
|
|
286
|
+
id: this.generateId(),
|
|
287
|
+
name,
|
|
288
|
+
key: hashedKey,
|
|
289
|
+
keyPreview: rawKey.slice(-4),
|
|
290
|
+
createdBy,
|
|
291
|
+
createdAt: new Date(),
|
|
292
|
+
expiresAt: expiresIn ? this.calculateExpiry(expiresIn) : undefined,
|
|
293
|
+
scopes,
|
|
294
|
+
};
|
|
295
|
+
config.apiKeys.push(apiKey);
|
|
296
|
+
await this.saveAuthConfig(config);
|
|
297
|
+
return { apiKey, rawKey };
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Verify API key
|
|
301
|
+
*/
|
|
302
|
+
async verifyApiKey(rawKey) {
|
|
303
|
+
const config = await this.loadAuthConfig();
|
|
304
|
+
for (const apiKey of config.apiKeys) {
|
|
305
|
+
const isValid = await bcrypt.compare(rawKey, apiKey.key);
|
|
306
|
+
if (isValid) {
|
|
307
|
+
// Check expiration
|
|
308
|
+
if (apiKey.expiresAt && new Date(apiKey.expiresAt) < new Date()) {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
// Update last used
|
|
312
|
+
await this.updateApiKeyLastUsed(apiKey.id);
|
|
313
|
+
return apiKey;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Update API key last used time
|
|
320
|
+
*/
|
|
321
|
+
async updateApiKeyLastUsed(apiKeyId) {
|
|
322
|
+
const config = await this.loadAuthConfig();
|
|
323
|
+
const apiKey = config.apiKeys.find((k) => k.id === apiKeyId);
|
|
324
|
+
if (apiKey) {
|
|
325
|
+
apiKey.lastUsedAt = new Date();
|
|
326
|
+
await this.saveAuthConfig(config);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Delete API key
|
|
331
|
+
*/
|
|
332
|
+
async deleteApiKey(apiKeyId) {
|
|
333
|
+
const config = await this.loadAuthConfig();
|
|
334
|
+
const keyIndex = config.apiKeys.findIndex((k) => k.id === apiKeyId);
|
|
335
|
+
if (keyIndex === -1) {
|
|
336
|
+
throw new Error('API key not found');
|
|
337
|
+
}
|
|
338
|
+
config.apiKeys.splice(keyIndex, 1);
|
|
339
|
+
await this.saveAuthConfig(config);
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* List all API keys (without raw keys)
|
|
343
|
+
*/
|
|
344
|
+
async listApiKeys() {
|
|
345
|
+
const config = await this.loadAuthConfig();
|
|
346
|
+
return config.apiKeys;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Generate a secure random ID
|
|
350
|
+
*/
|
|
351
|
+
generateId() {
|
|
352
|
+
return crypto.randomBytes(16).toString('hex');
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Generate a secure random secret
|
|
356
|
+
*/
|
|
357
|
+
generateSecret() {
|
|
358
|
+
return crypto.randomBytes(32).toString('hex');
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Generate a secure API key
|
|
362
|
+
*/
|
|
363
|
+
generateApiKey() {
|
|
364
|
+
return `nut_${crypto.randomBytes(32).toString('hex')}`;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Parse expiry string to seconds
|
|
368
|
+
*/
|
|
369
|
+
parseExpiry(expiry) {
|
|
370
|
+
const match = expiry.match(/^(\d+)([smhd])$/);
|
|
371
|
+
if (!match) {
|
|
372
|
+
return 7 * 24 * 60 * 60; // Default to 7 days
|
|
373
|
+
}
|
|
374
|
+
const value = parseInt(match[1]);
|
|
375
|
+
const unit = match[2];
|
|
376
|
+
switch (unit) {
|
|
377
|
+
case 's':
|
|
378
|
+
return value;
|
|
379
|
+
case 'm':
|
|
380
|
+
return value * 60;
|
|
381
|
+
case 'h':
|
|
382
|
+
return value * 60 * 60;
|
|
383
|
+
case 'd':
|
|
384
|
+
return value * 24 * 60 * 60;
|
|
385
|
+
default:
|
|
386
|
+
return 7 * 24 * 60 * 60;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Calculate expiry date from string
|
|
391
|
+
*/
|
|
392
|
+
calculateExpiry(expiresIn) {
|
|
393
|
+
const seconds = this.parseExpiry(expiresIn);
|
|
394
|
+
return new Date(Date.now() + seconds * 1000);
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Clear cached config (useful for testing)
|
|
398
|
+
*/
|
|
399
|
+
clearCache() {
|
|
400
|
+
this.authConfig = null;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// Singleton instance
|
|
404
|
+
let authManagerInstance = null;
|
|
405
|
+
export function getAuthManager(dataPath) {
|
|
406
|
+
if (!authManagerInstance) {
|
|
407
|
+
const path = dataPath || process.env.GAIT_DATA_PATH || process.cwd();
|
|
408
|
+
authManagerInstance = new AuthManager(path);
|
|
409
|
+
}
|
|
410
|
+
return authManagerInstance;
|
|
411
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Context, Next } from 'hono';
|
|
2
|
+
import { AuthSession } from '@lovelybunch/types';
|
|
3
|
+
/**
|
|
4
|
+
* Authentication middleware
|
|
5
|
+
* Checks for valid JWT token in cookie or Authorization header
|
|
6
|
+
*/
|
|
7
|
+
export declare function authMiddleware(c: Context, next: Next): Promise<void | (Response & import("hono").TypedResponse<{
|
|
8
|
+
error: string;
|
|
9
|
+
message: string;
|
|
10
|
+
}, 401, "json">) | (Response & import("hono").TypedResponse<{
|
|
11
|
+
error: string;
|
|
12
|
+
message: string;
|
|
13
|
+
}, 403, "json">)>;
|
|
14
|
+
/**
|
|
15
|
+
* Helper to get current session from context
|
|
16
|
+
*/
|
|
17
|
+
export declare function getSession(c: Context): AuthSession | null;
|
|
18
|
+
/**
|
|
19
|
+
* Helper to check if user is admin
|
|
20
|
+
*/
|
|
21
|
+
export declare function isAdmin(c: Context): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Helper to require authentication (throws if not authenticated)
|
|
24
|
+
* Returns null if auth is disabled
|
|
25
|
+
*/
|
|
26
|
+
export declare function requireAuth(c: Context): AuthSession | null;
|
|
27
|
+
/**
|
|
28
|
+
* Helper to require admin access (throws if not admin)
|
|
29
|
+
* Returns null if auth is disabled
|
|
30
|
+
*/
|
|
31
|
+
export declare function requireAdmin(c: Context): AuthSession | null;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { getCookie } from 'hono/cookie';
|
|
2
|
+
import { getAuthManager } from '../lib/auth/auth-manager.js';
|
|
3
|
+
// Public routes that don't require authentication
|
|
4
|
+
const PUBLIC_ROUTES = [
|
|
5
|
+
'/api/health',
|
|
6
|
+
'/api/v1/auth/status',
|
|
7
|
+
'/api/v1/auth/login',
|
|
8
|
+
'/api/v1/auth/register',
|
|
9
|
+
'/api/v1/auth/oauth',
|
|
10
|
+
'/api/v1/auth/callback',
|
|
11
|
+
];
|
|
12
|
+
// Routes that require specific roles
|
|
13
|
+
const ADMIN_ROUTES = [
|
|
14
|
+
'/api/v1/auth-settings',
|
|
15
|
+
'/api/v1/config',
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* Check if a route is public (doesn't require auth)
|
|
19
|
+
*/
|
|
20
|
+
function isPublicRoute(path) {
|
|
21
|
+
return PUBLIC_ROUTES.some((route) => path.startsWith(route));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Check if a route requires admin access
|
|
25
|
+
*/
|
|
26
|
+
function isAdminRoute(path) {
|
|
27
|
+
return ADMIN_ROUTES.some((route) => path.startsWith(route));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Authentication middleware
|
|
31
|
+
* Checks for valid JWT token in cookie or Authorization header
|
|
32
|
+
*/
|
|
33
|
+
export async function authMiddleware(c, next) {
|
|
34
|
+
const authManager = getAuthManager();
|
|
35
|
+
// Check if auth is enabled
|
|
36
|
+
const authEnabled = await authManager.isAuthEnabled();
|
|
37
|
+
// Store auth status in context so helpers can check it
|
|
38
|
+
c.set('authEnabled', authEnabled);
|
|
39
|
+
if (!authEnabled) {
|
|
40
|
+
// Auth is disabled, allow all requests
|
|
41
|
+
return next();
|
|
42
|
+
}
|
|
43
|
+
const path = c.req.path;
|
|
44
|
+
// Allow public routes without authentication
|
|
45
|
+
if (isPublicRoute(path)) {
|
|
46
|
+
return next();
|
|
47
|
+
}
|
|
48
|
+
// Try to get token from cookie first
|
|
49
|
+
let token;
|
|
50
|
+
try {
|
|
51
|
+
const config = await authManager.loadAuthConfig();
|
|
52
|
+
token = getCookie(c, config.session.cookieName);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
// Config not available, try default cookie name
|
|
56
|
+
token = getCookie(c, 'nut-session');
|
|
57
|
+
}
|
|
58
|
+
// If no cookie, try Authorization header
|
|
59
|
+
if (!token) {
|
|
60
|
+
const authHeader = c.req.header('Authorization');
|
|
61
|
+
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
62
|
+
token = authHeader.substring(7);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// If no token, check for API key
|
|
66
|
+
if (!token) {
|
|
67
|
+
const apiKey = c.req.header('X-API-Key');
|
|
68
|
+
if (apiKey) {
|
|
69
|
+
const validApiKey = await authManager.verifyApiKey(apiKey);
|
|
70
|
+
if (validApiKey) {
|
|
71
|
+
// Store API key info in context
|
|
72
|
+
c.set('apiKey', validApiKey);
|
|
73
|
+
c.set('authType', 'apikey');
|
|
74
|
+
return next();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (!token) {
|
|
79
|
+
return c.json({ error: 'Unauthorized', message: 'No authentication token provided' }, 401);
|
|
80
|
+
}
|
|
81
|
+
// Verify token
|
|
82
|
+
const session = await authManager.verifyToken(token);
|
|
83
|
+
if (!session) {
|
|
84
|
+
return c.json({ error: 'Unauthorized', message: 'Invalid or expired token' }, 401);
|
|
85
|
+
}
|
|
86
|
+
// Check if route requires admin access
|
|
87
|
+
if (isAdminRoute(path) && session.role !== 'admin') {
|
|
88
|
+
return c.json({ error: 'Forbidden', message: 'Admin access required' }, 403);
|
|
89
|
+
}
|
|
90
|
+
// Store session in context
|
|
91
|
+
c.set('session', session);
|
|
92
|
+
c.set('authType', 'session');
|
|
93
|
+
return next();
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Helper to get current session from context
|
|
97
|
+
*/
|
|
98
|
+
export function getSession(c) {
|
|
99
|
+
return c.get('session') || null;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Helper to check if user is admin
|
|
103
|
+
*/
|
|
104
|
+
export function isAdmin(c) {
|
|
105
|
+
const session = getSession(c);
|
|
106
|
+
return session?.role === 'admin';
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Helper to require authentication (throws if not authenticated)
|
|
110
|
+
* Returns null if auth is disabled
|
|
111
|
+
*/
|
|
112
|
+
export function requireAuth(c) {
|
|
113
|
+
const authEnabled = c.get('authEnabled');
|
|
114
|
+
// If auth is disabled, return null (no authentication required)
|
|
115
|
+
if (authEnabled === false) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
const session = getSession(c);
|
|
119
|
+
if (!session) {
|
|
120
|
+
throw new Error('Authentication required');
|
|
121
|
+
}
|
|
122
|
+
return session;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Helper to require admin access (throws if not admin)
|
|
126
|
+
* Returns null if auth is disabled
|
|
127
|
+
*/
|
|
128
|
+
export function requireAdmin(c) {
|
|
129
|
+
const authEnabled = c.get('authEnabled');
|
|
130
|
+
// If auth is disabled, return null (no admin check required)
|
|
131
|
+
if (authEnabled === false) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
const session = requireAuth(c);
|
|
135
|
+
if (!session) {
|
|
136
|
+
throw new Error('Authentication required');
|
|
137
|
+
}
|
|
138
|
+
if (session.role !== 'admin') {
|
|
139
|
+
throw new Error('Admin access required');
|
|
140
|
+
}
|
|
141
|
+
return session;
|
|
142
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './route.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './route.js';
|