najm-auth 6.3.1 → 6.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{NajmAuthClient-BygfBCpF.d.ts → NajmAuthClient-B1Gv8PIw.d.ts} +1 -1
- package/dist/client/edge.js +1 -1
- package/dist/client/index.d.ts +3 -3
- package/dist/client/react/index.d.ts +2 -2
- package/dist/client/server/index.d.ts +4 -4
- package/dist/client/server/index.js +1 -1
- package/dist/client/server/react.d.ts +2 -2
- package/dist/{getSession-DBX8XpsR.d.ts → getSession-DAQCguP3.d.ts} +1 -1
- package/dist/index.d.ts +37 -7
- package/dist/index.js +101 -29
- package/dist/schema/pg.d.ts +2 -2
- package/dist/schema/pg.js +7 -3
- package/dist/schema/sqlite.d.ts +2 -2
- package/dist/schema/sqlite.js +5 -3
- package/dist/{types-CI2t8wpJ.d.ts → types-axhOL6D1.d.ts} +2 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { R as RetryConfig, a as RequestOptions, A as AuthUser, b as AuthClientConfig, L as LoginCredentials, c as LoginResult, O as OAuthProvider, d as OAuthLoginOptions, e as AuthState, f as AuthEvent, g as AuthEventHandler } from './types-
|
|
1
|
+
import { R as RetryConfig, a as RequestOptions, A as AuthUser, b as AuthClientConfig, L as LoginCredentials, c as LoginResult, O as OAuthProvider, d as OAuthLoginOptions, e as AuthState, f as AuthEvent, g as AuthEventHandler } from './types-axhOL6D1.js';
|
|
2
2
|
|
|
3
3
|
interface FetchClientConfig {
|
|
4
4
|
baseURL: string;
|
package/dist/client/edge.js
CHANGED
|
@@ -139,7 +139,7 @@ function isRecord(value) {
|
|
|
139
139
|
}
|
|
140
140
|
__name(isRecord, "isRecord");
|
|
141
141
|
function isValidUser(value) {
|
|
142
|
-
return isRecord(value) && typeof value.id === "string" && value.id.length > 0 && typeof value.email === "string" && value.email.length > 0 && (value.role === void 0 || value.role === null || typeof value.role === "string");
|
|
142
|
+
return isRecord(value) && typeof value.id === "string" && value.id.length > 0 && (value.email === null || typeof value.email === "string" && value.email.length > 0) && (value.role === void 0 || value.role === null || typeof value.role === "string");
|
|
143
143
|
}
|
|
144
144
|
__name(isValidUser, "isValidUser");
|
|
145
145
|
function isStringArray(value) {
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { F as FetchClient, H as HydrateSession, N as NajmAuthClient, c as createAuthClient } from '../NajmAuthClient-
|
|
2
|
-
import { D as DecodedToken, T as TabSyncMessage, S as SyncPayload } from '../types-
|
|
3
|
-
export { b as AuthClientConfig, h as AuthError, f as AuthEvent, g as AuthEventHandler, i as AuthEventMap, e as AuthState, A as AuthUser, j as AuthenticatedLogin, C as CredentialSetupPending, L as LoginCredentials, c as LoginResult, d as OAuthLoginOptions, O as OAuthProvider, a as RequestOptions, R as RetryConfig, k as ServerResponse, l as TokenPair } from '../types-
|
|
1
|
+
export { F as FetchClient, H as HydrateSession, N as NajmAuthClient, c as createAuthClient } from '../NajmAuthClient-B1Gv8PIw.js';
|
|
2
|
+
import { D as DecodedToken, T as TabSyncMessage, S as SyncPayload } from '../types-axhOL6D1.js';
|
|
3
|
+
export { b as AuthClientConfig, h as AuthError, f as AuthEvent, g as AuthEventHandler, i as AuthEventMap, e as AuthState, A as AuthUser, j as AuthenticatedLogin, C as CredentialSetupPending, L as LoginCredentials, c as LoginResult, d as OAuthLoginOptions, O as OAuthProvider, a as RequestOptions, R as RetryConfig, k as ServerResponse, l as TokenPair } from '../types-axhOL6D1.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Decode a JWT token payload without verification.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode, CSSProperties, ReactElement } from 'react';
|
|
3
|
-
import { N as NajmAuthClient, H as HydrateSession } from '../../NajmAuthClient-
|
|
4
|
-
import { e as AuthState, A as AuthUser, c as LoginResult, h as AuthError, L as LoginCredentials, d as OAuthLoginOptions, f as AuthEvent, i as AuthEventMap } from '../../types-
|
|
3
|
+
import { N as NajmAuthClient, H as HydrateSession } from '../../NajmAuthClient-B1Gv8PIw.js';
|
|
4
|
+
import { e as AuthState, A as AuthUser, c as LoginResult, h as AuthError, L as LoginCredentials, d as OAuthLoginOptions, f as AuthEvent, i as AuthEventMap } from '../../types-axhOL6D1.js';
|
|
5
5
|
|
|
6
6
|
interface AuthProviderProps {
|
|
7
7
|
client: NajmAuthClient;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { A as AuthUser, R as RetryConfig } from '../../types-
|
|
2
|
-
import { F as FetchClient, N as NajmAuthClient } from '../../NajmAuthClient-
|
|
1
|
+
import { A as AuthUser, R as RetryConfig } from '../../types-axhOL6D1.js';
|
|
2
|
+
import { F as FetchClient, N as NajmAuthClient } from '../../NajmAuthClient-B1Gv8PIw.js';
|
|
3
3
|
import { AuthProxyOptions, ProxySessionMode } from '../edge.js';
|
|
4
4
|
export { withAuthMiddleware } from '../edge.js';
|
|
5
|
-
import { G as GetSessionConfig, S as ServerSession$1 } from '../../getSession-
|
|
6
|
-
export { A as AuthConfigError, a as AuthTransportError, N as NoSessionError, g as getSession } from '../../getSession-
|
|
5
|
+
import { G as GetSessionConfig, S as ServerSession$1 } from '../../getSession-DAQCguP3.js';
|
|
6
|
+
export { A as AuthConfigError, a as AuthTransportError, N as NoSessionError, g as getSession } from '../../getSession-DAQCguP3.js';
|
|
7
7
|
import { S as SessionRecoveryFailure } from '../../sessionRecovery-D5Fa0yZ1.js';
|
|
8
8
|
export { a as SessionRecoveryErrorDetails, b as SessionRecoveryFailureReason } from '../../sessionRecovery-D5Fa0yZ1.js';
|
|
9
9
|
import 'next/server';
|
|
@@ -134,7 +134,7 @@ function isRecord(value) {
|
|
|
134
134
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
135
135
|
}
|
|
136
136
|
function isValidUser(value) {
|
|
137
|
-
return isRecord(value) && typeof value.id === "string" && value.id.length > 0 && typeof value.email === "string" && value.email.length > 0 && (value.role === void 0 || value.role === null || typeof value.role === "string");
|
|
137
|
+
return isRecord(value) && typeof value.id === "string" && value.id.length > 0 && (value.email === null || typeof value.email === "string" && value.email.length > 0) && (value.role === void 0 || value.role === null || typeof value.role === "string");
|
|
138
138
|
}
|
|
139
139
|
function isStringArray(value) {
|
|
140
140
|
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
package/dist/index.d.ts
CHANGED
|
@@ -379,7 +379,8 @@ interface TokenPair {
|
|
|
379
379
|
*/
|
|
380
380
|
interface AuthUser {
|
|
381
381
|
id: string;
|
|
382
|
-
|
|
382
|
+
/** Null for an account reached by phone only. */
|
|
383
|
+
email: string | null;
|
|
383
384
|
name?: string | null;
|
|
384
385
|
role?: string;
|
|
385
386
|
status?: string;
|
|
@@ -462,6 +463,7 @@ var users = {
|
|
|
462
463
|
notFound: "User not found",
|
|
463
464
|
idExists: "User ID already exists",
|
|
464
465
|
emailRequired: "Email is required",
|
|
466
|
+
emailOrPhoneRequired: "An email address or a phone number is required",
|
|
465
467
|
phoneExists: "Phone number already registered",
|
|
466
468
|
passwordRequired: "Password is required",
|
|
467
469
|
invalidEmail: "Invalid email format",
|
|
@@ -592,6 +594,7 @@ declare const AUTH_LOCALES: {
|
|
|
592
594
|
notFound: string;
|
|
593
595
|
idExists: string;
|
|
594
596
|
emailRequired: string;
|
|
597
|
+
emailOrPhoneRequired: string;
|
|
595
598
|
phoneExists: string;
|
|
596
599
|
passwordRequired: string;
|
|
597
600
|
invalidEmail: string;
|
|
@@ -667,7 +670,7 @@ interface SessionCookieData {
|
|
|
667
670
|
/** Must satisfy the AuthUser contract — see AuthUser in ../types. */
|
|
668
671
|
user: {
|
|
669
672
|
id: string;
|
|
670
|
-
email: string;
|
|
673
|
+
email: string | null;
|
|
671
674
|
name?: string | null;
|
|
672
675
|
role?: string;
|
|
673
676
|
status?: string;
|
|
@@ -744,6 +747,10 @@ declare class UserRepository {
|
|
|
744
747
|
getByEmailInsensitive(email: string): Promise<(User & {
|
|
745
748
|
role?: string | null;
|
|
746
749
|
}) | undefined>;
|
|
750
|
+
/** Login by phone reads the row it matched; the account may have no email. */
|
|
751
|
+
getByPhoneWithCredentials(phone: string): Promise<(User & {
|
|
752
|
+
role?: string | null;
|
|
753
|
+
}) | undefined>;
|
|
747
754
|
create(data: NewUser): Promise<User>;
|
|
748
755
|
/**
|
|
749
756
|
* Ids of everyone holding a role. Used to end sessions when the role's
|
|
@@ -779,11 +786,17 @@ declare class UserValidator {
|
|
|
779
786
|
/**
|
|
780
787
|
* Check if email already exists in database
|
|
781
788
|
*/
|
|
782
|
-
checkEmailUnique(email: string, excludeId?: string): Promise<void>;
|
|
789
|
+
checkEmailUnique(email: string | null | undefined, excludeId?: string): Promise<void>;
|
|
783
790
|
/**
|
|
784
791
|
* Check that a normalized phone number is not already taken.
|
|
785
792
|
*/
|
|
786
793
|
checkPhoneUnique(phone: string, excludeId?: string): Promise<void>;
|
|
794
|
+
/**
|
|
795
|
+
* An account is reached by its email, its phone, or both. Refused here with
|
|
796
|
+
* a message the caller can show; the `users_email_or_phone` check is the
|
|
797
|
+
* database's own copy of the rule.
|
|
798
|
+
*/
|
|
799
|
+
checkHasIdentity(email: string | null | undefined, phone: string | null | undefined): void;
|
|
787
800
|
/** The database remains authoritative when two validation reads both pass. */
|
|
788
801
|
writeUnique<T>(write: () => Promise<T>): Promise<T>;
|
|
789
802
|
checkCredentialUnchanged(updated: unknown): void;
|
|
@@ -1219,6 +1232,11 @@ declare class UserService {
|
|
|
1219
1232
|
* so, and a second call is harmless.
|
|
1220
1233
|
*/
|
|
1221
1234
|
private invalidateSessions;
|
|
1235
|
+
/**
|
|
1236
|
+
* Stored in the same normalized form login lookup resolves to, or phone
|
|
1237
|
+
* login would never find the row. Blank means no phone.
|
|
1238
|
+
*/
|
|
1239
|
+
private normalizePhone;
|
|
1222
1240
|
private sanitizeUser;
|
|
1223
1241
|
private sanitizeUsers;
|
|
1224
1242
|
private requireUser;
|
|
@@ -1240,6 +1258,10 @@ declare class UserService {
|
|
|
1240
1258
|
role?: string | null;
|
|
1241
1259
|
}) | undefined>;
|
|
1242
1260
|
findByPhone(phone: string): Promise<UserWithPermissions>;
|
|
1261
|
+
/** The credential row behind a phone login, for timing-safe authentication. */
|
|
1262
|
+
findCredentialsByPhone(phone: string): Promise<(User & {
|
|
1263
|
+
role?: string | null;
|
|
1264
|
+
}) | undefined>;
|
|
1243
1265
|
getAuthRecordById(id: string): Promise<User | undefined>;
|
|
1244
1266
|
create(data: Record<string, any>, options?: {
|
|
1245
1267
|
validatePasswordStrength?: boolean;
|
|
@@ -1599,7 +1621,8 @@ declare class TokenService {
|
|
|
1599
1621
|
|
|
1600
1622
|
declare const createUserDto: z.ZodObject<{
|
|
1601
1623
|
name: z.ZodOptional<z.ZodString>;
|
|
1602
|
-
email: z.ZodString
|
|
1624
|
+
email: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
1625
|
+
phone: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
1603
1626
|
password: z.ZodString;
|
|
1604
1627
|
roleId: z.ZodOptional<z.ZodString>;
|
|
1605
1628
|
image: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
@@ -1612,7 +1635,8 @@ declare const createUserDto: z.ZodObject<{
|
|
|
1612
1635
|
}, z.core.$strip>;
|
|
1613
1636
|
declare const updateUserDto: z.ZodObject<{
|
|
1614
1637
|
name: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
1615
|
-
email: z.ZodOptional<z.ZodString
|
|
1638
|
+
email: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodString>>>;
|
|
1639
|
+
phone: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodString>>>;
|
|
1616
1640
|
password: z.ZodOptional<z.ZodString>;
|
|
1617
1641
|
roleId: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
1618
1642
|
image: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodString>>>;
|
|
@@ -1837,9 +1861,13 @@ declare class PasswordSetupService {
|
|
|
1837
1861
|
type ProvisionUserInput = {
|
|
1838
1862
|
id?: string;
|
|
1839
1863
|
name?: string;
|
|
1840
|
-
|
|
1864
|
+
/**
|
|
1865
|
+
* At least one of `email` and `phone` is required. Blank counts as absent;
|
|
1866
|
+
* an account with no email is created without an invitation mail.
|
|
1867
|
+
*/
|
|
1868
|
+
email?: string | null;
|
|
1841
1869
|
/** Normalized through the configured identity preset before it is stored. */
|
|
1842
|
-
phone?: string;
|
|
1870
|
+
phone?: string | null;
|
|
1843
1871
|
role?: string;
|
|
1844
1872
|
roleId?: string;
|
|
1845
1873
|
image?: string | null;
|
|
@@ -2970,6 +2998,8 @@ declare class AuthQueries {
|
|
|
2970
2998
|
name: any;
|
|
2971
2999
|
email: any;
|
|
2972
3000
|
emailVerified: any;
|
|
3001
|
+
phone: any;
|
|
3002
|
+
phoneVerified: any;
|
|
2973
3003
|
image: any;
|
|
2974
3004
|
status: any;
|
|
2975
3005
|
roleId: any;
|
package/dist/index.js
CHANGED
|
@@ -18,7 +18,7 @@ var AUTH_PERMISSIONS = Symbol.for("najm:auth:permissions");
|
|
|
18
18
|
var AUTH_ENCRYPTION_KEY = Symbol.for("najm:auth:encryption-key");
|
|
19
19
|
|
|
20
20
|
// src/schema/pg.ts
|
|
21
|
-
import { pgTable, text, boolean, timestamp, pgEnum, primaryKey, integer, index, uniqueIndex } from "drizzle-orm/pg-core";
|
|
21
|
+
import { pgTable, text, boolean, timestamp, pgEnum, primaryKey, integer, index, uniqueIndex, check } from "drizzle-orm/pg-core";
|
|
22
22
|
import { sql } from "drizzle-orm";
|
|
23
23
|
import { nanoid } from "nanoid";
|
|
24
24
|
|
|
@@ -49,7 +49,10 @@ var rolesTable = pgTable("roles", {
|
|
|
49
49
|
var usersTable = pgTable("users", {
|
|
50
50
|
...baseFields(8),
|
|
51
51
|
name: text("name"),
|
|
52
|
-
email
|
|
52
|
+
// An account is reached by its email, its phone, or both — never neither.
|
|
53
|
+
// Email is nullable so several phone-only accounts can coexist: a NOT NULL
|
|
54
|
+
// column forced them all to "" and the second one broke the unique index.
|
|
55
|
+
email: text("email").unique(),
|
|
53
56
|
emailVerified: boolean("email_verified").default(false),
|
|
54
57
|
phone: text("phone").unique(),
|
|
55
58
|
phoneVerified: boolean("phone_verified").default(false),
|
|
@@ -61,7 +64,8 @@ var usersTable = pgTable("users", {
|
|
|
61
64
|
failedLoginAttempts: integer("failed_login_attempts").default(0),
|
|
62
65
|
lockoutUntil: timestamp("lockout_until", { mode: "string" })
|
|
63
66
|
}, (table) => ({
|
|
64
|
-
roleIdx: index("users_role_id_idx").on(table.roleId)
|
|
67
|
+
roleIdx: index("users_role_id_idx").on(table.roleId),
|
|
68
|
+
emailOrPhone: check("users_email_or_phone", sql`${table.email} is not null or ${table.phone} is not null`)
|
|
65
69
|
}));
|
|
66
70
|
var oauthAccountsTable = pgTable("oauth_accounts", {
|
|
67
71
|
...baseFields(10),
|
|
@@ -137,7 +141,7 @@ var authSchema = {
|
|
|
137
141
|
};
|
|
138
142
|
|
|
139
143
|
// src/schema/sqlite.ts
|
|
140
|
-
import { sqliteTable, text as text2, integer as integer2, primaryKey as primaryKey2, uniqueIndex as uniqueIndex2, index as index2 } from "drizzle-orm/sqlite-core";
|
|
144
|
+
import { sqliteTable, text as text2, integer as integer2, primaryKey as primaryKey2, uniqueIndex as uniqueIndex2, index as index2, check as check2 } from "drizzle-orm/sqlite-core";
|
|
141
145
|
import { sql as sql2 } from "drizzle-orm";
|
|
142
146
|
import { nanoid as nanoid2 } from "nanoid";
|
|
143
147
|
var baseFields2 = /* @__PURE__ */ __name((idLength = 5) => ({
|
|
@@ -156,7 +160,8 @@ var rolesTable2 = sqliteTable("roles", {
|
|
|
156
160
|
var usersTable2 = sqliteTable("users", {
|
|
157
161
|
...baseFields2(8),
|
|
158
162
|
name: text2("name"),
|
|
159
|
-
email
|
|
163
|
+
// Nullable, with at least one of email or phone; see schema/pg.ts.
|
|
164
|
+
email: text2("email").unique(),
|
|
160
165
|
emailVerified: integer2("email_verified", { mode: "boolean" }).default(false),
|
|
161
166
|
phone: text2("phone").unique(),
|
|
162
167
|
phoneVerified: integer2("phone_verified", { mode: "boolean" }).default(false),
|
|
@@ -168,7 +173,8 @@ var usersTable2 = sqliteTable("users", {
|
|
|
168
173
|
failedLoginAttempts: integer2("failed_login_attempts").default(0),
|
|
169
174
|
lockoutUntil: text2("lockout_until")
|
|
170
175
|
}, (table) => ({
|
|
171
|
-
roleIdx: index2("users_role_id_idx").on(table.roleId)
|
|
176
|
+
roleIdx: index2("users_role_id_idx").on(table.roleId),
|
|
177
|
+
emailOrPhone: check2("users_email_or_phone", sql2`${table.email} is not null or ${table.phone} is not null`)
|
|
172
178
|
}));
|
|
173
179
|
var oauthAccountsTable2 = sqliteTable("oauth_accounts", {
|
|
174
180
|
...baseFields2(10),
|
|
@@ -385,7 +391,7 @@ function isRecord(value) {
|
|
|
385
391
|
}
|
|
386
392
|
__name(isRecord, "isRecord");
|
|
387
393
|
function isValidUser(value) {
|
|
388
|
-
return isRecord(value) && typeof value.id === "string" && value.id.length > 0 && typeof value.email === "string" && value.email.length > 0 && (value.role === void 0 || value.role === null || typeof value.role === "string");
|
|
394
|
+
return isRecord(value) && typeof value.id === "string" && value.id.length > 0 && (value.email === null || typeof value.email === "string" && value.email.length > 0) && (value.role === void 0 || value.role === null || typeof value.role === "string");
|
|
389
395
|
}
|
|
390
396
|
__name(isValidUser, "isValidUser");
|
|
391
397
|
function isStringArray(value) {
|
|
@@ -537,6 +543,10 @@ var AuthQueries = class {
|
|
|
537
543
|
name: this.schema.users.name,
|
|
538
544
|
email: this.schema.users.email,
|
|
539
545
|
emailVerified: this.schema.users.emailVerified,
|
|
546
|
+
// An account may be reached by phone only; without this the user list
|
|
547
|
+
// and profile showed no way to reach it at all.
|
|
548
|
+
phone: this.schema.users.phone,
|
|
549
|
+
phoneVerified: this.schema.users.phoneVerified,
|
|
540
550
|
image: this.schema.users.image,
|
|
541
551
|
status: this.schema.users.status,
|
|
542
552
|
roleId: this.schema.users.roleId,
|
|
@@ -682,6 +692,16 @@ var UserRepository = class UserRepository2 {
|
|
|
682
692
|
}).from(this.users).leftJoin(this.roles, eq2(this.users.roleId, this.roles.id)).where(sql3`lower(${this.users.email}) = ${email2.trim().toLowerCase()}`).limit(1);
|
|
683
693
|
return existingUser;
|
|
684
694
|
}
|
|
695
|
+
/** Login by phone reads the row it matched; the account may have no email. */
|
|
696
|
+
async getByPhoneWithCredentials(phone) {
|
|
697
|
+
const [existingUser] = await this.db.select({
|
|
698
|
+
...this.q.userSelection(),
|
|
699
|
+
password: this.users.password,
|
|
700
|
+
failedLoginAttempts: this.users.failedLoginAttempts,
|
|
701
|
+
lockoutUntil: this.users.lockoutUntil
|
|
702
|
+
}).from(this.users).leftJoin(this.roles, eq2(this.users.roleId, this.roles.id)).where(eq2(this.users.phone, phone)).limit(1);
|
|
703
|
+
return existingUser;
|
|
704
|
+
}
|
|
685
705
|
async create(data) {
|
|
686
706
|
const [newUser] = await this.db.insert(this.users).values(data).returning();
|
|
687
707
|
return newUser;
|
|
@@ -701,7 +721,7 @@ var UserRepository = class UserRepository2 {
|
|
|
701
721
|
}
|
|
702
722
|
/** The credential that authorized a delayed write must still own the account. */
|
|
703
723
|
async updateWithCredential(id, data, expected) {
|
|
704
|
-
const [updated] = await this.db.update(this.users).set(data).where(and(eq2(this.users.id, id), eq2(this.users.password, expected.password), eq2(this.users.email, expected.email), expected.status == null ? isNull(this.users.status) : eq2(this.users.status, expected.status), expected.emailVerified == null ? isNull(this.users.emailVerified) : eq2(this.users.emailVerified, expected.emailVerified))).returning();
|
|
724
|
+
const [updated] = await this.db.update(this.users).set(data).where(and(eq2(this.users.id, id), eq2(this.users.password, expected.password), expected.email == null ? isNull(this.users.email) : eq2(this.users.email, expected.email), expected.status == null ? isNull(this.users.status) : eq2(this.users.status, expected.status), expected.emailVerified == null ? isNull(this.users.emailVerified) : eq2(this.users.emailVerified, expected.emailVerified))).returning();
|
|
705
725
|
return updated;
|
|
706
726
|
}
|
|
707
727
|
async updateLastLogin(id) {
|
|
@@ -779,6 +799,21 @@ function isUniqueViolation(error) {
|
|
|
779
799
|
return false;
|
|
780
800
|
}
|
|
781
801
|
__name(isUniqueViolation, "isUniqueViolation");
|
|
802
|
+
function isMissingIdentityViolation(error) {
|
|
803
|
+
const visited = /* @__PURE__ */ new Set();
|
|
804
|
+
while (error && typeof error === "object" && !visited.has(error)) {
|
|
805
|
+
visited.add(error);
|
|
806
|
+
const candidate = error;
|
|
807
|
+
const constraint = candidate.constraint ?? candidate.constraint_name;
|
|
808
|
+
if (constraint === "users_email_or_phone")
|
|
809
|
+
return true;
|
|
810
|
+
if (typeof candidate.message === "string" && candidate.message.includes("users_email_or_phone"))
|
|
811
|
+
return true;
|
|
812
|
+
error = candidate.cause;
|
|
813
|
+
}
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
__name(isMissingIdentityViolation, "isMissingIdentityViolation");
|
|
782
817
|
|
|
783
818
|
// src/users/UserValidator.ts
|
|
784
819
|
var __decorate4 = function(decorators, target, key, desc) {
|
|
@@ -825,6 +860,16 @@ var UserValidator = class UserValidator2 {
|
|
|
825
860
|
Err(this.t("errors.phoneExists"), 409);
|
|
826
861
|
}
|
|
827
862
|
}
|
|
863
|
+
/**
|
|
864
|
+
* An account is reached by its email, its phone, or both. Refused here with
|
|
865
|
+
* a message the caller can show; the `users_email_or_phone` check is the
|
|
866
|
+
* database's own copy of the rule.
|
|
867
|
+
*/
|
|
868
|
+
checkHasIdentity(email2, phone) {
|
|
869
|
+
if (!email2 && !phone) {
|
|
870
|
+
Err(this.t("errors.emailOrPhoneRequired"), 400);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
828
873
|
/** The database remains authoritative when two validation reads both pass. */
|
|
829
874
|
async writeUnique(write) {
|
|
830
875
|
try {
|
|
@@ -832,6 +877,8 @@ var UserValidator = class UserValidator2 {
|
|
|
832
877
|
} catch (error) {
|
|
833
878
|
if (isUniqueViolation(error))
|
|
834
879
|
Err("User identity already exists", 409);
|
|
880
|
+
if (isMissingIdentityViolation(error))
|
|
881
|
+
Err(this.t("errors.emailOrPhoneRequired"), 400);
|
|
835
882
|
throw error;
|
|
836
883
|
}
|
|
837
884
|
}
|
|
@@ -1862,6 +1909,10 @@ var _g;
|
|
|
1862
1909
|
var _h;
|
|
1863
1910
|
var _j;
|
|
1864
1911
|
var E164_PHONE = /^\+[1-9]\d{7,14}$/;
|
|
1912
|
+
function normalizeEmail(value) {
|
|
1913
|
+
return typeof value === "string" && value.trim() ? value.trim().toLowerCase() : null;
|
|
1914
|
+
}
|
|
1915
|
+
__name(normalizeEmail, "normalizeEmail");
|
|
1865
1916
|
var UserService = class UserService2 {
|
|
1866
1917
|
static {
|
|
1867
1918
|
__name(this, "UserService");
|
|
@@ -1897,6 +1948,18 @@ var UserService = class UserService2 {
|
|
|
1897
1948
|
async invalidateSessions(userId) {
|
|
1898
1949
|
await this.sessionInvalidation?.invalidateUser(userId);
|
|
1899
1950
|
}
|
|
1951
|
+
/**
|
|
1952
|
+
* Stored in the same normalized form login lookup resolves to, or phone
|
|
1953
|
+
* login would never find the row. Blank means no phone.
|
|
1954
|
+
*/
|
|
1955
|
+
normalizePhone(phone) {
|
|
1956
|
+
if (phone == null || typeof phone === "string" && !phone.trim())
|
|
1957
|
+
return null;
|
|
1958
|
+
const normalized = this.authConfig.identity?.resolve(phone) ?? normalizeAuthIdentifier(phone);
|
|
1959
|
+
if (!normalized || !E164_PHONE.test(normalized))
|
|
1960
|
+
Err5("A valid phone number is required", 400);
|
|
1961
|
+
return normalized;
|
|
1962
|
+
}
|
|
1900
1963
|
sanitizeUser(user) {
|
|
1901
1964
|
if (!user)
|
|
1902
1965
|
return user;
|
|
@@ -1960,6 +2023,10 @@ var UserService = class UserService2 {
|
|
|
1960
2023
|
async findByPhone(phone) {
|
|
1961
2024
|
return await this.userRepository.findByPhone(phone);
|
|
1962
2025
|
}
|
|
2026
|
+
/** The credential row behind a phone login, for timing-safe authentication. */
|
|
2027
|
+
async findCredentialsByPhone(phone) {
|
|
2028
|
+
return await this.userRepository.getByPhoneWithCredentials(phone);
|
|
2029
|
+
}
|
|
1963
2030
|
async getAuthRecordById(id) {
|
|
1964
2031
|
return await this.userRepository.getRawById(id);
|
|
1965
2032
|
}
|
|
@@ -1973,12 +2040,11 @@ var UserService = class UserService2 {
|
|
|
1973
2040
|
this.userValidator.validatePasswordStrength(password);
|
|
1974
2041
|
}
|
|
1975
2042
|
let userId = id || nanoid3(10);
|
|
1976
|
-
|
|
2043
|
+
const normalizedEmail = normalizeEmail(email2);
|
|
2044
|
+
const normalizedPhone = this.normalizePhone(phone);
|
|
2045
|
+
this.userValidator.checkHasIdentity(normalizedEmail, normalizedPhone);
|
|
2046
|
+
await this.userValidator.checkEmailUnique(normalizedEmail);
|
|
1977
2047
|
await this.userValidator.checkUserIdIsUnique(userId);
|
|
1978
|
-
const normalizedPhone = phone == null ? null : this.authConfig.identity?.resolve(phone) ?? normalizeAuthIdentifier(phone);
|
|
1979
|
-
if (phone != null && (!normalizedPhone || !E164_PHONE.test(normalizedPhone))) {
|
|
1980
|
-
Err5("A valid phone number is required", 400);
|
|
1981
|
-
}
|
|
1982
2048
|
if (normalizedPhone) {
|
|
1983
2049
|
await this.userValidator.checkPhoneUnique(normalizedPhone, userId);
|
|
1984
2050
|
}
|
|
@@ -1987,7 +2053,7 @@ var UserService = class UserService2 {
|
|
|
1987
2053
|
const userDetails = {
|
|
1988
2054
|
id: userId,
|
|
1989
2055
|
name: name || null,
|
|
1990
|
-
email:
|
|
2056
|
+
email: normalizedEmail,
|
|
1991
2057
|
image,
|
|
1992
2058
|
password: hashedPassword,
|
|
1993
2059
|
roleId: resolvedRoleId,
|
|
@@ -2006,7 +2072,17 @@ var UserService = class UserService2 {
|
|
|
2006
2072
|
*/
|
|
2007
2073
|
async update(id, data, options = {}) {
|
|
2008
2074
|
const { password, image } = data;
|
|
2009
|
-
|
|
2075
|
+
const touchesEmail = data.email !== void 0;
|
|
2076
|
+
const touchesPhone = data.phone !== void 0;
|
|
2077
|
+
const email2 = touchesEmail ? normalizeEmail(data.email) : void 0;
|
|
2078
|
+
const phone = touchesPhone ? this.normalizePhone(data.phone) : void 0;
|
|
2079
|
+
if (touchesEmail && !email2 || touchesPhone && !phone) {
|
|
2080
|
+
const current = this.requireUser(await this.userRepository.getRawById(id));
|
|
2081
|
+
this.userValidator.checkHasIdentity(touchesEmail ? email2 : current.email, touchesPhone ? phone : current.phone);
|
|
2082
|
+
}
|
|
2083
|
+
await this.userValidator.checkEmailUnique(email2, id);
|
|
2084
|
+
if (phone)
|
|
2085
|
+
await this.userValidator.checkPhoneUnique(phone, id);
|
|
2010
2086
|
let hashedPassword;
|
|
2011
2087
|
if (password) {
|
|
2012
2088
|
if (options.validatePasswordStrength === false) {
|
|
@@ -2021,15 +2097,10 @@ var UserService = class UserService2 {
|
|
|
2021
2097
|
image,
|
|
2022
2098
|
...hashedPassword && { password: hashedPassword }
|
|
2023
2099
|
};
|
|
2024
|
-
if (
|
|
2025
|
-
updateData.email =
|
|
2026
|
-
if (
|
|
2027
|
-
const phone = this.authConfig.identity?.resolve(data.phone) ?? normalizeAuthIdentifier(data.phone);
|
|
2028
|
-
if (!phone || !E164_PHONE.test(phone))
|
|
2029
|
-
Err5("A valid phone number is required", 400);
|
|
2030
|
-
await this.userValidator.checkPhoneUnique(phone, id);
|
|
2100
|
+
if (touchesEmail)
|
|
2101
|
+
updateData.email = email2;
|
|
2102
|
+
if (touchesPhone)
|
|
2031
2103
|
updateData.phone = phone;
|
|
2032
|
-
}
|
|
2033
2104
|
const cleanedUpdateData = clean(updateData);
|
|
2034
2105
|
const updatedUser = await this.userValidator.writeUnique(() => options.expectedCredential ? this.userRepository.updateWithCredential(id, cleanedUpdateData, options.expectedCredential) : this.userRepository.update(id, cleanedUpdateData));
|
|
2035
2106
|
if (options.expectedCredential)
|
|
@@ -2891,7 +2962,7 @@ var TokenService = class TokenService2 {
|
|
|
2891
2962
|
*/
|
|
2892
2963
|
async generateSetPasswordToken(userId, type, expiresIn, expectedEmail) {
|
|
2893
2964
|
const user = await this.tokenRepository.getCredentialState(userId);
|
|
2894
|
-
if (!user || expectedEmail !== void 0 && user.email.trim().toLowerCase() !== expectedEmail.trim().toLowerCase()) {
|
|
2965
|
+
if (!user || expectedEmail !== void 0 && (user.email ?? "").trim().toLowerCase() !== expectedEmail.trim().toLowerCase()) {
|
|
2895
2966
|
Err7(this.t("errors.invalidResetToken"), 401);
|
|
2896
2967
|
}
|
|
2897
2968
|
const credentialState = credentialFingerprint(this.config.jwt.refreshSecret, user);
|
|
@@ -3960,8 +4031,7 @@ var AuthService = class AuthService2 {
|
|
|
3960
4031
|
if (identifier && isEmailIdentifier(identifier)) {
|
|
3961
4032
|
user = await this.userService.findByEmailInsensitive(identifier);
|
|
3962
4033
|
} else if (identifier) {
|
|
3963
|
-
|
|
3964
|
-
user = phoneUser ? await this.userService.findByEmail(phoneUser.email) : void 0;
|
|
4034
|
+
user = await this.userService.findCredentialsByPhone(identifier);
|
|
3965
4035
|
}
|
|
3966
4036
|
if (user?.lockoutUntil && !this.isLockoutActive(user.lockoutUntil)) {
|
|
3967
4037
|
await this.userService.resetFailedAttempts(user.id);
|
|
@@ -4542,7 +4612,8 @@ var passwordField = z.string().min(8, "Password must be at least 8 characters").
|
|
|
4542
4612
|
var optionalDateField = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be in YYYY-MM-DD format").nullable().optional();
|
|
4543
4613
|
var createUserDto = z.object({
|
|
4544
4614
|
name: z.string().max(100).optional(),
|
|
4545
|
-
email: emailField,
|
|
4615
|
+
email: emailField.nullish(),
|
|
4616
|
+
phone: z.string().trim().max(32).nullish(),
|
|
4546
4617
|
password: passwordField,
|
|
4547
4618
|
roleId: z.string().min(1).optional(),
|
|
4548
4619
|
image: z.string().nullish(),
|
|
@@ -7042,6 +7113,7 @@ var en_default = {
|
|
|
7042
7113
|
notFound: "User not found",
|
|
7043
7114
|
idExists: "User ID already exists",
|
|
7044
7115
|
emailRequired: "Email is required",
|
|
7116
|
+
emailOrPhoneRequired: "An email address or a phone number is required",
|
|
7045
7117
|
phoneExists: "Phone number already registered",
|
|
7046
7118
|
passwordRequired: "Password is required",
|
|
7047
7119
|
invalidEmail: "Invalid email format",
|
|
@@ -8374,7 +8446,7 @@ var authSeed = /* @__PURE__ */ __name((config) => ({
|
|
|
8374
8446
|
users: {
|
|
8375
8447
|
by: ["email"],
|
|
8376
8448
|
rows: /* @__PURE__ */ __name(async (seeded) => {
|
|
8377
|
-
|
|
8449
|
+
registerDto.pick({ email: true, password: true }).parse({
|
|
8378
8450
|
email: config.adminEmail,
|
|
8379
8451
|
password: config.adminPass
|
|
8380
8452
|
});
|
|
@@ -8392,7 +8464,7 @@ var authSeed = /* @__PURE__ */ __name((config) => ({
|
|
|
8392
8464
|
];
|
|
8393
8465
|
if (config.additionalUsers) {
|
|
8394
8466
|
for (const user of config.additionalUsers) {
|
|
8395
|
-
|
|
8467
|
+
registerDto.pick({ email: true, password: true }).parse({
|
|
8396
8468
|
email: user.email,
|
|
8397
8469
|
password: user.password
|
|
8398
8470
|
});
|
package/dist/schema/pg.d.ts
CHANGED
|
@@ -135,7 +135,7 @@ declare const usersTable: drizzle_orm_pg_core.PgTableWithColumns<{
|
|
|
135
135
|
columnType: "PgText";
|
|
136
136
|
data: string;
|
|
137
137
|
driverParam: string;
|
|
138
|
-
notNull:
|
|
138
|
+
notNull: false;
|
|
139
139
|
hasDefault: false;
|
|
140
140
|
isPrimaryKey: false;
|
|
141
141
|
isAutoincrement: false;
|
|
@@ -1208,7 +1208,7 @@ declare const authSchema: {
|
|
|
1208
1208
|
columnType: "PgText";
|
|
1209
1209
|
data: string;
|
|
1210
1210
|
driverParam: string;
|
|
1211
|
-
notNull:
|
|
1211
|
+
notNull: false;
|
|
1212
1212
|
hasDefault: false;
|
|
1213
1213
|
isPrimaryKey: false;
|
|
1214
1214
|
isAutoincrement: false;
|
package/dist/schema/pg.js
CHANGED
|
@@ -2,7 +2,7 @@ var __defProp = Object.defineProperty;
|
|
|
2
2
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
3
|
|
|
4
4
|
// src/schema/pg.ts
|
|
5
|
-
import { pgTable, text, boolean, timestamp, pgEnum, primaryKey, integer, index, uniqueIndex } from "drizzle-orm/pg-core";
|
|
5
|
+
import { pgTable, text, boolean, timestamp, pgEnum, primaryKey, integer, index, uniqueIndex, check } from "drizzle-orm/pg-core";
|
|
6
6
|
import { sql } from "drizzle-orm";
|
|
7
7
|
import { nanoid } from "nanoid";
|
|
8
8
|
|
|
@@ -33,7 +33,10 @@ var rolesTable = pgTable("roles", {
|
|
|
33
33
|
var usersTable = pgTable("users", {
|
|
34
34
|
...baseFields(8),
|
|
35
35
|
name: text("name"),
|
|
36
|
-
email
|
|
36
|
+
// An account is reached by its email, its phone, or both — never neither.
|
|
37
|
+
// Email is nullable so several phone-only accounts can coexist: a NOT NULL
|
|
38
|
+
// column forced them all to "" and the second one broke the unique index.
|
|
39
|
+
email: text("email").unique(),
|
|
37
40
|
emailVerified: boolean("email_verified").default(false),
|
|
38
41
|
phone: text("phone").unique(),
|
|
39
42
|
phoneVerified: boolean("phone_verified").default(false),
|
|
@@ -45,7 +48,8 @@ var usersTable = pgTable("users", {
|
|
|
45
48
|
failedLoginAttempts: integer("failed_login_attempts").default(0),
|
|
46
49
|
lockoutUntil: timestamp("lockout_until", { mode: "string" })
|
|
47
50
|
}, (table) => ({
|
|
48
|
-
roleIdx: index("users_role_id_idx").on(table.roleId)
|
|
51
|
+
roleIdx: index("users_role_id_idx").on(table.roleId),
|
|
52
|
+
emailOrPhone: check("users_email_or_phone", sql`${table.email} is not null or ${table.phone} is not null`)
|
|
49
53
|
}));
|
|
50
54
|
var oauthAccountsTable = pgTable("oauth_accounts", {
|
|
51
55
|
...baseFields(10),
|
package/dist/schema/sqlite.d.ts
CHANGED
|
@@ -144,7 +144,7 @@ declare const usersTable: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
|
|
|
144
144
|
columnType: "SQLiteText";
|
|
145
145
|
data: string;
|
|
146
146
|
driverParam: string;
|
|
147
|
-
notNull:
|
|
147
|
+
notNull: false;
|
|
148
148
|
hasDefault: false;
|
|
149
149
|
isPrimaryKey: false;
|
|
150
150
|
isAutoincrement: false;
|
|
@@ -1368,7 +1368,7 @@ declare const authSchema: {
|
|
|
1368
1368
|
columnType: "SQLiteText";
|
|
1369
1369
|
data: string;
|
|
1370
1370
|
driverParam: string;
|
|
1371
|
-
notNull:
|
|
1371
|
+
notNull: false;
|
|
1372
1372
|
hasDefault: false;
|
|
1373
1373
|
isPrimaryKey: false;
|
|
1374
1374
|
isAutoincrement: false;
|
package/dist/schema/sqlite.js
CHANGED
|
@@ -2,7 +2,7 @@ var __defProp = Object.defineProperty;
|
|
|
2
2
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
3
|
|
|
4
4
|
// src/schema/sqlite.ts
|
|
5
|
-
import { sqliteTable, text, integer, primaryKey, uniqueIndex, index } from "drizzle-orm/sqlite-core";
|
|
5
|
+
import { sqliteTable, text, integer, primaryKey, uniqueIndex, index, check } from "drizzle-orm/sqlite-core";
|
|
6
6
|
import { sql } from "drizzle-orm";
|
|
7
7
|
import { nanoid } from "nanoid";
|
|
8
8
|
var baseFields = /* @__PURE__ */ __name((idLength = 5) => ({
|
|
@@ -21,7 +21,8 @@ var rolesTable = sqliteTable("roles", {
|
|
|
21
21
|
var usersTable = sqliteTable("users", {
|
|
22
22
|
...baseFields(8),
|
|
23
23
|
name: text("name"),
|
|
24
|
-
email
|
|
24
|
+
// Nullable, with at least one of email or phone; see schema/pg.ts.
|
|
25
|
+
email: text("email").unique(),
|
|
25
26
|
emailVerified: integer("email_verified", { mode: "boolean" }).default(false),
|
|
26
27
|
phone: text("phone").unique(),
|
|
27
28
|
phoneVerified: integer("phone_verified", { mode: "boolean" }).default(false),
|
|
@@ -33,7 +34,8 @@ var usersTable = sqliteTable("users", {
|
|
|
33
34
|
failedLoginAttempts: integer("failed_login_attempts").default(0),
|
|
34
35
|
lockoutUntil: text("lockout_until")
|
|
35
36
|
}, (table) => ({
|
|
36
|
-
roleIdx: index("users_role_id_idx").on(table.roleId)
|
|
37
|
+
roleIdx: index("users_role_id_idx").on(table.roleId),
|
|
38
|
+
emailOrPhone: check("users_email_or_phone", sql`${table.email} is not null or ${table.phone} is not null`)
|
|
37
39
|
}));
|
|
38
40
|
var oauthAccountsTable = sqliteTable("oauth_accounts", {
|
|
39
41
|
...baseFields(10),
|