@spfn/auth 0.1.0-alpha.0 → 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +70 -12
- package/dist/api-BcQM4WKb.d.ts +45 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +1 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.js +8966 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/contracts/auth.d.ts +262 -0
- package/dist/lib/contracts/auth.js +2923 -0
- package/dist/lib/contracts/auth.js.map +1 -0
- package/dist/lib/contracts/index.d.ts +3 -0
- package/dist/lib/contracts/index.js +3162 -0
- package/dist/lib/contracts/index.js.map +1 -0
- package/dist/lib/contracts/invitation.d.ts +243 -0
- package/dist/lib/contracts/invitation.js +2883 -0
- package/dist/lib/contracts/invitation.js.map +1 -0
- package/dist/plugin.d.ts +12 -0
- package/dist/plugin.js +8949 -0
- package/dist/plugin.js.map +1 -0
- package/dist/server/entities/index.d.ts +10 -0
- package/dist/server/entities/index.js +399 -0
- package/dist/server/entities/index.js.map +1 -0
- package/dist/server/entities/invitations.d.ts +241 -0
- package/dist/server/entities/invitations.js +181 -0
- package/dist/server/entities/invitations.js.map +1 -0
- package/dist/server/entities/permissions.d.ts +196 -0
- package/dist/server/entities/permissions.js +44 -0
- package/dist/server/entities/permissions.js.map +1 -0
- package/dist/server/entities/role-permissions.d.ts +107 -0
- package/dist/server/entities/role-permissions.js +112 -0
- package/dist/server/entities/role-permissions.js.map +1 -0
- package/dist/server/entities/roles.d.ts +196 -0
- package/dist/server/entities/roles.js +45 -0
- package/dist/server/entities/roles.js.map +1 -0
- package/dist/server/entities/user-permissions.d.ts +163 -0
- package/dist/server/entities/user-permissions.js +191 -0
- package/dist/server/entities/user-permissions.js.map +1 -0
- package/dist/server/entities/user-public-keys.d.ts +227 -0
- package/dist/server/entities/user-public-keys.js +153 -0
- package/dist/server/entities/user-public-keys.js.map +1 -0
- package/dist/server/entities/user-social-accounts.d.ts +189 -0
- package/dist/server/entities/user-social-accounts.js +146 -0
- package/dist/server/entities/user-social-accounts.js.map +1 -0
- package/dist/server/entities/users.d.ts +235 -0
- package/dist/server/entities/users.js +113 -0
- package/dist/server/entities/users.js.map +1 -0
- package/dist/server/entities/verification-codes.d.ts +191 -0
- package/dist/server/entities/verification-codes.js +44 -0
- package/dist/server/entities/verification-codes.js.map +1 -0
- package/dist/server/routes/auth/index.d.ts +10 -0
- package/dist/server/routes/auth/index.js +4475 -0
- package/dist/server/routes/auth/index.js.map +1 -0
- package/dist/server/routes/index.d.ts +6 -0
- package/dist/server/routes/index.js +6352 -0
- package/dist/server/routes/index.js.map +1 -0
- package/dist/server/routes/invitations/index.d.ts +10 -0
- package/dist/server/routes/invitations/index.js +4209 -0
- package/dist/server/routes/invitations/index.js.map +1 -0
- package/dist/server.d.ts +1243 -0
- package/dist/server.js +2281 -0
- package/dist/server.js.map +1 -0
- package/migrations/0000_tired_gambit.sql +165 -0
- package/migrations/meta/0000_snapshot.json +1395 -0
- package/migrations/meta/_journal.json +13 -0
- package/package.json +32 -24
package/dist/server.js
ADDED
|
@@ -0,0 +1,2281 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/server/entities/roles.ts
|
|
12
|
+
import { text, boolean, integer, index } from "drizzle-orm/pg-core";
|
|
13
|
+
import { id, timestamps, createFunctionSchema } from "@spfn/core/db";
|
|
14
|
+
var schema, roles;
|
|
15
|
+
var init_roles = __esm({
|
|
16
|
+
"src/server/entities/roles.ts"() {
|
|
17
|
+
"use strict";
|
|
18
|
+
schema = createFunctionSchema("@spfn/auth");
|
|
19
|
+
roles = schema.table(
|
|
20
|
+
"roles",
|
|
21
|
+
{
|
|
22
|
+
// Primary key
|
|
23
|
+
id: id(),
|
|
24
|
+
// Role identifier (used in code, e.g., 'admin', 'editor')
|
|
25
|
+
// Must be unique, lowercase, kebab-case recommended
|
|
26
|
+
name: text("name").notNull().unique(),
|
|
27
|
+
// Display name for UI (e.g., 'Administrator', 'Content Editor')
|
|
28
|
+
displayName: text("display_name").notNull(),
|
|
29
|
+
// Role description
|
|
30
|
+
description: text("description"),
|
|
31
|
+
// Built-in role flag
|
|
32
|
+
// true: Core package roles (user, admin, superadmin) - cannot be deleted
|
|
33
|
+
// false: Custom or preset roles - can be deleted
|
|
34
|
+
isBuiltin: boolean("is_builtin").notNull().default(false),
|
|
35
|
+
// System role flag
|
|
36
|
+
// true: Defined in code (builtin or preset) - deletion restricted
|
|
37
|
+
// false: Runtime created custom role - fully manageable
|
|
38
|
+
isSystem: boolean("is_system").notNull().default(false),
|
|
39
|
+
// Active status
|
|
40
|
+
// false: Deactivated role (users cannot be assigned)
|
|
41
|
+
isActive: boolean("is_active").notNull().default(true),
|
|
42
|
+
// Priority level (higher = more privileged)
|
|
43
|
+
// superadmin: 100, admin: 80, user: 10
|
|
44
|
+
// Used for role hierarchy and conflict resolution
|
|
45
|
+
priority: integer("priority").notNull().default(10),
|
|
46
|
+
...timestamps()
|
|
47
|
+
},
|
|
48
|
+
(table) => [
|
|
49
|
+
index("roles_name_idx").on(table.name),
|
|
50
|
+
index("roles_is_system_idx").on(table.isSystem),
|
|
51
|
+
index("roles_is_active_idx").on(table.isActive),
|
|
52
|
+
index("roles_is_builtin_idx").on(table.isBuiltin),
|
|
53
|
+
index("roles_priority_idx").on(table.priority)
|
|
54
|
+
]
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// src/server/entities/users.ts
|
|
60
|
+
import { text as text2, timestamp, check, boolean as boolean2, bigint, index as index2 } from "drizzle-orm/pg-core";
|
|
61
|
+
import { id as id2, timestamps as timestamps2, createFunctionSchema as createFunctionSchema2 } from "@spfn/core/db";
|
|
62
|
+
import { sql } from "drizzle-orm";
|
|
63
|
+
var schema2, users;
|
|
64
|
+
var init_users = __esm({
|
|
65
|
+
"src/server/entities/users.ts"() {
|
|
66
|
+
"use strict";
|
|
67
|
+
init_roles();
|
|
68
|
+
schema2 = createFunctionSchema2("@spfn/auth");
|
|
69
|
+
users = schema2.table(
|
|
70
|
+
"users",
|
|
71
|
+
{
|
|
72
|
+
// Identity
|
|
73
|
+
id: id2(),
|
|
74
|
+
// Email address (unique identifier)
|
|
75
|
+
// Used for: login, password reset, notifications
|
|
76
|
+
email: text2("email").unique(),
|
|
77
|
+
// Phone number in E.164 international format
|
|
78
|
+
// Format: +[country code][number] (e.g., +821012345678)
|
|
79
|
+
// Used for: SMS login, 2FA, notifications
|
|
80
|
+
phone: text2("phone").unique(),
|
|
81
|
+
// Authentication
|
|
82
|
+
// Bcrypt password hash ($2b$10$[salt][hash], 60 chars)
|
|
83
|
+
// Nullable to support OAuth-only accounts
|
|
84
|
+
passwordHash: text2("password_hash"),
|
|
85
|
+
// Force password change on next login
|
|
86
|
+
// Use cases: initial setup, security breach, policy violation
|
|
87
|
+
passwordChangeRequired: boolean2("password_change_required").notNull().default(false),
|
|
88
|
+
// Authorization (Role-Based Access Control)
|
|
89
|
+
// Foreign key to roles table
|
|
90
|
+
// References built-in roles: user (default), admin, superadmin
|
|
91
|
+
// Can also reference custom roles created at runtime
|
|
92
|
+
roleId: bigint("role_id", { mode: "number" }).references(() => roles.id).notNull(),
|
|
93
|
+
// Account status
|
|
94
|
+
// - active: Normal operation (default)
|
|
95
|
+
// - inactive: Deactivated (user request, dormant)
|
|
96
|
+
// - suspended: Locked (security incident, ToS violation)
|
|
97
|
+
status: text2(
|
|
98
|
+
"status",
|
|
99
|
+
{
|
|
100
|
+
enum: ["active", "inactive", "suspended"]
|
|
101
|
+
}
|
|
102
|
+
).notNull().default("active"),
|
|
103
|
+
// Verification timestamps
|
|
104
|
+
// null = unverified, timestamp = verified at this time
|
|
105
|
+
// Email verification (via verification code or magic link)
|
|
106
|
+
emailVerifiedAt: timestamp("email_verified_at", { withTimezone: true }),
|
|
107
|
+
// Phone verification (via SMS OTP)
|
|
108
|
+
phoneVerifiedAt: timestamp("phone_verified_at", { withTimezone: true }),
|
|
109
|
+
// Metadata
|
|
110
|
+
// Last successful login timestamp
|
|
111
|
+
// Used for: security auditing, dormant account detection
|
|
112
|
+
lastLoginAt: timestamp("last_login_at", { withTimezone: true }),
|
|
113
|
+
...timestamps2()
|
|
114
|
+
},
|
|
115
|
+
(table) => [
|
|
116
|
+
// Database constraints
|
|
117
|
+
// Ensure at least one identifier exists (email OR phone)
|
|
118
|
+
check(
|
|
119
|
+
"email_or_phone_check",
|
|
120
|
+
sql`${table.email} IS NOT NULL OR ${table.phone} IS NOT NULL`
|
|
121
|
+
),
|
|
122
|
+
// Indexes for query optimization
|
|
123
|
+
index2("users_email_idx").on(table.email),
|
|
124
|
+
index2("users_phone_idx").on(table.phone),
|
|
125
|
+
index2("users_status_idx").on(table.status),
|
|
126
|
+
index2("users_role_id_idx").on(table.roleId)
|
|
127
|
+
]
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// src/server/entities/user-social-accounts.ts
|
|
133
|
+
import { text as text3, timestamp as timestamp2, uniqueIndex } from "drizzle-orm/pg-core";
|
|
134
|
+
import { id as id3, timestamps as timestamps3, foreignKey, createFunctionSchema as createFunctionSchema3 } from "@spfn/core/db";
|
|
135
|
+
var schema3, userSocialAccounts;
|
|
136
|
+
var init_user_social_accounts = __esm({
|
|
137
|
+
"src/server/entities/user-social-accounts.ts"() {
|
|
138
|
+
"use strict";
|
|
139
|
+
init_users();
|
|
140
|
+
schema3 = createFunctionSchema3("@spfn/auth");
|
|
141
|
+
userSocialAccounts = schema3.table(
|
|
142
|
+
"user_social_accounts",
|
|
143
|
+
{
|
|
144
|
+
id: id3(),
|
|
145
|
+
// Foreign key to users
|
|
146
|
+
userId: foreignKey("user", () => users.id),
|
|
147
|
+
// Provider info
|
|
148
|
+
provider: text3(
|
|
149
|
+
"provider",
|
|
150
|
+
{
|
|
151
|
+
enum: ["google", "github", "kakao", "naver"]
|
|
152
|
+
}
|
|
153
|
+
).notNull(),
|
|
154
|
+
providerUserId: text3("provider_user_id").notNull(),
|
|
155
|
+
providerEmail: text3("provider_email"),
|
|
156
|
+
// OAuth tokens (encrypted in production)
|
|
157
|
+
accessToken: text3("access_token"),
|
|
158
|
+
refreshToken: text3("refresh_token"),
|
|
159
|
+
tokenExpiresAt: timestamp2("token_expires_at", { withTimezone: true }),
|
|
160
|
+
...timestamps3()
|
|
161
|
+
},
|
|
162
|
+
(table) => [
|
|
163
|
+
// Unique constraint: one provider account per provider
|
|
164
|
+
uniqueIndex("provider_user_unique_idx").on(table.provider, table.providerUserId)
|
|
165
|
+
]
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// src/server/entities/user-public-keys.ts
|
|
171
|
+
import { text as text4, timestamp as timestamp3, boolean as boolean3, index as index3 } from "drizzle-orm/pg-core";
|
|
172
|
+
import { id as id4, foreignKey as foreignKey2, createFunctionSchema as createFunctionSchema4 } from "@spfn/core/db";
|
|
173
|
+
var schema4, userPublicKeys;
|
|
174
|
+
var init_user_public_keys = __esm({
|
|
175
|
+
"src/server/entities/user-public-keys.ts"() {
|
|
176
|
+
"use strict";
|
|
177
|
+
init_users();
|
|
178
|
+
schema4 = createFunctionSchema4("@spfn/auth");
|
|
179
|
+
userPublicKeys = schema4.table(
|
|
180
|
+
"user_public_keys",
|
|
181
|
+
{
|
|
182
|
+
id: id4(),
|
|
183
|
+
// User reference
|
|
184
|
+
userId: foreignKey2("user", () => users.id),
|
|
185
|
+
// Key identification (client-generated UUID)
|
|
186
|
+
keyId: text4("key_id").notNull().unique(),
|
|
187
|
+
// Public key in Base64-encoded DER format (SPKI)
|
|
188
|
+
publicKey: text4("public_key").notNull(),
|
|
189
|
+
// Algorithm used (ES256 recommended, RS256 fallback)
|
|
190
|
+
algorithm: text4("algorithm", {
|
|
191
|
+
enum: ["ES256", "RS256"]
|
|
192
|
+
}).notNull().default("ES256"),
|
|
193
|
+
// Key fingerprint (SHA-256 hash for quick identification)
|
|
194
|
+
fingerprint: text4("fingerprint").notNull(),
|
|
195
|
+
// Key status
|
|
196
|
+
isActive: boolean3("is_active").notNull().default(true),
|
|
197
|
+
// Timestamps
|
|
198
|
+
createdAt: timestamp3("created_at", { mode: "date", withTimezone: true }).notNull().defaultNow(),
|
|
199
|
+
lastUsedAt: timestamp3("last_used_at", { mode: "date", withTimezone: true }),
|
|
200
|
+
expiresAt: timestamp3("expires_at", { mode: "date", withTimezone: true }),
|
|
201
|
+
// Revocation
|
|
202
|
+
revokedAt: timestamp3("revoked_at", { mode: "date", withTimezone: true }),
|
|
203
|
+
revokedReason: text4("revoked_reason")
|
|
204
|
+
},
|
|
205
|
+
(table) => [
|
|
206
|
+
index3("user_public_keys_user_id_idx").on(table.userId),
|
|
207
|
+
index3("user_public_keys_key_id_idx").on(table.keyId),
|
|
208
|
+
index3("user_public_keys_active_idx").on(table.isActive),
|
|
209
|
+
index3("user_public_keys_fingerprint_idx").on(table.fingerprint)
|
|
210
|
+
]
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// src/server/entities/verification-codes.ts
|
|
216
|
+
import { text as text5, timestamp as timestamp4, index as index4 } from "drizzle-orm/pg-core";
|
|
217
|
+
import { id as id5, timestamps as timestamps4, createFunctionSchema as createFunctionSchema5 } from "@spfn/core/db";
|
|
218
|
+
var schema5, verificationCodes;
|
|
219
|
+
var init_verification_codes = __esm({
|
|
220
|
+
"src/server/entities/verification-codes.ts"() {
|
|
221
|
+
"use strict";
|
|
222
|
+
schema5 = createFunctionSchema5("@spfn/auth");
|
|
223
|
+
verificationCodes = schema5.table(
|
|
224
|
+
"verification_codes",
|
|
225
|
+
{
|
|
226
|
+
id: id5(),
|
|
227
|
+
// Target (email or phone)
|
|
228
|
+
target: text5("target").notNull(),
|
|
229
|
+
// Email address or E.164 phone number
|
|
230
|
+
targetType: text5(
|
|
231
|
+
"target_type",
|
|
232
|
+
{
|
|
233
|
+
enum: ["email", "phone"]
|
|
234
|
+
}
|
|
235
|
+
).notNull(),
|
|
236
|
+
// Code
|
|
237
|
+
code: text5("code").notNull(),
|
|
238
|
+
// 6-digit code by default (configurable)
|
|
239
|
+
// Purpose
|
|
240
|
+
purpose: text5(
|
|
241
|
+
"purpose",
|
|
242
|
+
{
|
|
243
|
+
enum: ["registration", "login", "password_reset", "email_change", "phone_change"]
|
|
244
|
+
}
|
|
245
|
+
).notNull(),
|
|
246
|
+
// Expiry
|
|
247
|
+
expiresAt: timestamp4("expires_at", { withTimezone: true }).notNull(),
|
|
248
|
+
// Usage tracking
|
|
249
|
+
usedAt: timestamp4("used_at", { withTimezone: true }),
|
|
250
|
+
attempts: text5("attempts").notNull().default("0"),
|
|
251
|
+
// Track failed verification attempts
|
|
252
|
+
...timestamps4()
|
|
253
|
+
},
|
|
254
|
+
(table) => [
|
|
255
|
+
// Index for quick lookup by target and purpose
|
|
256
|
+
index4("target_purpose_idx").on(table.target, table.purpose, table.expiresAt)
|
|
257
|
+
]
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// src/server/entities/invitations.ts
|
|
263
|
+
import { text as text6, timestamp as timestamp5, bigint as bigint2, index as index5, jsonb } from "drizzle-orm/pg-core";
|
|
264
|
+
import { id as id6, timestamps as timestamps5, createFunctionSchema as createFunctionSchema6 } from "@spfn/core/db";
|
|
265
|
+
var schema6, invitations;
|
|
266
|
+
var init_invitations = __esm({
|
|
267
|
+
"src/server/entities/invitations.ts"() {
|
|
268
|
+
"use strict";
|
|
269
|
+
init_roles();
|
|
270
|
+
init_users();
|
|
271
|
+
schema6 = createFunctionSchema6("@spfn/auth");
|
|
272
|
+
invitations = schema6.table(
|
|
273
|
+
"user_invitations",
|
|
274
|
+
{
|
|
275
|
+
// Primary key
|
|
276
|
+
id: id6(),
|
|
277
|
+
// Target email address for the invitation
|
|
278
|
+
// Will become the user's email upon acceptance
|
|
279
|
+
email: text6("email").notNull(),
|
|
280
|
+
// Unique invitation token (UUID v4)
|
|
281
|
+
// Used in invitation URL: /auth/invite/{token}
|
|
282
|
+
// Single-use token that expires after acceptance
|
|
283
|
+
token: text6("token").notNull().unique(),
|
|
284
|
+
// Role to be assigned when invitation is accepted
|
|
285
|
+
// Foreign key to roles table
|
|
286
|
+
roleId: bigint2("role_id", { mode: "number" }).references(() => roles.id).notNull(),
|
|
287
|
+
// User who created this invitation
|
|
288
|
+
// Foreign key to users table
|
|
289
|
+
// Used for: audit trail, permission checks
|
|
290
|
+
invitedBy: bigint2("invited_by", { mode: "number" }).references(() => users.id).notNull(),
|
|
291
|
+
// Invitation status
|
|
292
|
+
// - pending: Invitation sent, awaiting acceptance
|
|
293
|
+
// - accepted: User accepted and account created
|
|
294
|
+
// - expired: Invitation expired (automatic)
|
|
295
|
+
// - cancelled: Invitation cancelled by admin
|
|
296
|
+
status: text6(
|
|
297
|
+
"status",
|
|
298
|
+
{
|
|
299
|
+
enum: ["pending", "accepted", "expired", "cancelled"]
|
|
300
|
+
}
|
|
301
|
+
).notNull().default("pending"),
|
|
302
|
+
// Expiration timestamp (default: 7 days from creation)
|
|
303
|
+
// Invitation cannot be accepted after this time
|
|
304
|
+
// Background job should update status to 'expired'
|
|
305
|
+
expiresAt: timestamp5("expires_at", { withTimezone: true }).notNull(),
|
|
306
|
+
// Timestamp when invitation was accepted
|
|
307
|
+
// null = not yet accepted
|
|
308
|
+
// Used for: audit trail, analytics
|
|
309
|
+
acceptedAt: timestamp5("accepted_at", { withTimezone: true }),
|
|
310
|
+
// Timestamp when invitation was cancelled
|
|
311
|
+
// null = not cancelled
|
|
312
|
+
// Used for: audit trail
|
|
313
|
+
cancelledAt: timestamp5("cancelled_at", { withTimezone: true }),
|
|
314
|
+
// Additional metadata (JSONB)
|
|
315
|
+
// Use cases:
|
|
316
|
+
// - Custom welcome message
|
|
317
|
+
// - Onboarding instructions
|
|
318
|
+
// - Team/department assignment
|
|
319
|
+
// - Custom fields for app-specific data
|
|
320
|
+
// Example: { message: "Welcome!", department: "Engineering" }
|
|
321
|
+
metadata: jsonb("metadata"),
|
|
322
|
+
...timestamps5()
|
|
323
|
+
},
|
|
324
|
+
(table) => [
|
|
325
|
+
// Indexes for query optimization
|
|
326
|
+
index5("invitations_token_idx").on(table.token),
|
|
327
|
+
index5("invitations_email_idx").on(table.email),
|
|
328
|
+
index5("invitations_status_idx").on(table.status),
|
|
329
|
+
index5("invitations_invited_by_idx").on(table.invitedBy),
|
|
330
|
+
index5("invitations_expires_at_idx").on(table.expiresAt),
|
|
331
|
+
// For cleanup jobs
|
|
332
|
+
index5("invitations_role_id_idx").on(table.roleId)
|
|
333
|
+
]
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// src/server/entities/permissions.ts
|
|
339
|
+
import { text as text7, boolean as boolean4, index as index6 } from "drizzle-orm/pg-core";
|
|
340
|
+
import { id as id7, timestamps as timestamps6, createFunctionSchema as createFunctionSchema7 } from "@spfn/core/db";
|
|
341
|
+
var schema7, permissions;
|
|
342
|
+
var init_permissions = __esm({
|
|
343
|
+
"src/server/entities/permissions.ts"() {
|
|
344
|
+
"use strict";
|
|
345
|
+
schema7 = createFunctionSchema7("@spfn/auth");
|
|
346
|
+
permissions = schema7.table(
|
|
347
|
+
"permissions",
|
|
348
|
+
{
|
|
349
|
+
// Primary key
|
|
350
|
+
id: id7(),
|
|
351
|
+
// Permission identifier (e.g., 'user:delete', 'post:publish')
|
|
352
|
+
// Format: resource:action or namespace:resource:action
|
|
353
|
+
// Must be unique
|
|
354
|
+
name: text7("name").notNull().unique(),
|
|
355
|
+
// Display name for UI
|
|
356
|
+
displayName: text7("display_name").notNull(),
|
|
357
|
+
// Permission description
|
|
358
|
+
description: text7("description"),
|
|
359
|
+
// Category for grouping (e.g., 'user', 'post', 'admin', 'system')
|
|
360
|
+
category: text7("category"),
|
|
361
|
+
// Built-in permission flag
|
|
362
|
+
// true: Core package permissions - cannot be deleted
|
|
363
|
+
// false: Custom or preset permissions
|
|
364
|
+
isBuiltin: boolean4("is_builtin").notNull().default(false),
|
|
365
|
+
// System permission flag
|
|
366
|
+
// true: Defined in code (builtin or preset)
|
|
367
|
+
// false: Runtime created custom permission
|
|
368
|
+
isSystem: boolean4("is_system").notNull().default(false),
|
|
369
|
+
// Active status
|
|
370
|
+
// false: Deactivated permission (not enforced)
|
|
371
|
+
isActive: boolean4("is_active").notNull().default(true),
|
|
372
|
+
...timestamps6()
|
|
373
|
+
},
|
|
374
|
+
(table) => [
|
|
375
|
+
index6("permissions_name_idx").on(table.name),
|
|
376
|
+
index6("permissions_category_idx").on(table.category),
|
|
377
|
+
index6("permissions_is_system_idx").on(table.isSystem),
|
|
378
|
+
index6("permissions_is_active_idx").on(table.isActive),
|
|
379
|
+
index6("permissions_is_builtin_idx").on(table.isBuiltin)
|
|
380
|
+
]
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// src/server/entities/role-permissions.ts
|
|
386
|
+
import { bigint as bigint3, index as index7, unique } from "drizzle-orm/pg-core";
|
|
387
|
+
import { id as id8, timestamps as timestamps7, createFunctionSchema as createFunctionSchema8 } from "@spfn/core/db";
|
|
388
|
+
var schema8, rolePermissions;
|
|
389
|
+
var init_role_permissions = __esm({
|
|
390
|
+
"src/server/entities/role-permissions.ts"() {
|
|
391
|
+
"use strict";
|
|
392
|
+
init_roles();
|
|
393
|
+
init_permissions();
|
|
394
|
+
schema8 = createFunctionSchema8("@spfn/auth");
|
|
395
|
+
rolePermissions = schema8.table(
|
|
396
|
+
"role_permissions",
|
|
397
|
+
{
|
|
398
|
+
// Primary key
|
|
399
|
+
id: id8(),
|
|
400
|
+
// Foreign key to roles table
|
|
401
|
+
roleId: bigint3("role_id", { mode: "number" }).notNull().references(() => roles.id, { onDelete: "cascade" }),
|
|
402
|
+
// Foreign key to permissions table
|
|
403
|
+
permissionId: bigint3("permission_id", { mode: "number" }).notNull().references(() => permissions.id, { onDelete: "cascade" }),
|
|
404
|
+
...timestamps7()
|
|
405
|
+
},
|
|
406
|
+
(table) => [
|
|
407
|
+
// Indexes for query performance
|
|
408
|
+
index7("role_permissions_role_id_idx").on(table.roleId),
|
|
409
|
+
index7("role_permissions_permission_id_idx").on(table.permissionId),
|
|
410
|
+
// Unique constraint: one role-permission pair only
|
|
411
|
+
unique("role_permissions_unique").on(table.roleId, table.permissionId)
|
|
412
|
+
]
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// src/server/entities/user-permissions.ts
|
|
418
|
+
import { bigint as bigint4, boolean as boolean5, text as text8, timestamp as timestamp6, index as index8, unique as unique2 } from "drizzle-orm/pg-core";
|
|
419
|
+
import { id as id9, timestamps as timestamps8, createFunctionSchema as createFunctionSchema9 } from "@spfn/core/db";
|
|
420
|
+
var schema9, userPermissions;
|
|
421
|
+
var init_user_permissions = __esm({
|
|
422
|
+
"src/server/entities/user-permissions.ts"() {
|
|
423
|
+
"use strict";
|
|
424
|
+
init_users();
|
|
425
|
+
init_permissions();
|
|
426
|
+
schema9 = createFunctionSchema9("@spfn/auth");
|
|
427
|
+
userPermissions = schema9.table(
|
|
428
|
+
"user_permissions",
|
|
429
|
+
{
|
|
430
|
+
// Primary key
|
|
431
|
+
id: id9(),
|
|
432
|
+
// Foreign key to users table
|
|
433
|
+
userId: bigint4("user_id", { mode: "number" }).notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
434
|
+
// Foreign key to permissions table
|
|
435
|
+
permissionId: bigint4("permission_id", { mode: "number" }).notNull().references(() => permissions.id, { onDelete: "cascade" }),
|
|
436
|
+
// Grant or revoke
|
|
437
|
+
// true: Grant this permission (even if role doesn't have it)
|
|
438
|
+
// false: Revoke this permission (even if role has it)
|
|
439
|
+
granted: boolean5("granted").notNull().default(true),
|
|
440
|
+
// Reason for grant/revocation (audit trail)
|
|
441
|
+
reason: text8("reason"),
|
|
442
|
+
// Expiration timestamp (optional)
|
|
443
|
+
// null: Permanent override
|
|
444
|
+
// timestamp: Permission expires at this time
|
|
445
|
+
expiresAt: timestamp6("expires_at", { withTimezone: true }),
|
|
446
|
+
...timestamps8()
|
|
447
|
+
},
|
|
448
|
+
(table) => [
|
|
449
|
+
// Indexes for query performance
|
|
450
|
+
index8("user_permissions_user_id_idx").on(table.userId),
|
|
451
|
+
index8("user_permissions_permission_id_idx").on(table.permissionId),
|
|
452
|
+
index8("user_permissions_expires_at_idx").on(table.expiresAt),
|
|
453
|
+
// Unique constraint: one user-permission pair only
|
|
454
|
+
unique2("user_permissions_unique").on(table.userId, table.permissionId)
|
|
455
|
+
]
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// src/server/entities/index.ts
|
|
461
|
+
var entities_exports = {};
|
|
462
|
+
__export(entities_exports, {
|
|
463
|
+
invitations: () => invitations,
|
|
464
|
+
permissions: () => permissions,
|
|
465
|
+
rolePermissions: () => rolePermissions,
|
|
466
|
+
roles: () => roles,
|
|
467
|
+
userPermissions: () => userPermissions,
|
|
468
|
+
userPublicKeys: () => userPublicKeys,
|
|
469
|
+
userSocialAccounts: () => userSocialAccounts,
|
|
470
|
+
users: () => users,
|
|
471
|
+
verificationCodes: () => verificationCodes
|
|
472
|
+
});
|
|
473
|
+
var init_entities = __esm({
|
|
474
|
+
"src/server/entities/index.ts"() {
|
|
475
|
+
"use strict";
|
|
476
|
+
init_users();
|
|
477
|
+
init_user_social_accounts();
|
|
478
|
+
init_user_public_keys();
|
|
479
|
+
init_verification_codes();
|
|
480
|
+
init_invitations();
|
|
481
|
+
init_roles();
|
|
482
|
+
init_permissions();
|
|
483
|
+
init_role_permissions();
|
|
484
|
+
init_user_permissions();
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// src/server/services/role.service.ts
|
|
489
|
+
var role_service_exports = {};
|
|
490
|
+
__export(role_service_exports, {
|
|
491
|
+
addPermissionToRole: () => addPermissionToRole,
|
|
492
|
+
createRole: () => createRole,
|
|
493
|
+
deleteRole: () => deleteRole,
|
|
494
|
+
getAllRoles: () => getAllRoles,
|
|
495
|
+
getRoleByName: () => getRoleByName,
|
|
496
|
+
getRolePermissions: () => getRolePermissions,
|
|
497
|
+
removePermissionFromRole: () => removePermissionFromRole,
|
|
498
|
+
setRolePermissions: () => setRolePermissions,
|
|
499
|
+
updateRole: () => updateRole
|
|
500
|
+
});
|
|
501
|
+
import { getDatabase as getDatabase3 } from "@spfn/core/db";
|
|
502
|
+
import { eq as eq3, and as and3 } from "drizzle-orm";
|
|
503
|
+
async function createRole(data) {
|
|
504
|
+
const db = getDatabase3();
|
|
505
|
+
if (!db) {
|
|
506
|
+
throw new Error("[Auth] Database not initialized");
|
|
507
|
+
}
|
|
508
|
+
const existing = await db.select().from(roles).where(eq3(roles.name, data.name)).limit(1);
|
|
509
|
+
if (existing.length > 0) {
|
|
510
|
+
throw new Error(`Role with name '${data.name}' already exists`);
|
|
511
|
+
}
|
|
512
|
+
const [newRole] = await db.insert(roles).values({
|
|
513
|
+
name: data.name,
|
|
514
|
+
displayName: data.displayName,
|
|
515
|
+
description: data.description,
|
|
516
|
+
priority: data.priority ?? 10,
|
|
517
|
+
isSystem: false,
|
|
518
|
+
// Custom roles are never system roles
|
|
519
|
+
isBuiltin: false
|
|
520
|
+
}).returning();
|
|
521
|
+
if (data.permissionIds && data.permissionIds.length > 0) {
|
|
522
|
+
const mappings = data.permissionIds.map((permId) => ({
|
|
523
|
+
roleId: newRole.id,
|
|
524
|
+
permissionId: Number(permId)
|
|
525
|
+
}));
|
|
526
|
+
await db.insert(rolePermissions).values(mappings);
|
|
527
|
+
}
|
|
528
|
+
console.log(`[Auth] \u2705 Created custom role: ${data.name}`);
|
|
529
|
+
return newRole;
|
|
530
|
+
}
|
|
531
|
+
async function updateRole(roleId, data) {
|
|
532
|
+
const db = getDatabase3();
|
|
533
|
+
if (!db) {
|
|
534
|
+
throw new Error("[Auth] Database not initialized");
|
|
535
|
+
}
|
|
536
|
+
const roleIdNum = Number(roleId);
|
|
537
|
+
const [role] = await db.select().from(roles).where(eq3(roles.id, roleIdNum)).limit(1);
|
|
538
|
+
if (!role) {
|
|
539
|
+
throw new Error("Role not found");
|
|
540
|
+
}
|
|
541
|
+
if (role.isBuiltin && data.priority !== void 0) {
|
|
542
|
+
throw new Error("Cannot modify priority of built-in roles");
|
|
543
|
+
}
|
|
544
|
+
const [updated] = await db.update(roles).set(data).where(eq3(roles.id, roleIdNum)).returning();
|
|
545
|
+
return updated;
|
|
546
|
+
}
|
|
547
|
+
async function deleteRole(roleId) {
|
|
548
|
+
const db = getDatabase3();
|
|
549
|
+
if (!db) {
|
|
550
|
+
throw new Error("[Auth] Database not initialized");
|
|
551
|
+
}
|
|
552
|
+
const roleIdNum = Number(roleId);
|
|
553
|
+
const [role] = await db.select().from(roles).where(eq3(roles.id, roleIdNum)).limit(1);
|
|
554
|
+
if (!role) {
|
|
555
|
+
throw new Error("Role not found");
|
|
556
|
+
}
|
|
557
|
+
if (role.isBuiltin) {
|
|
558
|
+
throw new Error(`Cannot delete built-in role: ${role.name}`);
|
|
559
|
+
}
|
|
560
|
+
if (role.isSystem) {
|
|
561
|
+
throw new Error(`Cannot delete system role: ${role.name}. Deactivate it instead.`);
|
|
562
|
+
}
|
|
563
|
+
await db.delete(roles).where(eq3(roles.id, roleIdNum));
|
|
564
|
+
console.log(`[Auth] \u{1F5D1}\uFE0F Deleted role: ${role.name}`);
|
|
565
|
+
}
|
|
566
|
+
async function addPermissionToRole(roleId, permissionId) {
|
|
567
|
+
const db = getDatabase3();
|
|
568
|
+
if (!db) {
|
|
569
|
+
throw new Error("[Auth] Database not initialized");
|
|
570
|
+
}
|
|
571
|
+
const roleIdNum = Number(roleId);
|
|
572
|
+
const permissionIdNum = Number(permissionId);
|
|
573
|
+
const existing = await db.select().from(rolePermissions).where(
|
|
574
|
+
and3(
|
|
575
|
+
eq3(rolePermissions.roleId, roleIdNum),
|
|
576
|
+
eq3(rolePermissions.permissionId, permissionIdNum)
|
|
577
|
+
)
|
|
578
|
+
).limit(1);
|
|
579
|
+
if (existing.length > 0) {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
await db.insert(rolePermissions).values({
|
|
583
|
+
roleId: roleIdNum,
|
|
584
|
+
permissionId: permissionIdNum
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
async function removePermissionFromRole(roleId, permissionId) {
|
|
588
|
+
const db = getDatabase3();
|
|
589
|
+
if (!db) {
|
|
590
|
+
throw new Error("[Auth] Database not initialized");
|
|
591
|
+
}
|
|
592
|
+
const roleIdNum = Number(roleId);
|
|
593
|
+
const permissionIdNum = Number(permissionId);
|
|
594
|
+
await db.delete(rolePermissions).where(
|
|
595
|
+
and3(
|
|
596
|
+
eq3(rolePermissions.roleId, roleIdNum),
|
|
597
|
+
eq3(rolePermissions.permissionId, permissionIdNum)
|
|
598
|
+
)
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
async function setRolePermissions(roleId, permissionIds) {
|
|
602
|
+
const db = getDatabase3();
|
|
603
|
+
if (!db) {
|
|
604
|
+
throw new Error("[Auth] Database not initialized");
|
|
605
|
+
}
|
|
606
|
+
const roleIdNum = Number(roleId);
|
|
607
|
+
await db.delete(rolePermissions).where(eq3(rolePermissions.roleId, roleIdNum));
|
|
608
|
+
if (permissionIds.length > 0) {
|
|
609
|
+
const mappings = permissionIds.map((permId) => ({
|
|
610
|
+
roleId: roleIdNum,
|
|
611
|
+
permissionId: Number(permId)
|
|
612
|
+
}));
|
|
613
|
+
await db.insert(rolePermissions).values(mappings);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
async function getAllRoles(includeInactive = false) {
|
|
617
|
+
const db = getDatabase3();
|
|
618
|
+
if (!db) {
|
|
619
|
+
throw new Error("[Auth] Database not initialized");
|
|
620
|
+
}
|
|
621
|
+
const query = db.select().from(roles);
|
|
622
|
+
if (!includeInactive) {
|
|
623
|
+
return query.where(eq3(roles.isActive, true));
|
|
624
|
+
}
|
|
625
|
+
return query;
|
|
626
|
+
}
|
|
627
|
+
async function getRoleByName(name) {
|
|
628
|
+
const db = getDatabase3();
|
|
629
|
+
if (!db) {
|
|
630
|
+
throw new Error("[Auth] Database not initialized");
|
|
631
|
+
}
|
|
632
|
+
const [role] = await db.select().from(roles).where(eq3(roles.name, name)).limit(1);
|
|
633
|
+
return role || null;
|
|
634
|
+
}
|
|
635
|
+
async function getRolePermissions(roleId) {
|
|
636
|
+
const db = getDatabase3();
|
|
637
|
+
if (!db) {
|
|
638
|
+
throw new Error("[Auth] Database not initialized");
|
|
639
|
+
}
|
|
640
|
+
const roleIdNum = Number(roleId);
|
|
641
|
+
const perms = await db.select({ name: permissions.name }).from(rolePermissions).innerJoin(permissions, eq3(rolePermissions.permissionId, permissions.id)).where(eq3(rolePermissions.roleId, roleIdNum));
|
|
642
|
+
return perms.map((p) => p.name);
|
|
643
|
+
}
|
|
644
|
+
var init_role_service = __esm({
|
|
645
|
+
"src/server/services/role.service.ts"() {
|
|
646
|
+
"use strict";
|
|
647
|
+
init_entities();
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
// src/server/rbac/builtin.ts
|
|
652
|
+
var BUILTIN_ROLES = {
|
|
653
|
+
SUPERADMIN: {
|
|
654
|
+
name: "superadmin",
|
|
655
|
+
displayName: "Super Administrator",
|
|
656
|
+
description: "Full system access and RBAC management",
|
|
657
|
+
priority: 100,
|
|
658
|
+
isSystem: true,
|
|
659
|
+
isBuiltin: true
|
|
660
|
+
},
|
|
661
|
+
ADMIN: {
|
|
662
|
+
name: "admin",
|
|
663
|
+
displayName: "Administrator",
|
|
664
|
+
description: "User management and organization administration",
|
|
665
|
+
priority: 80,
|
|
666
|
+
isSystem: true,
|
|
667
|
+
isBuiltin: true
|
|
668
|
+
},
|
|
669
|
+
USER: {
|
|
670
|
+
name: "user",
|
|
671
|
+
displayName: "User",
|
|
672
|
+
description: "Default user role with basic permissions",
|
|
673
|
+
priority: 10,
|
|
674
|
+
isSystem: true,
|
|
675
|
+
isBuiltin: true
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
var BUILTIN_PERMISSIONS = {
|
|
679
|
+
// Self-service auth management
|
|
680
|
+
AUTH_SELF_MANAGE: {
|
|
681
|
+
name: "auth:self:manage",
|
|
682
|
+
displayName: "Manage Own Auth",
|
|
683
|
+
description: "Change own password, rotate keys, manage own sessions",
|
|
684
|
+
category: "auth",
|
|
685
|
+
isSystem: true,
|
|
686
|
+
isBuiltin: true
|
|
687
|
+
},
|
|
688
|
+
// User management (admin functions)
|
|
689
|
+
USER_READ: {
|
|
690
|
+
name: "user:read",
|
|
691
|
+
displayName: "Read Users",
|
|
692
|
+
description: "View user information and list users",
|
|
693
|
+
category: "user",
|
|
694
|
+
isSystem: true,
|
|
695
|
+
isBuiltin: true
|
|
696
|
+
},
|
|
697
|
+
USER_WRITE: {
|
|
698
|
+
name: "user:write",
|
|
699
|
+
displayName: "Write Users",
|
|
700
|
+
description: "Create and update user accounts",
|
|
701
|
+
category: "user",
|
|
702
|
+
isSystem: true,
|
|
703
|
+
isBuiltin: true
|
|
704
|
+
},
|
|
705
|
+
USER_DELETE: {
|
|
706
|
+
name: "user:delete",
|
|
707
|
+
displayName: "Delete Users",
|
|
708
|
+
description: "Delete user accounts",
|
|
709
|
+
category: "user",
|
|
710
|
+
isSystem: true,
|
|
711
|
+
isBuiltin: true
|
|
712
|
+
},
|
|
713
|
+
// RBAC management (superadmin functions)
|
|
714
|
+
RBAC_ROLE_MANAGE: {
|
|
715
|
+
name: "rbac:role:manage",
|
|
716
|
+
displayName: "Manage Roles",
|
|
717
|
+
description: "Create, update, and delete roles",
|
|
718
|
+
category: "rbac",
|
|
719
|
+
isSystem: true,
|
|
720
|
+
isBuiltin: true
|
|
721
|
+
},
|
|
722
|
+
RBAC_PERMISSION_MANAGE: {
|
|
723
|
+
name: "rbac:permission:manage",
|
|
724
|
+
displayName: "Manage Permissions",
|
|
725
|
+
description: "Assign permissions to roles and users",
|
|
726
|
+
category: "rbac",
|
|
727
|
+
isSystem: true,
|
|
728
|
+
isBuiltin: true
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
var BUILTIN_ROLE_PERMISSIONS = {
|
|
732
|
+
superadmin: [
|
|
733
|
+
"auth:self:manage",
|
|
734
|
+
"user:read",
|
|
735
|
+
"user:write",
|
|
736
|
+
"user:delete",
|
|
737
|
+
"rbac:role:manage",
|
|
738
|
+
"rbac:permission:manage"
|
|
739
|
+
],
|
|
740
|
+
admin: [
|
|
741
|
+
"auth:self:manage",
|
|
742
|
+
"user:read",
|
|
743
|
+
"user:write",
|
|
744
|
+
"user:delete"
|
|
745
|
+
],
|
|
746
|
+
user: [
|
|
747
|
+
"auth:self:manage"
|
|
748
|
+
]
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
// src/server/rbac/presets.ts
|
|
752
|
+
var PRESET_ROLES = {
|
|
753
|
+
MODERATOR: {
|
|
754
|
+
name: "moderator",
|
|
755
|
+
displayName: "Moderator",
|
|
756
|
+
description: "Content moderation and community management",
|
|
757
|
+
priority: 50,
|
|
758
|
+
isSystem: true
|
|
759
|
+
},
|
|
760
|
+
EDITOR: {
|
|
761
|
+
name: "editor",
|
|
762
|
+
displayName: "Editor",
|
|
763
|
+
description: "Content creation and editing",
|
|
764
|
+
priority: 30,
|
|
765
|
+
isSystem: true
|
|
766
|
+
},
|
|
767
|
+
VIEWER: {
|
|
768
|
+
name: "viewer",
|
|
769
|
+
displayName: "Viewer",
|
|
770
|
+
description: "Read-only access to content",
|
|
771
|
+
priority: 5,
|
|
772
|
+
isSystem: true
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
var PRESET_PERMISSIONS = {
|
|
776
|
+
// Content management
|
|
777
|
+
CONTENT_READ: {
|
|
778
|
+
name: "content:read",
|
|
779
|
+
displayName: "Read Content",
|
|
780
|
+
description: "View all content including drafts",
|
|
781
|
+
category: "content",
|
|
782
|
+
isSystem: true
|
|
783
|
+
},
|
|
784
|
+
CONTENT_WRITE: {
|
|
785
|
+
name: "content:write",
|
|
786
|
+
displayName: "Write Content",
|
|
787
|
+
description: "Create and edit content",
|
|
788
|
+
category: "content",
|
|
789
|
+
isSystem: true
|
|
790
|
+
},
|
|
791
|
+
CONTENT_DELETE: {
|
|
792
|
+
name: "content:delete",
|
|
793
|
+
displayName: "Delete Content",
|
|
794
|
+
description: "Delete any content",
|
|
795
|
+
category: "content",
|
|
796
|
+
isSystem: true
|
|
797
|
+
},
|
|
798
|
+
CONTENT_PUBLISH: {
|
|
799
|
+
name: "content:publish",
|
|
800
|
+
displayName: "Publish Content",
|
|
801
|
+
description: "Publish content to make it public",
|
|
802
|
+
category: "content",
|
|
803
|
+
isSystem: true
|
|
804
|
+
},
|
|
805
|
+
// Moderation
|
|
806
|
+
COMMENT_MODERATE: {
|
|
807
|
+
name: "comment:moderate",
|
|
808
|
+
displayName: "Moderate Comments",
|
|
809
|
+
description: "Review and delete inappropriate comments",
|
|
810
|
+
category: "moderation",
|
|
811
|
+
isSystem: true
|
|
812
|
+
},
|
|
813
|
+
// System
|
|
814
|
+
SYSTEM_CONFIG: {
|
|
815
|
+
name: "system:config",
|
|
816
|
+
displayName: "System Configuration",
|
|
817
|
+
description: "Configure application settings",
|
|
818
|
+
category: "system",
|
|
819
|
+
isSystem: true
|
|
820
|
+
},
|
|
821
|
+
// Analytics
|
|
822
|
+
ANALYTICS_VIEW: {
|
|
823
|
+
name: "analytics:view",
|
|
824
|
+
displayName: "View Analytics",
|
|
825
|
+
description: "Access analytics dashboard and reports",
|
|
826
|
+
category: "analytics",
|
|
827
|
+
isSystem: true
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
var PRESET_ROLE_PERMISSIONS = {
|
|
831
|
+
moderator: [
|
|
832
|
+
"auth:self:manage",
|
|
833
|
+
"user:read",
|
|
834
|
+
"content:read",
|
|
835
|
+
"content:write",
|
|
836
|
+
"content:delete",
|
|
837
|
+
"comment:moderate"
|
|
838
|
+
],
|
|
839
|
+
editor: [
|
|
840
|
+
"auth:self:manage",
|
|
841
|
+
"content:read",
|
|
842
|
+
"content:write",
|
|
843
|
+
"content:publish"
|
|
844
|
+
],
|
|
845
|
+
viewer: [
|
|
846
|
+
"auth:self:manage",
|
|
847
|
+
"content:read"
|
|
848
|
+
]
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
// src/server/services/auth.service.ts
|
|
852
|
+
init_entities();
|
|
853
|
+
import { findOne as findOne2, create as create3 } from "@spfn/core/db";
|
|
854
|
+
import { ValidationError as ValidationError2 } from "@spfn/core/errors";
|
|
855
|
+
|
|
856
|
+
// src/server/helpers/password.ts
|
|
857
|
+
import bcrypt from "bcrypt";
|
|
858
|
+
var SALT_ROUNDS = parseInt(
|
|
859
|
+
process.env.SPFN_AUTH_BCRYPT_SALT_ROUNDS || // New prefixed version (recommended)
|
|
860
|
+
process.env.BCRYPT_SALT_ROUNDS || // Legacy fallback
|
|
861
|
+
"10",
|
|
862
|
+
10
|
|
863
|
+
);
|
|
864
|
+
async function hashPassword(password) {
|
|
865
|
+
if (!password || password.length === 0) {
|
|
866
|
+
throw new Error("Password cannot be empty");
|
|
867
|
+
}
|
|
868
|
+
return bcrypt.hash(password, SALT_ROUNDS);
|
|
869
|
+
}
|
|
870
|
+
async function verifyPassword(password, hash) {
|
|
871
|
+
if (!password || password.length === 0) {
|
|
872
|
+
throw new Error("Password cannot be empty");
|
|
873
|
+
}
|
|
874
|
+
if (!hash || hash.length === 0) {
|
|
875
|
+
throw new Error("Hash cannot be empty");
|
|
876
|
+
}
|
|
877
|
+
return bcrypt.compare(password, hash);
|
|
878
|
+
}
|
|
879
|
+
function validatePasswordStrength(password) {
|
|
880
|
+
const errors = [];
|
|
881
|
+
if (password.length < 8) {
|
|
882
|
+
errors.push("Password must be at least 8 characters");
|
|
883
|
+
}
|
|
884
|
+
if (!/[A-Z]/.test(password)) {
|
|
885
|
+
errors.push("Password must contain at least one uppercase letter");
|
|
886
|
+
}
|
|
887
|
+
if (!/[a-z]/.test(password)) {
|
|
888
|
+
errors.push("Password must contain at least one lowercase letter");
|
|
889
|
+
}
|
|
890
|
+
if (!/[0-9]/.test(password)) {
|
|
891
|
+
errors.push("Password must contain at least one number");
|
|
892
|
+
}
|
|
893
|
+
if (!/[^A-Za-z0-9]/.test(password)) {
|
|
894
|
+
errors.push("Password must contain at least one special character");
|
|
895
|
+
}
|
|
896
|
+
return {
|
|
897
|
+
valid: errors.length === 0,
|
|
898
|
+
errors
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// src/server/helpers/jwt.ts
|
|
903
|
+
import jwt from "jsonwebtoken";
|
|
904
|
+
import crypto from "crypto";
|
|
905
|
+
var JWT_SECRET = process.env.SPFN_AUTH_JWT_SECRET || // New prefixed version (recommended)
|
|
906
|
+
process.env.JWT_SECRET || // Legacy fallback
|
|
907
|
+
"dev-secret-key-change-in-production";
|
|
908
|
+
var JWT_EXPIRES_IN = process.env.SPFN_AUTH_JWT_EXPIRES_IN || // New prefixed version (recommended)
|
|
909
|
+
process.env.JWT_EXPIRES_IN || // Legacy fallback
|
|
910
|
+
"7d";
|
|
911
|
+
function generateToken(payload) {
|
|
912
|
+
return jwt.sign(payload, JWT_SECRET, {
|
|
913
|
+
expiresIn: JWT_EXPIRES_IN
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
function verifyToken(token) {
|
|
917
|
+
return jwt.verify(token, JWT_SECRET);
|
|
918
|
+
}
|
|
919
|
+
function verifyClientToken(token, publicKeyB64, algorithm) {
|
|
920
|
+
try {
|
|
921
|
+
const publicKeyDER = Buffer.from(publicKeyB64, "base64");
|
|
922
|
+
const publicKeyObject = crypto.createPublicKey({
|
|
923
|
+
key: publicKeyDER,
|
|
924
|
+
format: "der",
|
|
925
|
+
type: "spki"
|
|
926
|
+
});
|
|
927
|
+
const decoded = jwt.verify(token, publicKeyObject, {
|
|
928
|
+
algorithms: [algorithm],
|
|
929
|
+
// Prevent algorithm confusion attacks
|
|
930
|
+
issuer: "spfn-client"
|
|
931
|
+
// Validate token issuer
|
|
932
|
+
});
|
|
933
|
+
if (typeof decoded === "string") {
|
|
934
|
+
throw new Error("Invalid token format: expected object payload");
|
|
935
|
+
}
|
|
936
|
+
return decoded;
|
|
937
|
+
} catch (error) {
|
|
938
|
+
if (error instanceof jwt.TokenExpiredError) {
|
|
939
|
+
throw new Error("Token has expired");
|
|
940
|
+
}
|
|
941
|
+
if (error instanceof jwt.JsonWebTokenError) {
|
|
942
|
+
throw new Error("Invalid token signature");
|
|
943
|
+
}
|
|
944
|
+
throw new Error(`Token verification failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
function decodeToken(token) {
|
|
948
|
+
try {
|
|
949
|
+
return jwt.decode(token);
|
|
950
|
+
} catch {
|
|
951
|
+
return null;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
function verifyKeyFingerprint(publicKeyB64, expectedFingerprint) {
|
|
955
|
+
try {
|
|
956
|
+
const publicKeyDER = Buffer.from(publicKeyB64, "base64");
|
|
957
|
+
const fingerprint = crypto.createHash("sha256").update(publicKeyDER).digest("hex");
|
|
958
|
+
return fingerprint === expectedFingerprint;
|
|
959
|
+
} catch (error) {
|
|
960
|
+
console.error("Failed to verify key fingerprint:", error);
|
|
961
|
+
return false;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// src/server/helpers/verification.ts
|
|
966
|
+
init_verification_codes();
|
|
967
|
+
import jwt2 from "jsonwebtoken";
|
|
968
|
+
import { getDatabase, create } from "@spfn/core/db";
|
|
969
|
+
import { eq, and } from "drizzle-orm";
|
|
970
|
+
function getVerificationTokenSecret() {
|
|
971
|
+
const secret = process.env.SPFN_AUTH_VERIFICATION_TOKEN_SECRET || // New prefixed version (recommended)
|
|
972
|
+
process.env.VERIFICATION_TOKEN_SECRET || // Legacy fallback
|
|
973
|
+
process.env.SPFN_AUTH_JWT_SECRET || // New JWT secret fallback
|
|
974
|
+
process.env.JWT_SECRET;
|
|
975
|
+
if (!secret || secret.length < 32) {
|
|
976
|
+
throw new Error("SPFN_AUTH_VERIFICATION_TOKEN_SECRET must be at least 32 characters long");
|
|
977
|
+
}
|
|
978
|
+
return secret;
|
|
979
|
+
}
|
|
980
|
+
var VERIFICATION_TOKEN_EXPIRY = "15m";
|
|
981
|
+
var VERIFICATION_CODE_EXPIRY_MINUTES = 5;
|
|
982
|
+
var MAX_VERIFICATION_ATTEMPTS = 5;
|
|
983
|
+
function generateVerificationCode() {
|
|
984
|
+
const code = Math.floor(Math.random() * 1e6).toString().padStart(6, "0");
|
|
985
|
+
return code;
|
|
986
|
+
}
|
|
987
|
+
async function storeVerificationCode(target, targetType, code, purpose) {
|
|
988
|
+
const db = getDatabase();
|
|
989
|
+
if (!db) {
|
|
990
|
+
throw new Error("Database not initialized");
|
|
991
|
+
}
|
|
992
|
+
const expiresAt = /* @__PURE__ */ new Date();
|
|
993
|
+
expiresAt.setMinutes(expiresAt.getMinutes() + VERIFICATION_CODE_EXPIRY_MINUTES);
|
|
994
|
+
const record = await create(verificationCodes, {
|
|
995
|
+
target,
|
|
996
|
+
targetType,
|
|
997
|
+
code,
|
|
998
|
+
purpose,
|
|
999
|
+
expiresAt,
|
|
1000
|
+
attempts: "0"
|
|
1001
|
+
});
|
|
1002
|
+
return record;
|
|
1003
|
+
}
|
|
1004
|
+
async function validateVerificationCode(target, targetType, code, purpose) {
|
|
1005
|
+
const db = getDatabase();
|
|
1006
|
+
if (!db) {
|
|
1007
|
+
throw new Error("Database not initialized");
|
|
1008
|
+
}
|
|
1009
|
+
const records = await db.select().from(verificationCodes).where(
|
|
1010
|
+
and(
|
|
1011
|
+
eq(verificationCodes.target, target),
|
|
1012
|
+
eq(verificationCodes.targetType, targetType),
|
|
1013
|
+
eq(verificationCodes.code, code),
|
|
1014
|
+
eq(verificationCodes.purpose, purpose)
|
|
1015
|
+
)
|
|
1016
|
+
).limit(1);
|
|
1017
|
+
if (records.length === 0) {
|
|
1018
|
+
return { valid: false, error: "Invalid verification code" };
|
|
1019
|
+
}
|
|
1020
|
+
const record = records[0];
|
|
1021
|
+
if (record.usedAt) {
|
|
1022
|
+
return { valid: false, error: "Verification code already used" };
|
|
1023
|
+
}
|
|
1024
|
+
if (/* @__PURE__ */ new Date() > new Date(record.expiresAt)) {
|
|
1025
|
+
return { valid: false, error: "Verification code expired" };
|
|
1026
|
+
}
|
|
1027
|
+
const attempts = parseInt(record.attempts, 10);
|
|
1028
|
+
if (attempts >= MAX_VERIFICATION_ATTEMPTS) {
|
|
1029
|
+
return { valid: false, error: "Too many attempts, please request a new code" };
|
|
1030
|
+
}
|
|
1031
|
+
await db.update(verificationCodes).set({ attempts: (attempts + 1).toString() }).where(eq(verificationCodes.id, record.id));
|
|
1032
|
+
return { valid: true, codeId: record.id };
|
|
1033
|
+
}
|
|
1034
|
+
async function markCodeAsUsed(codeId) {
|
|
1035
|
+
const db = getDatabase();
|
|
1036
|
+
if (!db) {
|
|
1037
|
+
throw new Error("Database not initialized");
|
|
1038
|
+
}
|
|
1039
|
+
await db.update(verificationCodes).set({ usedAt: /* @__PURE__ */ new Date() }).where(eq(verificationCodes.id, codeId));
|
|
1040
|
+
}
|
|
1041
|
+
function createVerificationToken(payload) {
|
|
1042
|
+
const secret = getVerificationTokenSecret();
|
|
1043
|
+
return jwt2.sign(payload, secret, {
|
|
1044
|
+
expiresIn: VERIFICATION_TOKEN_EXPIRY,
|
|
1045
|
+
issuer: "spfn-auth",
|
|
1046
|
+
audience: "spfn-client"
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
function validateVerificationToken(token) {
|
|
1050
|
+
try {
|
|
1051
|
+
const secret = getVerificationTokenSecret();
|
|
1052
|
+
const decoded = jwt2.verify(token, secret, {
|
|
1053
|
+
issuer: "spfn-auth",
|
|
1054
|
+
audience: "spfn-client"
|
|
1055
|
+
});
|
|
1056
|
+
if (typeof decoded === "object" && decoded !== null && "target" in decoded && "targetType" in decoded && "purpose" in decoded && "codeId" in decoded) {
|
|
1057
|
+
return decoded;
|
|
1058
|
+
}
|
|
1059
|
+
return null;
|
|
1060
|
+
} catch (error) {
|
|
1061
|
+
console.error("[validateVerificationToken] Error:", error);
|
|
1062
|
+
return null;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
async function sendVerificationEmail(email, code, purpose) {
|
|
1066
|
+
console.log(`[VERIFICATION EMAIL] To: ${email}, Code: ${code}, Purpose: ${purpose}`);
|
|
1067
|
+
}
|
|
1068
|
+
async function sendVerificationSMS(phone, code, purpose) {
|
|
1069
|
+
console.log(`[VERIFICATION SMS] To: ${phone}, Code: ${code}, Purpose: ${purpose}`);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// src/server/helpers/context.ts
|
|
1073
|
+
function getAuth(c) {
|
|
1074
|
+
if ("raw" in c && c.raw) {
|
|
1075
|
+
return c.raw.get("auth");
|
|
1076
|
+
}
|
|
1077
|
+
return c.get("auth");
|
|
1078
|
+
}
|
|
1079
|
+
function getUser(c) {
|
|
1080
|
+
return getAuth(c).user;
|
|
1081
|
+
}
|
|
1082
|
+
function getUserId(c) {
|
|
1083
|
+
return getAuth(c).userId;
|
|
1084
|
+
}
|
|
1085
|
+
function getKeyId(c) {
|
|
1086
|
+
return getAuth(c).keyId;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// src/server/errors/auth-errors.ts
|
|
1090
|
+
import {
|
|
1091
|
+
ValidationError,
|
|
1092
|
+
UnauthorizedError,
|
|
1093
|
+
ForbiddenError,
|
|
1094
|
+
ConflictError
|
|
1095
|
+
} from "@spfn/core/errors";
|
|
1096
|
+
var InvalidCredentialsError = class extends UnauthorizedError {
|
|
1097
|
+
constructor(message = "Invalid credentials") {
|
|
1098
|
+
super(message);
|
|
1099
|
+
this.name = "InvalidCredentialsError";
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
var InvalidTokenError = class extends UnauthorizedError {
|
|
1103
|
+
constructor(message = "Invalid authentication token") {
|
|
1104
|
+
super(message);
|
|
1105
|
+
this.name = "InvalidTokenError";
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
var TokenExpiredError = class extends UnauthorizedError {
|
|
1109
|
+
constructor(message = "Authentication token has expired") {
|
|
1110
|
+
super(message);
|
|
1111
|
+
this.name = "TokenExpiredError";
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1114
|
+
var KeyExpiredError = class extends UnauthorizedError {
|
|
1115
|
+
constructor(message = "Public key has expired") {
|
|
1116
|
+
super(message);
|
|
1117
|
+
this.name = "KeyExpiredError";
|
|
1118
|
+
}
|
|
1119
|
+
};
|
|
1120
|
+
var AccountDisabledError = class extends ForbiddenError {
|
|
1121
|
+
constructor(status = "disabled") {
|
|
1122
|
+
super(`Account is ${status}`);
|
|
1123
|
+
this.name = "AccountDisabledError";
|
|
1124
|
+
this.details = { status };
|
|
1125
|
+
}
|
|
1126
|
+
};
|
|
1127
|
+
var AccountAlreadyExistsError = class extends ConflictError {
|
|
1128
|
+
constructor(identifier, identifierType) {
|
|
1129
|
+
super("Account already exists");
|
|
1130
|
+
this.name = "AccountAlreadyExistsError";
|
|
1131
|
+
this.details = { identifier, identifierType };
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
var InvalidVerificationCodeError = class extends ValidationError {
|
|
1135
|
+
constructor(reason = "Invalid verification code") {
|
|
1136
|
+
super(reason);
|
|
1137
|
+
this.name = "InvalidVerificationCodeError";
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
var InvalidVerificationTokenError = class extends ValidationError {
|
|
1141
|
+
constructor(message = "Invalid or expired verification token") {
|
|
1142
|
+
super(message);
|
|
1143
|
+
this.name = "InvalidVerificationTokenError";
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
var InvalidKeyFingerprintError = class extends ValidationError {
|
|
1147
|
+
constructor(message = "Invalid key fingerprint") {
|
|
1148
|
+
super(message);
|
|
1149
|
+
this.name = "InvalidKeyFingerprintError";
|
|
1150
|
+
}
|
|
1151
|
+
};
|
|
1152
|
+
var VerificationTokenPurposeMismatchError = class extends ValidationError {
|
|
1153
|
+
constructor(expected, actual) {
|
|
1154
|
+
super(`Verification token is for ${actual}, but ${expected} was expected`);
|
|
1155
|
+
this.name = "VerificationTokenPurposeMismatchError";
|
|
1156
|
+
this.details = { expected, actual };
|
|
1157
|
+
}
|
|
1158
|
+
};
|
|
1159
|
+
var VerificationTokenTargetMismatchError = class extends ValidationError {
|
|
1160
|
+
constructor() {
|
|
1161
|
+
super("Verification token does not match provided email/phone");
|
|
1162
|
+
this.name = "VerificationTokenTargetMismatchError";
|
|
1163
|
+
}
|
|
1164
|
+
};
|
|
1165
|
+
|
|
1166
|
+
// src/server/services/key.service.ts
|
|
1167
|
+
init_entities();
|
|
1168
|
+
import { create as create2, getDatabase as getDatabase2 } from "@spfn/core/db";
|
|
1169
|
+
import { eq as eq2, and as and2 } from "drizzle-orm";
|
|
1170
|
+
function getKeyExpiryDate() {
|
|
1171
|
+
const expiresAt = /* @__PURE__ */ new Date();
|
|
1172
|
+
expiresAt.setDate(expiresAt.getDate() + 90);
|
|
1173
|
+
return expiresAt;
|
|
1174
|
+
}
|
|
1175
|
+
async function registerPublicKeyService(params) {
|
|
1176
|
+
const { userId, keyId, publicKey, fingerprint, algorithm = "ES256" } = params;
|
|
1177
|
+
const isValidFingerprint = verifyKeyFingerprint(publicKey, fingerprint);
|
|
1178
|
+
if (!isValidFingerprint) {
|
|
1179
|
+
throw new InvalidKeyFingerprintError();
|
|
1180
|
+
}
|
|
1181
|
+
await create2(userPublicKeys, {
|
|
1182
|
+
userId,
|
|
1183
|
+
keyId,
|
|
1184
|
+
publicKey,
|
|
1185
|
+
algorithm,
|
|
1186
|
+
fingerprint,
|
|
1187
|
+
isActive: true,
|
|
1188
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
1189
|
+
expiresAt: getKeyExpiryDate()
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
async function rotateKeyService(params) {
|
|
1193
|
+
const { userId, oldKeyId, newKeyId, newPublicKey, fingerprint, algorithm = "ES256" } = params;
|
|
1194
|
+
const isValidFingerprint = verifyKeyFingerprint(newPublicKey, fingerprint);
|
|
1195
|
+
if (!isValidFingerprint) {
|
|
1196
|
+
throw new InvalidKeyFingerprintError();
|
|
1197
|
+
}
|
|
1198
|
+
const db = getDatabase2();
|
|
1199
|
+
await db.update(userPublicKeys).set({
|
|
1200
|
+
isActive: false,
|
|
1201
|
+
revokedAt: /* @__PURE__ */ new Date(),
|
|
1202
|
+
revokedReason: "Replaced by key rotation"
|
|
1203
|
+
}).where(
|
|
1204
|
+
and2(
|
|
1205
|
+
eq2(userPublicKeys.keyId, oldKeyId),
|
|
1206
|
+
eq2(userPublicKeys.userId, userId)
|
|
1207
|
+
)
|
|
1208
|
+
);
|
|
1209
|
+
await create2(userPublicKeys, {
|
|
1210
|
+
userId,
|
|
1211
|
+
keyId: newKeyId,
|
|
1212
|
+
publicKey: newPublicKey,
|
|
1213
|
+
algorithm,
|
|
1214
|
+
fingerprint,
|
|
1215
|
+
isActive: true,
|
|
1216
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
1217
|
+
expiresAt: getKeyExpiryDate()
|
|
1218
|
+
});
|
|
1219
|
+
return {
|
|
1220
|
+
success: true,
|
|
1221
|
+
keyId: newKeyId
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
async function revokeKeyService(params) {
|
|
1225
|
+
const { userId, keyId, reason } = params;
|
|
1226
|
+
const db = getDatabase2();
|
|
1227
|
+
await db.update(userPublicKeys).set({
|
|
1228
|
+
isActive: false,
|
|
1229
|
+
revokedAt: /* @__PURE__ */ new Date(),
|
|
1230
|
+
revokedReason: reason
|
|
1231
|
+
}).where(
|
|
1232
|
+
and2(
|
|
1233
|
+
eq2(userPublicKeys.keyId, keyId),
|
|
1234
|
+
eq2(userPublicKeys.userId, userId)
|
|
1235
|
+
)
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// src/server/services/user.service.ts
|
|
1240
|
+
init_entities();
|
|
1241
|
+
import { findOne, updateOne } from "@spfn/core/db";
|
|
1242
|
+
async function getUserByIdService(userId) {
|
|
1243
|
+
return await findOne(users, { id: userId });
|
|
1244
|
+
}
|
|
1245
|
+
async function getUserByEmailService(email) {
|
|
1246
|
+
return await findOne(users, { email });
|
|
1247
|
+
}
|
|
1248
|
+
async function getUserByPhoneService(phone) {
|
|
1249
|
+
return await findOne(users, { phone });
|
|
1250
|
+
}
|
|
1251
|
+
async function updateLastLoginService(userId) {
|
|
1252
|
+
await updateOne(users, { id: userId }, {
|
|
1253
|
+
lastLoginAt: /* @__PURE__ */ new Date()
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
async function updateUserService(userId, updates) {
|
|
1257
|
+
await updateOne(users, { id: userId }, {
|
|
1258
|
+
...updates,
|
|
1259
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// src/server/services/auth.service.ts
|
|
1264
|
+
async function checkAccountExistsService(params) {
|
|
1265
|
+
const { email, phone } = params;
|
|
1266
|
+
let identifier;
|
|
1267
|
+
let identifierType;
|
|
1268
|
+
let user;
|
|
1269
|
+
if (email) {
|
|
1270
|
+
identifier = email;
|
|
1271
|
+
identifierType = "email";
|
|
1272
|
+
user = await findOne2(users, { email });
|
|
1273
|
+
} else if (phone) {
|
|
1274
|
+
identifier = phone;
|
|
1275
|
+
identifierType = "phone";
|
|
1276
|
+
user = await findOne2(users, { phone });
|
|
1277
|
+
} else {
|
|
1278
|
+
throw new ValidationError2("Either email or phone must be provided");
|
|
1279
|
+
}
|
|
1280
|
+
return {
|
|
1281
|
+
exists: !!user,
|
|
1282
|
+
identifier,
|
|
1283
|
+
identifierType
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
async function registerService(params) {
|
|
1287
|
+
const { email, phone, verificationToken, password, publicKey, keyId, fingerprint, algorithm } = params;
|
|
1288
|
+
const tokenPayload = validateVerificationToken(verificationToken);
|
|
1289
|
+
if (!tokenPayload) {
|
|
1290
|
+
throw new InvalidVerificationTokenError();
|
|
1291
|
+
}
|
|
1292
|
+
if (tokenPayload.purpose !== "registration") {
|
|
1293
|
+
throw new VerificationTokenPurposeMismatchError("registration", tokenPayload.purpose);
|
|
1294
|
+
}
|
|
1295
|
+
const providedTarget = email || phone;
|
|
1296
|
+
if (tokenPayload.target !== providedTarget) {
|
|
1297
|
+
throw new VerificationTokenTargetMismatchError();
|
|
1298
|
+
}
|
|
1299
|
+
const providedTargetType = email ? "email" : "phone";
|
|
1300
|
+
if (tokenPayload.targetType !== providedTargetType) {
|
|
1301
|
+
throw new VerificationTokenTargetMismatchError();
|
|
1302
|
+
}
|
|
1303
|
+
let existingUser;
|
|
1304
|
+
if (email) {
|
|
1305
|
+
existingUser = await findOne2(users, { email });
|
|
1306
|
+
} else if (phone) {
|
|
1307
|
+
existingUser = await findOne2(users, { phone });
|
|
1308
|
+
} else {
|
|
1309
|
+
throw new ValidationError2("Either email or phone must be provided");
|
|
1310
|
+
}
|
|
1311
|
+
if (existingUser) {
|
|
1312
|
+
const identifierType = email ? "email" : "phone";
|
|
1313
|
+
throw new AccountAlreadyExistsError(email || phone, identifierType);
|
|
1314
|
+
}
|
|
1315
|
+
const passwordHash = await hashPassword(password);
|
|
1316
|
+
const { getRoleByName: getRoleByName2 } = await Promise.resolve().then(() => (init_role_service(), role_service_exports));
|
|
1317
|
+
const userRole = await getRoleByName2("user");
|
|
1318
|
+
if (!userRole) {
|
|
1319
|
+
throw new Error("Default user role not found. Run initializeAuth() first.");
|
|
1320
|
+
}
|
|
1321
|
+
const newUser = await create3(users, {
|
|
1322
|
+
email: email || null,
|
|
1323
|
+
phone: phone || null,
|
|
1324
|
+
passwordHash,
|
|
1325
|
+
passwordChangeRequired: false,
|
|
1326
|
+
roleId: userRole.id,
|
|
1327
|
+
status: "active",
|
|
1328
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
1329
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1330
|
+
});
|
|
1331
|
+
await registerPublicKeyService({
|
|
1332
|
+
userId: newUser.id,
|
|
1333
|
+
keyId,
|
|
1334
|
+
publicKey,
|
|
1335
|
+
fingerprint,
|
|
1336
|
+
algorithm
|
|
1337
|
+
});
|
|
1338
|
+
return {
|
|
1339
|
+
userId: String(newUser.id),
|
|
1340
|
+
email: newUser.email || void 0,
|
|
1341
|
+
phone: newUser.phone || void 0
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
async function loginService(params) {
|
|
1345
|
+
const { email, phone, password, publicKey, keyId, fingerprint, oldKeyId, algorithm } = params;
|
|
1346
|
+
let user;
|
|
1347
|
+
if (email) {
|
|
1348
|
+
user = await findOne2(users, { email });
|
|
1349
|
+
} else if (phone) {
|
|
1350
|
+
user = await findOne2(users, { phone });
|
|
1351
|
+
} else {
|
|
1352
|
+
throw new ValidationError2("Either email or phone must be provided");
|
|
1353
|
+
}
|
|
1354
|
+
if (!user || !user.passwordHash) {
|
|
1355
|
+
throw new InvalidCredentialsError();
|
|
1356
|
+
}
|
|
1357
|
+
const isValid = await verifyPassword(password, user.passwordHash);
|
|
1358
|
+
if (!isValid) {
|
|
1359
|
+
throw new InvalidCredentialsError();
|
|
1360
|
+
}
|
|
1361
|
+
if (user.status !== "active") {
|
|
1362
|
+
throw new AccountDisabledError(user.status);
|
|
1363
|
+
}
|
|
1364
|
+
if (oldKeyId) {
|
|
1365
|
+
await revokeKeyService({
|
|
1366
|
+
userId: user.id,
|
|
1367
|
+
keyId: oldKeyId,
|
|
1368
|
+
reason: "Replaced by new key on login"
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
await registerPublicKeyService({
|
|
1372
|
+
userId: user.id,
|
|
1373
|
+
keyId,
|
|
1374
|
+
publicKey,
|
|
1375
|
+
fingerprint,
|
|
1376
|
+
algorithm
|
|
1377
|
+
});
|
|
1378
|
+
await updateLastLoginService(user.id);
|
|
1379
|
+
return {
|
|
1380
|
+
userId: String(user.id),
|
|
1381
|
+
email: user.email || void 0,
|
|
1382
|
+
phone: user.phone || void 0,
|
|
1383
|
+
passwordChangeRequired: user.passwordChangeRequired
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
async function logoutService(params) {
|
|
1387
|
+
const { userId, keyId } = params;
|
|
1388
|
+
await revokeKeyService({
|
|
1389
|
+
userId,
|
|
1390
|
+
keyId,
|
|
1391
|
+
reason: "Revoked by logout"
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
async function changePasswordService(params) {
|
|
1395
|
+
const { userId, currentPassword, newPassword, passwordHash: providedHash } = params;
|
|
1396
|
+
let passwordHash;
|
|
1397
|
+
if (providedHash) {
|
|
1398
|
+
passwordHash = providedHash;
|
|
1399
|
+
} else {
|
|
1400
|
+
const user = await findOne2(users, { id: userId });
|
|
1401
|
+
if (!user) {
|
|
1402
|
+
throw new ValidationError2("User not found");
|
|
1403
|
+
}
|
|
1404
|
+
passwordHash = user.passwordHash;
|
|
1405
|
+
}
|
|
1406
|
+
if (!passwordHash) {
|
|
1407
|
+
throw new ValidationError2("No password set for this account");
|
|
1408
|
+
}
|
|
1409
|
+
const isValid = await verifyPassword(currentPassword, passwordHash);
|
|
1410
|
+
if (!isValid) {
|
|
1411
|
+
throw new InvalidCredentialsError("Current password is incorrect");
|
|
1412
|
+
}
|
|
1413
|
+
const newPasswordHash = await hashPassword(newPassword);
|
|
1414
|
+
const { updateOne: updateOne2 } = await import("@spfn/core/db");
|
|
1415
|
+
await updateOne2(users, { id: userId }, {
|
|
1416
|
+
passwordHash: newPasswordHash,
|
|
1417
|
+
passwordChangeRequired: false,
|
|
1418
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// src/server/services/verification.service.ts
|
|
1423
|
+
async function sendVerificationCodeService(params) {
|
|
1424
|
+
const { target, targetType, purpose } = params;
|
|
1425
|
+
const code = generateVerificationCode();
|
|
1426
|
+
const codeRecord = await storeVerificationCode(target, targetType, code, purpose);
|
|
1427
|
+
if (targetType === "email") {
|
|
1428
|
+
await sendVerificationEmail(target, code, purpose);
|
|
1429
|
+
} else {
|
|
1430
|
+
await sendVerificationSMS(target, code, purpose);
|
|
1431
|
+
}
|
|
1432
|
+
return {
|
|
1433
|
+
success: true,
|
|
1434
|
+
expiresAt: codeRecord.expiresAt.toISOString()
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
async function verifyCodeService(params) {
|
|
1438
|
+
const { target, targetType, code, purpose } = params;
|
|
1439
|
+
const validation = await validateVerificationCode(target, targetType, code, purpose);
|
|
1440
|
+
if (!validation.valid) {
|
|
1441
|
+
throw new InvalidVerificationCodeError(validation.error || "Invalid verification code");
|
|
1442
|
+
}
|
|
1443
|
+
await markCodeAsUsed(validation.codeId);
|
|
1444
|
+
const verificationToken = createVerificationToken({
|
|
1445
|
+
target,
|
|
1446
|
+
targetType,
|
|
1447
|
+
purpose,
|
|
1448
|
+
codeId: validation.codeId
|
|
1449
|
+
});
|
|
1450
|
+
return {
|
|
1451
|
+
valid: true,
|
|
1452
|
+
verificationToken
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// src/server/services/rbac.service.ts
|
|
1457
|
+
init_entities();
|
|
1458
|
+
import { getDatabase as getDatabase4 } from "@spfn/core/db";
|
|
1459
|
+
import { eq as eq4, and as and4, inArray } from "drizzle-orm";
|
|
1460
|
+
async function initializeAuth(options = {}) {
|
|
1461
|
+
const db = getDatabase4();
|
|
1462
|
+
if (!db) {
|
|
1463
|
+
throw new Error("[Auth] Database not initialized. Call initDatabase() first.");
|
|
1464
|
+
}
|
|
1465
|
+
console.log("[Auth] \u{1F510} Initializing RBAC system...");
|
|
1466
|
+
const allRoles = [...Object.values(BUILTIN_ROLES)];
|
|
1467
|
+
if (options.usePresets) {
|
|
1468
|
+
allRoles.push(...Object.values(PRESET_ROLES));
|
|
1469
|
+
} else if (options.presetRoles) {
|
|
1470
|
+
for (const presetKey of options.presetRoles) {
|
|
1471
|
+
if (PRESET_ROLES[presetKey]) {
|
|
1472
|
+
allRoles.push(PRESET_ROLES[presetKey]);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
if (options.roles) {
|
|
1477
|
+
allRoles.push(...options.roles);
|
|
1478
|
+
}
|
|
1479
|
+
for (const roleConfig of allRoles) {
|
|
1480
|
+
await upsertRole(roleConfig);
|
|
1481
|
+
}
|
|
1482
|
+
const allPermissions = [...Object.values(BUILTIN_PERMISSIONS)];
|
|
1483
|
+
if (options.usePresets) {
|
|
1484
|
+
allPermissions.push(...Object.values(PRESET_PERMISSIONS));
|
|
1485
|
+
} else if (options.presetPermissions) {
|
|
1486
|
+
for (const presetKey of options.presetPermissions) {
|
|
1487
|
+
if (PRESET_PERMISSIONS[presetKey]) {
|
|
1488
|
+
allPermissions.push(PRESET_PERMISSIONS[presetKey]);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
if (options.permissions) {
|
|
1493
|
+
allPermissions.push(...options.permissions);
|
|
1494
|
+
}
|
|
1495
|
+
for (const permConfig of allPermissions) {
|
|
1496
|
+
await upsertPermission(permConfig);
|
|
1497
|
+
}
|
|
1498
|
+
const allMappings = { ...BUILTIN_ROLE_PERMISSIONS };
|
|
1499
|
+
if (options.usePresets) {
|
|
1500
|
+
Object.assign(allMappings, PRESET_ROLE_PERMISSIONS);
|
|
1501
|
+
}
|
|
1502
|
+
if (options.rolePermissions) {
|
|
1503
|
+
for (const [roleName, permNames] of Object.entries(options.rolePermissions)) {
|
|
1504
|
+
if (allMappings[roleName]) {
|
|
1505
|
+
allMappings[roleName] = [
|
|
1506
|
+
.../* @__PURE__ */ new Set([...allMappings[roleName], ...permNames])
|
|
1507
|
+
];
|
|
1508
|
+
} else {
|
|
1509
|
+
allMappings[roleName] = permNames;
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
for (const [roleName, permNames] of Object.entries(allMappings)) {
|
|
1514
|
+
await assignPermissionsToRole(roleName, permNames);
|
|
1515
|
+
}
|
|
1516
|
+
console.log("[Auth] \u2705 RBAC initialization complete");
|
|
1517
|
+
console.log(`[Auth] \u{1F4CA} Roles: ${allRoles.length}, Permissions: ${allPermissions.length}`);
|
|
1518
|
+
console.log(`[Auth] \u{1F512} Built-in roles: user, admin, superadmin`);
|
|
1519
|
+
}
|
|
1520
|
+
async function upsertRole(config) {
|
|
1521
|
+
const db = getDatabase4();
|
|
1522
|
+
const existing = await db.select().from(roles).where(eq4(roles.name, config.name)).limit(1);
|
|
1523
|
+
if (existing.length === 0) {
|
|
1524
|
+
await db.insert(roles).values({
|
|
1525
|
+
name: config.name,
|
|
1526
|
+
displayName: config.displayName,
|
|
1527
|
+
description: config.description,
|
|
1528
|
+
priority: config.priority ?? 10,
|
|
1529
|
+
isSystem: config.isSystem ?? false,
|
|
1530
|
+
isBuiltin: config.isBuiltin ?? false
|
|
1531
|
+
});
|
|
1532
|
+
console.log(`[Auth] \u2705 Created role: ${config.name}`);
|
|
1533
|
+
} else {
|
|
1534
|
+
const updateData = {
|
|
1535
|
+
displayName: config.displayName,
|
|
1536
|
+
description: config.description
|
|
1537
|
+
};
|
|
1538
|
+
if (!existing[0].isBuiltin) {
|
|
1539
|
+
updateData.priority = config.priority ?? existing[0].priority;
|
|
1540
|
+
}
|
|
1541
|
+
await db.update(roles).set(updateData).where(eq4(roles.id, existing[0].id));
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
async function upsertPermission(config) {
|
|
1545
|
+
const db = getDatabase4();
|
|
1546
|
+
const existing = await db.select().from(permissions).where(eq4(permissions.name, config.name)).limit(1);
|
|
1547
|
+
if (existing.length === 0) {
|
|
1548
|
+
await db.insert(permissions).values({
|
|
1549
|
+
name: config.name,
|
|
1550
|
+
displayName: config.displayName,
|
|
1551
|
+
description: config.description,
|
|
1552
|
+
category: config.category,
|
|
1553
|
+
isSystem: config.isSystem ?? false,
|
|
1554
|
+
isBuiltin: config.isBuiltin ?? false
|
|
1555
|
+
});
|
|
1556
|
+
console.log(`[Auth] \u2705 Created permission: ${config.name}`);
|
|
1557
|
+
} else {
|
|
1558
|
+
await db.update(permissions).set({
|
|
1559
|
+
displayName: config.displayName,
|
|
1560
|
+
description: config.description,
|
|
1561
|
+
category: config.category
|
|
1562
|
+
}).where(eq4(permissions.id, existing[0].id));
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
async function assignPermissionsToRole(roleName, permissionNames) {
|
|
1566
|
+
const db = getDatabase4();
|
|
1567
|
+
const [role] = await db.select().from(roles).where(eq4(roles.name, roleName)).limit(1);
|
|
1568
|
+
if (!role) {
|
|
1569
|
+
console.warn(`[Auth] \u26A0\uFE0F Role not found: ${roleName}, skipping permission assignment`);
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
const perms = await db.select().from(permissions).where(inArray(permissions.name, permissionNames));
|
|
1573
|
+
if (perms.length === 0) {
|
|
1574
|
+
console.warn(`[Auth] \u26A0\uFE0F No permissions found for role: ${roleName}`);
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
for (const perm of perms) {
|
|
1578
|
+
const existing = await db.select().from(rolePermissions).where(
|
|
1579
|
+
and4(
|
|
1580
|
+
eq4(rolePermissions.roleId, role.id),
|
|
1581
|
+
eq4(rolePermissions.permissionId, perm.id)
|
|
1582
|
+
)
|
|
1583
|
+
).limit(1);
|
|
1584
|
+
if (existing.length === 0) {
|
|
1585
|
+
await db.insert(rolePermissions).values({
|
|
1586
|
+
roleId: role.id,
|
|
1587
|
+
permissionId: perm.id
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
// src/server/services/permission.service.ts
|
|
1594
|
+
init_entities();
|
|
1595
|
+
import { getDatabase as getDatabase5 } from "@spfn/core/db";
|
|
1596
|
+
import { eq as eq5, and as and5 } from "drizzle-orm";
|
|
1597
|
+
async function getUserPermissions(userId) {
|
|
1598
|
+
const db = getDatabase5();
|
|
1599
|
+
if (!db) {
|
|
1600
|
+
throw new Error("[Auth] Database not initialized");
|
|
1601
|
+
}
|
|
1602
|
+
const userIdNum = typeof userId === "string" ? Number(userId) : Number(userId);
|
|
1603
|
+
const [user] = await db.select({ roleId: users.roleId }).from(users).where(eq5(users.id, userIdNum)).limit(1);
|
|
1604
|
+
if (!user || !user.roleId) {
|
|
1605
|
+
return [];
|
|
1606
|
+
}
|
|
1607
|
+
const permSet = /* @__PURE__ */ new Set();
|
|
1608
|
+
const rolePerms = await db.select({ name: permissions.name }).from(rolePermissions).innerJoin(permissions, eq5(rolePermissions.permissionId, permissions.id)).where(
|
|
1609
|
+
and5(
|
|
1610
|
+
eq5(rolePermissions.roleId, user.roleId),
|
|
1611
|
+
eq5(permissions.isActive, true)
|
|
1612
|
+
)
|
|
1613
|
+
);
|
|
1614
|
+
for (const perm of rolePerms) {
|
|
1615
|
+
permSet.add(perm.name);
|
|
1616
|
+
}
|
|
1617
|
+
const userPerms = await db.select({
|
|
1618
|
+
name: permissions.name,
|
|
1619
|
+
granted: userPermissions.granted,
|
|
1620
|
+
expiresAt: userPermissions.expiresAt
|
|
1621
|
+
}).from(userPermissions).innerJoin(permissions, eq5(userPermissions.permissionId, permissions.id)).where(eq5(userPermissions.userId, userIdNum));
|
|
1622
|
+
const now = /* @__PURE__ */ new Date();
|
|
1623
|
+
for (const userPerm of userPerms) {
|
|
1624
|
+
if (userPerm.expiresAt && userPerm.expiresAt < now) {
|
|
1625
|
+
continue;
|
|
1626
|
+
}
|
|
1627
|
+
if (userPerm.granted) {
|
|
1628
|
+
permSet.add(userPerm.name);
|
|
1629
|
+
} else {
|
|
1630
|
+
permSet.delete(userPerm.name);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
return Array.from(permSet);
|
|
1634
|
+
}
|
|
1635
|
+
async function hasPermission(userId, permissionName) {
|
|
1636
|
+
const perms = await getUserPermissions(userId);
|
|
1637
|
+
return perms.includes(permissionName);
|
|
1638
|
+
}
|
|
1639
|
+
async function hasAnyPermission(userId, permissionNames) {
|
|
1640
|
+
const perms = await getUserPermissions(userId);
|
|
1641
|
+
return permissionNames.some((p) => perms.includes(p));
|
|
1642
|
+
}
|
|
1643
|
+
async function hasAllPermissions(userId, permissionNames) {
|
|
1644
|
+
const perms = await getUserPermissions(userId);
|
|
1645
|
+
return permissionNames.every((p) => perms.includes(p));
|
|
1646
|
+
}
|
|
1647
|
+
async function hasRole(userId, roleName) {
|
|
1648
|
+
const db = getDatabase5();
|
|
1649
|
+
if (!db) {
|
|
1650
|
+
throw new Error("[Auth] Database not initialized");
|
|
1651
|
+
}
|
|
1652
|
+
const userIdNum = typeof userId === "string" ? Number(userId) : Number(userId);
|
|
1653
|
+
const [user] = await db.select({ roleId: users.roleId }).from(users).where(eq5(users.id, userIdNum)).limit(1);
|
|
1654
|
+
if (!user || !user.roleId) {
|
|
1655
|
+
return false;
|
|
1656
|
+
}
|
|
1657
|
+
const [role] = await db.select({ name: roles.name }).from(roles).where(eq5(roles.id, user.roleId)).limit(1);
|
|
1658
|
+
return role?.name === roleName;
|
|
1659
|
+
}
|
|
1660
|
+
async function hasAnyRole(userId, roleNames) {
|
|
1661
|
+
for (const roleName of roleNames) {
|
|
1662
|
+
if (await hasRole(userId, roleName)) {
|
|
1663
|
+
return true;
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
return false;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
// src/server/services/index.ts
|
|
1670
|
+
init_role_service();
|
|
1671
|
+
|
|
1672
|
+
// src/server/services/invitation.service.ts
|
|
1673
|
+
init_entities();
|
|
1674
|
+
import { getDatabase as getDatabase6 } from "@spfn/core/db";
|
|
1675
|
+
import { eq as eq6, and as and6, lt, desc, sql as sql2 } from "drizzle-orm";
|
|
1676
|
+
import crypto2 from "crypto";
|
|
1677
|
+
function generateInvitationToken() {
|
|
1678
|
+
return crypto2.randomUUID();
|
|
1679
|
+
}
|
|
1680
|
+
function calculateExpiresAt(days = 7) {
|
|
1681
|
+
const expiresAt = /* @__PURE__ */ new Date();
|
|
1682
|
+
expiresAt.setDate(expiresAt.getDate() + days);
|
|
1683
|
+
return expiresAt;
|
|
1684
|
+
}
|
|
1685
|
+
async function createInvitation(params) {
|
|
1686
|
+
const db = getDatabase6();
|
|
1687
|
+
if (!db) {
|
|
1688
|
+
throw new Error("[Auth] Database not initialized");
|
|
1689
|
+
}
|
|
1690
|
+
const { email, roleId, invitedBy, expiresInDays = 7, metadata } = params;
|
|
1691
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1692
|
+
if (!emailRegex.test(email)) {
|
|
1693
|
+
throw new Error("Invalid email format");
|
|
1694
|
+
}
|
|
1695
|
+
const existingUser = await db.select().from(users).where(eq6(users.email, email)).limit(1);
|
|
1696
|
+
if (existingUser.length > 0) {
|
|
1697
|
+
throw new Error("User with this email already exists");
|
|
1698
|
+
}
|
|
1699
|
+
const existingInvitation = await db.select().from(invitations).where(
|
|
1700
|
+
and6(
|
|
1701
|
+
eq6(invitations.email, email),
|
|
1702
|
+
eq6(invitations.status, "pending")
|
|
1703
|
+
)
|
|
1704
|
+
).limit(1);
|
|
1705
|
+
if (existingInvitation.length > 0) {
|
|
1706
|
+
throw new Error("Pending invitation already exists for this email");
|
|
1707
|
+
}
|
|
1708
|
+
const role = await db.select().from(roles).where(eq6(roles.id, roleId)).limit(1);
|
|
1709
|
+
if (role.length === 0) {
|
|
1710
|
+
throw new Error(`Role with id ${roleId} not found`);
|
|
1711
|
+
}
|
|
1712
|
+
const inviter = await db.select().from(users).where(eq6(users.id, invitedBy)).limit(1);
|
|
1713
|
+
if (inviter.length === 0) {
|
|
1714
|
+
throw new Error(`User with id ${invitedBy} not found`);
|
|
1715
|
+
}
|
|
1716
|
+
const token = generateInvitationToken();
|
|
1717
|
+
const expiresAt = calculateExpiresAt(expiresInDays);
|
|
1718
|
+
const [invitation] = await db.insert(invitations).values({
|
|
1719
|
+
email,
|
|
1720
|
+
token,
|
|
1721
|
+
roleId,
|
|
1722
|
+
invitedBy,
|
|
1723
|
+
status: "pending",
|
|
1724
|
+
expiresAt,
|
|
1725
|
+
metadata: metadata || null
|
|
1726
|
+
}).returning();
|
|
1727
|
+
console.log(`[Auth] \u2705 Created invitation: ${email} as ${role[0].name} (expires: ${expiresAt.toISOString()})`);
|
|
1728
|
+
return invitation;
|
|
1729
|
+
}
|
|
1730
|
+
async function getInvitationByToken(token) {
|
|
1731
|
+
const db = getDatabase6();
|
|
1732
|
+
if (!db) {
|
|
1733
|
+
throw new Error("[Auth] Database not initialized");
|
|
1734
|
+
}
|
|
1735
|
+
const result = await db.select().from(invitations).where(eq6(invitations.token, token)).limit(1);
|
|
1736
|
+
return result[0] || null;
|
|
1737
|
+
}
|
|
1738
|
+
async function getInvitationWithDetails(token) {
|
|
1739
|
+
const db = getDatabase6();
|
|
1740
|
+
if (!db) {
|
|
1741
|
+
throw new Error("[Auth] Database not initialized");
|
|
1742
|
+
}
|
|
1743
|
+
const result = await db.select({
|
|
1744
|
+
id: invitations.id,
|
|
1745
|
+
email: invitations.email,
|
|
1746
|
+
token: invitations.token,
|
|
1747
|
+
roleId: invitations.roleId,
|
|
1748
|
+
invitedBy: invitations.invitedBy,
|
|
1749
|
+
status: invitations.status,
|
|
1750
|
+
expiresAt: invitations.expiresAt,
|
|
1751
|
+
acceptedAt: invitations.acceptedAt,
|
|
1752
|
+
cancelledAt: invitations.cancelledAt,
|
|
1753
|
+
metadata: invitations.metadata,
|
|
1754
|
+
createdAt: invitations.createdAt,
|
|
1755
|
+
updatedAt: invitations.updatedAt,
|
|
1756
|
+
role: {
|
|
1757
|
+
id: roles.id,
|
|
1758
|
+
name: roles.name,
|
|
1759
|
+
displayName: roles.displayName
|
|
1760
|
+
},
|
|
1761
|
+
inviter: {
|
|
1762
|
+
id: users.id,
|
|
1763
|
+
email: users.email
|
|
1764
|
+
}
|
|
1765
|
+
}).from(invitations).innerJoin(roles, eq6(invitations.roleId, roles.id)).innerJoin(users, eq6(invitations.invitedBy, users.id)).where(eq6(invitations.token, token)).limit(1);
|
|
1766
|
+
return result[0] || null;
|
|
1767
|
+
}
|
|
1768
|
+
async function validateInvitation(token) {
|
|
1769
|
+
const invitation = await getInvitationByToken(token);
|
|
1770
|
+
if (!invitation) {
|
|
1771
|
+
return { valid: false, error: "Invitation not found" };
|
|
1772
|
+
}
|
|
1773
|
+
if (invitation.status === "accepted") {
|
|
1774
|
+
return { valid: false, error: "Invitation already accepted", invitation };
|
|
1775
|
+
}
|
|
1776
|
+
if (invitation.status === "cancelled") {
|
|
1777
|
+
return { valid: false, error: "Invitation was cancelled", invitation };
|
|
1778
|
+
}
|
|
1779
|
+
if (invitation.status === "expired") {
|
|
1780
|
+
return { valid: false, error: "Invitation has expired", invitation };
|
|
1781
|
+
}
|
|
1782
|
+
if (/* @__PURE__ */ new Date() > new Date(invitation.expiresAt)) {
|
|
1783
|
+
return { valid: false, error: "Invitation has expired", invitation };
|
|
1784
|
+
}
|
|
1785
|
+
return { valid: true, invitation };
|
|
1786
|
+
}
|
|
1787
|
+
async function acceptInvitation(params) {
|
|
1788
|
+
const db = getDatabase6();
|
|
1789
|
+
if (!db) {
|
|
1790
|
+
throw new Error("[Auth] Database not initialized");
|
|
1791
|
+
}
|
|
1792
|
+
const { token, password, publicKey, keyId, fingerprint, algorithm } = params;
|
|
1793
|
+
const validation = await validateInvitation(token);
|
|
1794
|
+
if (!validation.valid || !validation.invitation) {
|
|
1795
|
+
throw new Error(validation.error || "Invalid invitation");
|
|
1796
|
+
}
|
|
1797
|
+
const invitation = validation.invitation;
|
|
1798
|
+
const role = await db.select().from(roles).where(eq6(roles.id, invitation.roleId)).limit(1);
|
|
1799
|
+
if (role.length === 0) {
|
|
1800
|
+
throw new Error("Role not found");
|
|
1801
|
+
}
|
|
1802
|
+
const passwordHash = await hashPassword(password);
|
|
1803
|
+
const result = await db.transaction(async (tx) => {
|
|
1804
|
+
const [newUser] = await tx.insert(users).values({
|
|
1805
|
+
email: invitation.email,
|
|
1806
|
+
passwordHash,
|
|
1807
|
+
roleId: invitation.roleId,
|
|
1808
|
+
emailVerifiedAt: /* @__PURE__ */ new Date(),
|
|
1809
|
+
// Auto-verify invited users
|
|
1810
|
+
passwordChangeRequired: false,
|
|
1811
|
+
status: "active"
|
|
1812
|
+
}).returning();
|
|
1813
|
+
const { userPublicKeys: userPublicKeys2 } = await Promise.resolve().then(() => (init_entities(), entities_exports));
|
|
1814
|
+
await tx.insert(userPublicKeys2).values({
|
|
1815
|
+
userId: newUser.id,
|
|
1816
|
+
keyId,
|
|
1817
|
+
publicKey,
|
|
1818
|
+
algorithm,
|
|
1819
|
+
fingerprint,
|
|
1820
|
+
isActive: true,
|
|
1821
|
+
expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1e3)
|
|
1822
|
+
// 90 days
|
|
1823
|
+
});
|
|
1824
|
+
await tx.update(invitations).set({
|
|
1825
|
+
status: "accepted",
|
|
1826
|
+
acceptedAt: /* @__PURE__ */ new Date(),
|
|
1827
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1828
|
+
}).where(eq6(invitations.id, invitation.id));
|
|
1829
|
+
return { newUser, role: role[0] };
|
|
1830
|
+
});
|
|
1831
|
+
console.log(`[Auth] \u2705 Invitation accepted: ${invitation.email} as ${result.role.name}`);
|
|
1832
|
+
return {
|
|
1833
|
+
userId: result.newUser.id,
|
|
1834
|
+
email: result.newUser.email,
|
|
1835
|
+
role: result.role.name
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
async function listInvitations(params) {
|
|
1839
|
+
const db = getDatabase6();
|
|
1840
|
+
if (!db) {
|
|
1841
|
+
throw new Error("[Auth] Database not initialized");
|
|
1842
|
+
}
|
|
1843
|
+
const { status, invitedBy, page = 1, limit = 20 } = params;
|
|
1844
|
+
const offset = (page - 1) * limit;
|
|
1845
|
+
const conditions = [];
|
|
1846
|
+
if (status) {
|
|
1847
|
+
conditions.push(eq6(invitations.status, status));
|
|
1848
|
+
}
|
|
1849
|
+
if (invitedBy) {
|
|
1850
|
+
conditions.push(eq6(invitations.invitedBy, invitedBy));
|
|
1851
|
+
}
|
|
1852
|
+
const whereClause = conditions.length > 0 ? and6(...conditions) : void 0;
|
|
1853
|
+
const countResult = await db.select({ count: sql2`count(*)` }).from(invitations).where(whereClause);
|
|
1854
|
+
const total = Number(countResult[0]?.count || 0);
|
|
1855
|
+
const results = await db.select({
|
|
1856
|
+
id: invitations.id,
|
|
1857
|
+
email: invitations.email,
|
|
1858
|
+
token: invitations.token,
|
|
1859
|
+
roleId: invitations.roleId,
|
|
1860
|
+
invitedBy: invitations.invitedBy,
|
|
1861
|
+
status: invitations.status,
|
|
1862
|
+
expiresAt: invitations.expiresAt,
|
|
1863
|
+
acceptedAt: invitations.acceptedAt,
|
|
1864
|
+
cancelledAt: invitations.cancelledAt,
|
|
1865
|
+
metadata: invitations.metadata,
|
|
1866
|
+
createdAt: invitations.createdAt,
|
|
1867
|
+
updatedAt: invitations.updatedAt,
|
|
1868
|
+
role: {
|
|
1869
|
+
id: roles.id,
|
|
1870
|
+
name: roles.name,
|
|
1871
|
+
displayName: roles.displayName
|
|
1872
|
+
},
|
|
1873
|
+
inviter: {
|
|
1874
|
+
id: users.id,
|
|
1875
|
+
email: users.email
|
|
1876
|
+
}
|
|
1877
|
+
}).from(invitations).innerJoin(roles, eq6(invitations.roleId, roles.id)).innerJoin(users, eq6(invitations.invitedBy, users.id)).where(whereClause).orderBy(desc(invitations.createdAt)).limit(limit).offset(offset);
|
|
1878
|
+
return {
|
|
1879
|
+
invitations: results,
|
|
1880
|
+
total,
|
|
1881
|
+
page,
|
|
1882
|
+
limit,
|
|
1883
|
+
totalPages: Math.ceil(total / limit)
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
async function cancelInvitation(id10, cancelledBy, reason) {
|
|
1887
|
+
const db = getDatabase6();
|
|
1888
|
+
if (!db) {
|
|
1889
|
+
throw new Error("[Auth] Database not initialized");
|
|
1890
|
+
}
|
|
1891
|
+
const invitation = await db.select().from(invitations).where(eq6(invitations.id, id10)).limit(1);
|
|
1892
|
+
if (invitation.length === 0) {
|
|
1893
|
+
throw new Error("Invitation not found");
|
|
1894
|
+
}
|
|
1895
|
+
if (invitation[0].status !== "pending") {
|
|
1896
|
+
throw new Error(`Cannot cancel ${invitation[0].status} invitation`);
|
|
1897
|
+
}
|
|
1898
|
+
await db.update(invitations).set({
|
|
1899
|
+
status: "cancelled",
|
|
1900
|
+
cancelledAt: /* @__PURE__ */ new Date(),
|
|
1901
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
1902
|
+
metadata: invitation[0].metadata ? { ...invitation[0].metadata, cancelReason: reason, cancelledBy } : { cancelReason: reason, cancelledBy }
|
|
1903
|
+
}).where(eq6(invitations.id, id10));
|
|
1904
|
+
console.log(`[Auth] \u26A0\uFE0F Invitation cancelled: ${invitation[0].email} (reason: ${reason || "none"})`);
|
|
1905
|
+
}
|
|
1906
|
+
async function deleteInvitation(id10) {
|
|
1907
|
+
const db = getDatabase6();
|
|
1908
|
+
if (!db) {
|
|
1909
|
+
throw new Error("[Auth] Database not initialized");
|
|
1910
|
+
}
|
|
1911
|
+
await db.delete(invitations).where(eq6(invitations.id, id10));
|
|
1912
|
+
console.log(`[Auth] \u{1F5D1}\uFE0F Invitation deleted: ${id10}`);
|
|
1913
|
+
}
|
|
1914
|
+
async function expireOldInvitations() {
|
|
1915
|
+
const db = getDatabase6();
|
|
1916
|
+
if (!db) {
|
|
1917
|
+
throw new Error("[Auth] Database not initialized");
|
|
1918
|
+
}
|
|
1919
|
+
const now = /* @__PURE__ */ new Date();
|
|
1920
|
+
const expiredInvitations = await db.select().from(invitations).where(
|
|
1921
|
+
and6(
|
|
1922
|
+
eq6(invitations.status, "pending"),
|
|
1923
|
+
lt(invitations.expiresAt, now)
|
|
1924
|
+
)
|
|
1925
|
+
);
|
|
1926
|
+
if (expiredInvitations.length === 0) {
|
|
1927
|
+
return 0;
|
|
1928
|
+
}
|
|
1929
|
+
await db.update(invitations).set({
|
|
1930
|
+
status: "expired",
|
|
1931
|
+
updatedAt: now
|
|
1932
|
+
}).where(
|
|
1933
|
+
and6(
|
|
1934
|
+
eq6(invitations.status, "pending"),
|
|
1935
|
+
lt(invitations.expiresAt, now)
|
|
1936
|
+
)
|
|
1937
|
+
);
|
|
1938
|
+
console.log(`[Auth] \u23F0 Expired ${expiredInvitations.length} old invitations`);
|
|
1939
|
+
return expiredInvitations.length;
|
|
1940
|
+
}
|
|
1941
|
+
async function resendInvitation(id10, expiresInDays = 7) {
|
|
1942
|
+
const db = getDatabase6();
|
|
1943
|
+
if (!db) {
|
|
1944
|
+
throw new Error("[Auth] Database not initialized");
|
|
1945
|
+
}
|
|
1946
|
+
const invitation = await db.select().from(invitations).where(eq6(invitations.id, id10)).limit(1);
|
|
1947
|
+
if (invitation.length === 0) {
|
|
1948
|
+
throw new Error("Invitation not found");
|
|
1949
|
+
}
|
|
1950
|
+
if (!["pending", "expired"].includes(invitation[0].status)) {
|
|
1951
|
+
throw new Error(`Cannot resend ${invitation[0].status} invitation`);
|
|
1952
|
+
}
|
|
1953
|
+
const newExpiresAt = calculateExpiresAt(expiresInDays);
|
|
1954
|
+
const [updated] = await db.update(invitations).set({
|
|
1955
|
+
status: "pending",
|
|
1956
|
+
expiresAt: newExpiresAt,
|
|
1957
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1958
|
+
}).where(eq6(invitations.id, id10)).returning();
|
|
1959
|
+
console.log(`[Auth] \u{1F4E7} Invitation resent: ${invitation[0].email} (new expiry: ${newExpiresAt.toISOString()})`);
|
|
1960
|
+
return updated;
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
// src/server/middleware/authenticate.ts
|
|
1964
|
+
init_entities();
|
|
1965
|
+
import { findOne as findOne3, getDatabase as getDatabase7 } from "@spfn/core/db";
|
|
1966
|
+
import { UnauthorizedError as UnauthorizedError2 } from "@spfn/core/errors";
|
|
1967
|
+
import { eq as eq7, and as and7 } from "drizzle-orm";
|
|
1968
|
+
async function authenticate(c, next) {
|
|
1969
|
+
const authHeader = c.req.header("Authorization");
|
|
1970
|
+
const keyId = c.req.header("X-Key-Id");
|
|
1971
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
1972
|
+
throw new UnauthorizedError2("Missing or invalid authorization header");
|
|
1973
|
+
}
|
|
1974
|
+
if (!keyId) {
|
|
1975
|
+
throw new UnauthorizedError2("Missing X-Key-Id header");
|
|
1976
|
+
}
|
|
1977
|
+
const token = authHeader.substring(7);
|
|
1978
|
+
const db = getDatabase7();
|
|
1979
|
+
const [keyRecord] = await db.select().from(userPublicKeys).where(
|
|
1980
|
+
and7(
|
|
1981
|
+
eq7(userPublicKeys.keyId, keyId),
|
|
1982
|
+
eq7(userPublicKeys.isActive, true)
|
|
1983
|
+
)
|
|
1984
|
+
);
|
|
1985
|
+
if (!keyRecord) {
|
|
1986
|
+
throw new UnauthorizedError2("Invalid or revoked key");
|
|
1987
|
+
}
|
|
1988
|
+
if (keyRecord.expiresAt && /* @__PURE__ */ new Date() > keyRecord.expiresAt) {
|
|
1989
|
+
throw new KeyExpiredError();
|
|
1990
|
+
}
|
|
1991
|
+
try {
|
|
1992
|
+
verifyClientToken(
|
|
1993
|
+
token,
|
|
1994
|
+
keyRecord.publicKey,
|
|
1995
|
+
keyRecord.algorithm
|
|
1996
|
+
);
|
|
1997
|
+
} catch (err) {
|
|
1998
|
+
if (err instanceof Error) {
|
|
1999
|
+
if (err.name === "TokenExpiredError") {
|
|
2000
|
+
throw new TokenExpiredError();
|
|
2001
|
+
}
|
|
2002
|
+
if (err.name === "JsonWebTokenError") {
|
|
2003
|
+
throw new InvalidTokenError("Invalid token signature");
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
throw new UnauthorizedError2("Authentication failed");
|
|
2007
|
+
}
|
|
2008
|
+
const user = await findOne3(users, { id: keyRecord.userId });
|
|
2009
|
+
if (!user) {
|
|
2010
|
+
throw new UnauthorizedError2("User not found");
|
|
2011
|
+
}
|
|
2012
|
+
if (user.status !== "active") {
|
|
2013
|
+
throw new AccountDisabledError(user.status);
|
|
2014
|
+
}
|
|
2015
|
+
db.update(userPublicKeys).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq7(userPublicKeys.id, keyRecord.id)).execute().catch((err) => console.error("Failed to update lastUsedAt:", err));
|
|
2016
|
+
c.set("auth", {
|
|
2017
|
+
user,
|
|
2018
|
+
userId: String(user.id),
|
|
2019
|
+
keyId
|
|
2020
|
+
});
|
|
2021
|
+
await next();
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
// src/server/middleware/require-permission.ts
|
|
2025
|
+
import { ForbiddenError as ForbiddenError2 } from "@spfn/core/errors";
|
|
2026
|
+
function requirePermissions(...permissionNames) {
|
|
2027
|
+
return async (c, next) => {
|
|
2028
|
+
const auth = getAuth(c);
|
|
2029
|
+
if (!auth) {
|
|
2030
|
+
throw new ForbiddenError2("Authentication required");
|
|
2031
|
+
}
|
|
2032
|
+
const { userId } = auth;
|
|
2033
|
+
const allowed = await hasAllPermissions(userId, permissionNames);
|
|
2034
|
+
if (!allowed) {
|
|
2035
|
+
throw new ForbiddenError2(
|
|
2036
|
+
`Missing required permissions: ${permissionNames.join(", ")}`
|
|
2037
|
+
);
|
|
2038
|
+
}
|
|
2039
|
+
await next();
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
function requireAnyPermission(...permissionNames) {
|
|
2043
|
+
return async (c, next) => {
|
|
2044
|
+
const auth = getAuth(c);
|
|
2045
|
+
if (!auth) {
|
|
2046
|
+
throw new ForbiddenError2("Authentication required");
|
|
2047
|
+
}
|
|
2048
|
+
const { userId } = auth;
|
|
2049
|
+
const allowed = await hasAnyPermission(userId, permissionNames);
|
|
2050
|
+
if (!allowed) {
|
|
2051
|
+
throw new ForbiddenError2(
|
|
2052
|
+
`Requires one of: ${permissionNames.join(", ")}`
|
|
2053
|
+
);
|
|
2054
|
+
}
|
|
2055
|
+
await next();
|
|
2056
|
+
};
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
// src/server/middleware/require-role.ts
|
|
2060
|
+
import { ForbiddenError as ForbiddenError3 } from "@spfn/core/errors";
|
|
2061
|
+
function requireRole(...roleNames) {
|
|
2062
|
+
return async (c, next) => {
|
|
2063
|
+
const auth = getAuth(c);
|
|
2064
|
+
if (!auth) {
|
|
2065
|
+
throw new ForbiddenError3("Authentication required");
|
|
2066
|
+
}
|
|
2067
|
+
const { userId } = auth;
|
|
2068
|
+
const allowed = await hasAnyRole(userId, roleNames);
|
|
2069
|
+
if (!allowed) {
|
|
2070
|
+
throw new ForbiddenError3(
|
|
2071
|
+
`Required roles: ${roleNames.join(", ")}`
|
|
2072
|
+
);
|
|
2073
|
+
}
|
|
2074
|
+
await next();
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
// src/server/setup.ts
|
|
2079
|
+
init_entities();
|
|
2080
|
+
import { findOne as findOne4, create as create4 } from "@spfn/core/db";
|
|
2081
|
+
init_role_service();
|
|
2082
|
+
function parseAdminAccounts() {
|
|
2083
|
+
const accounts = [];
|
|
2084
|
+
if (process.env.SPFN_AUTH_ADMIN_ACCOUNTS || process.env.ADMIN_ACCOUNTS) {
|
|
2085
|
+
try {
|
|
2086
|
+
const accountsJson = process.env.SPFN_AUTH_ADMIN_ACCOUNTS || // New prefixed version (recommended)
|
|
2087
|
+
process.env.ADMIN_ACCOUNTS;
|
|
2088
|
+
const parsed = JSON.parse(accountsJson);
|
|
2089
|
+
if (!Array.isArray(parsed)) {
|
|
2090
|
+
console.error("[Auth] \u274C SPFN_AUTH_ADMIN_ACCOUNTS must be an array");
|
|
2091
|
+
return accounts;
|
|
2092
|
+
}
|
|
2093
|
+
for (const item of parsed) {
|
|
2094
|
+
if (!item.email || !item.password) {
|
|
2095
|
+
console.warn("[Auth] \u26A0\uFE0F Skipping account: missing email or password");
|
|
2096
|
+
continue;
|
|
2097
|
+
}
|
|
2098
|
+
accounts.push({
|
|
2099
|
+
email: item.email,
|
|
2100
|
+
password: item.password,
|
|
2101
|
+
role: item.role || "user",
|
|
2102
|
+
phone: item.phone,
|
|
2103
|
+
passwordChangeRequired: item.passwordChangeRequired !== false
|
|
2104
|
+
// Default: true
|
|
2105
|
+
});
|
|
2106
|
+
}
|
|
2107
|
+
return accounts;
|
|
2108
|
+
} catch (error) {
|
|
2109
|
+
const err = error;
|
|
2110
|
+
console.error("[Auth] \u274C Failed to parse SPFN_AUTH_ADMIN_ACCOUNTS:", err.message);
|
|
2111
|
+
return accounts;
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
const adminEmails = process.env.SPFN_AUTH_ADMIN_EMAILS || // New prefixed version (recommended)
|
|
2115
|
+
process.env.ADMIN_EMAILS;
|
|
2116
|
+
if (adminEmails) {
|
|
2117
|
+
const emails = adminEmails.split(",").map((s) => s.trim());
|
|
2118
|
+
const passwords = (process.env.SPFN_AUTH_ADMIN_PASSWORDS || // New prefixed version (recommended)
|
|
2119
|
+
process.env.ADMIN_PASSWORDS || // Legacy fallback
|
|
2120
|
+
"").split(",").map((s) => s.trim());
|
|
2121
|
+
const roles2 = (process.env.SPFN_AUTH_ADMIN_ROLES || // New prefixed version (recommended)
|
|
2122
|
+
process.env.ADMIN_ROLES || // Legacy fallback
|
|
2123
|
+
"").split(",").map((s) => s.trim());
|
|
2124
|
+
if (passwords.length !== emails.length) {
|
|
2125
|
+
console.error("[Auth] \u274C SPFN_AUTH_ADMIN_EMAILS and SPFN_AUTH_ADMIN_PASSWORDS length mismatch");
|
|
2126
|
+
return accounts;
|
|
2127
|
+
}
|
|
2128
|
+
for (let i = 0; i < emails.length; i++) {
|
|
2129
|
+
const email = emails[i];
|
|
2130
|
+
const password = passwords[i];
|
|
2131
|
+
const role = roles2[i] || "user";
|
|
2132
|
+
if (!email || !password) {
|
|
2133
|
+
console.warn(`[Auth] \u26A0\uFE0F Skipping account ${i + 1}: missing email or password`);
|
|
2134
|
+
continue;
|
|
2135
|
+
}
|
|
2136
|
+
accounts.push({
|
|
2137
|
+
email,
|
|
2138
|
+
password,
|
|
2139
|
+
role,
|
|
2140
|
+
passwordChangeRequired: true
|
|
2141
|
+
});
|
|
2142
|
+
}
|
|
2143
|
+
return accounts;
|
|
2144
|
+
}
|
|
2145
|
+
const adminEmail = process.env.SPFN_AUTH_ADMIN_EMAIL || // New prefixed version (recommended)
|
|
2146
|
+
process.env.ADMIN_EMAIL;
|
|
2147
|
+
const adminPassword = process.env.SPFN_AUTH_ADMIN_PASSWORD || // New prefixed version (recommended)
|
|
2148
|
+
process.env.ADMIN_PASSWORD;
|
|
2149
|
+
if (adminEmail && adminPassword) {
|
|
2150
|
+
accounts.push({
|
|
2151
|
+
email: adminEmail,
|
|
2152
|
+
password: adminPassword,
|
|
2153
|
+
role: "superadmin",
|
|
2154
|
+
passwordChangeRequired: true
|
|
2155
|
+
});
|
|
2156
|
+
}
|
|
2157
|
+
return accounts;
|
|
2158
|
+
}
|
|
2159
|
+
async function ensureAdminExists() {
|
|
2160
|
+
const accounts = parseAdminAccounts();
|
|
2161
|
+
if (accounts.length === 0) {
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
2164
|
+
console.log(`[Auth] Creating ${accounts.length} admin account(s)...`);
|
|
2165
|
+
let created = 0;
|
|
2166
|
+
let skipped = 0;
|
|
2167
|
+
let failed = 0;
|
|
2168
|
+
for (const account of accounts) {
|
|
2169
|
+
try {
|
|
2170
|
+
const existing = await findOne4(users, { email: account.email });
|
|
2171
|
+
if (existing) {
|
|
2172
|
+
console.log(`[Auth] \u26A0\uFE0F Account already exists: ${account.email} (skipped)`);
|
|
2173
|
+
skipped++;
|
|
2174
|
+
continue;
|
|
2175
|
+
}
|
|
2176
|
+
const roleName = account.role || "user";
|
|
2177
|
+
const role = await getRoleByName(roleName);
|
|
2178
|
+
if (!role) {
|
|
2179
|
+
console.error(`[Auth] \u274C Role '${roleName}' not found for ${account.email}. Run initializeAuth() first.`);
|
|
2180
|
+
failed++;
|
|
2181
|
+
continue;
|
|
2182
|
+
}
|
|
2183
|
+
const passwordHash = await hashPassword(account.password);
|
|
2184
|
+
await create4(users, {
|
|
2185
|
+
email: account.email,
|
|
2186
|
+
phone: account.phone || null,
|
|
2187
|
+
passwordHash,
|
|
2188
|
+
roleId: role.id,
|
|
2189
|
+
emailVerifiedAt: /* @__PURE__ */ new Date(),
|
|
2190
|
+
// Auto-verify admin
|
|
2191
|
+
passwordChangeRequired: account.passwordChangeRequired !== false,
|
|
2192
|
+
status: "active"
|
|
2193
|
+
});
|
|
2194
|
+
console.log(`[Auth] \u2705 Admin account created: ${account.email} (${roleName})`);
|
|
2195
|
+
created++;
|
|
2196
|
+
} catch (error) {
|
|
2197
|
+
const err = error;
|
|
2198
|
+
console.error(`[Auth] \u274C Failed to create account ${account.email}:`, err.message);
|
|
2199
|
+
failed++;
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
console.log(`[Auth] \u{1F4CA} Summary: ${created} created, ${skipped} skipped, ${failed} failed`);
|
|
2203
|
+
if (created > 0) {
|
|
2204
|
+
console.log("[Auth] \u26A0\uFE0F Please change passwords on first login!");
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
export {
|
|
2208
|
+
BUILTIN_PERMISSIONS,
|
|
2209
|
+
BUILTIN_ROLES,
|
|
2210
|
+
BUILTIN_ROLE_PERMISSIONS,
|
|
2211
|
+
PRESET_PERMISSIONS,
|
|
2212
|
+
PRESET_ROLES,
|
|
2213
|
+
PRESET_ROLE_PERMISSIONS,
|
|
2214
|
+
acceptInvitation,
|
|
2215
|
+
addPermissionToRole,
|
|
2216
|
+
authenticate,
|
|
2217
|
+
cancelInvitation,
|
|
2218
|
+
changePasswordService,
|
|
2219
|
+
checkAccountExistsService,
|
|
2220
|
+
createInvitation,
|
|
2221
|
+
createRole,
|
|
2222
|
+
createVerificationToken,
|
|
2223
|
+
decodeToken,
|
|
2224
|
+
deleteInvitation,
|
|
2225
|
+
deleteRole,
|
|
2226
|
+
ensureAdminExists,
|
|
2227
|
+
expireOldInvitations,
|
|
2228
|
+
generateToken,
|
|
2229
|
+
generateVerificationCode,
|
|
2230
|
+
getAllRoles,
|
|
2231
|
+
getAuth,
|
|
2232
|
+
getInvitationByToken,
|
|
2233
|
+
getInvitationWithDetails,
|
|
2234
|
+
getKeyId,
|
|
2235
|
+
getRoleByName,
|
|
2236
|
+
getRolePermissions,
|
|
2237
|
+
getUser,
|
|
2238
|
+
getUserByEmailService,
|
|
2239
|
+
getUserByIdService,
|
|
2240
|
+
getUserByPhoneService,
|
|
2241
|
+
getUserId,
|
|
2242
|
+
getUserPermissions,
|
|
2243
|
+
hasAllPermissions,
|
|
2244
|
+
hasAnyPermission,
|
|
2245
|
+
hasAnyRole,
|
|
2246
|
+
hasPermission,
|
|
2247
|
+
hasRole,
|
|
2248
|
+
hashPassword,
|
|
2249
|
+
initializeAuth,
|
|
2250
|
+
listInvitations,
|
|
2251
|
+
loginService,
|
|
2252
|
+
logoutService,
|
|
2253
|
+
markCodeAsUsed,
|
|
2254
|
+
registerPublicKeyService,
|
|
2255
|
+
registerService,
|
|
2256
|
+
removePermissionFromRole,
|
|
2257
|
+
requireAnyPermission,
|
|
2258
|
+
requirePermissions,
|
|
2259
|
+
requireRole,
|
|
2260
|
+
resendInvitation,
|
|
2261
|
+
revokeKeyService,
|
|
2262
|
+
rotateKeyService,
|
|
2263
|
+
sendVerificationCodeService,
|
|
2264
|
+
sendVerificationEmail,
|
|
2265
|
+
sendVerificationSMS,
|
|
2266
|
+
setRolePermissions,
|
|
2267
|
+
storeVerificationCode,
|
|
2268
|
+
updateLastLoginService,
|
|
2269
|
+
updateRole,
|
|
2270
|
+
updateUserService,
|
|
2271
|
+
validateInvitation,
|
|
2272
|
+
validatePasswordStrength,
|
|
2273
|
+
validateVerificationCode,
|
|
2274
|
+
validateVerificationToken,
|
|
2275
|
+
verifyClientToken,
|
|
2276
|
+
verifyCodeService,
|
|
2277
|
+
verifyKeyFingerprint,
|
|
2278
|
+
verifyPassword,
|
|
2279
|
+
verifyToken
|
|
2280
|
+
};
|
|
2281
|
+
//# sourceMappingURL=server.js.map
|