@mesob/auth-hono 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index-CwcbaCwi.d.ts +71 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +2397 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/crypto.d.ts +7 -0
- package/dist/lib/crypto.js +94 -0
- package/dist/lib/crypto.js.map +1 -0
- package/dist/lib/jwt.d.ts +15 -0
- package/dist/lib/jwt.js +25 -0
- package/dist/lib/jwt.js.map +1 -0
- package/dist/lib/send-email.d.ts +14 -0
- package/dist/lib/send-email.js +32 -0
- package/dist/lib/send-email.js.map +1 -0
- package/dist/lib/session.d.ts +23 -0
- package/dist/lib/session.js +58 -0
- package/dist/lib/session.js.map +1 -0
- package/dist/lib/tenant.d.ts +5 -0
- package/dist/lib/tenant.js +22 -0
- package/dist/lib/tenant.js.map +1 -0
- package/package.json +56 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2397 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// src/db/index.ts
|
|
8
|
+
import { drizzle } from "drizzle-orm/node-postgres";
|
|
9
|
+
import { Pool } from "pg";
|
|
10
|
+
|
|
11
|
+
// src/db/relations.ts
|
|
12
|
+
var relations_exports = {};
|
|
13
|
+
__export(relations_exports, {
|
|
14
|
+
accountChangesInIamRelations: () => accountChangesInIamRelations,
|
|
15
|
+
accountsInIamRelations: () => accountsInIamRelations,
|
|
16
|
+
domainsInIamRelations: () => domainsInIamRelations,
|
|
17
|
+
permissionsInIamRelations: () => permissionsInIamRelations,
|
|
18
|
+
rolePermissionsInIamRelations: () => rolePermissionsInIamRelations,
|
|
19
|
+
rolesInIamRelations: () => rolesInIamRelations,
|
|
20
|
+
sessionsInIamRelations: () => sessionsInIamRelations,
|
|
21
|
+
tenantsInIamRelations: () => tenantsInIamRelations,
|
|
22
|
+
userRolesInIamRelations: () => userRolesInIamRelations,
|
|
23
|
+
usersInIamRelations: () => usersInIamRelations,
|
|
24
|
+
verificationsInIamRelations: () => verificationsInIamRelations
|
|
25
|
+
});
|
|
26
|
+
import { relations } from "drizzle-orm/relations";
|
|
27
|
+
|
|
28
|
+
// src/db/schema.ts
|
|
29
|
+
var schema_exports = {};
|
|
30
|
+
__export(schema_exports, {
|
|
31
|
+
accountChangesInIam: () => accountChangesInIam,
|
|
32
|
+
accountsInIam: () => accountsInIam,
|
|
33
|
+
domainsInIam: () => domainsInIam,
|
|
34
|
+
iam: () => iam,
|
|
35
|
+
permissionsInIam: () => permissionsInIam,
|
|
36
|
+
rolePermissionsInIam: () => rolePermissionsInIam,
|
|
37
|
+
rolesInIam: () => rolesInIam,
|
|
38
|
+
sessionsInIam: () => sessionsInIam,
|
|
39
|
+
tenantsInIam: () => tenantsInIam,
|
|
40
|
+
userRolesInIam: () => userRolesInIam,
|
|
41
|
+
usersInIam: () => usersInIam,
|
|
42
|
+
verificationsInIam: () => verificationsInIam
|
|
43
|
+
});
|
|
44
|
+
import { pgSchema, uniqueIndex, foreignKey, unique, pgPolicy, check, uuid, varchar, timestamp, text, boolean, smallint, index, inet, jsonb } from "drizzle-orm/pg-core";
|
|
45
|
+
import { sql } from "drizzle-orm";
|
|
46
|
+
var iam = pgSchema("iam");
|
|
47
|
+
var usersInIam = iam.table("users", {
|
|
48
|
+
id: uuid().default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
|
49
|
+
tenantId: varchar("tenant_id", { length: 30 }).notNull(),
|
|
50
|
+
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }).default(sql`CURRENT_TIMESTAMP`).notNull(),
|
|
51
|
+
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "string" }).default(sql`CURRENT_TIMESTAMP`).notNull(),
|
|
52
|
+
fullName: text("full_name").notNull(),
|
|
53
|
+
image: text(),
|
|
54
|
+
phone: text(),
|
|
55
|
+
email: text(),
|
|
56
|
+
handle: text().notNull(),
|
|
57
|
+
emailVerified: boolean("email_verified").default(false).notNull(),
|
|
58
|
+
phoneVerified: boolean("phone_verified").default(false).notNull(),
|
|
59
|
+
bannedUntil: timestamp("banned_until", { withTimezone: true, mode: "string" }),
|
|
60
|
+
lastSignInAt: timestamp("last_sign_in_at", { withTimezone: true, mode: "string" }),
|
|
61
|
+
loginAttempt: smallint("login_attempt").default(0).notNull()
|
|
62
|
+
}, (table) => [
|
|
63
|
+
uniqueIndex("users_tenant_lower_email_idx").using("btree", sql`tenant_id`, sql`lower(email)`),
|
|
64
|
+
uniqueIndex("users_tenant_lower_handle_idx").using("btree", sql`tenant_id`, sql`lower(handle)`),
|
|
65
|
+
foreignKey({
|
|
66
|
+
columns: [table.tenantId],
|
|
67
|
+
foreignColumns: [tenantsInIam.id],
|
|
68
|
+
name: "users_tenant_id_fkey"
|
|
69
|
+
}).onUpdate("cascade").onDelete("cascade"),
|
|
70
|
+
unique("users_tenant_phone_key").on(table.tenantId, table.phone),
|
|
71
|
+
pgPolicy("tenant_isolation", { as: "permissive", for: "all", to: ["public"], using: sql`((tenant_id)::text = (iam.current_tenant_id())::text)`, withCheck: sql`((tenant_id)::text = (iam.current_tenant_id())::text)` }),
|
|
72
|
+
check("users_login_attempt_nonnegative_check", sql`login_attempt >= 0`),
|
|
73
|
+
check("users_contact_required_check", sql`(email IS NOT NULL) OR (phone IS NOT NULL)`)
|
|
74
|
+
]);
|
|
75
|
+
var sessionsInIam = iam.table("sessions", {
|
|
76
|
+
id: uuid().default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
|
77
|
+
tenantId: varchar("tenant_id", { length: 30 }).notNull(),
|
|
78
|
+
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }).default(sql`CURRENT_TIMESTAMP`).notNull(),
|
|
79
|
+
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "string" }).default(sql`CURRENT_TIMESTAMP`).notNull(),
|
|
80
|
+
userId: uuid("user_id").notNull(),
|
|
81
|
+
expiresAt: timestamp("expires_at", { withTimezone: true, mode: "string" }).notNull(),
|
|
82
|
+
userAgent: text("user_agent"),
|
|
83
|
+
ip: inet(),
|
|
84
|
+
meta: jsonb(),
|
|
85
|
+
token: text().notNull(),
|
|
86
|
+
rotatedAt: timestamp("rotated_at", { withTimezone: true, mode: "string" }).default(sql`CURRENT_TIMESTAMP`)
|
|
87
|
+
}, (table) => [
|
|
88
|
+
index("sessions_expires_at_idx").using("btree", table.expiresAt.asc().nullsLast().op("timestamptz_ops")),
|
|
89
|
+
index("sessions_tenant_user_idx").using("btree", table.tenantId.asc().nullsLast().op("uuid_ops"), table.userId.asc().nullsLast().op("text_ops")),
|
|
90
|
+
foreignKey({
|
|
91
|
+
columns: [table.tenantId],
|
|
92
|
+
foreignColumns: [tenantsInIam.id],
|
|
93
|
+
name: "sessions_tenant_id_fkey"
|
|
94
|
+
}).onUpdate("cascade").onDelete("cascade"),
|
|
95
|
+
foreignKey({
|
|
96
|
+
columns: [table.userId],
|
|
97
|
+
foreignColumns: [usersInIam.id],
|
|
98
|
+
name: "sessions_user_id_fkey"
|
|
99
|
+
}).onUpdate("cascade").onDelete("cascade"),
|
|
100
|
+
unique("sessions_token_key").on(table.token),
|
|
101
|
+
pgPolicy("tenant_isolation", { as: "permissive", for: "all", to: ["public"], using: sql`((tenant_id)::text = (iam.current_tenant_id())::text)`, withCheck: sql`((tenant_id)::text = (iam.current_tenant_id())::text)` }),
|
|
102
|
+
check("sessions_expires_after_created_check", sql`expires_at > created_at`)
|
|
103
|
+
]);
|
|
104
|
+
var verificationsInIam = iam.table("verifications", {
|
|
105
|
+
id: uuid().default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
|
106
|
+
tenantId: varchar("tenant_id", { length: 30 }).notNull(),
|
|
107
|
+
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }).default(sql`CURRENT_TIMESTAMP`).notNull(),
|
|
108
|
+
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "string" }).default(sql`CURRENT_TIMESTAMP`).notNull(),
|
|
109
|
+
userId: uuid("user_id").notNull(),
|
|
110
|
+
code: text().notNull(),
|
|
111
|
+
expiresAt: timestamp("expires_at", { withTimezone: true, mode: "string" }).notNull(),
|
|
112
|
+
type: text(),
|
|
113
|
+
attempt: smallint().default(0),
|
|
114
|
+
to: text()
|
|
115
|
+
}, (table) => [
|
|
116
|
+
index("verifications_expires_at_idx").using("btree", table.expiresAt.asc().nullsLast().op("timestamptz_ops")),
|
|
117
|
+
index("verifications_lookup_idx").using("btree", table.tenantId.asc().nullsLast().op("text_ops"), table.userId.asc().nullsLast().op("text_ops"), table.type.asc().nullsLast().op("uuid_ops"), table.to.asc().nullsLast().op("text_ops"), table.code.asc().nullsLast().op("text_ops")),
|
|
118
|
+
foreignKey({
|
|
119
|
+
columns: [table.tenantId],
|
|
120
|
+
foreignColumns: [tenantsInIam.id],
|
|
121
|
+
name: "verifications_tenant_id_fkey"
|
|
122
|
+
}).onUpdate("cascade").onDelete("cascade"),
|
|
123
|
+
foreignKey({
|
|
124
|
+
columns: [table.userId],
|
|
125
|
+
foreignColumns: [usersInIam.id],
|
|
126
|
+
name: "verifications_user_id_fkey"
|
|
127
|
+
}).onUpdate("cascade").onDelete("cascade"),
|
|
128
|
+
pgPolicy("tenant_isolation", { as: "permissive", for: "all", to: ["public"], using: sql`((tenant_id)::text = (iam.current_tenant_id())::text)`, withCheck: sql`((tenant_id)::text = (iam.current_tenant_id())::text)` }),
|
|
129
|
+
check("verifications_attempt_nonnegative_check", sql`attempt >= 0`),
|
|
130
|
+
check("verifications_expires_after_created_check", sql`expires_at > created_at`)
|
|
131
|
+
]);
|
|
132
|
+
var accountChangesInIam = iam.table("account_changes", {
|
|
133
|
+
id: uuid().default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
|
134
|
+
tenantId: varchar("tenant_id", { length: 30 }).notNull(),
|
|
135
|
+
userId: uuid("user_id").notNull(),
|
|
136
|
+
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }).default(sql`CURRENT_TIMESTAMP`).notNull(),
|
|
137
|
+
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "string" }).default(sql`CURRENT_TIMESTAMP`).notNull(),
|
|
138
|
+
changeType: text("change_type").notNull(),
|
|
139
|
+
oldEmail: varchar("old_email"),
|
|
140
|
+
newEmail: varchar("new_email"),
|
|
141
|
+
oldPhone: text("old_phone"),
|
|
142
|
+
newPhone: text("new_phone"),
|
|
143
|
+
status: varchar().notNull(),
|
|
144
|
+
expiresAt: timestamp("expires_at", { withTimezone: true, mode: "string" }).notNull(),
|
|
145
|
+
confirmedAt: timestamp("confirmed_at", { withTimezone: true, mode: "string" }),
|
|
146
|
+
cancelledAt: timestamp("cancelled_at", { withTimezone: true, mode: "string" }),
|
|
147
|
+
reason: text()
|
|
148
|
+
}, (table) => [
|
|
149
|
+
index("account_changes_expires_at_idx").using("btree", table.expiresAt.asc().nullsLast().op("timestamptz_ops")),
|
|
150
|
+
index("account_changes_tenant_user_status_idx").using("btree", table.tenantId.asc().nullsLast().op("uuid_ops"), table.userId.asc().nullsLast().op("text_ops"), table.status.asc().nullsLast().op("text_ops")),
|
|
151
|
+
foreignKey({
|
|
152
|
+
columns: [table.tenantId],
|
|
153
|
+
foreignColumns: [tenantsInIam.id],
|
|
154
|
+
name: "account_changes_tenant_id_fkey"
|
|
155
|
+
}).onUpdate("cascade").onDelete("cascade"),
|
|
156
|
+
foreignKey({
|
|
157
|
+
columns: [table.userId],
|
|
158
|
+
foreignColumns: [usersInIam.id],
|
|
159
|
+
name: "account_changes_user_id_fkey"
|
|
160
|
+
}).onUpdate("cascade").onDelete("cascade"),
|
|
161
|
+
pgPolicy("tenant_isolation", { as: "permissive", for: "all", to: ["public"], using: sql`((tenant_id)::text = (iam.current_tenant_id())::text)`, withCheck: sql`((tenant_id)::text = (iam.current_tenant_id())::text)` }),
|
|
162
|
+
check("account_changes_status_check", sql`(status)::text = ANY ((ARRAY['pending'::character varying, 'applied'::character varying, 'cancelled'::character varying, 'expired'::character varying])::text[])`),
|
|
163
|
+
check("account_changes_change_type_check", sql`((change_type = 'email'::text) AND (old_email IS NOT NULL) AND (new_email IS NOT NULL) AND (old_phone IS NULL) AND (new_phone IS NULL)) OR ((change_type = 'phone'::text) AND (old_phone IS NOT NULL) AND (new_phone IS NOT NULL) AND (old_email IS NULL) AND (new_email IS NULL))`),
|
|
164
|
+
check("account_changes_expires_after_created_check", sql`expires_at > created_at`)
|
|
165
|
+
]);
|
|
166
|
+
var tenantsInIam = iam.table("tenants", {
|
|
167
|
+
id: varchar({ length: 30 }).primaryKey().notNull(),
|
|
168
|
+
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }).default(sql`CURRENT_TIMESTAMP`).notNull(),
|
|
169
|
+
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "string" }).default(sql`CURRENT_TIMESTAMP`).notNull(),
|
|
170
|
+
name: jsonb().notNull(),
|
|
171
|
+
description: jsonb(),
|
|
172
|
+
theme: jsonb(),
|
|
173
|
+
supportedLanguages: jsonb("supported_languages"),
|
|
174
|
+
defaultLanguage: text("default_language"),
|
|
175
|
+
supportedCurrency: jsonb("supported_currency"),
|
|
176
|
+
defaultCurrency: text("default_currency"),
|
|
177
|
+
timezone: text(),
|
|
178
|
+
isActive: boolean("is_active").default(true).notNull(),
|
|
179
|
+
locale: jsonb(),
|
|
180
|
+
settings: jsonb(),
|
|
181
|
+
seo: jsonb()
|
|
182
|
+
}, (table) => [
|
|
183
|
+
index("tenants_is_active_idx").using("btree", table.isActive.asc().nullsLast().op("bool_ops"))
|
|
184
|
+
]);
|
|
185
|
+
var rolesInIam = iam.table("roles", {
|
|
186
|
+
id: uuid().default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
|
187
|
+
tenantId: varchar("tenant_id", { length: 30 }).notNull(),
|
|
188
|
+
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }).default(sql`CURRENT_TIMESTAMP`).notNull(),
|
|
189
|
+
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "string" }).default(sql`CURRENT_TIMESTAMP`).notNull(),
|
|
190
|
+
name: jsonb().notNull(),
|
|
191
|
+
description: jsonb().notNull(),
|
|
192
|
+
code: text().notNull()
|
|
193
|
+
}, (table) => [
|
|
194
|
+
foreignKey({
|
|
195
|
+
columns: [table.tenantId],
|
|
196
|
+
foreignColumns: [tenantsInIam.id],
|
|
197
|
+
name: "roles_tenant_id_fkey"
|
|
198
|
+
}).onUpdate("cascade").onDelete("cascade"),
|
|
199
|
+
unique("roles_tenant_code_unique").on(table.tenantId, table.code),
|
|
200
|
+
pgPolicy("tenant_isolation", { as: "permissive", for: "all", to: ["public"], using: sql`((tenant_id)::text = (iam.current_tenant_id())::text)`, withCheck: sql`((tenant_id)::text = (iam.current_tenant_id())::text)` })
|
|
201
|
+
]);
|
|
202
|
+
var permissionsInIam = iam.table("permissions", {
|
|
203
|
+
id: text().primaryKey().notNull(),
|
|
204
|
+
description: jsonb().notNull(),
|
|
205
|
+
activity: text().notNull(),
|
|
206
|
+
application: text().notNull(),
|
|
207
|
+
feature: text().notNull()
|
|
208
|
+
}, (table) => [
|
|
209
|
+
unique("permissions_activity_application_feature_key").on(table.activity, table.application, table.feature)
|
|
210
|
+
]);
|
|
211
|
+
var rolePermissionsInIam = iam.table("role_permissions", {
|
|
212
|
+
id: uuid().default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
|
213
|
+
tenantId: varchar("tenant_id", { length: 30 }).notNull(),
|
|
214
|
+
roleId: uuid("role_id").notNull(),
|
|
215
|
+
permissionId: text("permission_id").notNull()
|
|
216
|
+
}, (table) => [
|
|
217
|
+
foreignKey({
|
|
218
|
+
columns: [table.tenantId],
|
|
219
|
+
foreignColumns: [tenantsInIam.id],
|
|
220
|
+
name: "role_permissions_tenant_id_fkey"
|
|
221
|
+
}).onUpdate("cascade").onDelete("cascade"),
|
|
222
|
+
foreignKey({
|
|
223
|
+
columns: [table.roleId],
|
|
224
|
+
foreignColumns: [rolesInIam.id],
|
|
225
|
+
name: "role_permissions_role_id_fkey"
|
|
226
|
+
}).onUpdate("cascade").onDelete("cascade"),
|
|
227
|
+
foreignKey({
|
|
228
|
+
columns: [table.permissionId],
|
|
229
|
+
foreignColumns: [permissionsInIam.id],
|
|
230
|
+
name: "role_permissions_permission_id_fkey"
|
|
231
|
+
}).onUpdate("cascade").onDelete("cascade"),
|
|
232
|
+
unique("role_permissions_unique").on(table.roleId, table.permissionId),
|
|
233
|
+
pgPolicy("tenant_isolation", { as: "permissive", for: "all", to: ["public"], using: sql`((tenant_id)::text = (iam.current_tenant_id())::text)`, withCheck: sql`((tenant_id)::text = (iam.current_tenant_id())::text)` })
|
|
234
|
+
]);
|
|
235
|
+
var userRolesInIam = iam.table("user_roles", {
|
|
236
|
+
id: uuid().default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
|
237
|
+
tenantId: varchar("tenant_id", { length: 30 }).notNull(),
|
|
238
|
+
userId: uuid("user_id").notNull(),
|
|
239
|
+
roleId: uuid("role_id").notNull()
|
|
240
|
+
}, (table) => [
|
|
241
|
+
foreignKey({
|
|
242
|
+
columns: [table.tenantId],
|
|
243
|
+
foreignColumns: [tenantsInIam.id],
|
|
244
|
+
name: "user_roles_tenant_id_fkey"
|
|
245
|
+
}).onUpdate("cascade").onDelete("cascade"),
|
|
246
|
+
foreignKey({
|
|
247
|
+
columns: [table.userId],
|
|
248
|
+
foreignColumns: [usersInIam.id],
|
|
249
|
+
name: "user_roles_user_id_fkey"
|
|
250
|
+
}).onUpdate("cascade").onDelete("cascade"),
|
|
251
|
+
foreignKey({
|
|
252
|
+
columns: [table.roleId],
|
|
253
|
+
foreignColumns: [rolesInIam.id],
|
|
254
|
+
name: "user_roles_role_id_fkey"
|
|
255
|
+
}).onUpdate("cascade").onDelete("cascade"),
|
|
256
|
+
unique("user_roles_unique").on(table.userId, table.roleId),
|
|
257
|
+
pgPolicy("tenant_isolation", { as: "permissive", for: "all", to: ["public"], using: sql`((tenant_id)::text = (iam.current_tenant_id())::text)`, withCheck: sql`((tenant_id)::text = (iam.current_tenant_id())::text)` })
|
|
258
|
+
]);
|
|
259
|
+
var accountsInIam = iam.table("accounts", {
|
|
260
|
+
id: uuid().default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
|
261
|
+
tenantId: varchar("tenant_id", { length: 30 }).notNull(),
|
|
262
|
+
userId: uuid("user_id").notNull(),
|
|
263
|
+
provider: text().notNull(),
|
|
264
|
+
providerAccountId: text("provider_account_id").notNull(),
|
|
265
|
+
password: text(),
|
|
266
|
+
passwordLastChangedAt: timestamp("password_last_changed_at", { withTimezone: true, mode: "string" }),
|
|
267
|
+
idToken: text("id_token"),
|
|
268
|
+
accessToken: text("access_token"),
|
|
269
|
+
accessTokenExpiresAt: timestamp("access_token_expires_at", { withTimezone: true, mode: "string" }),
|
|
270
|
+
refreshToken: text("refresh_token"),
|
|
271
|
+
refreshTokenExpiresAt: timestamp("refresh_token_expires_at", { withTimezone: true, mode: "string" }),
|
|
272
|
+
scope: text(),
|
|
273
|
+
expiresAt: timestamp("expires_at", { withTimezone: true, mode: "string" }),
|
|
274
|
+
meta: jsonb()
|
|
275
|
+
}, (table) => [
|
|
276
|
+
foreignKey({
|
|
277
|
+
columns: [table.tenantId],
|
|
278
|
+
foreignColumns: [tenantsInIam.id],
|
|
279
|
+
name: "accounts_tenant_id_fkey"
|
|
280
|
+
}).onUpdate("cascade").onDelete("cascade"),
|
|
281
|
+
foreignKey({
|
|
282
|
+
columns: [table.userId],
|
|
283
|
+
foreignColumns: [usersInIam.id],
|
|
284
|
+
name: "accounts_user_id_fkey"
|
|
285
|
+
}).onUpdate("cascade").onDelete("cascade"),
|
|
286
|
+
unique("accounts_provider_account_unique").on(table.provider, table.providerAccountId),
|
|
287
|
+
pgPolicy("tenant_isolation", { as: "permissive", for: "all", to: ["public"], using: sql`((tenant_id)::text = (iam.current_tenant_id())::text)`, withCheck: sql`((tenant_id)::text = (iam.current_tenant_id())::text)` })
|
|
288
|
+
]);
|
|
289
|
+
var domainsInIam = iam.table("domains", {
|
|
290
|
+
id: uuid().default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
|
291
|
+
tenantId: varchar("tenant_id", { length: 30 }).notNull(),
|
|
292
|
+
domain: text().notNull(),
|
|
293
|
+
status: text().default("pending").notNull(),
|
|
294
|
+
meta: jsonb(),
|
|
295
|
+
isPrimary: boolean("is_primary").default(false).notNull(),
|
|
296
|
+
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }).default(sql`CURRENT_TIMESTAMP`).notNull(),
|
|
297
|
+
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "string" }).default(sql`CURRENT_TIMESTAMP`).notNull(),
|
|
298
|
+
createdBy: uuid("created_by"),
|
|
299
|
+
updatedBy: uuid("updated_by")
|
|
300
|
+
}, (table) => [
|
|
301
|
+
uniqueIndex("domains_domain_unique_idx").using("btree", sql`lower(domain)`),
|
|
302
|
+
uniqueIndex("domains_primary_per_tenant_idx").using("btree", table.tenantId.asc().nullsLast().op("text_ops")).where(sql`(is_primary = true)`),
|
|
303
|
+
index("domains_tenant_id_idx").using("btree", table.tenantId.asc().nullsLast().op("text_ops")),
|
|
304
|
+
index("domains_tenant_status_idx").using("btree", table.tenantId.asc().nullsLast().op("text_ops"), table.status.asc().nullsLast().op("text_ops")),
|
|
305
|
+
foreignKey({
|
|
306
|
+
columns: [table.tenantId],
|
|
307
|
+
foreignColumns: [tenantsInIam.id],
|
|
308
|
+
name: "domains_tenant_id_fkey"
|
|
309
|
+
}).onUpdate("cascade").onDelete("cascade"),
|
|
310
|
+
pgPolicy("tenant_isolation", { as: "permissive", for: "all", to: ["public"], using: sql`((tenant_id)::text = (iam.current_tenant_id())::text)`, withCheck: sql`((tenant_id)::text = (iam.current_tenant_id())::text)` }),
|
|
311
|
+
check("domains_status_check", sql`status = ANY (ARRAY['pending'::text, 'active'::text, 'disabled'::text, 'deleted'::text])`),
|
|
312
|
+
check("domains_domain_format_check", sql`domain ~ '^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)+$'::text`)
|
|
313
|
+
]);
|
|
314
|
+
|
|
315
|
+
// src/db/relations.ts
|
|
316
|
+
var usersInIamRelations = relations(usersInIam, ({ one, many }) => ({
|
|
317
|
+
tenantsInIam: one(tenantsInIam, {
|
|
318
|
+
fields: [usersInIam.tenantId],
|
|
319
|
+
references: [tenantsInIam.id]
|
|
320
|
+
}),
|
|
321
|
+
sessionsInIam: many(sessionsInIam),
|
|
322
|
+
verificationsInIam: many(verificationsInIam),
|
|
323
|
+
accountChangesInIam: many(accountChangesInIam),
|
|
324
|
+
userRolesInIam: many(userRolesInIam),
|
|
325
|
+
accountsInIam: many(accountsInIam)
|
|
326
|
+
}));
|
|
327
|
+
var tenantsInIamRelations = relations(tenantsInIam, ({ many }) => ({
|
|
328
|
+
usersInIam: many(usersInIam),
|
|
329
|
+
sessionsInIam: many(sessionsInIam),
|
|
330
|
+
verificationsInIam: many(verificationsInIam),
|
|
331
|
+
accountChangesInIam: many(accountChangesInIam),
|
|
332
|
+
rolesInIam: many(rolesInIam),
|
|
333
|
+
rolePermissionsInIam: many(rolePermissionsInIam),
|
|
334
|
+
userRolesInIam: many(userRolesInIam),
|
|
335
|
+
accountsInIam: many(accountsInIam),
|
|
336
|
+
domainsInIam: many(domainsInIam)
|
|
337
|
+
}));
|
|
338
|
+
var sessionsInIamRelations = relations(sessionsInIam, ({ one }) => ({
|
|
339
|
+
tenantsInIam: one(tenantsInIam, {
|
|
340
|
+
fields: [sessionsInIam.tenantId],
|
|
341
|
+
references: [tenantsInIam.id]
|
|
342
|
+
}),
|
|
343
|
+
usersInIam: one(usersInIam, {
|
|
344
|
+
fields: [sessionsInIam.userId],
|
|
345
|
+
references: [usersInIam.id]
|
|
346
|
+
})
|
|
347
|
+
}));
|
|
348
|
+
var verificationsInIamRelations = relations(verificationsInIam, ({ one }) => ({
|
|
349
|
+
tenantsInIam: one(tenantsInIam, {
|
|
350
|
+
fields: [verificationsInIam.tenantId],
|
|
351
|
+
references: [tenantsInIam.id]
|
|
352
|
+
}),
|
|
353
|
+
usersInIam: one(usersInIam, {
|
|
354
|
+
fields: [verificationsInIam.userId],
|
|
355
|
+
references: [usersInIam.id]
|
|
356
|
+
})
|
|
357
|
+
}));
|
|
358
|
+
var accountChangesInIamRelations = relations(accountChangesInIam, ({ one }) => ({
|
|
359
|
+
tenantsInIam: one(tenantsInIam, {
|
|
360
|
+
fields: [accountChangesInIam.tenantId],
|
|
361
|
+
references: [tenantsInIam.id]
|
|
362
|
+
}),
|
|
363
|
+
usersInIam: one(usersInIam, {
|
|
364
|
+
fields: [accountChangesInIam.userId],
|
|
365
|
+
references: [usersInIam.id]
|
|
366
|
+
})
|
|
367
|
+
}));
|
|
368
|
+
var rolesInIamRelations = relations(rolesInIam, ({ one, many }) => ({
|
|
369
|
+
tenantsInIam: one(tenantsInIam, {
|
|
370
|
+
fields: [rolesInIam.tenantId],
|
|
371
|
+
references: [tenantsInIam.id]
|
|
372
|
+
}),
|
|
373
|
+
rolePermissionsInIam: many(rolePermissionsInIam),
|
|
374
|
+
userRolesInIam: many(userRolesInIam)
|
|
375
|
+
}));
|
|
376
|
+
var rolePermissionsInIamRelations = relations(rolePermissionsInIam, ({ one }) => ({
|
|
377
|
+
tenantsInIam: one(tenantsInIam, {
|
|
378
|
+
fields: [rolePermissionsInIam.tenantId],
|
|
379
|
+
references: [tenantsInIam.id]
|
|
380
|
+
}),
|
|
381
|
+
rolesInIam: one(rolesInIam, {
|
|
382
|
+
fields: [rolePermissionsInIam.roleId],
|
|
383
|
+
references: [rolesInIam.id]
|
|
384
|
+
}),
|
|
385
|
+
permissionsInIam: one(permissionsInIam, {
|
|
386
|
+
fields: [rolePermissionsInIam.permissionId],
|
|
387
|
+
references: [permissionsInIam.id]
|
|
388
|
+
})
|
|
389
|
+
}));
|
|
390
|
+
var permissionsInIamRelations = relations(permissionsInIam, ({ many }) => ({
|
|
391
|
+
rolePermissionsInIam: many(rolePermissionsInIam)
|
|
392
|
+
}));
|
|
393
|
+
var userRolesInIamRelations = relations(userRolesInIam, ({ one }) => ({
|
|
394
|
+
tenantsInIam: one(tenantsInIam, {
|
|
395
|
+
fields: [userRolesInIam.tenantId],
|
|
396
|
+
references: [tenantsInIam.id]
|
|
397
|
+
}),
|
|
398
|
+
usersInIam: one(usersInIam, {
|
|
399
|
+
fields: [userRolesInIam.userId],
|
|
400
|
+
references: [usersInIam.id]
|
|
401
|
+
}),
|
|
402
|
+
rolesInIam: one(rolesInIam, {
|
|
403
|
+
fields: [userRolesInIam.roleId],
|
|
404
|
+
references: [rolesInIam.id]
|
|
405
|
+
})
|
|
406
|
+
}));
|
|
407
|
+
var accountsInIamRelations = relations(accountsInIam, ({ one }) => ({
|
|
408
|
+
tenantsInIam: one(tenantsInIam, {
|
|
409
|
+
fields: [accountsInIam.tenantId],
|
|
410
|
+
references: [tenantsInIam.id]
|
|
411
|
+
}),
|
|
412
|
+
usersInIam: one(usersInIam, {
|
|
413
|
+
fields: [accountsInIam.userId],
|
|
414
|
+
references: [usersInIam.id]
|
|
415
|
+
})
|
|
416
|
+
}));
|
|
417
|
+
var domainsInIamRelations = relations(domainsInIam, ({ one }) => ({
|
|
418
|
+
tenantsInIam: one(tenantsInIam, {
|
|
419
|
+
fields: [domainsInIam.tenantId],
|
|
420
|
+
references: [tenantsInIam.id]
|
|
421
|
+
})
|
|
422
|
+
}));
|
|
423
|
+
|
|
424
|
+
// src/db/index.ts
|
|
425
|
+
var schemaConfig = { schema: { ...schema_exports, ...relations_exports } };
|
|
426
|
+
var createDatabase = (connectionString) => {
|
|
427
|
+
const pool = new Pool({ connectionString });
|
|
428
|
+
return drizzle({ client: pool, ...schemaConfig });
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
// src/db/orm/iam/sessions/find-session-by-token.ts
|
|
432
|
+
import { and, eq, gt } from "drizzle-orm";
|
|
433
|
+
var findSessionByToken = (db, hashedToken) => {
|
|
434
|
+
return db.select({
|
|
435
|
+
id: sessionsInIam.id,
|
|
436
|
+
tenantId: sessionsInIam.tenantId,
|
|
437
|
+
userId: sessionsInIam.userId,
|
|
438
|
+
expiresAt: sessionsInIam.expiresAt,
|
|
439
|
+
createdAt: sessionsInIam.createdAt,
|
|
440
|
+
updatedAt: sessionsInIam.updatedAt,
|
|
441
|
+
userAgent: sessionsInIam.userAgent,
|
|
442
|
+
ip: sessionsInIam.ip
|
|
443
|
+
}).from(sessionsInIam).where(
|
|
444
|
+
and(
|
|
445
|
+
eq(sessionsInIam.token, hashedToken),
|
|
446
|
+
gt(sessionsInIam.expiresAt, (/* @__PURE__ */ new Date()).toISOString())
|
|
447
|
+
)
|
|
448
|
+
).limit(1).then(([session]) => session || null);
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
// src/db/orm/iam/users/find-user-by-id.ts
|
|
452
|
+
import { and as and2, eq as eq2 } from "drizzle-orm";
|
|
453
|
+
var findUserById = (db, tenantId, userId) => {
|
|
454
|
+
return db.select({
|
|
455
|
+
id: usersInIam.id,
|
|
456
|
+
tenantId: usersInIam.tenantId,
|
|
457
|
+
fullName: usersInIam.fullName,
|
|
458
|
+
email: usersInIam.email,
|
|
459
|
+
phone: usersInIam.phone,
|
|
460
|
+
handle: usersInIam.handle,
|
|
461
|
+
image: usersInIam.image,
|
|
462
|
+
emailVerified: usersInIam.emailVerified,
|
|
463
|
+
phoneVerified: usersInIam.phoneVerified,
|
|
464
|
+
lastSignInAt: usersInIam.lastSignInAt
|
|
465
|
+
}).from(usersInIam).where(and2(eq2(usersInIam.id, userId), eq2(usersInIam.tenantId, tenantId))).limit(1).then(([user]) => user || null);
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
// src/handler.ts
|
|
469
|
+
import { OpenAPIHono as OpenAPIHono2 } from "@hono/zod-openapi";
|
|
470
|
+
import { getCookie as getCookie2 } from "hono/cookie";
|
|
471
|
+
import { HTTPException as HTTPException11 } from "hono/http-exception";
|
|
472
|
+
|
|
473
|
+
// src/lib/crypto.ts
|
|
474
|
+
import { scrypt } from "@noble/hashes/scrypt.js";
|
|
475
|
+
import { randomBytes } from "@noble/hashes/utils.js";
|
|
476
|
+
var encoder = new TextEncoder();
|
|
477
|
+
var toHex = (buffer) => {
|
|
478
|
+
return Array.from(
|
|
479
|
+
buffer,
|
|
480
|
+
(b) => b.toString(16).padStart(2, "0")
|
|
481
|
+
).join("");
|
|
482
|
+
};
|
|
483
|
+
var hexToBytes = (hex) => {
|
|
484
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
485
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
486
|
+
bytes[i / 2] = Number.parseInt(hex.slice(i, i + 2), 16);
|
|
487
|
+
}
|
|
488
|
+
return bytes;
|
|
489
|
+
};
|
|
490
|
+
var SCRYPT_KEYLEN = 64;
|
|
491
|
+
var SCRYPT_COST = 16384;
|
|
492
|
+
var SCRYPT_BLOCK_SIZE = 8;
|
|
493
|
+
var SCRYPT_PARALLELISM = 1;
|
|
494
|
+
var hashPassword = async (password) => {
|
|
495
|
+
const salt = randomBytes(16);
|
|
496
|
+
const saltHex = toHex(salt);
|
|
497
|
+
const passwordBytes = encoder.encode(password);
|
|
498
|
+
const derivedKey = await Promise.resolve(
|
|
499
|
+
scrypt(passwordBytes, salt, {
|
|
500
|
+
N: SCRYPT_COST,
|
|
501
|
+
r: SCRYPT_BLOCK_SIZE,
|
|
502
|
+
p: SCRYPT_PARALLELISM,
|
|
503
|
+
dkLen: SCRYPT_KEYLEN
|
|
504
|
+
})
|
|
505
|
+
);
|
|
506
|
+
return `${saltHex}:${toHex(derivedKey)}`;
|
|
507
|
+
};
|
|
508
|
+
var verifyPassword = async (password, hashed) => {
|
|
509
|
+
if (!hashed) {
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
const [saltHex, keyHex] = hashed.split(":");
|
|
513
|
+
if (!(saltHex && keyHex)) {
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
const salt = hexToBytes(saltHex);
|
|
517
|
+
const passwordBytes = encoder.encode(password);
|
|
518
|
+
const derivedKey = await Promise.resolve(
|
|
519
|
+
scrypt(passwordBytes, salt, {
|
|
520
|
+
N: SCRYPT_COST,
|
|
521
|
+
r: SCRYPT_BLOCK_SIZE,
|
|
522
|
+
p: SCRYPT_PARALLELISM,
|
|
523
|
+
dkLen: SCRYPT_KEYLEN
|
|
524
|
+
})
|
|
525
|
+
);
|
|
526
|
+
const derived = toHex(derivedKey);
|
|
527
|
+
if (derived.length !== keyHex.length) {
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
let result = 0;
|
|
531
|
+
for (let i = 0; i < derived.length; i++) {
|
|
532
|
+
result |= derived.charCodeAt(i) ^ keyHex.charCodeAt(i);
|
|
533
|
+
}
|
|
534
|
+
return result === 0;
|
|
535
|
+
};
|
|
536
|
+
var hashToken = async (token, secret) => {
|
|
537
|
+
const key = await crypto.subtle.importKey(
|
|
538
|
+
"raw",
|
|
539
|
+
encoder.encode(secret),
|
|
540
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
541
|
+
false,
|
|
542
|
+
["sign"]
|
|
543
|
+
);
|
|
544
|
+
const signature = await crypto.subtle.sign(
|
|
545
|
+
"HMAC",
|
|
546
|
+
key,
|
|
547
|
+
encoder.encode(token)
|
|
548
|
+
);
|
|
549
|
+
return toHex(new Uint8Array(signature));
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
// src/routes/auth.route.ts
|
|
553
|
+
import { createRoute, OpenAPIHono } from "@hono/zod-openapi";
|
|
554
|
+
import { z as z2 } from "zod";
|
|
555
|
+
|
|
556
|
+
// src/routes/auth.schema.ts
|
|
557
|
+
import { z } from "zod";
|
|
558
|
+
var emailField = z.string().trim().email("Invalid email address").max(255, "Email too long");
|
|
559
|
+
var phoneField = z.string().trim().min(6, "Phone too short").max(30, "Phone too long").regex(/^[+()\d\s-]+$/, "Invalid phone number format");
|
|
560
|
+
var passwordField = z.string().min(8, "Password must be at least 8 characters").max(128, "Password too long");
|
|
561
|
+
var userSchema = z.object({
|
|
562
|
+
id: z.string().uuid(),
|
|
563
|
+
tenantId: z.string(),
|
|
564
|
+
fullName: z.string(),
|
|
565
|
+
email: z.string().email().nullable(),
|
|
566
|
+
phone: z.string().nullable(),
|
|
567
|
+
handle: z.string(),
|
|
568
|
+
image: z.string().nullable(),
|
|
569
|
+
emailVerified: z.boolean(),
|
|
570
|
+
phoneVerified: z.boolean(),
|
|
571
|
+
lastSignInAt: z.string().datetime().nullable()
|
|
572
|
+
});
|
|
573
|
+
var sessionSchema = z.object({
|
|
574
|
+
id: z.string().uuid(),
|
|
575
|
+
expiresAt: z.string().datetime(),
|
|
576
|
+
createdAt: z.string().datetime().optional(),
|
|
577
|
+
userAgent: z.string().nullable().optional(),
|
|
578
|
+
ip: z.string().nullable().optional()
|
|
579
|
+
});
|
|
580
|
+
var authSuccessSchema = z.object({
|
|
581
|
+
user: userSchema,
|
|
582
|
+
session: sessionSchema,
|
|
583
|
+
sessionToken: z.string(),
|
|
584
|
+
sessionExpiresAt: z.string().datetime()
|
|
585
|
+
});
|
|
586
|
+
var messageSchema = z.object({
|
|
587
|
+
message: z.string()
|
|
588
|
+
});
|
|
589
|
+
var errorResponseSchema = z.object({
|
|
590
|
+
error: z.string().describe("Error message"),
|
|
591
|
+
code: z.string().optional().describe("Error code"),
|
|
592
|
+
details: z.record(z.string(), z.any()).optional()
|
|
593
|
+
});
|
|
594
|
+
var signUpSchema = z.object({
|
|
595
|
+
email: emailField.optional(),
|
|
596
|
+
phone: phoneField.optional(),
|
|
597
|
+
password: passwordField,
|
|
598
|
+
fullName: z.string().min(2),
|
|
599
|
+
handle: z.string().optional(),
|
|
600
|
+
image: z.string().url().optional()
|
|
601
|
+
}).refine((data) => data.email || data.phone, {
|
|
602
|
+
message: "Either email or phone is required",
|
|
603
|
+
path: ["email"]
|
|
604
|
+
});
|
|
605
|
+
var signInSchema = z.object({
|
|
606
|
+
identifier: z.string(),
|
|
607
|
+
password: passwordField
|
|
608
|
+
});
|
|
609
|
+
var signUpResponseSchema = authSuccessSchema.extend({
|
|
610
|
+
verificationId: z.string().uuid().optional(),
|
|
611
|
+
requiresVerification: z.boolean().optional(),
|
|
612
|
+
debugCode: z.string().optional()
|
|
613
|
+
});
|
|
614
|
+
var signInResponseSchema = authSuccessSchema.extend({
|
|
615
|
+
verificationId: z.string().uuid().optional(),
|
|
616
|
+
requiresVerification: z.boolean().optional()
|
|
617
|
+
});
|
|
618
|
+
var emailVerificationRequestSchema = z.object({
|
|
619
|
+
email: emailField.optional()
|
|
620
|
+
});
|
|
621
|
+
var emailVerificationConfirmSchema = z.object({
|
|
622
|
+
verificationId: z.string().uuid(),
|
|
623
|
+
code: z.string().length(6)
|
|
624
|
+
});
|
|
625
|
+
var phoneVerificationRequestSchema = z.object({
|
|
626
|
+
phone: phoneField,
|
|
627
|
+
context: z.enum(["sign-up", "sign-in", "change-phone"])
|
|
628
|
+
});
|
|
629
|
+
var phoneVerificationConfirmSchema = z.object({
|
|
630
|
+
verificationId: z.string().uuid(),
|
|
631
|
+
code: z.string(),
|
|
632
|
+
context: z.enum(["sign-up", "sign-in", "change-phone"])
|
|
633
|
+
});
|
|
634
|
+
var forgotPasswordSchema = z.object({
|
|
635
|
+
identifier: z.string()
|
|
636
|
+
});
|
|
637
|
+
var resetPasswordSchema = z.object({
|
|
638
|
+
verificationId: z.string().uuid(),
|
|
639
|
+
code: z.string(),
|
|
640
|
+
password: passwordField
|
|
641
|
+
});
|
|
642
|
+
var changePasswordSchema = z.object({
|
|
643
|
+
currentPassword: passwordField,
|
|
644
|
+
newPassword: passwordField
|
|
645
|
+
});
|
|
646
|
+
var messageWithVerificationIdSchema = messageSchema.extend({
|
|
647
|
+
verificationId: z.string().uuid().optional()
|
|
648
|
+
});
|
|
649
|
+
var checkUserSchema = z.object({
|
|
650
|
+
identifier: z.string()
|
|
651
|
+
});
|
|
652
|
+
var checkUserResponseSchema = z.object({
|
|
653
|
+
exists: z.boolean()
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
// src/db/orm/iam/users/find-user-by-email.ts
|
|
657
|
+
import { and as and3, eq as eq3, sql as sql2 } from "drizzle-orm";
|
|
658
|
+
var findUserByEmail = (db, tenantId, email) => {
|
|
659
|
+
return db.select({
|
|
660
|
+
id: usersInIam.id,
|
|
661
|
+
tenantId: usersInIam.tenantId,
|
|
662
|
+
fullName: usersInIam.fullName,
|
|
663
|
+
email: usersInIam.email,
|
|
664
|
+
phone: usersInIam.phone,
|
|
665
|
+
handle: usersInIam.handle,
|
|
666
|
+
image: usersInIam.image,
|
|
667
|
+
emailVerified: usersInIam.emailVerified,
|
|
668
|
+
phoneVerified: usersInIam.phoneVerified,
|
|
669
|
+
lastSignInAt: usersInIam.lastSignInAt
|
|
670
|
+
}).from(usersInIam).where(
|
|
671
|
+
and3(
|
|
672
|
+
eq3(usersInIam.tenantId, tenantId),
|
|
673
|
+
sql2`lower(${usersInIam.email}) = lower(${email})`
|
|
674
|
+
)
|
|
675
|
+
).limit(1).then(([user]) => user || null);
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
// src/db/orm/iam/users/find-user-by-phone.ts
|
|
679
|
+
import { and as and4, eq as eq4 } from "drizzle-orm";
|
|
680
|
+
var findUserByPhone = (db, tenantId, phone) => {
|
|
681
|
+
return db.select({
|
|
682
|
+
id: usersInIam.id,
|
|
683
|
+
tenantId: usersInIam.tenantId,
|
|
684
|
+
fullName: usersInIam.fullName,
|
|
685
|
+
email: usersInIam.email,
|
|
686
|
+
phone: usersInIam.phone,
|
|
687
|
+
handle: usersInIam.handle,
|
|
688
|
+
image: usersInIam.image,
|
|
689
|
+
emailVerified: usersInIam.emailVerified,
|
|
690
|
+
phoneVerified: usersInIam.phoneVerified,
|
|
691
|
+
lastSignInAt: usersInIam.lastSignInAt
|
|
692
|
+
}).from(usersInIam).where(and4(eq4(usersInIam.tenantId, tenantId), eq4(usersInIam.phone, phone))).limit(1).then(([user]) => user || null);
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
// src/db/orm/iam/users/find-user-by-identifier.ts
|
|
696
|
+
var findUserByIdentifier = async (db, tenantId, identifier) => {
|
|
697
|
+
const isEmail = identifier.includes("@");
|
|
698
|
+
if (isEmail) {
|
|
699
|
+
const user2 = await findUserByEmail(db, tenantId, identifier);
|
|
700
|
+
return { user: user2, type: "email" };
|
|
701
|
+
}
|
|
702
|
+
const user = await findUserByPhone(db, tenantId, identifier);
|
|
703
|
+
return { user, type: "phone" };
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
// src/lib/tenant.ts
|
|
707
|
+
import { HTTPException } from "hono/http-exception";
|
|
708
|
+
var ensureTenantId = (config, tenantId) => {
|
|
709
|
+
if (config.enableTenant) {
|
|
710
|
+
if (!tenantId) {
|
|
711
|
+
throw new HTTPException(400, {
|
|
712
|
+
message: "Missing tenantId. Tenant isolation is enabled."
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
return tenantId;
|
|
716
|
+
}
|
|
717
|
+
if (!config.tenantId) {
|
|
718
|
+
throw new HTTPException(500, {
|
|
719
|
+
message: "tenantId must be provided in config when enableTenant is false."
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
return config.tenantId;
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
// src/routes/handler/check-user.ts
|
|
726
|
+
var checkUserHandler = async (c) => {
|
|
727
|
+
const body = c.req.valid("json");
|
|
728
|
+
const { config, database, tenantId } = c.var;
|
|
729
|
+
const resolvedTenantId = ensureTenantId(config, tenantId);
|
|
730
|
+
const { identifier } = body;
|
|
731
|
+
const lookup = await findUserByIdentifier(
|
|
732
|
+
database,
|
|
733
|
+
resolvedTenantId,
|
|
734
|
+
identifier
|
|
735
|
+
);
|
|
736
|
+
return c.json({ exists: !!lookup.user });
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
// src/routes/handler/email-verification-confirm.ts
|
|
740
|
+
import { setCookie } from "hono/cookie";
|
|
741
|
+
import { HTTPException as HTTPException2 } from "hono/http-exception";
|
|
742
|
+
|
|
743
|
+
// src/db/orm/iam/sessions/insert-session.ts
|
|
744
|
+
var insertSession = (db, data) => {
|
|
745
|
+
return db.insert(sessionsInIam).values({
|
|
746
|
+
tenantId: data.tenantId,
|
|
747
|
+
userId: data.userId,
|
|
748
|
+
token: data.token,
|
|
749
|
+
expiresAt: data.expiresAt,
|
|
750
|
+
userAgent: data.userAgent || null,
|
|
751
|
+
ip: data.ip || null,
|
|
752
|
+
meta: data.meta || null
|
|
753
|
+
}).returning({
|
|
754
|
+
id: sessionsInIam.id,
|
|
755
|
+
tenantId: sessionsInIam.tenantId,
|
|
756
|
+
userId: sessionsInIam.userId,
|
|
757
|
+
expiresAt: sessionsInIam.expiresAt,
|
|
758
|
+
createdAt: sessionsInIam.createdAt,
|
|
759
|
+
updatedAt: sessionsInIam.updatedAt,
|
|
760
|
+
userAgent: sessionsInIam.userAgent,
|
|
761
|
+
ip: sessionsInIam.ip
|
|
762
|
+
}).then(([session]) => session);
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
// src/db/orm/iam/users/update-user-verified.ts
|
|
766
|
+
import { and as and5, eq as eq5 } from "drizzle-orm";
|
|
767
|
+
var updateUserVerified = (db, tenantId, userId, type) => {
|
|
768
|
+
return db.update(usersInIam).set({
|
|
769
|
+
[type === "email" ? "emailVerified" : "phoneVerified"]: true,
|
|
770
|
+
lastSignInAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
771
|
+
}).where(and5(eq5(usersInIam.id, userId), eq5(usersInIam.tenantId, tenantId)));
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
// src/db/orm/iam/verifications/consume-verification.ts
|
|
775
|
+
import { eq as eq6 } from "drizzle-orm";
|
|
776
|
+
var consumeVerification = (db, verificationId) => {
|
|
777
|
+
return db.delete(verificationsInIam).where(eq6(verificationsInIam.id, verificationId));
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
// src/db/orm/iam/verifications/find-verification-by-id.ts
|
|
781
|
+
import { eq as eq7 } from "drizzle-orm";
|
|
782
|
+
var findVerificationById = (db, verificationId) => {
|
|
783
|
+
return db.select({
|
|
784
|
+
id: verificationsInIam.id,
|
|
785
|
+
tenantId: verificationsInIam.tenantId,
|
|
786
|
+
userId: verificationsInIam.userId,
|
|
787
|
+
type: verificationsInIam.type,
|
|
788
|
+
code: verificationsInIam.code,
|
|
789
|
+
to: verificationsInIam.to,
|
|
790
|
+
expiresAt: verificationsInIam.expiresAt,
|
|
791
|
+
createdAt: verificationsInIam.createdAt,
|
|
792
|
+
attempt: verificationsInIam.attempt
|
|
793
|
+
}).from(verificationsInIam).where(eq7(verificationsInIam.id, verificationId)).limit(1).then(([verification]) => verification || null);
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
// src/db/orm/iam/verifications/update-verification-attempt.ts
|
|
797
|
+
import { eq as eq8 } from "drizzle-orm";
|
|
798
|
+
var updateVerificationAttempt = async (db, verificationId) => {
|
|
799
|
+
const verification = await findVerificationById(db, verificationId);
|
|
800
|
+
if (!verification) {
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
await db.update(verificationsInIam).set({ attempt: (verification.attempt || 0) + 1 }).where(eq8(verificationsInIam.id, verificationId));
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
// src/errors.ts
|
|
807
|
+
var AUTH_ERRORS = {
|
|
808
|
+
USER_NOT_FOUND: "USER_NOT_FOUND",
|
|
809
|
+
INVALID_PASSWORD: "INVALID_PASSWORD",
|
|
810
|
+
USER_EXISTS: "USER_EXISTS",
|
|
811
|
+
VERIFICATION_EXPIRED: "VERIFICATION_EXPIRED",
|
|
812
|
+
VERIFICATION_MISMATCH: "VERIFICATION_MISMATCH",
|
|
813
|
+
VERIFICATION_NOT_FOUND: "VERIFICATION_NOT_FOUND",
|
|
814
|
+
TOO_MANY_ATTEMPTS: "TOO_MANY_ATTEMPTS",
|
|
815
|
+
REQUIRES_VERIFICATION: "REQUIRES_VERIFICATION",
|
|
816
|
+
UNAUTHORIZED: "UNAUTHORIZED",
|
|
817
|
+
ACCESS_DENIED: "ACCESS_DENIED",
|
|
818
|
+
HAS_NO_PASSWORD: "HAS_NO_PASSWORD"
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
// src/lib/session.ts
|
|
822
|
+
import { dayjs } from "@mesob/common";
|
|
823
|
+
var generateHandle = (seed) => {
|
|
824
|
+
const base = seed?.replace(/[^a-zA-Z0-9]/g, "").toLowerCase() || `user${Math.random().toString(36).slice(2, 8)}`;
|
|
825
|
+
return `${base}-${Math.random().toString(36).slice(2, 6)}`;
|
|
826
|
+
};
|
|
827
|
+
var parseDuration = (duration) => {
|
|
828
|
+
const match = duration.match(/^(\d+)([smhd])$/);
|
|
829
|
+
if (!match) {
|
|
830
|
+
throw new Error(`Invalid duration format: ${duration}`);
|
|
831
|
+
}
|
|
832
|
+
const value = Number.parseInt(match[1], 10);
|
|
833
|
+
const unit = match[2];
|
|
834
|
+
const multipliers = {
|
|
835
|
+
s: 1,
|
|
836
|
+
m: 60,
|
|
837
|
+
h: 3600,
|
|
838
|
+
d: 86400
|
|
839
|
+
};
|
|
840
|
+
return value * (multipliers[unit] || 1);
|
|
841
|
+
};
|
|
842
|
+
var addDuration = (duration) => {
|
|
843
|
+
const seconds = parseDuration(duration);
|
|
844
|
+
return dayjs().add(seconds, "second").toISOString();
|
|
845
|
+
};
|
|
846
|
+
var generateOtpCode = (length = 6) => {
|
|
847
|
+
const digits = "0123456789";
|
|
848
|
+
let code = "";
|
|
849
|
+
for (let i = 0; i < length; i++) {
|
|
850
|
+
code += digits[Math.floor(Math.random() * digits.length)];
|
|
851
|
+
}
|
|
852
|
+
return code;
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
// src/routes/handler/email-verification-confirm.ts
|
|
856
|
+
var emailVerificationConfirmHandler = async (c) => {
|
|
857
|
+
const body = c.req.valid("json");
|
|
858
|
+
const { config, database, tenantId } = c.var;
|
|
859
|
+
const resolvedTenantId = ensureTenantId(config, tenantId);
|
|
860
|
+
const { verificationId, code } = body;
|
|
861
|
+
const verification = await findVerificationById(database, verificationId);
|
|
862
|
+
if (!verification) {
|
|
863
|
+
throw new HTTPException2(400, {
|
|
864
|
+
message: AUTH_ERRORS.VERIFICATION_NOT_FOUND
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
if (new Date(verification.expiresAt) < /* @__PURE__ */ new Date()) {
|
|
868
|
+
throw new HTTPException2(400, {
|
|
869
|
+
message: AUTH_ERRORS.VERIFICATION_EXPIRED
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
const hashedCode = await hashToken(code, config.secret);
|
|
873
|
+
if (verification.code !== hashedCode) {
|
|
874
|
+
await updateVerificationAttempt(database, verificationId);
|
|
875
|
+
throw new HTTPException2(400, {
|
|
876
|
+
message: AUTH_ERRORS.VERIFICATION_MISMATCH
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
await updateUserVerified(
|
|
880
|
+
database,
|
|
881
|
+
resolvedTenantId,
|
|
882
|
+
verification.userId,
|
|
883
|
+
"email"
|
|
884
|
+
);
|
|
885
|
+
await consumeVerification(database, verificationId);
|
|
886
|
+
const user = await findUserById(
|
|
887
|
+
database,
|
|
888
|
+
resolvedTenantId,
|
|
889
|
+
verification.userId
|
|
890
|
+
);
|
|
891
|
+
if (!user) {
|
|
892
|
+
throw new HTTPException2(500, { message: "User not found" });
|
|
893
|
+
}
|
|
894
|
+
const sessionToken = crypto.randomUUID();
|
|
895
|
+
const hashedToken = await hashToken(sessionToken, config.secret);
|
|
896
|
+
const expiresAt = addDuration(config.session.expiresIn);
|
|
897
|
+
const session = await insertSession(database, {
|
|
898
|
+
tenantId: resolvedTenantId,
|
|
899
|
+
userId: user.id,
|
|
900
|
+
token: hashedToken,
|
|
901
|
+
expiresAt,
|
|
902
|
+
userAgent: c.req.header("user-agent") || null,
|
|
903
|
+
ip: c.req.header("cf-connecting-ip") || c.req.header("x-forwarded-for") || null,
|
|
904
|
+
meta: { action: "email-verification" }
|
|
905
|
+
});
|
|
906
|
+
setCookie(c, "session_token", sessionToken, {
|
|
907
|
+
httpOnly: true,
|
|
908
|
+
secure: process.env.NODE_ENV === "production",
|
|
909
|
+
sameSite: "Lax",
|
|
910
|
+
path: "/",
|
|
911
|
+
expires: new Date(expiresAt)
|
|
912
|
+
});
|
|
913
|
+
return c.json({
|
|
914
|
+
user,
|
|
915
|
+
session: {
|
|
916
|
+
id: session.id,
|
|
917
|
+
expiresAt: session.expiresAt,
|
|
918
|
+
createdAt: session.createdAt,
|
|
919
|
+
userAgent: session.userAgent,
|
|
920
|
+
ip: session.ip
|
|
921
|
+
},
|
|
922
|
+
sessionToken,
|
|
923
|
+
sessionExpiresAt: session.expiresAt
|
|
924
|
+
});
|
|
925
|
+
};
|
|
926
|
+
|
|
927
|
+
// src/routes/handler/email-verification-request.ts
|
|
928
|
+
import { HTTPException as HTTPException3 } from "hono/http-exception";
|
|
929
|
+
|
|
930
|
+
// src/db/orm/iam/verifications/delete-verifications-by-user-and-type.ts
|
|
931
|
+
import { and as and6, eq as eq9 } from "drizzle-orm";
|
|
932
|
+
var deleteVerificationsByUserAndType = (db, tenantId, userId, type) => {
|
|
933
|
+
return db.delete(verificationsInIam).where(
|
|
934
|
+
and6(
|
|
935
|
+
eq9(verificationsInIam.tenantId, tenantId),
|
|
936
|
+
eq9(verificationsInIam.userId, userId),
|
|
937
|
+
eq9(verificationsInIam.type, type)
|
|
938
|
+
)
|
|
939
|
+
);
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
// src/db/orm/iam/verifications/insert-verification.ts
|
|
943
|
+
var insertVerification = async (db, data) => {
|
|
944
|
+
return await db.insert(verificationsInIam).values({
|
|
945
|
+
tenantId: data.tenantId,
|
|
946
|
+
userId: data.userId,
|
|
947
|
+
type: data.type,
|
|
948
|
+
code: data.code,
|
|
949
|
+
expiresAt: data.expiresAt,
|
|
950
|
+
to: data.to || null,
|
|
951
|
+
attempt: 0
|
|
952
|
+
}).returning({
|
|
953
|
+
id: verificationsInIam.id,
|
|
954
|
+
tenantId: verificationsInIam.tenantId,
|
|
955
|
+
userId: verificationsInIam.userId,
|
|
956
|
+
type: verificationsInIam.type,
|
|
957
|
+
code: verificationsInIam.code,
|
|
958
|
+
to: verificationsInIam.to,
|
|
959
|
+
expiresAt: verificationsInIam.expiresAt,
|
|
960
|
+
createdAt: verificationsInIam.createdAt,
|
|
961
|
+
attempt: verificationsInIam.attempt
|
|
962
|
+
}).then(([verification]) => verification);
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
// src/lib/send-email.ts
|
|
966
|
+
import { Resend } from "resend";
|
|
967
|
+
var SendEmail = async ({
|
|
968
|
+
key,
|
|
969
|
+
provider,
|
|
970
|
+
to,
|
|
971
|
+
subject,
|
|
972
|
+
html,
|
|
973
|
+
text: text2,
|
|
974
|
+
from
|
|
975
|
+
}) => {
|
|
976
|
+
switch (provider) {
|
|
977
|
+
case "resend":
|
|
978
|
+
return await sendEmailWithResend(key, to, subject, html, text2, from);
|
|
979
|
+
default:
|
|
980
|
+
throw new Error(`Unsupported email provider: ${provider}`);
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
var sendEmailWithResend = async (key, to, subject, html, text2, from) => {
|
|
984
|
+
const resend = new Resend(key);
|
|
985
|
+
return await resend.emails.send({
|
|
986
|
+
from,
|
|
987
|
+
to,
|
|
988
|
+
subject,
|
|
989
|
+
html,
|
|
990
|
+
text: text2
|
|
991
|
+
});
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
// src/providers/resend-email.ts
|
|
995
|
+
var ResendEmailProvider = class {
|
|
996
|
+
config;
|
|
997
|
+
constructor(config) {
|
|
998
|
+
this.config = config;
|
|
999
|
+
}
|
|
1000
|
+
async sendVerificationEmail(email, code, tenantName) {
|
|
1001
|
+
const subject = this.config.verificationSubject || `Verify your email${tenantName ? ` for ${tenantName}` : ""}`;
|
|
1002
|
+
const verificationPath = this.config.verificationPath || "/verify-email";
|
|
1003
|
+
const verificationUrl = `${this.config.frontendBaseUrl}${verificationPath}?token=${code}`;
|
|
1004
|
+
const html = `
|
|
1005
|
+
<!DOCTYPE html>
|
|
1006
|
+
<html>
|
|
1007
|
+
<head>
|
|
1008
|
+
<meta charset="utf-8">
|
|
1009
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1010
|
+
<title>${subject}</title>
|
|
1011
|
+
</head>
|
|
1012
|
+
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
|
1013
|
+
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
|
1014
|
+
<h1 style="color: #2563eb;">Verify Your Email</h1>
|
|
1015
|
+
<p>Your verification code is:</p>
|
|
1016
|
+
<div style="background: #f3f4f6; padding: 20px; text-align: center; font-size: 32px; font-weight: bold; letter-spacing: 8px; margin: 20px 0;">
|
|
1017
|
+
${code}
|
|
1018
|
+
</div>
|
|
1019
|
+
<p>Or click the link below:</p>
|
|
1020
|
+
<p>
|
|
1021
|
+
<a href="${verificationUrl}" style="background: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;">
|
|
1022
|
+
Verify Email
|
|
1023
|
+
</a>
|
|
1024
|
+
</p>
|
|
1025
|
+
<p style="color: #6b7280; font-size: 14px; margin-top: 30px;">
|
|
1026
|
+
This code will expire in 1 hour. If you didn't request this, please ignore this email.
|
|
1027
|
+
</p>
|
|
1028
|
+
</div>
|
|
1029
|
+
</body>
|
|
1030
|
+
</html>
|
|
1031
|
+
`;
|
|
1032
|
+
const text2 = `Your verification code is: ${code}
|
|
1033
|
+
|
|
1034
|
+
Or visit: ${verificationUrl}
|
|
1035
|
+
|
|
1036
|
+
This code will expire in 1 hour.`;
|
|
1037
|
+
await this.sendEmail({
|
|
1038
|
+
to: [email],
|
|
1039
|
+
subject,
|
|
1040
|
+
html,
|
|
1041
|
+
text: text2
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
async sendPasswordResetEmail(email, code, tenantName) {
|
|
1045
|
+
const subject = this.config.resetPasswordSubject || `Reset your password${tenantName ? ` for ${tenantName}` : ""}`;
|
|
1046
|
+
const resetPath = this.config.resetPasswordPath || "/reset-password";
|
|
1047
|
+
const resetUrl = `${this.config.frontendBaseUrl}${resetPath}?token=${code}`;
|
|
1048
|
+
const html = `
|
|
1049
|
+
<!DOCTYPE html>
|
|
1050
|
+
<html>
|
|
1051
|
+
<head>
|
|
1052
|
+
<meta charset="utf-8">
|
|
1053
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1054
|
+
<title>${subject}</title>
|
|
1055
|
+
</head>
|
|
1056
|
+
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
|
1057
|
+
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
|
1058
|
+
<h1 style="color: #2563eb;">Reset Your Password</h1>
|
|
1059
|
+
<p>Your password reset code is:</p>
|
|
1060
|
+
<div style="background: #f3f4f6; padding: 20px; text-align: center; font-size: 32px; font-weight: bold; letter-spacing: 8px; margin: 20px 0;">
|
|
1061
|
+
${code}
|
|
1062
|
+
</div>
|
|
1063
|
+
<p>Or click the link below:</p>
|
|
1064
|
+
<p>
|
|
1065
|
+
<a href="${resetUrl}" style="background: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;">
|
|
1066
|
+
Reset Password
|
|
1067
|
+
</a>
|
|
1068
|
+
</p>
|
|
1069
|
+
<p style="color: #6b7280; font-size: 14px; margin-top: 30px;">
|
|
1070
|
+
This code will expire in 1 hour. If you didn't request this, please ignore this email.
|
|
1071
|
+
</p>
|
|
1072
|
+
</div>
|
|
1073
|
+
</body>
|
|
1074
|
+
</html>
|
|
1075
|
+
`;
|
|
1076
|
+
const text2 = `Your password reset code is: ${code}
|
|
1077
|
+
|
|
1078
|
+
Or visit: ${resetUrl}
|
|
1079
|
+
|
|
1080
|
+
This code will expire in 1 hour.`;
|
|
1081
|
+
await this.sendEmail({
|
|
1082
|
+
to: [email],
|
|
1083
|
+
subject,
|
|
1084
|
+
html,
|
|
1085
|
+
text: text2
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
async sendEmail(data) {
|
|
1089
|
+
const res = await SendEmail({
|
|
1090
|
+
key: this.config.apiKey,
|
|
1091
|
+
provider: "resend",
|
|
1092
|
+
to: data.to,
|
|
1093
|
+
subject: data.subject,
|
|
1094
|
+
html: data.html,
|
|
1095
|
+
text: data.text,
|
|
1096
|
+
from: this.config.from
|
|
1097
|
+
});
|
|
1098
|
+
if (res.error) {
|
|
1099
|
+
throw new Error(res.error.message);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
};
|
|
1103
|
+
|
|
1104
|
+
// src/routes/handler/email-verification-request.ts
|
|
1105
|
+
var emailVerificationRequestHandler = async (c) => {
|
|
1106
|
+
const body = c.req.valid("json");
|
|
1107
|
+
const { config, database, tenantId, user } = c.var;
|
|
1108
|
+
const resolvedTenantId = ensureTenantId(config, tenantId);
|
|
1109
|
+
const email = body.email || user?.email;
|
|
1110
|
+
if (!email) {
|
|
1111
|
+
throw new HTTPException3(400, { message: "Email required" });
|
|
1112
|
+
}
|
|
1113
|
+
let userId = user?.id;
|
|
1114
|
+
if (!userId) {
|
|
1115
|
+
const lookup = await findUserByIdentifier(
|
|
1116
|
+
database,
|
|
1117
|
+
resolvedTenantId,
|
|
1118
|
+
email
|
|
1119
|
+
);
|
|
1120
|
+
if (!lookup.user) {
|
|
1121
|
+
throw new HTTPException3(404, { message: AUTH_ERRORS.USER_NOT_FOUND });
|
|
1122
|
+
}
|
|
1123
|
+
userId = lookup.user.id;
|
|
1124
|
+
}
|
|
1125
|
+
await deleteVerificationsByUserAndType(
|
|
1126
|
+
database,
|
|
1127
|
+
resolvedTenantId,
|
|
1128
|
+
userId,
|
|
1129
|
+
"email-verification"
|
|
1130
|
+
);
|
|
1131
|
+
const code = generateOtpCode(6);
|
|
1132
|
+
const hashedCode = await hashToken(code, config.secret);
|
|
1133
|
+
const expiresAt = addDuration(config.email.verificationExpiresIn);
|
|
1134
|
+
const verification = await insertVerification(database, {
|
|
1135
|
+
tenantId: resolvedTenantId,
|
|
1136
|
+
userId,
|
|
1137
|
+
type: "email-verification",
|
|
1138
|
+
code: hashedCode,
|
|
1139
|
+
expiresAt,
|
|
1140
|
+
to: email
|
|
1141
|
+
});
|
|
1142
|
+
const emailProvider = new ResendEmailProvider(config.email.resend);
|
|
1143
|
+
await emailProvider.sendVerificationEmail(email, code);
|
|
1144
|
+
return c.json({ verificationId: verification.id });
|
|
1145
|
+
};
|
|
1146
|
+
|
|
1147
|
+
// src/routes/handler/me.ts
|
|
1148
|
+
import { HTTPException as HTTPException4 } from "hono/http-exception";
|
|
1149
|
+
var meHandler = (c) => {
|
|
1150
|
+
const { user } = c.var;
|
|
1151
|
+
if (!user) {
|
|
1152
|
+
throw new HTTPException4(401, { message: "Unauthorized" });
|
|
1153
|
+
}
|
|
1154
|
+
return c.json({ user });
|
|
1155
|
+
};
|
|
1156
|
+
|
|
1157
|
+
// src/routes/handler/password-change.ts
|
|
1158
|
+
import { HTTPException as HTTPException5 } from "hono/http-exception";
|
|
1159
|
+
|
|
1160
|
+
// src/db/orm/iam/accounts/find-account-by-provider.ts
|
|
1161
|
+
import { and as and7, eq as eq10 } from "drizzle-orm";
|
|
1162
|
+
var findAccountByProvider = (db, tenantId, userId, provider) => {
|
|
1163
|
+
return db.select({
|
|
1164
|
+
id: accountsInIam.id,
|
|
1165
|
+
tenantId: accountsInIam.tenantId,
|
|
1166
|
+
userId: accountsInIam.userId,
|
|
1167
|
+
provider: accountsInIam.provider,
|
|
1168
|
+
providerAccountId: accountsInIam.providerAccountId,
|
|
1169
|
+
password: accountsInIam.password
|
|
1170
|
+
}).from(accountsInIam).where(
|
|
1171
|
+
and7(
|
|
1172
|
+
eq10(accountsInIam.tenantId, tenantId),
|
|
1173
|
+
eq10(accountsInIam.userId, userId),
|
|
1174
|
+
eq10(accountsInIam.provider, provider)
|
|
1175
|
+
)
|
|
1176
|
+
).limit(1).then(([account]) => account || null);
|
|
1177
|
+
};
|
|
1178
|
+
|
|
1179
|
+
// src/db/orm/iam/accounts/update-account-password.ts
|
|
1180
|
+
import { and as and8, eq as eq11 } from "drizzle-orm";
|
|
1181
|
+
var updateAccountPassword = (db, tenantId, userId, password) => {
|
|
1182
|
+
return db.update(accountsInIam).set({ password }).where(
|
|
1183
|
+
and8(
|
|
1184
|
+
eq11(accountsInIam.tenantId, tenantId),
|
|
1185
|
+
eq11(accountsInIam.userId, userId),
|
|
1186
|
+
eq11(accountsInIam.provider, "credentials")
|
|
1187
|
+
)
|
|
1188
|
+
);
|
|
1189
|
+
};
|
|
1190
|
+
|
|
1191
|
+
// src/db/orm/iam/sessions/delete-session-by-id.ts
|
|
1192
|
+
import { eq as eq12 } from "drizzle-orm";
|
|
1193
|
+
var deleteSessionById = (db, sessionId) => {
|
|
1194
|
+
return db.delete(sessionsInIam).where(eq12(sessionsInIam.id, sessionId));
|
|
1195
|
+
};
|
|
1196
|
+
|
|
1197
|
+
// src/db/orm/iam/sessions/list-sessions-for-user.ts
|
|
1198
|
+
import { and as and9, asc, eq as eq13, gt as gt2 } from "drizzle-orm";
|
|
1199
|
+
var listSessionsForUser = (db, tenantId, userId) => {
|
|
1200
|
+
return db.select({
|
|
1201
|
+
id: sessionsInIam.id,
|
|
1202
|
+
tenantId: sessionsInIam.tenantId,
|
|
1203
|
+
userId: sessionsInIam.userId,
|
|
1204
|
+
expiresAt: sessionsInIam.expiresAt,
|
|
1205
|
+
createdAt: sessionsInIam.createdAt,
|
|
1206
|
+
updatedAt: sessionsInIam.updatedAt,
|
|
1207
|
+
userAgent: sessionsInIam.userAgent,
|
|
1208
|
+
ip: sessionsInIam.ip
|
|
1209
|
+
}).from(sessionsInIam).where(
|
|
1210
|
+
and9(
|
|
1211
|
+
eq13(sessionsInIam.tenantId, tenantId),
|
|
1212
|
+
eq13(sessionsInIam.userId, userId),
|
|
1213
|
+
gt2(sessionsInIam.expiresAt, (/* @__PURE__ */ new Date()).toISOString())
|
|
1214
|
+
)
|
|
1215
|
+
).orderBy(asc(sessionsInIam.createdAt)).then((sessions) => sessions);
|
|
1216
|
+
};
|
|
1217
|
+
|
|
1218
|
+
// src/routes/handler/password-change.ts
|
|
1219
|
+
var changePasswordHandler = async (c) => {
|
|
1220
|
+
const body = c.req.valid("json");
|
|
1221
|
+
const { config, database, tenantId, userId, user } = c.var;
|
|
1222
|
+
if (!userId) {
|
|
1223
|
+
throw new HTTPException5(401, { message: AUTH_ERRORS.UNAUTHORIZED });
|
|
1224
|
+
}
|
|
1225
|
+
if (!user) {
|
|
1226
|
+
throw new HTTPException5(401, { message: AUTH_ERRORS.UNAUTHORIZED });
|
|
1227
|
+
}
|
|
1228
|
+
const resolvedTenantId = ensureTenantId(config, tenantId);
|
|
1229
|
+
const { currentPassword, newPassword } = body;
|
|
1230
|
+
const account = await findAccountByProvider(
|
|
1231
|
+
database,
|
|
1232
|
+
resolvedTenantId,
|
|
1233
|
+
userId,
|
|
1234
|
+
"credentials"
|
|
1235
|
+
);
|
|
1236
|
+
if (!account?.password) {
|
|
1237
|
+
throw new HTTPException5(401, { message: AUTH_ERRORS.HAS_NO_PASSWORD });
|
|
1238
|
+
}
|
|
1239
|
+
const passwordValid = await verifyPassword(currentPassword, account.password);
|
|
1240
|
+
if (!passwordValid) {
|
|
1241
|
+
throw new HTTPException5(401, { message: AUTH_ERRORS.INVALID_PASSWORD });
|
|
1242
|
+
}
|
|
1243
|
+
const passwordHash = await hashPassword(newPassword);
|
|
1244
|
+
await updateAccountPassword(database, resolvedTenantId, userId, passwordHash);
|
|
1245
|
+
const sessions = await listSessionsForUser(
|
|
1246
|
+
database,
|
|
1247
|
+
resolvedTenantId,
|
|
1248
|
+
userId
|
|
1249
|
+
);
|
|
1250
|
+
const currentSessionToken = c.req.cookie("session_token");
|
|
1251
|
+
if (currentSessionToken) {
|
|
1252
|
+
const hashedToken = await hashToken(currentSessionToken, config.secret);
|
|
1253
|
+
const currentSession = await findSessionByToken(database, hashedToken);
|
|
1254
|
+
if (currentSession) {
|
|
1255
|
+
for (const session of sessions) {
|
|
1256
|
+
if (session.id !== currentSession.id) {
|
|
1257
|
+
await deleteSessionById(database, session.id);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
return c.json({ message: "Password updated" });
|
|
1263
|
+
};
|
|
1264
|
+
|
|
1265
|
+
// src/providers/afro-sms.ts
|
|
1266
|
+
var AfroSmsProvider = class {
|
|
1267
|
+
config;
|
|
1268
|
+
constructor(config) {
|
|
1269
|
+
this.config = config;
|
|
1270
|
+
}
|
|
1271
|
+
async sendVerificationSms(phone, code) {
|
|
1272
|
+
const template = this.config.templateVerification || "Your verification code is {{code}}";
|
|
1273
|
+
const message = template.replace("{{code}}", code);
|
|
1274
|
+
const sms = {
|
|
1275
|
+
to: phone,
|
|
1276
|
+
message
|
|
1277
|
+
};
|
|
1278
|
+
await this.sendSms(this.config, sms);
|
|
1279
|
+
}
|
|
1280
|
+
async sendLoginSms(phone, code) {
|
|
1281
|
+
const template = this.config.templateLogin || "Your login code is {{code}}";
|
|
1282
|
+
const message = template.replace("{{code}}", code);
|
|
1283
|
+
const sms = {
|
|
1284
|
+
to: phone,
|
|
1285
|
+
message
|
|
1286
|
+
};
|
|
1287
|
+
await this.sendSms(this.config, sms);
|
|
1288
|
+
}
|
|
1289
|
+
async sendSms(config, sms) {
|
|
1290
|
+
const baseUrl = `${config.baseUrl}/send`;
|
|
1291
|
+
const identifierId = config?.identifierId;
|
|
1292
|
+
const senderName = config?.senderName;
|
|
1293
|
+
const key = config?.apiKey;
|
|
1294
|
+
if (!(baseUrl && identifierId && senderName && key)) {
|
|
1295
|
+
throw new Error("SMS configuration is not set");
|
|
1296
|
+
}
|
|
1297
|
+
const myHeaders = new Headers();
|
|
1298
|
+
myHeaders.append("Authorization", `Bearer ${key}`);
|
|
1299
|
+
myHeaders.append("Content-Type", "application/json");
|
|
1300
|
+
const sendSMS = {
|
|
1301
|
+
from: identifierId,
|
|
1302
|
+
sender: "",
|
|
1303
|
+
to: sms.to,
|
|
1304
|
+
message: sms.message,
|
|
1305
|
+
callback: sms?.callback
|
|
1306
|
+
};
|
|
1307
|
+
const requestOptions = {
|
|
1308
|
+
method: "POST",
|
|
1309
|
+
headers: myHeaders,
|
|
1310
|
+
body: JSON.stringify(sendSMS)
|
|
1311
|
+
};
|
|
1312
|
+
const response = await fetch(baseUrl, requestOptions);
|
|
1313
|
+
if (!response.ok) {
|
|
1314
|
+
throw new Error(`Failed to send SMS: ${response.statusText}`);
|
|
1315
|
+
}
|
|
1316
|
+
return response;
|
|
1317
|
+
}
|
|
1318
|
+
};
|
|
1319
|
+
|
|
1320
|
+
// src/routes/handler/password-forgot.ts
|
|
1321
|
+
var forgotPasswordHandler = async (c) => {
|
|
1322
|
+
const body = c.req.valid("json");
|
|
1323
|
+
const { config, database, tenantId } = c.var;
|
|
1324
|
+
const resolvedTenantId = ensureTenantId(config, tenantId);
|
|
1325
|
+
const { identifier } = body;
|
|
1326
|
+
const lookup = await findUserByIdentifier(
|
|
1327
|
+
database,
|
|
1328
|
+
resolvedTenantId,
|
|
1329
|
+
identifier
|
|
1330
|
+
);
|
|
1331
|
+
if (!lookup.user) {
|
|
1332
|
+
return c.json({ message: "If account exists, reset code sent" });
|
|
1333
|
+
}
|
|
1334
|
+
const isEmail = lookup.type === "email";
|
|
1335
|
+
let verificationId;
|
|
1336
|
+
if (isEmail) {
|
|
1337
|
+
const code = generateOtpCode(6);
|
|
1338
|
+
const hashedCode = await hashToken(code, config.secret);
|
|
1339
|
+
const expiresAt = addDuration(config.email.resetPasswordExpiresIn);
|
|
1340
|
+
const verification = await insertVerification(database, {
|
|
1341
|
+
tenantId: resolvedTenantId,
|
|
1342
|
+
userId: lookup.user.id,
|
|
1343
|
+
type: "password-reset",
|
|
1344
|
+
code: hashedCode,
|
|
1345
|
+
expiresAt,
|
|
1346
|
+
to: lookup.user.email
|
|
1347
|
+
});
|
|
1348
|
+
verificationId = verification.id;
|
|
1349
|
+
const emailProvider = new ResendEmailProvider(config.email.resend);
|
|
1350
|
+
await emailProvider.sendPasswordResetEmail(lookup.user.email, code);
|
|
1351
|
+
} else {
|
|
1352
|
+
const code = generateOtpCode(config.phone.otpLength);
|
|
1353
|
+
const hashedCode = await hashToken(code, config.secret);
|
|
1354
|
+
const expiresAt = addDuration(config.phone.otpExpiresIn);
|
|
1355
|
+
const verification = await insertVerification(database, {
|
|
1356
|
+
tenantId: resolvedTenantId,
|
|
1357
|
+
userId: lookup.user.id,
|
|
1358
|
+
type: "password-reset-otp",
|
|
1359
|
+
code: hashedCode,
|
|
1360
|
+
expiresAt,
|
|
1361
|
+
to: lookup.user.phone
|
|
1362
|
+
});
|
|
1363
|
+
verificationId = verification.id;
|
|
1364
|
+
const smsProvider = new AfroSmsProvider(config.phone.smsConfig);
|
|
1365
|
+
await smsProvider.sendVerificationSms(lookup.user.phone, code);
|
|
1366
|
+
}
|
|
1367
|
+
return c.json({
|
|
1368
|
+
message: "If account exists, reset code sent",
|
|
1369
|
+
verificationId
|
|
1370
|
+
});
|
|
1371
|
+
};
|
|
1372
|
+
|
|
1373
|
+
// src/routes/handler/password-reset.ts
|
|
1374
|
+
import { setCookie as setCookie2 } from "hono/cookie";
|
|
1375
|
+
import { HTTPException as HTTPException6 } from "hono/http-exception";
|
|
1376
|
+
var resetPasswordHandler = async (c) => {
|
|
1377
|
+
const body = c.req.valid("json");
|
|
1378
|
+
const { config, database, tenantId } = c.var;
|
|
1379
|
+
const resolvedTenantId = ensureTenantId(config, tenantId);
|
|
1380
|
+
const { verificationId, code, password } = body;
|
|
1381
|
+
const verification = await findVerificationById(database, verificationId);
|
|
1382
|
+
if (!verification) {
|
|
1383
|
+
throw new HTTPException6(400, {
|
|
1384
|
+
message: AUTH_ERRORS.VERIFICATION_NOT_FOUND
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
if (new Date(verification.expiresAt) < /* @__PURE__ */ new Date()) {
|
|
1388
|
+
throw new HTTPException6(400, { message: AUTH_ERRORS.VERIFICATION_EXPIRED });
|
|
1389
|
+
}
|
|
1390
|
+
const hashedCode = await hashToken(code, config.secret);
|
|
1391
|
+
if (verification.code !== hashedCode) {
|
|
1392
|
+
await updateVerificationAttempt(database, verificationId);
|
|
1393
|
+
throw new HTTPException6(400, {
|
|
1394
|
+
message: AUTH_ERRORS.VERIFICATION_MISMATCH
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
const passwordHash = await hashPassword(password);
|
|
1398
|
+
await updateAccountPassword(
|
|
1399
|
+
database,
|
|
1400
|
+
resolvedTenantId,
|
|
1401
|
+
verification.userId,
|
|
1402
|
+
passwordHash
|
|
1403
|
+
);
|
|
1404
|
+
const sessions = await listSessionsForUser(
|
|
1405
|
+
database,
|
|
1406
|
+
resolvedTenantId,
|
|
1407
|
+
verification.userId
|
|
1408
|
+
);
|
|
1409
|
+
for (const session2 of sessions) {
|
|
1410
|
+
await deleteSessionById(database, session2.id);
|
|
1411
|
+
}
|
|
1412
|
+
await consumeVerification(database, verificationId);
|
|
1413
|
+
const sessionToken = crypto.randomUUID();
|
|
1414
|
+
const hashedToken = await hashToken(sessionToken, config.secret);
|
|
1415
|
+
const expiresAt = addDuration(config.session.expiresIn);
|
|
1416
|
+
const session = await insertSession(database, {
|
|
1417
|
+
tenantId: resolvedTenantId,
|
|
1418
|
+
userId: verification.userId,
|
|
1419
|
+
token: hashedToken,
|
|
1420
|
+
expiresAt,
|
|
1421
|
+
userAgent: c.req.header("user-agent") || null,
|
|
1422
|
+
ip: c.req.header("cf-connecting-ip") || c.req.header("x-forwarded-for") || null,
|
|
1423
|
+
meta: { action: "password-reset" }
|
|
1424
|
+
});
|
|
1425
|
+
const user = await findUserById(
|
|
1426
|
+
database,
|
|
1427
|
+
resolvedTenantId,
|
|
1428
|
+
verification.userId
|
|
1429
|
+
);
|
|
1430
|
+
if (!user) {
|
|
1431
|
+
throw new HTTPException6(500, { message: "User not found" });
|
|
1432
|
+
}
|
|
1433
|
+
setCookie2(c, "session_token", sessionToken, {
|
|
1434
|
+
httpOnly: true,
|
|
1435
|
+
secure: process.env.NODE_ENV === "production",
|
|
1436
|
+
sameSite: "Lax",
|
|
1437
|
+
path: "/",
|
|
1438
|
+
expires: new Date(expiresAt)
|
|
1439
|
+
});
|
|
1440
|
+
return c.json({
|
|
1441
|
+
user,
|
|
1442
|
+
session: {
|
|
1443
|
+
id: session.id,
|
|
1444
|
+
expiresAt: session.expiresAt,
|
|
1445
|
+
createdAt: session.createdAt,
|
|
1446
|
+
userAgent: session.userAgent,
|
|
1447
|
+
ip: session.ip
|
|
1448
|
+
},
|
|
1449
|
+
sessionToken,
|
|
1450
|
+
sessionExpiresAt: session.expiresAt
|
|
1451
|
+
});
|
|
1452
|
+
};
|
|
1453
|
+
|
|
1454
|
+
// src/routes/handler/phone-verification-confirm.ts
|
|
1455
|
+
import { setCookie as setCookie3 } from "hono/cookie";
|
|
1456
|
+
import { HTTPException as HTTPException7 } from "hono/http-exception";
|
|
1457
|
+
var phoneVerificationConfirmHandler = async (c) => {
|
|
1458
|
+
const body = c.req.valid("json");
|
|
1459
|
+
const { config, database, tenantId } = c.var;
|
|
1460
|
+
const resolvedTenantId = ensureTenantId(config, tenantId);
|
|
1461
|
+
const { verificationId, code, context } = body;
|
|
1462
|
+
const verification = await findVerificationById(database, verificationId);
|
|
1463
|
+
if (!verification) {
|
|
1464
|
+
throw new HTTPException7(400, {
|
|
1465
|
+
message: AUTH_ERRORS.VERIFICATION_NOT_FOUND
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
if (new Date(verification.expiresAt) < /* @__PURE__ */ new Date()) {
|
|
1469
|
+
throw new HTTPException7(400, { message: AUTH_ERRORS.VERIFICATION_EXPIRED });
|
|
1470
|
+
}
|
|
1471
|
+
const hashedCode = await hashToken(code, config.secret);
|
|
1472
|
+
if (verification.code !== hashedCode) {
|
|
1473
|
+
await updateVerificationAttempt(database, verificationId);
|
|
1474
|
+
throw new HTTPException7(400, {
|
|
1475
|
+
message: AUTH_ERRORS.VERIFICATION_MISMATCH
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
await consumeVerification(database, verificationId);
|
|
1479
|
+
if (context === "change-phone" && verification.userId) {
|
|
1480
|
+
await updateUserVerified(
|
|
1481
|
+
database,
|
|
1482
|
+
resolvedTenantId,
|
|
1483
|
+
verification.userId,
|
|
1484
|
+
"phone"
|
|
1485
|
+
);
|
|
1486
|
+
} else if (context === "sign-in" && verification.userId) {
|
|
1487
|
+
await updateUserVerified(
|
|
1488
|
+
database,
|
|
1489
|
+
resolvedTenantId,
|
|
1490
|
+
verification.userId,
|
|
1491
|
+
"phone"
|
|
1492
|
+
);
|
|
1493
|
+
} else if (context === "sign-up" && verification.userId) {
|
|
1494
|
+
await updateUserVerified(
|
|
1495
|
+
database,
|
|
1496
|
+
resolvedTenantId,
|
|
1497
|
+
verification.userId,
|
|
1498
|
+
"phone"
|
|
1499
|
+
);
|
|
1500
|
+
}
|
|
1501
|
+
const user = verification.userId ? await findUserById(database, resolvedTenantId, verification.userId) : null;
|
|
1502
|
+
if (!user) {
|
|
1503
|
+
throw new HTTPException7(500, { message: "User not found" });
|
|
1504
|
+
}
|
|
1505
|
+
if (context === "sign-in" || context === "change-phone" || context === "sign-up") {
|
|
1506
|
+
const sessionToken = crypto.randomUUID();
|
|
1507
|
+
const hashedToken = await hashToken(sessionToken, config.secret);
|
|
1508
|
+
const expiresAt = addDuration(config.session.expiresIn);
|
|
1509
|
+
const session = await insertSession(database, {
|
|
1510
|
+
tenantId: resolvedTenantId,
|
|
1511
|
+
userId: user.id,
|
|
1512
|
+
token: hashedToken,
|
|
1513
|
+
expiresAt,
|
|
1514
|
+
userAgent: c.req.header("user-agent") || null,
|
|
1515
|
+
ip: c.req.header("cf-connecting-ip") || c.req.header("x-forwarded-for") || null,
|
|
1516
|
+
meta: {
|
|
1517
|
+
action: context === "sign-up" ? "phone-verification-sign-up" : `phone-verification-${context}`
|
|
1518
|
+
}
|
|
1519
|
+
});
|
|
1520
|
+
setCookie3(c, "session_token", sessionToken, {
|
|
1521
|
+
httpOnly: true,
|
|
1522
|
+
secure: process.env.NODE_ENV === "production",
|
|
1523
|
+
sameSite: "Lax",
|
|
1524
|
+
path: "/",
|
|
1525
|
+
expires: new Date(expiresAt)
|
|
1526
|
+
});
|
|
1527
|
+
return c.json({
|
|
1528
|
+
user,
|
|
1529
|
+
session: {
|
|
1530
|
+
id: session.id,
|
|
1531
|
+
expiresAt: session.expiresAt,
|
|
1532
|
+
createdAt: session.createdAt,
|
|
1533
|
+
userAgent: session.userAgent,
|
|
1534
|
+
ip: session.ip
|
|
1535
|
+
},
|
|
1536
|
+
sessionToken,
|
|
1537
|
+
sessionExpiresAt: session.expiresAt
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
return c.json({
|
|
1541
|
+
user: user || null,
|
|
1542
|
+
session: null,
|
|
1543
|
+
verified: true
|
|
1544
|
+
});
|
|
1545
|
+
};
|
|
1546
|
+
|
|
1547
|
+
// src/routes/handler/phone-verification-request.ts
|
|
1548
|
+
import { HTTPException as HTTPException8 } from "hono/http-exception";
|
|
1549
|
+
var phoneVerificationRequestHandler = async (c) => {
|
|
1550
|
+
const body = c.req.valid("json");
|
|
1551
|
+
const { config, database, tenantId, user } = c.var;
|
|
1552
|
+
const resolvedTenantId = ensureTenantId(config, tenantId);
|
|
1553
|
+
const { phone, context } = body;
|
|
1554
|
+
if (!phone) {
|
|
1555
|
+
throw new HTTPException8(400, { message: "Phone required" });
|
|
1556
|
+
}
|
|
1557
|
+
let userId = user?.id;
|
|
1558
|
+
if (!userId) {
|
|
1559
|
+
const lookup = await findUserByIdentifier(
|
|
1560
|
+
database,
|
|
1561
|
+
resolvedTenantId,
|
|
1562
|
+
phone
|
|
1563
|
+
);
|
|
1564
|
+
if (!lookup.user) {
|
|
1565
|
+
throw new HTTPException8(404, { message: AUTH_ERRORS.USER_NOT_FOUND });
|
|
1566
|
+
}
|
|
1567
|
+
userId = lookup.user.id;
|
|
1568
|
+
}
|
|
1569
|
+
if (!userId) {
|
|
1570
|
+
throw new HTTPException8(404, { message: AUTH_ERRORS.USER_NOT_FOUND });
|
|
1571
|
+
}
|
|
1572
|
+
await deleteVerificationsByUserAndType(
|
|
1573
|
+
database,
|
|
1574
|
+
resolvedTenantId,
|
|
1575
|
+
userId,
|
|
1576
|
+
`phone-otp-${context}`
|
|
1577
|
+
);
|
|
1578
|
+
const code = generateOtpCode(config.phone.otpLength);
|
|
1579
|
+
const hashedCode = await hashToken(code, config.secret);
|
|
1580
|
+
const expiresAt = addDuration(config.phone.otpExpiresIn);
|
|
1581
|
+
const verification = await insertVerification(database, {
|
|
1582
|
+
tenantId: resolvedTenantId,
|
|
1583
|
+
userId,
|
|
1584
|
+
type: `phone-otp-${context}`,
|
|
1585
|
+
code: hashedCode,
|
|
1586
|
+
expiresAt,
|
|
1587
|
+
to: phone
|
|
1588
|
+
});
|
|
1589
|
+
const smsProvider = new AfroSmsProvider(config.phone.smsConfig);
|
|
1590
|
+
await smsProvider.sendVerificationSms(phone, code);
|
|
1591
|
+
return c.json({ verificationId: verification.id });
|
|
1592
|
+
};
|
|
1593
|
+
|
|
1594
|
+
// src/routes/handler/session.ts
|
|
1595
|
+
var sessionHandler = (c) => {
|
|
1596
|
+
const { user, session } = c.var;
|
|
1597
|
+
return c.json({
|
|
1598
|
+
user: user || null,
|
|
1599
|
+
session: session ? {
|
|
1600
|
+
id: session.id,
|
|
1601
|
+
expiresAt: session.expiresAt,
|
|
1602
|
+
createdAt: session.createdAt,
|
|
1603
|
+
userAgent: session.userAgent,
|
|
1604
|
+
ip: session.ip
|
|
1605
|
+
} : null
|
|
1606
|
+
});
|
|
1607
|
+
};
|
|
1608
|
+
|
|
1609
|
+
// src/routes/handler/sign-in.ts
|
|
1610
|
+
import { setCookie as setCookie4 } from "hono/cookie";
|
|
1611
|
+
import { HTTPException as HTTPException9 } from "hono/http-exception";
|
|
1612
|
+
|
|
1613
|
+
// src/db/orm/iam/sessions/delete-oldest-sessions.ts
|
|
1614
|
+
var deleteOldestSessions = async (db, tenantId, userId, keepCount) => {
|
|
1615
|
+
const sessions = await listSessionsForUser(db, tenantId, userId);
|
|
1616
|
+
if (sessions.length <= keepCount) {
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
const toDelete = sessions.slice(0, sessions.length - keepCount);
|
|
1620
|
+
for (const session of toDelete) {
|
|
1621
|
+
await deleteSessionById(db, session.id);
|
|
1622
|
+
}
|
|
1623
|
+
};
|
|
1624
|
+
|
|
1625
|
+
// src/db/orm/iam/users/update-last-sign-in.ts
|
|
1626
|
+
import { and as and10, eq as eq14 } from "drizzle-orm";
|
|
1627
|
+
var updateLastSignIn = (db, tenantId, userId) => {
|
|
1628
|
+
return db.update(usersInIam).set({ lastSignInAt: (/* @__PURE__ */ new Date()).toISOString(), loginAttempt: 0 }).where(and10(eq14(usersInIam.id, userId), eq14(usersInIam.tenantId, tenantId)));
|
|
1629
|
+
};
|
|
1630
|
+
|
|
1631
|
+
// src/routes/handler/sign-in.ts
|
|
1632
|
+
var signInHandler = async (c) => {
|
|
1633
|
+
const body = c.req.valid("json");
|
|
1634
|
+
const { config, database, tenantId } = c.var;
|
|
1635
|
+
const resolvedTenantId = ensureTenantId(config, tenantId);
|
|
1636
|
+
const { identifier, password } = body;
|
|
1637
|
+
const lookup = await findUserByIdentifier(
|
|
1638
|
+
database,
|
|
1639
|
+
resolvedTenantId,
|
|
1640
|
+
identifier
|
|
1641
|
+
);
|
|
1642
|
+
if (!lookup.user) {
|
|
1643
|
+
throw new HTTPException9(401, { message: AUTH_ERRORS.USER_NOT_FOUND });
|
|
1644
|
+
}
|
|
1645
|
+
const account = await findAccountByProvider(
|
|
1646
|
+
database,
|
|
1647
|
+
resolvedTenantId,
|
|
1648
|
+
lookup.user.id,
|
|
1649
|
+
"credentials"
|
|
1650
|
+
);
|
|
1651
|
+
if (!account?.password) {
|
|
1652
|
+
throw new HTTPException9(401, { message: AUTH_ERRORS.HAS_NO_PASSWORD });
|
|
1653
|
+
}
|
|
1654
|
+
const passwordValid = await verifyPassword(password, account.password);
|
|
1655
|
+
if (!passwordValid) {
|
|
1656
|
+
throw new HTTPException9(401, { message: AUTH_ERRORS.INVALID_PASSWORD });
|
|
1657
|
+
}
|
|
1658
|
+
const isEmail = lookup.type === "email";
|
|
1659
|
+
const isVerified = isEmail ? lookup.user.emailVerified : lookup.user.phoneVerified;
|
|
1660
|
+
if (isEmail && config.email.verificationRequired && !isVerified) {
|
|
1661
|
+
const code = generateOtpCode(6);
|
|
1662
|
+
const hashedCode = await hashToken(code, config.secret);
|
|
1663
|
+
const expiresAt2 = addDuration(config.email.verificationExpiresIn);
|
|
1664
|
+
const verification = await insertVerification(database, {
|
|
1665
|
+
tenantId: resolvedTenantId,
|
|
1666
|
+
userId: lookup.user.id,
|
|
1667
|
+
type: "email-verification",
|
|
1668
|
+
code: hashedCode,
|
|
1669
|
+
expiresAt: expiresAt2,
|
|
1670
|
+
to: lookup.user.email
|
|
1671
|
+
});
|
|
1672
|
+
const emailProvider = new ResendEmailProvider(config.email.resend);
|
|
1673
|
+
await emailProvider.sendVerificationEmail(lookup.user.email, code);
|
|
1674
|
+
return c.json({
|
|
1675
|
+
verificationId: verification.id,
|
|
1676
|
+
requiresVerification: true
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
if (!isEmail && config.phone.verificationRequired && !isVerified) {
|
|
1680
|
+
const code = generateOtpCode(config.phone.otpLength);
|
|
1681
|
+
const hashedCode = await hashToken(code, config.secret);
|
|
1682
|
+
const expiresAt2 = addDuration(config.phone.otpExpiresIn);
|
|
1683
|
+
const verification = await insertVerification(database, {
|
|
1684
|
+
tenantId: resolvedTenantId,
|
|
1685
|
+
userId: lookup.user.id,
|
|
1686
|
+
type: "phone-otp",
|
|
1687
|
+
code: hashedCode,
|
|
1688
|
+
expiresAt: expiresAt2,
|
|
1689
|
+
to: lookup.user.phone
|
|
1690
|
+
});
|
|
1691
|
+
const smsProvider = new AfroSmsProvider(config.phone.smsConfig);
|
|
1692
|
+
await smsProvider.sendVerificationSms(lookup.user.phone, code);
|
|
1693
|
+
return c.json({
|
|
1694
|
+
verificationId: verification.id,
|
|
1695
|
+
requiresVerification: true
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
if (config.session.maxPerUser) {
|
|
1699
|
+
await deleteOldestSessions(
|
|
1700
|
+
database,
|
|
1701
|
+
resolvedTenantId,
|
|
1702
|
+
lookup.user.id,
|
|
1703
|
+
config.session.maxPerUser
|
|
1704
|
+
);
|
|
1705
|
+
}
|
|
1706
|
+
await updateLastSignIn(database, resolvedTenantId, lookup.user.id);
|
|
1707
|
+
const sessionToken = crypto.randomUUID();
|
|
1708
|
+
const hashedToken = await hashToken(sessionToken, config.secret);
|
|
1709
|
+
const expiresAt = addDuration(config.session.expiresIn);
|
|
1710
|
+
const session = await insertSession(database, {
|
|
1711
|
+
tenantId: resolvedTenantId,
|
|
1712
|
+
userId: lookup.user.id,
|
|
1713
|
+
token: hashedToken,
|
|
1714
|
+
expiresAt,
|
|
1715
|
+
userAgent: c.req.header("user-agent") || null,
|
|
1716
|
+
ip: c.req.header("cf-connecting-ip") || c.req.header("x-forwarded-for") || null,
|
|
1717
|
+
meta: { action: "sign-in" }
|
|
1718
|
+
});
|
|
1719
|
+
setCookie4(c, "session_token", sessionToken, {
|
|
1720
|
+
httpOnly: true,
|
|
1721
|
+
secure: process.env.NODE_ENV === "production",
|
|
1722
|
+
sameSite: "Lax",
|
|
1723
|
+
path: "/",
|
|
1724
|
+
expires: new Date(expiresAt)
|
|
1725
|
+
});
|
|
1726
|
+
return c.json({
|
|
1727
|
+
user: lookup.user,
|
|
1728
|
+
session: {
|
|
1729
|
+
id: session.id,
|
|
1730
|
+
expiresAt: session.expiresAt,
|
|
1731
|
+
createdAt: session.createdAt,
|
|
1732
|
+
userAgent: session.userAgent,
|
|
1733
|
+
ip: session.ip
|
|
1734
|
+
},
|
|
1735
|
+
sessionToken,
|
|
1736
|
+
sessionExpiresAt: session.expiresAt
|
|
1737
|
+
});
|
|
1738
|
+
};
|
|
1739
|
+
|
|
1740
|
+
// src/routes/handler/sign-out.ts
|
|
1741
|
+
import { deleteCookie, getCookie } from "hono/cookie";
|
|
1742
|
+
var signOutHandler = async (c) => {
|
|
1743
|
+
const { config, database, tenantId } = c.var;
|
|
1744
|
+
ensureTenantId(config, tenantId);
|
|
1745
|
+
const sessionToken = getCookie(c, "session_token");
|
|
1746
|
+
if (!sessionToken) {
|
|
1747
|
+
return c.json({ message: "Signed out" });
|
|
1748
|
+
}
|
|
1749
|
+
const hashedToken = await hashToken(sessionToken, config.secret);
|
|
1750
|
+
const session = await findSessionByToken(database, hashedToken);
|
|
1751
|
+
if (session) {
|
|
1752
|
+
await deleteSessionById(database, session.id);
|
|
1753
|
+
}
|
|
1754
|
+
deleteCookie(c, "session_token", {
|
|
1755
|
+
httpOnly: true,
|
|
1756
|
+
secure: process.env.NODE_ENV === "production",
|
|
1757
|
+
sameSite: "Lax",
|
|
1758
|
+
path: "/",
|
|
1759
|
+
expires: /* @__PURE__ */ new Date(0)
|
|
1760
|
+
});
|
|
1761
|
+
return c.json({ message: "Signed out" });
|
|
1762
|
+
};
|
|
1763
|
+
|
|
1764
|
+
// src/routes/handler/sign-up.ts
|
|
1765
|
+
import { setCookie as setCookie5 } from "hono/cookie";
|
|
1766
|
+
import { HTTPException as HTTPException10 } from "hono/http-exception";
|
|
1767
|
+
|
|
1768
|
+
// src/db/orm/iam/accounts/insert-credentials-account.ts
|
|
1769
|
+
var insertCredentialsAccount = (db, data) => {
|
|
1770
|
+
return db.insert(accountsInIam).values({
|
|
1771
|
+
tenantId: data.tenantId,
|
|
1772
|
+
userId: data.userId,
|
|
1773
|
+
provider: "credentials",
|
|
1774
|
+
providerAccountId: data.providerAccountId,
|
|
1775
|
+
password: data.password
|
|
1776
|
+
});
|
|
1777
|
+
};
|
|
1778
|
+
|
|
1779
|
+
// src/db/orm/iam/users/find-user-by-handle.ts
|
|
1780
|
+
import { and as and11, eq as eq15, sql as sql3 } from "drizzle-orm";
|
|
1781
|
+
var findUserByHandle = (db, tenantId, handle) => {
|
|
1782
|
+
return db.select({
|
|
1783
|
+
id: usersInIam.id,
|
|
1784
|
+
tenantId: usersInIam.tenantId,
|
|
1785
|
+
fullName: usersInIam.fullName,
|
|
1786
|
+
email: usersInIam.email,
|
|
1787
|
+
phone: usersInIam.phone,
|
|
1788
|
+
handle: usersInIam.handle,
|
|
1789
|
+
image: usersInIam.image,
|
|
1790
|
+
emailVerified: usersInIam.emailVerified,
|
|
1791
|
+
phoneVerified: usersInIam.phoneVerified,
|
|
1792
|
+
lastSignInAt: usersInIam.lastSignInAt
|
|
1793
|
+
}).from(usersInIam).where(
|
|
1794
|
+
and11(
|
|
1795
|
+
eq15(usersInIam.tenantId, tenantId),
|
|
1796
|
+
sql3`lower(${usersInIam.handle}) = lower(${handle})`
|
|
1797
|
+
)
|
|
1798
|
+
).limit(1).then(([user]) => user || null);
|
|
1799
|
+
};
|
|
1800
|
+
|
|
1801
|
+
// src/db/orm/iam/users/insert-user.ts
|
|
1802
|
+
var insertUser = (db, data) => {
|
|
1803
|
+
return db.insert(usersInIam).values({
|
|
1804
|
+
tenantId: data.tenantId,
|
|
1805
|
+
fullName: data.fullName,
|
|
1806
|
+
handle: data.handle,
|
|
1807
|
+
email: data.email || null,
|
|
1808
|
+
phone: data.phone || null,
|
|
1809
|
+
image: data.image || null,
|
|
1810
|
+
emailVerified: Boolean(data.emailVerified),
|
|
1811
|
+
phoneVerified: Boolean(data.phoneVerified)
|
|
1812
|
+
}).returning({
|
|
1813
|
+
id: usersInIam.id,
|
|
1814
|
+
tenantId: usersInIam.tenantId,
|
|
1815
|
+
fullName: usersInIam.fullName,
|
|
1816
|
+
email: usersInIam.email,
|
|
1817
|
+
phone: usersInIam.phone,
|
|
1818
|
+
handle: usersInIam.handle,
|
|
1819
|
+
image: usersInIam.image,
|
|
1820
|
+
emailVerified: usersInIam.emailVerified,
|
|
1821
|
+
phoneVerified: usersInIam.phoneVerified,
|
|
1822
|
+
lastSignInAt: usersInIam.lastSignInAt
|
|
1823
|
+
}).then(([user]) => user);
|
|
1824
|
+
};
|
|
1825
|
+
|
|
1826
|
+
// src/routes/handler/sign-up.ts
|
|
1827
|
+
var signUpHandler = async (c) => {
|
|
1828
|
+
const body = c.req.valid("json");
|
|
1829
|
+
const { config, database, tenantId } = c.var;
|
|
1830
|
+
const resolvedTenantId = ensureTenantId(config, tenantId);
|
|
1831
|
+
const { email, phone, password, fullName, handle } = body;
|
|
1832
|
+
if (!(email || phone)) {
|
|
1833
|
+
throw new HTTPException10(400, {
|
|
1834
|
+
message: "Either email or phone is required"
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
const identifier = email || phone;
|
|
1838
|
+
const existing = await findUserByIdentifier(
|
|
1839
|
+
database,
|
|
1840
|
+
resolvedTenantId,
|
|
1841
|
+
identifier
|
|
1842
|
+
);
|
|
1843
|
+
if (existing.user) {
|
|
1844
|
+
throw new HTTPException10(409, { message: AUTH_ERRORS.USER_EXISTS });
|
|
1845
|
+
}
|
|
1846
|
+
const userHandle = handle || generateHandle(email || phone);
|
|
1847
|
+
const existingHandle = await findUserByHandle(
|
|
1848
|
+
database,
|
|
1849
|
+
resolvedTenantId,
|
|
1850
|
+
userHandle
|
|
1851
|
+
);
|
|
1852
|
+
if (existingHandle) {
|
|
1853
|
+
throw new HTTPException10(409, { message: "Handle already taken" });
|
|
1854
|
+
}
|
|
1855
|
+
const user = await insertUser(database, {
|
|
1856
|
+
tenantId: resolvedTenantId,
|
|
1857
|
+
fullName,
|
|
1858
|
+
handle: userHandle,
|
|
1859
|
+
email: email || null,
|
|
1860
|
+
phone: phone || null,
|
|
1861
|
+
emailVerified: email ? !config.email.verificationRequired : false,
|
|
1862
|
+
phoneVerified: phone ? !config.phone.verificationRequired : false
|
|
1863
|
+
});
|
|
1864
|
+
const passwordHash = await hashPassword(password);
|
|
1865
|
+
await insertCredentialsAccount(database, {
|
|
1866
|
+
tenantId: resolvedTenantId,
|
|
1867
|
+
userId: user.id,
|
|
1868
|
+
providerAccountId: identifier,
|
|
1869
|
+
password: passwordHash
|
|
1870
|
+
});
|
|
1871
|
+
if (phone && config.phone.verificationRequired) {
|
|
1872
|
+
const code = generateOtpCode(config.phone.otpLength);
|
|
1873
|
+
const hashedCode = await hashToken(code, config.secret);
|
|
1874
|
+
const expiresAt2 = addDuration(config.phone.otpExpiresIn);
|
|
1875
|
+
const verification = await insertVerification(database, {
|
|
1876
|
+
tenantId: resolvedTenantId,
|
|
1877
|
+
userId: user.id,
|
|
1878
|
+
type: "phone-otp-sign-up",
|
|
1879
|
+
code: hashedCode,
|
|
1880
|
+
expiresAt: expiresAt2,
|
|
1881
|
+
to: phone
|
|
1882
|
+
});
|
|
1883
|
+
const smsProvider = new AfroSmsProvider(config.phone.smsConfig);
|
|
1884
|
+
await smsProvider.sendVerificationSms(phone, code);
|
|
1885
|
+
return c.json({
|
|
1886
|
+
user: { id: user.id, phone },
|
|
1887
|
+
session: null,
|
|
1888
|
+
verificationId: verification.id,
|
|
1889
|
+
requiresVerification: true
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
if (email && config.email.verificationRequired) {
|
|
1893
|
+
const code = generateOtpCode(6);
|
|
1894
|
+
const hashedCode = await hashToken(code, config.secret);
|
|
1895
|
+
const expiresAt2 = addDuration(config.email.verificationExpiresIn);
|
|
1896
|
+
const verification = await insertVerification(database, {
|
|
1897
|
+
tenantId: resolvedTenantId,
|
|
1898
|
+
userId: user.id,
|
|
1899
|
+
type: "email-verification",
|
|
1900
|
+
code: hashedCode,
|
|
1901
|
+
expiresAt: expiresAt2,
|
|
1902
|
+
to: email
|
|
1903
|
+
});
|
|
1904
|
+
const emailProvider = new ResendEmailProvider(config.email.resend);
|
|
1905
|
+
await emailProvider.sendVerificationEmail(email, code);
|
|
1906
|
+
return c.json({
|
|
1907
|
+
user: { id: user.id, email },
|
|
1908
|
+
session: null,
|
|
1909
|
+
verificationId: verification.id,
|
|
1910
|
+
requiresVerification: true
|
|
1911
|
+
});
|
|
1912
|
+
}
|
|
1913
|
+
const sessionToken = crypto.randomUUID();
|
|
1914
|
+
const hashedToken = await hashToken(sessionToken, config.secret);
|
|
1915
|
+
const expiresAt = addDuration(config.session.expiresIn);
|
|
1916
|
+
const session = await insertSession(database, {
|
|
1917
|
+
tenantId: resolvedTenantId,
|
|
1918
|
+
userId: user.id,
|
|
1919
|
+
token: hashedToken,
|
|
1920
|
+
expiresAt,
|
|
1921
|
+
userAgent: c.req.header("user-agent") || null,
|
|
1922
|
+
ip: c.req.header("cf-connecting-ip") || c.req.header("x-forwarded-for") || null,
|
|
1923
|
+
meta: { action: "sign-up" }
|
|
1924
|
+
});
|
|
1925
|
+
setCookie5(c, "session_token", sessionToken, {
|
|
1926
|
+
httpOnly: true,
|
|
1927
|
+
secure: process.env.NODE_ENV === "production",
|
|
1928
|
+
sameSite: "Lax",
|
|
1929
|
+
path: "/",
|
|
1930
|
+
expires: new Date(expiresAt)
|
|
1931
|
+
});
|
|
1932
|
+
return c.json({
|
|
1933
|
+
user,
|
|
1934
|
+
session: {
|
|
1935
|
+
id: session.id,
|
|
1936
|
+
expiresAt: session.expiresAt,
|
|
1937
|
+
createdAt: session.createdAt,
|
|
1938
|
+
userAgent: session.userAgent,
|
|
1939
|
+
ip: session.ip
|
|
1940
|
+
},
|
|
1941
|
+
sessionToken,
|
|
1942
|
+
sessionExpiresAt: session.expiresAt
|
|
1943
|
+
});
|
|
1944
|
+
};
|
|
1945
|
+
|
|
1946
|
+
// src/routes/auth.route.ts
|
|
1947
|
+
var signUpRoute = createRoute({
|
|
1948
|
+
method: "post",
|
|
1949
|
+
path: "/sign-up",
|
|
1950
|
+
tags: ["Auth"],
|
|
1951
|
+
summary: "Sign up with email or phone",
|
|
1952
|
+
request: {
|
|
1953
|
+
body: {
|
|
1954
|
+
content: {
|
|
1955
|
+
"application/json": {
|
|
1956
|
+
schema: signUpSchema
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
},
|
|
1961
|
+
responses: {
|
|
1962
|
+
201: {
|
|
1963
|
+
content: {
|
|
1964
|
+
"application/json": {
|
|
1965
|
+
schema: signUpResponseSchema
|
|
1966
|
+
}
|
|
1967
|
+
},
|
|
1968
|
+
description: "Account created"
|
|
1969
|
+
},
|
|
1970
|
+
409: {
|
|
1971
|
+
content: {
|
|
1972
|
+
"application/json": {
|
|
1973
|
+
schema: errorResponseSchema
|
|
1974
|
+
}
|
|
1975
|
+
},
|
|
1976
|
+
description: "User already exists"
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
});
|
|
1980
|
+
var signInRoute = createRoute({
|
|
1981
|
+
method: "post",
|
|
1982
|
+
path: "/sign-in",
|
|
1983
|
+
tags: ["Auth"],
|
|
1984
|
+
summary: "Sign in with email or phone",
|
|
1985
|
+
request: {
|
|
1986
|
+
body: {
|
|
1987
|
+
content: {
|
|
1988
|
+
"application/json": {
|
|
1989
|
+
schema: signInSchema
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
},
|
|
1994
|
+
responses: {
|
|
1995
|
+
200: {
|
|
1996
|
+
content: {
|
|
1997
|
+
"application/json": {
|
|
1998
|
+
schema: signInResponseSchema
|
|
1999
|
+
}
|
|
2000
|
+
},
|
|
2001
|
+
description: "Signed in"
|
|
2002
|
+
},
|
|
2003
|
+
401: {
|
|
2004
|
+
content: {
|
|
2005
|
+
"application/json": {
|
|
2006
|
+
schema: errorResponseSchema
|
|
2007
|
+
}
|
|
2008
|
+
},
|
|
2009
|
+
description: "Invalid credentials"
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
});
|
|
2013
|
+
var checkUserRoute = createRoute({
|
|
2014
|
+
method: "post",
|
|
2015
|
+
path: "/check-user",
|
|
2016
|
+
tags: ["Auth"],
|
|
2017
|
+
summary: "Check if user exists",
|
|
2018
|
+
request: {
|
|
2019
|
+
body: {
|
|
2020
|
+
content: {
|
|
2021
|
+
"application/json": {
|
|
2022
|
+
schema: checkUserSchema
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
},
|
|
2027
|
+
responses: {
|
|
2028
|
+
200: {
|
|
2029
|
+
content: {
|
|
2030
|
+
"application/json": {
|
|
2031
|
+
schema: checkUserResponseSchema
|
|
2032
|
+
}
|
|
2033
|
+
},
|
|
2034
|
+
description: "User check result"
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
});
|
|
2038
|
+
var signOutRoute = createRoute({
|
|
2039
|
+
method: "post",
|
|
2040
|
+
path: "/sign-out",
|
|
2041
|
+
tags: ["Auth"],
|
|
2042
|
+
summary: "Sign out current session",
|
|
2043
|
+
responses: {
|
|
2044
|
+
200: {
|
|
2045
|
+
content: { "application/json": { schema: messageSchema } },
|
|
2046
|
+
description: "Signed out"
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
});
|
|
2050
|
+
var meRoute = createRoute({
|
|
2051
|
+
method: "get",
|
|
2052
|
+
path: "/me",
|
|
2053
|
+
tags: ["Auth"],
|
|
2054
|
+
summary: "Get current user",
|
|
2055
|
+
responses: {
|
|
2056
|
+
200: {
|
|
2057
|
+
content: {
|
|
2058
|
+
"application/json": {
|
|
2059
|
+
schema: z2.object({ user: userSchema })
|
|
2060
|
+
}
|
|
2061
|
+
},
|
|
2062
|
+
description: "Current user"
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
});
|
|
2066
|
+
var sessionRoute = createRoute({
|
|
2067
|
+
method: "get",
|
|
2068
|
+
path: "/session",
|
|
2069
|
+
tags: ["Auth"],
|
|
2070
|
+
summary: "Get current session",
|
|
2071
|
+
responses: {
|
|
2072
|
+
200: {
|
|
2073
|
+
content: {
|
|
2074
|
+
"application/json": {
|
|
2075
|
+
schema: z2.object({
|
|
2076
|
+
user: userSchema.nullable(),
|
|
2077
|
+
session: z2.object({
|
|
2078
|
+
id: z2.string().uuid(),
|
|
2079
|
+
expiresAt: z2.string().datetime(),
|
|
2080
|
+
createdAt: z2.string().datetime(),
|
|
2081
|
+
userAgent: z2.string().nullable(),
|
|
2082
|
+
ip: z2.string().nullable()
|
|
2083
|
+
}).nullable()
|
|
2084
|
+
})
|
|
2085
|
+
}
|
|
2086
|
+
},
|
|
2087
|
+
description: "Current session"
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
});
|
|
2091
|
+
var emailVerificationRequestRoute = createRoute({
|
|
2092
|
+
method: "post",
|
|
2093
|
+
path: "/email/verification/request",
|
|
2094
|
+
tags: ["Email"],
|
|
2095
|
+
summary: "Request email verification",
|
|
2096
|
+
request: {
|
|
2097
|
+
body: {
|
|
2098
|
+
content: {
|
|
2099
|
+
"application/json": {
|
|
2100
|
+
schema: emailVerificationRequestSchema
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
},
|
|
2105
|
+
responses: {
|
|
2106
|
+
200: {
|
|
2107
|
+
content: {
|
|
2108
|
+
"application/json": {
|
|
2109
|
+
schema: messageWithVerificationIdSchema
|
|
2110
|
+
}
|
|
2111
|
+
},
|
|
2112
|
+
description: "Verification code sent"
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
});
|
|
2116
|
+
var emailVerificationConfirmRoute = createRoute({
|
|
2117
|
+
method: "post",
|
|
2118
|
+
path: "/email/verification/confirm",
|
|
2119
|
+
tags: ["Email"],
|
|
2120
|
+
summary: "Confirm email verification",
|
|
2121
|
+
request: {
|
|
2122
|
+
body: {
|
|
2123
|
+
content: {
|
|
2124
|
+
"application/json": {
|
|
2125
|
+
schema: emailVerificationConfirmSchema
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
},
|
|
2130
|
+
responses: {
|
|
2131
|
+
200: {
|
|
2132
|
+
content: {
|
|
2133
|
+
"application/json": {
|
|
2134
|
+
schema: authSuccessSchema
|
|
2135
|
+
}
|
|
2136
|
+
},
|
|
2137
|
+
description: "Email verified"
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
});
|
|
2141
|
+
var phoneVerificationRequestRoute = createRoute({
|
|
2142
|
+
method: "post",
|
|
2143
|
+
path: "/phone/verification/request",
|
|
2144
|
+
tags: ["Phone"],
|
|
2145
|
+
summary: "Request phone OTP",
|
|
2146
|
+
request: {
|
|
2147
|
+
body: {
|
|
2148
|
+
content: {
|
|
2149
|
+
"application/json": {
|
|
2150
|
+
schema: phoneVerificationRequestSchema
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
},
|
|
2155
|
+
responses: {
|
|
2156
|
+
200: {
|
|
2157
|
+
content: {
|
|
2158
|
+
"application/json": {
|
|
2159
|
+
schema: messageWithVerificationIdSchema
|
|
2160
|
+
}
|
|
2161
|
+
},
|
|
2162
|
+
description: "OTP sent"
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
});
|
|
2166
|
+
var phoneVerificationConfirmRoute = createRoute({
|
|
2167
|
+
method: "post",
|
|
2168
|
+
path: "/phone/verification/confirm",
|
|
2169
|
+
tags: ["Phone"],
|
|
2170
|
+
summary: "Confirm phone OTP",
|
|
2171
|
+
request: {
|
|
2172
|
+
body: {
|
|
2173
|
+
content: {
|
|
2174
|
+
"application/json": {
|
|
2175
|
+
schema: phoneVerificationConfirmSchema
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
},
|
|
2180
|
+
responses: {
|
|
2181
|
+
200: {
|
|
2182
|
+
content: {
|
|
2183
|
+
"application/json": {
|
|
2184
|
+
schema: authSuccessSchema
|
|
2185
|
+
}
|
|
2186
|
+
},
|
|
2187
|
+
description: "Phone verified"
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
});
|
|
2191
|
+
var forgotPasswordRoute = createRoute({
|
|
2192
|
+
method: "post",
|
|
2193
|
+
path: "/password/forgot",
|
|
2194
|
+
tags: ["Password"],
|
|
2195
|
+
summary: "Request password reset",
|
|
2196
|
+
request: {
|
|
2197
|
+
body: {
|
|
2198
|
+
content: {
|
|
2199
|
+
"application/json": {
|
|
2200
|
+
schema: forgotPasswordSchema
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
},
|
|
2205
|
+
responses: {
|
|
2206
|
+
200: {
|
|
2207
|
+
content: {
|
|
2208
|
+
"application/json": {
|
|
2209
|
+
schema: messageWithVerificationIdSchema
|
|
2210
|
+
}
|
|
2211
|
+
},
|
|
2212
|
+
description: "Reset code sent if account exists"
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
});
|
|
2216
|
+
var resetPasswordRoute = createRoute({
|
|
2217
|
+
method: "post",
|
|
2218
|
+
path: "/password/reset",
|
|
2219
|
+
tags: ["Password"],
|
|
2220
|
+
summary: "Reset password",
|
|
2221
|
+
request: {
|
|
2222
|
+
body: {
|
|
2223
|
+
content: {
|
|
2224
|
+
"application/json": {
|
|
2225
|
+
schema: resetPasswordSchema
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
},
|
|
2230
|
+
responses: {
|
|
2231
|
+
200: {
|
|
2232
|
+
content: {
|
|
2233
|
+
"application/json": {
|
|
2234
|
+
schema: authSuccessSchema
|
|
2235
|
+
}
|
|
2236
|
+
},
|
|
2237
|
+
description: "Password reset and new session"
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
});
|
|
2241
|
+
var changePasswordRoute = createRoute({
|
|
2242
|
+
method: "post",
|
|
2243
|
+
path: "/password/change",
|
|
2244
|
+
tags: ["Password"],
|
|
2245
|
+
summary: "Change password",
|
|
2246
|
+
request: {
|
|
2247
|
+
body: {
|
|
2248
|
+
content: {
|
|
2249
|
+
"application/json": {
|
|
2250
|
+
schema: changePasswordSchema
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
},
|
|
2255
|
+
responses: {
|
|
2256
|
+
200: {
|
|
2257
|
+
content: { "application/json": { schema: messageSchema } },
|
|
2258
|
+
description: "Password updated"
|
|
2259
|
+
},
|
|
2260
|
+
401: {
|
|
2261
|
+
content: { "application/json": { schema: errorResponseSchema } },
|
|
2262
|
+
description: "Invalid password"
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
});
|
|
2266
|
+
var createAuthRoutes = () => {
|
|
2267
|
+
const authRoutes = new OpenAPIHono().openapi(signUpRoute, signUpHandler).openapi(signInRoute, signInHandler).openapi(checkUserRoute, checkUserHandler).openapi(signOutRoute, signOutHandler).openapi(meRoute, meHandler).openapi(sessionRoute, sessionHandler).openapi(emailVerificationRequestRoute, emailVerificationRequestHandler).openapi(emailVerificationConfirmRoute, emailVerificationConfirmHandler).openapi(phoneVerificationRequestRoute, phoneVerificationRequestHandler).openapi(phoneVerificationConfirmRoute, phoneVerificationConfirmHandler).openapi(forgotPasswordRoute, forgotPasswordHandler).openapi(resetPasswordRoute, resetPasswordHandler).openapi(changePasswordRoute, changePasswordHandler);
|
|
2268
|
+
return authRoutes;
|
|
2269
|
+
};
|
|
2270
|
+
|
|
2271
|
+
// src/handler.ts
|
|
2272
|
+
var createAuthMiddleware = (config, database, getTenantId) => {
|
|
2273
|
+
const enableTenant = config.enableTenant ?? true;
|
|
2274
|
+
return async (c, next) => {
|
|
2275
|
+
const sessionToken = getCookie2(c, "session_token") || void 0;
|
|
2276
|
+
let tenantId = getTenantId(c);
|
|
2277
|
+
if (enableTenant) {
|
|
2278
|
+
if (!tenantId) {
|
|
2279
|
+
throw new HTTPException11(400, {
|
|
2280
|
+
message: "Missing tenantId. Tenant isolation is enabled."
|
|
2281
|
+
});
|
|
2282
|
+
}
|
|
2283
|
+
} else {
|
|
2284
|
+
tenantId = config.tenantId;
|
|
2285
|
+
if (!tenantId) {
|
|
2286
|
+
throw new HTTPException11(500, {
|
|
2287
|
+
message: "tenantId must be provided in config when enableTenant is false."
|
|
2288
|
+
});
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
c.set("config", config);
|
|
2292
|
+
c.set("database", database);
|
|
2293
|
+
c.set("tenantId", tenantId);
|
|
2294
|
+
c.set("userId", void 0);
|
|
2295
|
+
c.set("user", void 0);
|
|
2296
|
+
c.set("session", void 0);
|
|
2297
|
+
if (sessionToken) {
|
|
2298
|
+
try {
|
|
2299
|
+
const hashedToken = await hashToken(sessionToken, config.secret);
|
|
2300
|
+
const session = await findSessionByToken(database, hashedToken);
|
|
2301
|
+
if (session) {
|
|
2302
|
+
const user = await findUserById(
|
|
2303
|
+
database,
|
|
2304
|
+
session.tenantId,
|
|
2305
|
+
session.userId
|
|
2306
|
+
);
|
|
2307
|
+
if (user) {
|
|
2308
|
+
c.set("tenantId", enableTenant ? session.tenantId : tenantId);
|
|
2309
|
+
c.set("userId", user.id);
|
|
2310
|
+
c.set("user", user);
|
|
2311
|
+
c.set("session", session);
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
} catch {
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
await next();
|
|
2318
|
+
};
|
|
2319
|
+
};
|
|
2320
|
+
var createAuthHandler = (config) => {
|
|
2321
|
+
const app = new OpenAPIHono2();
|
|
2322
|
+
const database = createDatabase(config.connectionString);
|
|
2323
|
+
app.use(
|
|
2324
|
+
"*",
|
|
2325
|
+
createAuthMiddleware(
|
|
2326
|
+
config,
|
|
2327
|
+
database,
|
|
2328
|
+
(c) => c.req.header("x-tenant-id") || config.tenantId
|
|
2329
|
+
)
|
|
2330
|
+
);
|
|
2331
|
+
app.route("/", createAuthRoutes());
|
|
2332
|
+
return app;
|
|
2333
|
+
};
|
|
2334
|
+
var createAuthRoutes2 = (config) => {
|
|
2335
|
+
const app = new OpenAPIHono2();
|
|
2336
|
+
const database = createDatabase(config.connectionString);
|
|
2337
|
+
app.use(
|
|
2338
|
+
"*",
|
|
2339
|
+
createAuthMiddleware(
|
|
2340
|
+
config,
|
|
2341
|
+
database,
|
|
2342
|
+
(c) => c.get("tenantId") || config.tenantId
|
|
2343
|
+
)
|
|
2344
|
+
);
|
|
2345
|
+
app.route("/", createAuthRoutes());
|
|
2346
|
+
return app;
|
|
2347
|
+
};
|
|
2348
|
+
|
|
2349
|
+
// src/index.ts
|
|
2350
|
+
var jiretAuth = (config) => {
|
|
2351
|
+
const handler = createAuthHandler(config);
|
|
2352
|
+
const routes = createAuthRoutes2(config);
|
|
2353
|
+
const database = createDatabase(config.connectionString);
|
|
2354
|
+
const getSession = async (headers) => {
|
|
2355
|
+
const cookieHeader = headers.get("cookie");
|
|
2356
|
+
if (!cookieHeader) {
|
|
2357
|
+
return { session: null, user: null, sessionToken: null };
|
|
2358
|
+
}
|
|
2359
|
+
const cookies = Object.fromEntries(
|
|
2360
|
+
cookieHeader.split("; ").map((c) => {
|
|
2361
|
+
const [key, ...rest] = c.split("=");
|
|
2362
|
+
return [key, rest.join("=")];
|
|
2363
|
+
})
|
|
2364
|
+
);
|
|
2365
|
+
const sessionToken = cookies.session_token;
|
|
2366
|
+
if (!sessionToken) {
|
|
2367
|
+
return { session: null, user: null, sessionToken: null };
|
|
2368
|
+
}
|
|
2369
|
+
try {
|
|
2370
|
+
const hashedToken = await hashToken(sessionToken, config.secret);
|
|
2371
|
+
const session = await findSessionByToken(database, hashedToken);
|
|
2372
|
+
if (!session) {
|
|
2373
|
+
return { session: null, user: null, sessionToken: null };
|
|
2374
|
+
}
|
|
2375
|
+
const user = await findUserById(
|
|
2376
|
+
database,
|
|
2377
|
+
session.tenantId,
|
|
2378
|
+
session.userId
|
|
2379
|
+
);
|
|
2380
|
+
if (!user) {
|
|
2381
|
+
return { session: null, user: null, sessionToken: null };
|
|
2382
|
+
}
|
|
2383
|
+
return { session, user, sessionToken };
|
|
2384
|
+
} catch {
|
|
2385
|
+
return { session: null, user: null, sessionToken: null };
|
|
2386
|
+
}
|
|
2387
|
+
};
|
|
2388
|
+
return {
|
|
2389
|
+
handler,
|
|
2390
|
+
routes,
|
|
2391
|
+
getSession
|
|
2392
|
+
};
|
|
2393
|
+
};
|
|
2394
|
+
export {
|
|
2395
|
+
jiretAuth
|
|
2396
|
+
};
|
|
2397
|
+
//# sourceMappingURL=index.js.map
|