@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,86 @@
|
|
|
1
|
+
import { BaseOAuthProvider } from "./base-provider.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import("../types.js").AuthConfig} AuthConfig
|
|
5
|
+
* @typedef {import("../types.js").OAuthUserData} OAuthUserData
|
|
6
|
+
* @typedef {import("../types.js").GitHubProviderConfig} GitHubProviderConfig
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export class GitHubProvider extends BaseOAuthProvider {
|
|
10
|
+
/**
|
|
11
|
+
* @param {GitHubProviderConfig} config
|
|
12
|
+
* @param {AuthConfig} authConfig
|
|
13
|
+
* @param {import("../auth-manager.js").AuthManager} authManager
|
|
14
|
+
*/
|
|
15
|
+
constructor(config, authConfig, authManager) {
|
|
16
|
+
super(config, authConfig, authManager)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Build the GitHub authorization URL.
|
|
21
|
+
* @param {string} [state]
|
|
22
|
+
* @param {string[]} [scopes]
|
|
23
|
+
* @returns {string}
|
|
24
|
+
*/
|
|
25
|
+
getAuthUrl(state, scopes) {
|
|
26
|
+
const params = new URLSearchParams({
|
|
27
|
+
client_id: this.config.clientId,
|
|
28
|
+
redirect_uri: this.config.redirectUri,
|
|
29
|
+
scope: scopes?.join(" ") || "user:email",
|
|
30
|
+
state: state || crypto.randomUUID(),
|
|
31
|
+
response_type: "code",
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
return `https://github.com/login/oauth/authorize?${params}`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Exchange the callback code and resolve the GitHub user profile.
|
|
39
|
+
* @param {import("express").Request} req
|
|
40
|
+
* @returns {Promise<OAuthUserData>}
|
|
41
|
+
* @throws {Error} when no code is provided or no verified email is found
|
|
42
|
+
*/
|
|
43
|
+
async getUserData(req) {
|
|
44
|
+
const code = req.query.code
|
|
45
|
+
if (!code) {
|
|
46
|
+
throw new Error("No authorization code provided")
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// exchange code for access token
|
|
50
|
+
const accessToken = await this.exchangeCodeForToken(code, "https://github.com/login/oauth/access_token")
|
|
51
|
+
|
|
52
|
+
const apiHeaders = {
|
|
53
|
+
Accept: "application/vnd.github+json",
|
|
54
|
+
"User-Agent": this.authConfig.githubUserAgent || "EasyAccess",
|
|
55
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const [user, emails] = await Promise.all([
|
|
59
|
+
this.fetchUserFromAPI(accessToken, "https://api.github.com/user", apiHeaders),
|
|
60
|
+
this.fetchUserFromAPI(accessToken, "https://api.github.com/user/emails", apiHeaders),
|
|
61
|
+
])
|
|
62
|
+
|
|
63
|
+
const verifiedEmails = Array.isArray(emails) ? emails.filter((email) => email.verified) : []
|
|
64
|
+
const primaryEmail = verifiedEmails.find((email) => email.primary)?.email
|
|
65
|
+
const fallbackEmail = primaryEmail || verifiedEmails[0]?.email
|
|
66
|
+
|
|
67
|
+
if (!fallbackEmail) {
|
|
68
|
+
throw new Error("No verified email found in GitHub account")
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
id: user.id.toString(),
|
|
73
|
+
email: fallbackEmail,
|
|
74
|
+
username: user.login,
|
|
75
|
+
name: user.name || user.login,
|
|
76
|
+
avatar: user.avatar_url,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @returns {string}
|
|
82
|
+
*/
|
|
83
|
+
getProviderName() {
|
|
84
|
+
return "github"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { BaseOAuthProvider } from "./base-provider.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import("../types.js").AuthConfig} AuthConfig
|
|
5
|
+
* @typedef {import("../types.js").OAuthUserData} OAuthUserData
|
|
6
|
+
* @typedef {import("../types.js").GoogleProviderConfig} GoogleProviderConfig
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export class GoogleProvider extends BaseOAuthProvider {
|
|
10
|
+
/**
|
|
11
|
+
* @param {GoogleProviderConfig} config
|
|
12
|
+
* @param {AuthConfig} authConfig
|
|
13
|
+
* @param {import("../auth-manager.js").AuthManager} authManager
|
|
14
|
+
*/
|
|
15
|
+
constructor(config, authConfig, authManager) {
|
|
16
|
+
super(config, authConfig, authManager)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Build the Google authorization URL.
|
|
21
|
+
* @param {string} [state]
|
|
22
|
+
* @param {string[]} [scopes]
|
|
23
|
+
* @returns {string}
|
|
24
|
+
*/
|
|
25
|
+
getAuthUrl(state, scopes) {
|
|
26
|
+
const params = new URLSearchParams({
|
|
27
|
+
client_id: this.config.clientId,
|
|
28
|
+
redirect_uri: this.config.redirectUri,
|
|
29
|
+
scope: scopes?.join(" ") || "openid profile email",
|
|
30
|
+
state: state || crypto.randomUUID(),
|
|
31
|
+
response_type: "code",
|
|
32
|
+
access_type: "offline",
|
|
33
|
+
prompt: "consent",
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
return `https://accounts.google.com/o/oauth2/v2/auth?${params}`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Exchange the callback code and resolve the Google user profile.
|
|
41
|
+
* @param {import("express").Request} req
|
|
42
|
+
* @returns {Promise<OAuthUserData>}
|
|
43
|
+
* @throws {Error} when no code is provided or no email is found
|
|
44
|
+
*/
|
|
45
|
+
async getUserData(req) {
|
|
46
|
+
const code = req.query.code
|
|
47
|
+
if (!code) {
|
|
48
|
+
throw new Error("No authorization code provided")
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// exchange code for access token
|
|
52
|
+
const accessToken = await this.exchangeCodeForToken(code, "https://oauth2.googleapis.com/token")
|
|
53
|
+
|
|
54
|
+
// fetch user data
|
|
55
|
+
const user = await this.fetchUserFromAPI(accessToken, "https://www.googleapis.com/oauth2/v2/userinfo")
|
|
56
|
+
|
|
57
|
+
if (!user.email) {
|
|
58
|
+
throw new Error("No email found in Google account")
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
id: user.id,
|
|
63
|
+
email: user.email,
|
|
64
|
+
username: user.email.split("@")[0], // use email prefix as username
|
|
65
|
+
name: user.name,
|
|
66
|
+
avatar: user.picture,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @returns {string}
|
|
72
|
+
*/
|
|
73
|
+
getProviderName() {
|
|
74
|
+
return "google"
|
|
75
|
+
}
|
|
76
|
+
}
|
package/src/queries.js
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import { notifyInvalidation } from "./invalidation.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import("./types.js").AuthConfig} AuthConfig
|
|
5
|
+
* @typedef {import("./types.js").AuthAccount} AuthAccount
|
|
6
|
+
* @typedef {import("./types.js").AuthConfirmation} AuthConfirmation
|
|
7
|
+
* @typedef {import("./types.js").AuthRemember} AuthRemember
|
|
8
|
+
* @typedef {import("./types.js").AuthReset} AuthReset
|
|
9
|
+
* @typedef {import("./types.js").AuthProvider} AuthProvider
|
|
10
|
+
* @typedef {import("./types.js").TwoFactorMethod} TwoFactorMethod
|
|
11
|
+
* @typedef {import("./types.js").TwoFactorToken} TwoFactorToken
|
|
12
|
+
* @typedef {import("./types.js").TwoFactorMechanism} TwoFactorMechanism
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export class AuthQueries {
|
|
16
|
+
/**
|
|
17
|
+
* @param {AuthConfig} config
|
|
18
|
+
*/
|
|
19
|
+
constructor(config) {
|
|
20
|
+
this.config = config
|
|
21
|
+
this.db = config.db
|
|
22
|
+
this.tablePrefix = config.tablePrefix || "user_"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get accountsTable() {
|
|
26
|
+
return `${this.tablePrefix}accounts`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get confirmationsTable() {
|
|
30
|
+
return `${this.tablePrefix}confirmations`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get remembersTable() {
|
|
34
|
+
return `${this.tablePrefix}remembers`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get resetsTable() {
|
|
38
|
+
return `${this.tablePrefix}resets`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get providersTable() {
|
|
42
|
+
return `${this.tablePrefix}providers`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get twoFactorMethodsTable() {
|
|
46
|
+
return `${this.tablePrefix}2fa_methods`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get twoFactorTokensTable() {
|
|
50
|
+
return `${this.tablePrefix}2fa_tokens`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {number} id
|
|
55
|
+
* @returns {Promise<AuthAccount | null>}
|
|
56
|
+
*/
|
|
57
|
+
async findAccountById(id) {
|
|
58
|
+
const sql = `SELECT * FROM ${this.accountsTable} WHERE id = $1`
|
|
59
|
+
const result = await this.db.query(sql, [id])
|
|
60
|
+
return result.rows[0] || null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {string | number} userId
|
|
65
|
+
* @returns {Promise<AuthAccount | null>}
|
|
66
|
+
*/
|
|
67
|
+
async findAccountByUserId(userId) {
|
|
68
|
+
const sql = `SELECT * FROM ${this.accountsTable} WHERE user_id = $1`
|
|
69
|
+
const result = await this.db.query(sql, [userId])
|
|
70
|
+
return result.rows[0] || null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {string} email
|
|
75
|
+
* @returns {Promise<AuthAccount | null>}
|
|
76
|
+
*/
|
|
77
|
+
async findAccountByEmail(email) {
|
|
78
|
+
const sql = `SELECT * FROM ${this.accountsTable} WHERE email = $1`
|
|
79
|
+
const result = await this.db.query(sql, [email])
|
|
80
|
+
return result.rows[0] || null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* List accounts with optional email search, newest first. Used by the
|
|
85
|
+
* @prsm/devtools admin panel binding.
|
|
86
|
+
* @param {{ limit?: number, offset?: number, search?: string }} [opts]
|
|
87
|
+
* @returns {Promise<AuthAccount[]>}
|
|
88
|
+
*/
|
|
89
|
+
async listAccounts({ limit = 50, offset = 0, search } = {}) {
|
|
90
|
+
const params = []
|
|
91
|
+
let where = ""
|
|
92
|
+
if (search) {
|
|
93
|
+
params.push(`%${search}%`)
|
|
94
|
+
where = `WHERE email ILIKE $1`
|
|
95
|
+
}
|
|
96
|
+
const sql = `SELECT * FROM ${this.accountsTable} ${where} ORDER BY id DESC LIMIT $${params.length + 1} OFFSET $${params.length + 2}`
|
|
97
|
+
params.push(limit, offset)
|
|
98
|
+
const result = await this.db.query(sql, params)
|
|
99
|
+
return result.rows
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {string} [search]
|
|
104
|
+
* @returns {Promise<number>}
|
|
105
|
+
*/
|
|
106
|
+
async countAccounts(search) {
|
|
107
|
+
const params = []
|
|
108
|
+
let where = ""
|
|
109
|
+
if (search) {
|
|
110
|
+
params.push(`%${search}%`)
|
|
111
|
+
where = `WHERE email ILIKE $1`
|
|
112
|
+
}
|
|
113
|
+
const result = await this.db.query(`SELECT COUNT(*) as count FROM ${this.accountsTable} ${where}`, params)
|
|
114
|
+
return parseInt(result.rows[0]?.count || "0")
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @param {{ userId: string | number, email: string, password: string | null, verified: boolean, status: number, rolemask: number }} data
|
|
119
|
+
* @returns {Promise<AuthAccount>}
|
|
120
|
+
*/
|
|
121
|
+
async createAccount(data) {
|
|
122
|
+
const sql = `
|
|
123
|
+
INSERT INTO ${this.accountsTable} (
|
|
124
|
+
user_id, email, password, verified, status, rolemask,
|
|
125
|
+
force_logout, resettable, registered
|
|
126
|
+
)
|
|
127
|
+
VALUES ($1, $2, $3, $4, $5, $6, 0, true, NOW())
|
|
128
|
+
RETURNING *
|
|
129
|
+
`
|
|
130
|
+
|
|
131
|
+
const result = await this.db.query(sql, [data.userId, data.email, data.password, data.verified, data.status, data.rolemask])
|
|
132
|
+
|
|
133
|
+
return result.rows[0]
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @param {number} id
|
|
138
|
+
* @param {Partial<AuthAccount>} updates
|
|
139
|
+
* @returns {Promise<void>}
|
|
140
|
+
*/
|
|
141
|
+
async updateAccount(id, updates) {
|
|
142
|
+
const fields = []
|
|
143
|
+
const values = []
|
|
144
|
+
let paramIndex = 1
|
|
145
|
+
|
|
146
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
147
|
+
if (key === "id") continue // don't update id
|
|
148
|
+
fields.push(`${key} = $${paramIndex++}`)
|
|
149
|
+
values.push(value)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (fields.length === 0) return
|
|
153
|
+
|
|
154
|
+
values.push(id)
|
|
155
|
+
const sql = `UPDATE ${this.accountsTable} SET ${fields.join(", ")} WHERE id = $${paramIndex}`
|
|
156
|
+
await this.db.query(sql, values)
|
|
157
|
+
|
|
158
|
+
// signal other instances to resync this account when a security-relevant
|
|
159
|
+
// field changed, so role/status/password updates propagate fleet-wide
|
|
160
|
+
if ("status" in updates || "rolemask" in updates || "password" in updates || "verified" in updates || "force_logout" in updates) {
|
|
161
|
+
await notifyInvalidation(this.config, id)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* @param {number} id
|
|
167
|
+
* @returns {Promise<void>}
|
|
168
|
+
*/
|
|
169
|
+
async updateAccountLastLogin(id) {
|
|
170
|
+
const sql = `UPDATE ${this.accountsTable} SET last_login = NOW() WHERE id = $1`
|
|
171
|
+
await this.db.query(sql, [id])
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @param {number} id
|
|
176
|
+
* @returns {Promise<void>}
|
|
177
|
+
*/
|
|
178
|
+
async incrementForceLogout(id) {
|
|
179
|
+
const sql = `UPDATE ${this.accountsTable} SET force_logout = force_logout + 1 WHERE id = $1`
|
|
180
|
+
await this.db.query(sql, [id])
|
|
181
|
+
await notifyInvalidation(this.config, id)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* @param {number} id
|
|
186
|
+
* @returns {Promise<void>}
|
|
187
|
+
*/
|
|
188
|
+
async deleteAccount(id) {
|
|
189
|
+
await this.db.query(`DELETE FROM ${this.twoFactorTokensTable} WHERE account_id = $1`, [id])
|
|
190
|
+
await this.db.query(`DELETE FROM ${this.twoFactorMethodsTable} WHERE account_id = $1`, [id])
|
|
191
|
+
await this.db.query(`DELETE FROM ${this.providersTable} WHERE account_id = $1`, [id])
|
|
192
|
+
await this.db.query(`DELETE FROM ${this.confirmationsTable} WHERE account_id = $1`, [id])
|
|
193
|
+
await this.db.query(`DELETE FROM ${this.remembersTable} WHERE account_id = $1`, [id])
|
|
194
|
+
await this.db.query(`DELETE FROM ${this.resetsTable} WHERE account_id = $1`, [id])
|
|
195
|
+
|
|
196
|
+
await this.db.query(`DELETE FROM ${this.accountsTable} WHERE id = $1`, [id])
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* @param {{ accountId: number, token: string, email: string, expires: Date }} data
|
|
201
|
+
* @returns {Promise<void>}
|
|
202
|
+
*/
|
|
203
|
+
async createConfirmation(data) {
|
|
204
|
+
await this.db.query(`DELETE FROM ${this.confirmationsTable} WHERE account_id = $1`, [data.accountId])
|
|
205
|
+
|
|
206
|
+
const sql = `
|
|
207
|
+
INSERT INTO ${this.confirmationsTable} (account_id, token, email, expires)
|
|
208
|
+
VALUES ($1, $2, $3, $4)
|
|
209
|
+
`
|
|
210
|
+
|
|
211
|
+
await this.db.query(sql, [data.accountId, data.token, data.email, data.expires])
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* @param {string} token
|
|
216
|
+
* @returns {Promise<AuthConfirmation | null>}
|
|
217
|
+
*/
|
|
218
|
+
async findConfirmation(token) {
|
|
219
|
+
const sql = `SELECT * FROM ${this.confirmationsTable} WHERE token = $1`
|
|
220
|
+
const result = await this.db.query(sql, [token])
|
|
221
|
+
return result.rows[0] || null
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @param {number} accountId
|
|
226
|
+
* @returns {Promise<AuthConfirmation | null>}
|
|
227
|
+
*/
|
|
228
|
+
async findLatestConfirmationForAccount(accountId) {
|
|
229
|
+
const sql = `
|
|
230
|
+
SELECT * FROM ${this.confirmationsTable}
|
|
231
|
+
WHERE account_id = $1
|
|
232
|
+
ORDER BY expires DESC
|
|
233
|
+
LIMIT 1
|
|
234
|
+
`
|
|
235
|
+
const result = await this.db.query(sql, [accountId])
|
|
236
|
+
return result.rows[0] || null
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* @param {string} token
|
|
241
|
+
* @returns {Promise<void>}
|
|
242
|
+
*/
|
|
243
|
+
async deleteConfirmation(token) {
|
|
244
|
+
await this.db.query(`DELETE FROM ${this.confirmationsTable} WHERE token = $1`, [token])
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* @param {{ accountId: number, token: string, expires: Date }} data
|
|
249
|
+
* @returns {Promise<void>}
|
|
250
|
+
*/
|
|
251
|
+
async createRememberToken(data) {
|
|
252
|
+
await this.db.query(`DELETE FROM ${this.remembersTable} WHERE account_id = $1`, [data.accountId])
|
|
253
|
+
|
|
254
|
+
const sql = `
|
|
255
|
+
INSERT INTO ${this.remembersTable} (account_id, token, expires)
|
|
256
|
+
VALUES ($1, $2, $3)
|
|
257
|
+
`
|
|
258
|
+
|
|
259
|
+
await this.db.query(sql, [data.accountId, data.token, data.expires])
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* @param {string} token
|
|
264
|
+
* @returns {Promise<AuthRemember | null>}
|
|
265
|
+
*/
|
|
266
|
+
async findRememberToken(token) {
|
|
267
|
+
const sql = `SELECT * FROM ${this.remembersTable} WHERE token = $1`
|
|
268
|
+
const result = await this.db.query(sql, [token])
|
|
269
|
+
return result.rows[0] || null
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* @param {string} token
|
|
274
|
+
* @returns {Promise<void>}
|
|
275
|
+
*/
|
|
276
|
+
async deleteRememberToken(token) {
|
|
277
|
+
await this.db.query(`DELETE FROM ${this.remembersTable} WHERE token = $1`, [token])
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* @param {number} accountId
|
|
282
|
+
* @returns {Promise<void>}
|
|
283
|
+
*/
|
|
284
|
+
async deleteRememberTokensForAccount(accountId) {
|
|
285
|
+
await this.db.query(`DELETE FROM ${this.remembersTable} WHERE account_id = $1`, [accountId])
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* @param {number} accountId
|
|
290
|
+
* @returns {Promise<void>}
|
|
291
|
+
*/
|
|
292
|
+
async deleteExpiredRememberTokensForAccount(accountId) {
|
|
293
|
+
await this.db.query(`DELETE FROM ${this.remembersTable} WHERE account_id = $1 AND expires <= NOW()`, [accountId])
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* @param {{ accountId: number, token: string, expires: Date }} data
|
|
298
|
+
* @returns {Promise<void>}
|
|
299
|
+
*/
|
|
300
|
+
async createResetToken(data) {
|
|
301
|
+
const sql = `
|
|
302
|
+
INSERT INTO ${this.resetsTable} (account_id, token, expires)
|
|
303
|
+
VALUES ($1, $2, $3)
|
|
304
|
+
`
|
|
305
|
+
|
|
306
|
+
await this.db.query(sql, [data.accountId, data.token, data.expires])
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* @param {string} token
|
|
311
|
+
* @returns {Promise<AuthReset | null>}
|
|
312
|
+
*/
|
|
313
|
+
async findResetToken(token) {
|
|
314
|
+
const sql = `
|
|
315
|
+
SELECT * FROM ${this.resetsTable}
|
|
316
|
+
WHERE token = $1
|
|
317
|
+
ORDER BY expires DESC
|
|
318
|
+
LIMIT 1
|
|
319
|
+
`
|
|
320
|
+
const result = await this.db.query(sql, [token])
|
|
321
|
+
return result.rows[0] || null
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* @param {number} accountId
|
|
326
|
+
* @returns {Promise<number>}
|
|
327
|
+
*/
|
|
328
|
+
async countActiveResetTokensForAccount(accountId) {
|
|
329
|
+
const sql = `
|
|
330
|
+
SELECT COUNT(*) as count FROM ${this.resetsTable}
|
|
331
|
+
WHERE account_id = $1 AND expires >= NOW()
|
|
332
|
+
`
|
|
333
|
+
const result = await this.db.query(sql, [accountId])
|
|
334
|
+
return parseInt(result.rows[0]?.count || "0")
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* @param {string} token
|
|
339
|
+
* @returns {Promise<void>}
|
|
340
|
+
*/
|
|
341
|
+
async deleteResetToken(token) {
|
|
342
|
+
await this.db.query(`DELETE FROM ${this.resetsTable} WHERE token = $1`, [token])
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* @param {number} accountId
|
|
347
|
+
* @returns {Promise<void>}
|
|
348
|
+
*/
|
|
349
|
+
async deleteResetTokensForAccount(accountId) {
|
|
350
|
+
await this.db.query(`DELETE FROM ${this.resetsTable} WHERE account_id = $1`, [accountId])
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* @param {{ accountId: number, provider: string, providerId: string, providerEmail: string | null, providerUsername: string | null, providerName: string | null, providerAvatar: string | null }} data
|
|
355
|
+
* @returns {Promise<AuthProvider>}
|
|
356
|
+
*/
|
|
357
|
+
async createProvider(data) {
|
|
358
|
+
const sql = `
|
|
359
|
+
INSERT INTO ${this.providersTable} (
|
|
360
|
+
account_id, provider, provider_id, provider_email,
|
|
361
|
+
provider_username, provider_name, provider_avatar
|
|
362
|
+
)
|
|
363
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
364
|
+
RETURNING *
|
|
365
|
+
`
|
|
366
|
+
|
|
367
|
+
const result = await this.db.query(sql, [data.accountId, data.provider, data.providerId, data.providerEmail, data.providerUsername, data.providerName, data.providerAvatar])
|
|
368
|
+
|
|
369
|
+
return result.rows[0]
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* @param {string} providerId
|
|
374
|
+
* @param {string} provider
|
|
375
|
+
* @returns {Promise<AuthProvider | null>}
|
|
376
|
+
*/
|
|
377
|
+
async findProviderByProviderIdAndType(providerId, provider) {
|
|
378
|
+
const sql = `SELECT * FROM ${this.providersTable} WHERE provider_id = $1 AND provider = $2`
|
|
379
|
+
const result = await this.db.query(sql, [providerId, provider])
|
|
380
|
+
return result.rows[0] || null
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* @param {number} accountId
|
|
385
|
+
* @returns {Promise<AuthProvider[]>}
|
|
386
|
+
*/
|
|
387
|
+
async findProvidersByAccountId(accountId) {
|
|
388
|
+
const sql = `SELECT * FROM ${this.providersTable} WHERE account_id = $1 ORDER BY created_at DESC`
|
|
389
|
+
const result = await this.db.query(sql, [accountId])
|
|
390
|
+
return result.rows
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* @param {number} id
|
|
395
|
+
* @returns {Promise<void>}
|
|
396
|
+
*/
|
|
397
|
+
async deleteProvider(id) {
|
|
398
|
+
await this.db.query(`DELETE FROM ${this.providersTable} WHERE id = $1`, [id])
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* @param {number} accountId
|
|
403
|
+
* @returns {Promise<void>}
|
|
404
|
+
*/
|
|
405
|
+
async deleteProvidersByAccountId(accountId) {
|
|
406
|
+
await this.db.query(`DELETE FROM ${this.providersTable} WHERE account_id = $1`, [accountId])
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// two-factor authentication methods
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* @param {number} accountId
|
|
413
|
+
* @returns {Promise<TwoFactorMethod[]>}
|
|
414
|
+
*/
|
|
415
|
+
async findTwoFactorMethodsByAccountId(accountId) {
|
|
416
|
+
const sql = `SELECT * FROM ${this.twoFactorMethodsTable} WHERE account_id = $1 ORDER BY created_at DESC`
|
|
417
|
+
const result = await this.db.query(sql, [accountId])
|
|
418
|
+
return result.rows
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* @param {number} accountId
|
|
423
|
+
* @param {TwoFactorMechanism} mechanism
|
|
424
|
+
* @returns {Promise<TwoFactorMethod | null>}
|
|
425
|
+
*/
|
|
426
|
+
async findTwoFactorMethodByAccountAndMechanism(accountId, mechanism) {
|
|
427
|
+
const sql = `SELECT * FROM ${this.twoFactorMethodsTable} WHERE account_id = $1 AND mechanism = $2`
|
|
428
|
+
const result = await this.db.query(sql, [accountId, mechanism])
|
|
429
|
+
return result.rows[0] || null
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* @param {{ accountId: number, mechanism: TwoFactorMechanism, secret?: string, backupCodes?: string[], verified?: boolean }} data
|
|
434
|
+
* @returns {Promise<TwoFactorMethod>}
|
|
435
|
+
*/
|
|
436
|
+
async createTwoFactorMethod(data) {
|
|
437
|
+
const sql = `
|
|
438
|
+
INSERT INTO ${this.twoFactorMethodsTable} (
|
|
439
|
+
account_id, mechanism, secret, backup_codes, verified
|
|
440
|
+
)
|
|
441
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
442
|
+
RETURNING *
|
|
443
|
+
`
|
|
444
|
+
|
|
445
|
+
const result = await this.db.query(sql, [data.accountId, data.mechanism, data.secret || null, data.backupCodes || null, data.verified || false])
|
|
446
|
+
|
|
447
|
+
return result.rows[0]
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* @param {number} id
|
|
452
|
+
* @param {Partial<Pick<TwoFactorMethod, "secret" | "backup_codes" | "verified" | "last_used_at">>} updates
|
|
453
|
+
* @returns {Promise<void>}
|
|
454
|
+
*/
|
|
455
|
+
async updateTwoFactorMethod(id, updates) {
|
|
456
|
+
const fields = []
|
|
457
|
+
const values = []
|
|
458
|
+
let paramIndex = 1
|
|
459
|
+
|
|
460
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
461
|
+
if (key === "id") continue
|
|
462
|
+
fields.push(`${key} = $${paramIndex++}`)
|
|
463
|
+
values.push(value)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (fields.length === 0) return
|
|
467
|
+
|
|
468
|
+
values.push(id)
|
|
469
|
+
const sql = `UPDATE ${this.twoFactorMethodsTable} SET ${fields.join(", ")} WHERE id = $${paramIndex}`
|
|
470
|
+
await this.db.query(sql, values)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* @param {number} id
|
|
475
|
+
* @returns {Promise<void>}
|
|
476
|
+
*/
|
|
477
|
+
async deleteTwoFactorMethod(id) {
|
|
478
|
+
await this.db.query(`DELETE FROM ${this.twoFactorMethodsTable} WHERE id = $1`, [id])
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* @param {number} accountId
|
|
483
|
+
* @returns {Promise<void>}
|
|
484
|
+
*/
|
|
485
|
+
async deleteTwoFactorMethodsByAccountId(accountId) {
|
|
486
|
+
await this.db.query(`DELETE FROM ${this.twoFactorMethodsTable} WHERE account_id = $1`, [accountId])
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// two-factor authentication tokens
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* @param {{ accountId: number, mechanism: TwoFactorMechanism, selector: string, tokenHash: string, expiresAt: Date }} data
|
|
493
|
+
* @returns {Promise<TwoFactorToken>}
|
|
494
|
+
*/
|
|
495
|
+
async createTwoFactorToken(data) {
|
|
496
|
+
const sql = `
|
|
497
|
+
INSERT INTO ${this.twoFactorTokensTable} (
|
|
498
|
+
account_id, mechanism, selector, token_hash, expires_at
|
|
499
|
+
)
|
|
500
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
501
|
+
RETURNING *
|
|
502
|
+
`
|
|
503
|
+
|
|
504
|
+
const result = await this.db.query(sql, [data.accountId, data.mechanism, data.selector, data.tokenHash, data.expiresAt])
|
|
505
|
+
|
|
506
|
+
return result.rows[0]
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* @param {string} selector
|
|
511
|
+
* @returns {Promise<TwoFactorToken | null>}
|
|
512
|
+
*/
|
|
513
|
+
async findTwoFactorTokenBySelector(selector) {
|
|
514
|
+
const sql = `SELECT * FROM ${this.twoFactorTokensTable} WHERE selector = $1 AND expires_at > NOW()`
|
|
515
|
+
const result = await this.db.query(sql, [selector])
|
|
516
|
+
return result.rows[0] || null
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* @param {number} id
|
|
521
|
+
* @returns {Promise<void>}
|
|
522
|
+
*/
|
|
523
|
+
async deleteTwoFactorToken(id) {
|
|
524
|
+
await this.db.query(`DELETE FROM ${this.twoFactorTokensTable} WHERE id = $1`, [id])
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* @param {number} accountId
|
|
529
|
+
* @returns {Promise<void>}
|
|
530
|
+
*/
|
|
531
|
+
async deleteTwoFactorTokensByAccountId(accountId) {
|
|
532
|
+
await this.db.query(`DELETE FROM ${this.twoFactorTokensTable} WHERE account_id = $1`, [accountId])
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* @param {number} accountId
|
|
537
|
+
* @param {TwoFactorMechanism} mechanism
|
|
538
|
+
* @returns {Promise<void>}
|
|
539
|
+
*/
|
|
540
|
+
async deleteTwoFactorTokensByAccountAndMechanism(accountId, mechanism) {
|
|
541
|
+
await this.db.query(`DELETE FROM ${this.twoFactorTokensTable} WHERE account_id = $1 AND mechanism = $2`, [accountId, mechanism])
|
|
542
|
+
}
|
|
543
|
+
}
|