@prsm/auth 1.0.0
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/README.md +226 -0
- package/index.d.ts +19 -0
- package/package.json +76 -0
- package/src/__tests__/auth.test.js +1171 -0
- package/src/__tests__/impersonation-test-setup.js +208 -0
- package/src/__tests__/impersonation.test.js +473 -0
- package/src/__tests__/oauth-test-setup.js +136 -0
- package/src/__tests__/oauth.test.js +400 -0
- package/src/__tests__/prsm.test.js +215 -0
- package/src/__tests__/test-setup.js +385 -0
- package/src/__tests__/totp.test.js +158 -0
- package/src/__tests__/two-factor-test-setup.js +331 -0
- package/src/__tests__/two-factor.test.js +396 -0
- package/src/activity-logger.js +228 -0
- package/src/auth-context.js +120 -0
- package/src/auth-functions.js +520 -0
- package/src/auth-manager.js +1371 -0
- package/src/errors.js +173 -0
- package/src/hooks.js +41 -0
- package/src/index.js +23 -0
- package/src/invalidation.js +166 -0
- package/src/middleware.js +33 -0
- package/src/providers/azure-provider.js +114 -0
- package/src/providers/base-provider.js +152 -0
- package/src/providers/github-provider.js +86 -0
- package/src/providers/google-provider.js +76 -0
- package/src/providers/index.js +4 -0
- package/src/queries.js +543 -0
- package/src/schema.js +261 -0
- package/src/totp.js +221 -0
- package/src/two-factor/index.js +3 -0
- package/src/two-factor/otp-provider.js +128 -0
- package/src/two-factor/totp-provider.js +98 -0
- package/src/two-factor/two-factor-manager.js +676 -0
- package/src/types.js +399 -0
- package/src/user-roles.js +128 -0
- package/src/util.js +32 -0
- package/types/activity-logger.d.ts +73 -0
- package/types/auth-context.d.ts +88 -0
- package/types/auth-functions.d.ts +151 -0
- package/types/auth-manager.d.ts +365 -0
- package/types/errors.d.ts +108 -0
- package/types/hooks.d.ts +30 -0
- package/types/index.d.ts +13 -0
- package/types/invalidation.d.ts +40 -0
- package/types/middleware.d.ts +11 -0
- package/types/providers/azure-provider.d.ts +35 -0
- package/types/providers/base-provider.d.ts +52 -0
- package/types/providers/github-provider.d.ts +29 -0
- package/types/providers/google-provider.d.ts +29 -0
- package/types/providers/index.d.ts +4 -0
- package/types/queries.d.ts +287 -0
- package/types/schema.d.ts +37 -0
- package/types/totp.d.ts +72 -0
- package/types/two-factor/index.d.ts +3 -0
- package/types/two-factor/otp-provider.d.ts +57 -0
- package/types/two-factor/totp-provider.d.ts +58 -0
- package/types/two-factor/two-factor-manager.d.ts +191 -0
- package/types/types.d.ts +688 -0
- package/types/user-roles.d.ts +47 -0
- package/types/util.d.ts +3 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest"
|
|
2
|
+
import request from "supertest"
|
|
3
|
+
import { createTestApp, createTestDatabase } from "./test-setup.js"
|
|
4
|
+
import { createAuthContext, createAuthTables, dropAuthTables, ActivityLogger, AuthStatus, AuthRole, AuthActivityAction, TwoFactorMechanism } from "../index.js"
|
|
5
|
+
import { ensureListener, notifyInvalidation, wasInvalidatedSince, closeInvalidationListeners } from "../invalidation.js"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {() => boolean} predicate
|
|
9
|
+
* @param {number} timeoutMs
|
|
10
|
+
*/
|
|
11
|
+
async function waitFor(predicate, timeoutMs = 4000) {
|
|
12
|
+
const start = Date.now()
|
|
13
|
+
while (Date.now() - start < timeoutMs) {
|
|
14
|
+
if (predicate()) return true
|
|
15
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
16
|
+
}
|
|
17
|
+
return false
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("optional limiter (duck-typed @prsm/limit)", () => {
|
|
21
|
+
let app
|
|
22
|
+
let cleanup
|
|
23
|
+
let allowed = true
|
|
24
|
+
|
|
25
|
+
beforeAll(async () => {
|
|
26
|
+
// duck-typed limiter using the tokenBucket verb (take); auth should not care
|
|
27
|
+
const limiter = { take: async () => ({ allowed, retryAfter: 1000 }) }
|
|
28
|
+
const ctx = await createTestApp({ tablePrefix: "prsm_lim_", limiter })
|
|
29
|
+
app = ctx.app
|
|
30
|
+
cleanup = ctx.cleanup
|
|
31
|
+
|
|
32
|
+
await request(app).post("/register").send({ email: "lim@example.com", password: "password123" })
|
|
33
|
+
// verify so login can proceed when allowed
|
|
34
|
+
const pool = ctx.pool
|
|
35
|
+
await pool.query(`UPDATE prsm_lim_accounts SET verified = true WHERE email = $1`, ["lim@example.com"])
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
afterAll(async () => {
|
|
39
|
+
await cleanup()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it("permits login when the limiter allows it", async () => {
|
|
43
|
+
allowed = true
|
|
44
|
+
const res = await request(app).post("/login").send({ email: "lim@example.com", password: "password123" })
|
|
45
|
+
expect(res.status).toBe(200)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it("rejects login with RateLimitedError when the limiter denies it", async () => {
|
|
49
|
+
allowed = false
|
|
50
|
+
const res = await request(app).post("/login").send({ email: "lim@example.com", password: "password123" })
|
|
51
|
+
expect(res.status).toBe(401)
|
|
52
|
+
expect(res.body.errorType).toBe("RateLimitedError")
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe("optional tracer (duck-typed @prsm/trace)", () => {
|
|
57
|
+
let app
|
|
58
|
+
let cleanup
|
|
59
|
+
const spans = []
|
|
60
|
+
|
|
61
|
+
beforeAll(async () => {
|
|
62
|
+
const tracer = {
|
|
63
|
+
span: (name, attributes, fn) => {
|
|
64
|
+
spans.push(name)
|
|
65
|
+
return fn()
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
const ctx = await createTestApp({ tablePrefix: "prsm_trc_", tracer })
|
|
69
|
+
app = ctx.app
|
|
70
|
+
cleanup = ctx.cleanup
|
|
71
|
+
|
|
72
|
+
await request(app).post("/register").send({ email: "trc@example.com", password: "password123" })
|
|
73
|
+
await ctx.pool.query(`UPDATE prsm_trc_accounts SET verified = true WHERE email = $1`, ["trc@example.com"])
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
afterAll(async () => {
|
|
77
|
+
await cleanup()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it("wraps login in a tracing span", async () => {
|
|
81
|
+
await request(app).post("/login").send({ email: "trc@example.com", password: "password123" })
|
|
82
|
+
expect(spans).toContain("auth.login")
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe("createAuthContext devtools surface", () => {
|
|
87
|
+
let pool
|
|
88
|
+
let config
|
|
89
|
+
let ctx
|
|
90
|
+
|
|
91
|
+
beforeAll(async () => {
|
|
92
|
+
pool = await createTestDatabase()
|
|
93
|
+
config = { db: pool, tablePrefix: "prsm_ctx_", minPasswordLength: 6 }
|
|
94
|
+
await dropAuthTables(config)
|
|
95
|
+
await createAuthTables(config)
|
|
96
|
+
ctx = createAuthContext(config)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
afterAll(async () => {
|
|
100
|
+
await closeInvalidationListeners()
|
|
101
|
+
await pool.end()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it("lists accounts with a total count", async () => {
|
|
105
|
+
await ctx.createUser({ email: "a@example.com", password: "password123" }, "u-a")
|
|
106
|
+
await ctx.createUser({ email: "b@example.com", password: "password123" }, "u-b")
|
|
107
|
+
const { accounts, total } = await ctx.listAccounts({ limit: 10 })
|
|
108
|
+
expect(total).toBe(2)
|
|
109
|
+
expect(accounts.length).toBe(2)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it("searches accounts by email", async () => {
|
|
113
|
+
const { accounts, total } = await ctx.listAccounts({ search: "a@" })
|
|
114
|
+
expect(total).toBe(1)
|
|
115
|
+
expect(accounts[0].email).toBe("a@example.com")
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it("gets a single account by identifier", async () => {
|
|
119
|
+
const account = await ctx.getAccount({ email: "b@example.com" })
|
|
120
|
+
expect(account.user_id).toBe("u-b")
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it("throws UserNotFoundError for a missing account", async () => {
|
|
124
|
+
await expect(ctx.getAccount({ email: "nope@example.com" })).rejects.toThrow("User not found")
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it("exposes the role map", () => {
|
|
128
|
+
expect(ctx.getRoles()).toBe(AuthRole)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it("returns table stats", async () => {
|
|
132
|
+
const stats = await ctx.getStats()
|
|
133
|
+
expect(stats.accounts).toBe(2)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it("returns recent activity entries", async () => {
|
|
137
|
+
const activity = await ctx.getRecentActivity(10)
|
|
138
|
+
expect(Array.isArray(activity)).toBe(true)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
// regression: metadata is a JSONB column, so node-pg returns it already
|
|
142
|
+
// parsed. getRecentActivity used to call JSON.parse on it unconditionally,
|
|
143
|
+
// which threw and made the method silently return [] for any row with
|
|
144
|
+
// metadata. it must round-trip metadata as a parsed object and surface the
|
|
145
|
+
// parsed user-agent fields
|
|
146
|
+
it("round-trips activity metadata and parses the user agent", async () => {
|
|
147
|
+
const account = await ctx.getAccount({ email: "a@example.com" })
|
|
148
|
+
const logger = new ActivityLogger(config)
|
|
149
|
+
const req = {
|
|
150
|
+
headers: { "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36" },
|
|
151
|
+
socket: { remoteAddress: "10.0.0.9" },
|
|
152
|
+
}
|
|
153
|
+
await logger.logActivity(account.id, AuthActivityAction.Login, req, true, { email: "a@example.com", remember: true })
|
|
154
|
+
|
|
155
|
+
const activity = await ctx.getRecentActivity(10, account.id)
|
|
156
|
+
expect(activity.length).toBeGreaterThan(0)
|
|
157
|
+
const entry = activity[0]
|
|
158
|
+
expect(entry.action).toBe(AuthActivityAction.Login)
|
|
159
|
+
expect(entry.metadata).toEqual({ email: "a@example.com", remember: true })
|
|
160
|
+
expect(entry.browser).toBe("Chrome")
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it("exposes the status map", () => {
|
|
164
|
+
expect(ctx.getStatuses()).toBe(AuthStatus)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it("exposes the two-factor mechanism map", () => {
|
|
168
|
+
expect(ctx.getMechanisms()).toBe(TwoFactorMechanism)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it("reflects role changes through getAccount", async () => {
|
|
172
|
+
await ctx.addRoleForUserBy({ email: "a@example.com" }, AuthRole.Admin)
|
|
173
|
+
const account = await ctx.getAccount({ email: "a@example.com" })
|
|
174
|
+
expect((account.rolemask & AuthRole.Admin) === AuthRole.Admin).toBe(true)
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
describe("cross-instance invalidation (postgres LISTEN/NOTIFY)", () => {
|
|
179
|
+
let pool
|
|
180
|
+
let config
|
|
181
|
+
|
|
182
|
+
beforeAll(async () => {
|
|
183
|
+
pool = await createTestDatabase()
|
|
184
|
+
config = { db: pool, tablePrefix: "prsm_inv_", minPasswordLength: 6, invalidation: { listen: true } }
|
|
185
|
+
await dropAuthTables(config)
|
|
186
|
+
await createAuthTables(config)
|
|
187
|
+
ensureListener(config)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
afterAll(async () => {
|
|
191
|
+
await closeInvalidationListeners()
|
|
192
|
+
await pool.end()
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it("delivers an explicit notify to the listener", async () => {
|
|
196
|
+
// give the LISTEN connection a moment to attach, then notify
|
|
197
|
+
await waitFor(() => false, 300)
|
|
198
|
+
await notifyInvalidation(config, 4242)
|
|
199
|
+
const got = await waitFor(() => wasInvalidatedSince(config, 4242, 0))
|
|
200
|
+
expect(got).toBe(true)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it("notifies when a security-relevant account field changes", async () => {
|
|
204
|
+
const ctx = createAuthContext(config)
|
|
205
|
+
const account = await ctx.createUser({ email: "inv@example.com", password: "password123" }, "u-inv")
|
|
206
|
+
const since = Date.now() - 1
|
|
207
|
+
await ctx.setStatusForUserBy({ accountId: account.id }, AuthStatus.Banned)
|
|
208
|
+
const got = await waitFor(() => wasInvalidatedSince(config, account.id, since))
|
|
209
|
+
expect(got).toBe(true)
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it("does not report invalidation for an untouched account", () => {
|
|
213
|
+
expect(wasInvalidatedSince(config, 999999, 0)).toBe(false)
|
|
214
|
+
})
|
|
215
|
+
})
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import express from "express"
|
|
2
|
+
import session from "express-session"
|
|
3
|
+
import cookieParser from "cookie-parser"
|
|
4
|
+
import pg from "pg"
|
|
5
|
+
import { createAuthMiddleware, createAuthTables, dropAuthTables, AuthRole, closeInvalidationListeners } from "../index.js"
|
|
6
|
+
|
|
7
|
+
const { Pool } = pg
|
|
8
|
+
|
|
9
|
+
export async function createTestDatabase() {
|
|
10
|
+
const url = process.env.AUTH_TEST_POSTGRES_URL
|
|
11
|
+
// keep test pools small so the whole suite can't exhaust postgres connections
|
|
12
|
+
const poolOptions = { max: 15, idleTimeoutMillis: 1000 }
|
|
13
|
+
const pool = url
|
|
14
|
+
? new Pool({ connectionString: url, ...poolOptions })
|
|
15
|
+
: new Pool({
|
|
16
|
+
host: process.env.PGHOST || "localhost",
|
|
17
|
+
port: parseInt(process.env.PGPORT || "5432"),
|
|
18
|
+
database: process.env.PGDATABASE || "auth_test",
|
|
19
|
+
user: process.env.PGUSER || "auth",
|
|
20
|
+
password: process.env.PGPASSWORD || "auth_password",
|
|
21
|
+
...poolOptions,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
await pool.query("SELECT NOW()")
|
|
26
|
+
} catch (error) {
|
|
27
|
+
throw new Error("Failed to connect to test database. Make sure to run: make up")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return pool
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {Partial<import("../index.js").AuthConfig>} [configOverrides]
|
|
35
|
+
*/
|
|
36
|
+
export async function createTestApp(configOverrides) {
|
|
37
|
+
const pool = await createTestDatabase()
|
|
38
|
+
|
|
39
|
+
const app = express()
|
|
40
|
+
|
|
41
|
+
app.use(express.json())
|
|
42
|
+
app.use(cookieParser())
|
|
43
|
+
app.use(
|
|
44
|
+
session({
|
|
45
|
+
secret: "test-secret-key-for-testing-only",
|
|
46
|
+
resave: false,
|
|
47
|
+
saveUninitialized: false,
|
|
48
|
+
cookie: { secure: false, httpOnly: true },
|
|
49
|
+
}),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const authConfig = {
|
|
53
|
+
db: pool,
|
|
54
|
+
tablePrefix: "test_",
|
|
55
|
+
minPasswordLength: 6,
|
|
56
|
+
maxPasswordLength: 50,
|
|
57
|
+
rememberDuration: "7d",
|
|
58
|
+
rememberCookieName: "test_remember_token",
|
|
59
|
+
resyncInterval: "30s",
|
|
60
|
+
...configOverrides,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await dropAuthTables(authConfig)
|
|
64
|
+
await createAuthTables(authConfig)
|
|
65
|
+
|
|
66
|
+
app.use(createAuthMiddleware(authConfig))
|
|
67
|
+
|
|
68
|
+
app.post("/register", async (req, res) => {
|
|
69
|
+
try {
|
|
70
|
+
const { email, password, userId, requireConfirmation } = req.body
|
|
71
|
+
let confirmationToken
|
|
72
|
+
|
|
73
|
+
const account = await req.auth.register(
|
|
74
|
+
email,
|
|
75
|
+
password,
|
|
76
|
+
userId || undefined,
|
|
77
|
+
requireConfirmation
|
|
78
|
+
? (token) => {
|
|
79
|
+
confirmationToken = token
|
|
80
|
+
}
|
|
81
|
+
: undefined,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
res.json({
|
|
85
|
+
success: true,
|
|
86
|
+
account: {
|
|
87
|
+
id: account.id,
|
|
88
|
+
email: account.email,
|
|
89
|
+
verified: account.verified,
|
|
90
|
+
status: account.status,
|
|
91
|
+
user_id: account.user_id,
|
|
92
|
+
},
|
|
93
|
+
confirmationToken,
|
|
94
|
+
})
|
|
95
|
+
} catch (error) {
|
|
96
|
+
res.status(400).json({ error: error.message, errorType: error.constructor.name })
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
app.post("/login", async (req, res) => {
|
|
101
|
+
try {
|
|
102
|
+
const { email, password, remember } = req.body
|
|
103
|
+
await req.auth.login(email, password, remember)
|
|
104
|
+
res.json({ success: true })
|
|
105
|
+
} catch (error) {
|
|
106
|
+
res.status(401).json({ error: error.message, errorType: error.constructor.name })
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
app.post("/logout", async (req, res) => {
|
|
111
|
+
try {
|
|
112
|
+
await req.auth.logout()
|
|
113
|
+
res.json({ success: true })
|
|
114
|
+
} catch (error) {
|
|
115
|
+
res.status(500).json({ error: error.message })
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
app.get("/profile", async (req, res) => {
|
|
120
|
+
if (!req.auth.isLoggedIn()) {
|
|
121
|
+
return res.status(401).json({ error: "Not logged in" })
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
res.json({
|
|
125
|
+
id: req.auth.getId(),
|
|
126
|
+
email: req.auth.getEmail(),
|
|
127
|
+
status: req.auth.getStatus(),
|
|
128
|
+
statusName: req.auth.getStatusName(),
|
|
129
|
+
verified: req.auth.getVerified(),
|
|
130
|
+
hasPassword: req.auth.hasPassword(),
|
|
131
|
+
roles: req.auth.getRoleNames(),
|
|
132
|
+
remembered: req.auth.isRemembered(),
|
|
133
|
+
isAdmin: await req.auth.isAdmin(),
|
|
134
|
+
hasRole: req.query.role ? await req.auth.hasRole(parseInt(req.query.role)) : undefined,
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
app.post("/confirm-email", async (req, res) => {
|
|
139
|
+
try {
|
|
140
|
+
const { token, autoLogin } = req.body
|
|
141
|
+
if (autoLogin) {
|
|
142
|
+
await req.auth.confirmEmailAndLogin(token)
|
|
143
|
+
} else {
|
|
144
|
+
const email = await req.auth.confirmEmail(token)
|
|
145
|
+
res.json({ success: true, email })
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
res.json({ success: true })
|
|
149
|
+
} catch (error) {
|
|
150
|
+
res.status(400).json({ error: error.message, errorType: error.constructor.name })
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
app.post("/reset-password", async (req, res) => {
|
|
155
|
+
try {
|
|
156
|
+
const { email, expiresAfter, maxRequests } = req.body
|
|
157
|
+
let resetToken
|
|
158
|
+
|
|
159
|
+
await req.auth.resetPassword(email, expiresAfter || "1h", maxRequests || 3, (token) => {
|
|
160
|
+
resetToken = token
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
res.json({ success: true, resetToken })
|
|
164
|
+
} catch (error) {
|
|
165
|
+
res.status(400).json({ error: error.message, errorType: error.constructor.name })
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
app.post("/confirm-reset", async (req, res) => {
|
|
170
|
+
try {
|
|
171
|
+
const { token, password, logout } = req.body
|
|
172
|
+
await req.auth.confirmResetPassword(token, password, logout)
|
|
173
|
+
res.json({ success: true })
|
|
174
|
+
} catch (error) {
|
|
175
|
+
res.status(400).json({ error: error.message, errorType: error.constructor.name })
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
app.post("/change-email", async (req, res) => {
|
|
180
|
+
try {
|
|
181
|
+
const { newEmail } = req.body
|
|
182
|
+
let confirmationToken
|
|
183
|
+
|
|
184
|
+
await req.auth.changeEmail(newEmail, (token) => {
|
|
185
|
+
confirmationToken = token
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
res.json({ success: true, confirmationToken })
|
|
189
|
+
} catch (error) {
|
|
190
|
+
res.status(400).json({ error: error.message, errorType: error.constructor.name })
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
app.post("/verify-password", async (req, res) => {
|
|
195
|
+
try {
|
|
196
|
+
const { password } = req.body
|
|
197
|
+
const isValid = await req.auth.verifyPassword(password)
|
|
198
|
+
res.json({ success: true, isValid })
|
|
199
|
+
} catch (error) {
|
|
200
|
+
res.status(400).json({ error: error.message, errorType: error.constructor.name })
|
|
201
|
+
}
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
app.post("/logout-everywhere", async (req, res) => {
|
|
205
|
+
try {
|
|
206
|
+
await req.auth.logoutEverywhere()
|
|
207
|
+
res.json({ success: true })
|
|
208
|
+
} catch (error) {
|
|
209
|
+
res.status(500).json({ error: error.message })
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
app.post("/logout-everywhere-else", async (req, res) => {
|
|
214
|
+
try {
|
|
215
|
+
await req.auth.logoutEverywhereElse()
|
|
216
|
+
res.json({ success: true })
|
|
217
|
+
} catch (error) {
|
|
218
|
+
res.status(500).json({ error: error.message })
|
|
219
|
+
}
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
// admin routes
|
|
223
|
+
app.post("/admin/create-user", async (req, res) => {
|
|
224
|
+
try {
|
|
225
|
+
const { email, password, userId, requireConfirmation } = req.body
|
|
226
|
+
let confirmationToken
|
|
227
|
+
|
|
228
|
+
const account = await req.auth.createUser(
|
|
229
|
+
{ email, password },
|
|
230
|
+
userId || "test-admin-user-456",
|
|
231
|
+
requireConfirmation
|
|
232
|
+
? (token) => {
|
|
233
|
+
confirmationToken = token
|
|
234
|
+
}
|
|
235
|
+
: undefined,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
res.json({
|
|
239
|
+
success: true,
|
|
240
|
+
account: {
|
|
241
|
+
id: account.id,
|
|
242
|
+
email: account.email,
|
|
243
|
+
verified: account.verified,
|
|
244
|
+
},
|
|
245
|
+
confirmationToken,
|
|
246
|
+
})
|
|
247
|
+
} catch (error) {
|
|
248
|
+
res.status(400).json({ error: error.message, errorType: error.constructor.name })
|
|
249
|
+
}
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
app.post("/admin/login-as", async (req, res) => {
|
|
253
|
+
try {
|
|
254
|
+
const identifier = req.body
|
|
255
|
+
await req.auth.loginAsUserBy(identifier)
|
|
256
|
+
res.json({ success: true })
|
|
257
|
+
} catch (error) {
|
|
258
|
+
res.status(400).json({ error: error.message, errorType: error.constructor.name })
|
|
259
|
+
}
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
app.post("/admin/add-role", async (req, res) => {
|
|
263
|
+
try {
|
|
264
|
+
const { identifier, role } = req.body
|
|
265
|
+
await req.auth.addRoleForUserBy(identifier, role)
|
|
266
|
+
res.json({ success: true })
|
|
267
|
+
} catch (error) {
|
|
268
|
+
res.status(400).json({ error: error.message, errorType: error.constructor.name })
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
app.post("/admin/remove-role", async (req, res) => {
|
|
273
|
+
try {
|
|
274
|
+
const { identifier, role } = req.body
|
|
275
|
+
await req.auth.removeRoleForUserBy(identifier, role)
|
|
276
|
+
res.json({ success: true })
|
|
277
|
+
} catch (error) {
|
|
278
|
+
res.status(400).json({ error: error.message, errorType: error.constructor.name })
|
|
279
|
+
}
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
app.post("/admin/has-role", async (req, res) => {
|
|
283
|
+
try {
|
|
284
|
+
const { identifier, role } = req.body
|
|
285
|
+
const hasRole = await req.auth.hasRoleForUserBy(identifier, role)
|
|
286
|
+
res.json({ success: true, hasRole })
|
|
287
|
+
} catch (error) {
|
|
288
|
+
res.status(400).json({ error: error.message, errorType: error.constructor.name })
|
|
289
|
+
}
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
app.post("/admin/change-password", async (req, res) => {
|
|
293
|
+
try {
|
|
294
|
+
const { identifier, password } = req.body
|
|
295
|
+
await req.auth.changePasswordForUserBy(identifier, password)
|
|
296
|
+
res.json({ success: true })
|
|
297
|
+
} catch (error) {
|
|
298
|
+
res.status(400).json({ error: error.message, errorType: error.constructor.name })
|
|
299
|
+
}
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
app.post("/admin/set-status", async (req, res) => {
|
|
303
|
+
try {
|
|
304
|
+
const { identifier, status } = req.body
|
|
305
|
+
await req.auth.setStatusForUserBy(identifier, status)
|
|
306
|
+
res.json({ success: true })
|
|
307
|
+
} catch (error) {
|
|
308
|
+
res.status(400).json({ error: error.message, errorType: error.constructor.name })
|
|
309
|
+
}
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
app.post("/admin/initiate-reset", async (req, res) => {
|
|
313
|
+
try {
|
|
314
|
+
const { identifier, expiresAfter } = req.body
|
|
315
|
+
let resetToken
|
|
316
|
+
await req.auth.initiatePasswordResetForUserBy(identifier, expiresAfter, (token) => {
|
|
317
|
+
resetToken = token
|
|
318
|
+
})
|
|
319
|
+
res.json({ success: true, resetToken })
|
|
320
|
+
} catch (error) {
|
|
321
|
+
res.status(400).json({ error: error.message, errorType: error.constructor.name })
|
|
322
|
+
}
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
app.post("/admin/user-exists", async (req, res) => {
|
|
326
|
+
try {
|
|
327
|
+
const { email } = req.body
|
|
328
|
+
const exists = await req.auth.userExistsByEmail(email)
|
|
329
|
+
res.json({ success: true, exists })
|
|
330
|
+
} catch (error) {
|
|
331
|
+
res.status(400).json({ error: error.message, errorType: error.constructor.name })
|
|
332
|
+
}
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
app.post("/admin/delete-user", async (req, res) => {
|
|
336
|
+
try {
|
|
337
|
+
const identifier = req.body
|
|
338
|
+
await req.auth.deleteUserBy(identifier)
|
|
339
|
+
res.json({ success: true })
|
|
340
|
+
} catch (error) {
|
|
341
|
+
res.status(400).json({ error: error.message, errorType: error.constructor.name })
|
|
342
|
+
}
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
app.post("/admin/force-logout-user", async (req, res) => {
|
|
346
|
+
try {
|
|
347
|
+
const identifier = req.body
|
|
348
|
+
await req.auth.forceLogoutForUserBy(identifier)
|
|
349
|
+
res.json({ success: true })
|
|
350
|
+
} catch (error) {
|
|
351
|
+
res.status(400).json({ error: error.message, errorType: error.constructor.name })
|
|
352
|
+
}
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
// utility routes
|
|
356
|
+
app.post("/set-user-session", (req, res) => {
|
|
357
|
+
req.session.userId = req.body.userId || "test-user-123"
|
|
358
|
+
req.session.save((err) => {
|
|
359
|
+
if (err) {
|
|
360
|
+
return res.status(500).json({ error: "Failed to save session" })
|
|
361
|
+
}
|
|
362
|
+
res.json({ success: true })
|
|
363
|
+
})
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
app.get("/session-info", (req, res) => {
|
|
367
|
+
res.json({
|
|
368
|
+
sessionId: req.sessionID,
|
|
369
|
+
userId: req.session.userId,
|
|
370
|
+
auth: req.session.auth || null,
|
|
371
|
+
})
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
app,
|
|
376
|
+
pool,
|
|
377
|
+
authConfig,
|
|
378
|
+
cleanup: async () => {
|
|
379
|
+
await closeInvalidationListeners()
|
|
380
|
+
await pool.end()
|
|
381
|
+
},
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export { AuthRole }
|