@meith/accounts 0.31.0 → 0.32.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/package.json +3 -2
- package/src/federation/service.ts +27 -9
- package/src/member-settings.ts +36 -0
- package/src/service.ts +24 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meith/accounts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"hash-wasm": "^4.12.0",
|
|
23
|
-
"@meith/i18n": "0.
|
|
23
|
+
"@meith/i18n": "0.32.0",
|
|
24
|
+
"@meith/subscriptions": "0.32.0"
|
|
24
25
|
}
|
|
25
26
|
}
|
|
@@ -33,6 +33,12 @@ export type FederationOutcome =
|
|
|
33
33
|
readonly login: LoginResult
|
|
34
34
|
readonly created: boolean
|
|
35
35
|
}
|
|
36
|
+
| {
|
|
37
|
+
readonly status: 'second-factor'
|
|
38
|
+
readonly account: AccountRecord
|
|
39
|
+
readonly token: string
|
|
40
|
+
readonly expiresAt: Date
|
|
41
|
+
}
|
|
36
42
|
| {
|
|
37
43
|
readonly status: 'pending'
|
|
38
44
|
readonly account: AccountRecord
|
|
@@ -67,9 +73,8 @@ export class FederationService {
|
|
|
67
73
|
)
|
|
68
74
|
}
|
|
69
75
|
|
|
70
|
-
const login = await this.identity.startSessionFor(account, context)
|
|
71
76
|
await this.identities.markUsed(linked.id, this.now())
|
|
72
|
-
return
|
|
77
|
+
return this.begin(account, context, false)
|
|
73
78
|
}
|
|
74
79
|
|
|
75
80
|
const email = normalisedEmail(input.profile)
|
|
@@ -85,13 +90,12 @@ export class FederationService {
|
|
|
85
90
|
)
|
|
86
91
|
}
|
|
87
92
|
|
|
88
|
-
const login = await this.identity.startSessionFor(existing, context)
|
|
89
93
|
await this.link({
|
|
90
94
|
userId: existing.id,
|
|
91
95
|
provider: input.provider,
|
|
92
96
|
profile: input.profile,
|
|
93
97
|
})
|
|
94
|
-
return
|
|
98
|
+
return this.begin(existing, context, false)
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
if (email === null) {
|
|
@@ -126,12 +130,26 @@ export class FederationService {
|
|
|
126
130
|
}
|
|
127
131
|
}
|
|
128
132
|
|
|
129
|
-
return
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
133
|
+
return this.begin(provisioned.account, context, true)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private async begin(
|
|
137
|
+
account: AccountRecord,
|
|
138
|
+
context: RequestContext,
|
|
139
|
+
created: boolean,
|
|
140
|
+
): Promise<FederationOutcome> {
|
|
141
|
+
const outcome = await this.identity.beginSessionFor(account, context)
|
|
142
|
+
|
|
143
|
+
if (outcome.status === 'second-factor') {
|
|
144
|
+
return {
|
|
145
|
+
status: 'second-factor',
|
|
146
|
+
account,
|
|
147
|
+
token: outcome.token,
|
|
148
|
+
expiresAt: outcome.expiresAt,
|
|
149
|
+
}
|
|
134
150
|
}
|
|
151
|
+
|
|
152
|
+
return { status: 'signed-in', account, login: outcome.login, created }
|
|
135
153
|
}
|
|
136
154
|
|
|
137
155
|
async linkToViewer(input: {
|
package/src/member-settings.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ValidationError } from '@meith/core'
|
|
2
2
|
import { msg, normaliseLocale } from '@meith/i18n'
|
|
3
|
+
import { parseSubscriptionMode, type SubscriptionMode } from '@meith/subscriptions'
|
|
3
4
|
|
|
4
5
|
import { foldIdentifier } from './case-fold'
|
|
5
6
|
import { hashPassword, needsRehash, verifyPassword } from './crypto/password'
|
|
@@ -33,6 +34,9 @@ export interface MemberSettings {
|
|
|
33
34
|
readonly bio: string | null
|
|
34
35
|
readonly displayGroupId: number | null
|
|
35
36
|
readonly massMailOptInAt: Date | null
|
|
37
|
+
readonly boardDigestCadence: string
|
|
38
|
+
readonly autoWatchOwnThreads: SubscriptionMode
|
|
39
|
+
readonly autoWatchRepliedThreads: SubscriptionMode
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
export interface MemberGroupChoice {
|
|
@@ -66,10 +70,17 @@ export interface MemberSettingsRepository {
|
|
|
66
70
|
readonly postsPerPage: number | null
|
|
67
71
|
readonly threadsPerPage: number | null
|
|
68
72
|
readonly invisible: boolean
|
|
73
|
+
readonly autoWatchOwnThreads: SubscriptionMode
|
|
74
|
+
readonly autoWatchRepliedThreads: SubscriptionMode
|
|
69
75
|
}): Promise<void>
|
|
70
76
|
|
|
71
77
|
saveMassMailOptIn(input: { readonly userId: number; readonly optIn: boolean }): Promise<void>
|
|
72
78
|
|
|
79
|
+
saveBoardDigestCadence(input: {
|
|
80
|
+
readonly userId: number
|
|
81
|
+
readonly cadence: string
|
|
82
|
+
}): Promise<void>
|
|
83
|
+
|
|
73
84
|
adoptEmail(input: {
|
|
74
85
|
readonly userId: number
|
|
75
86
|
readonly email: string
|
|
@@ -102,6 +113,12 @@ export function isLocalePreference(value: string): boolean {
|
|
|
102
113
|
return value === AUTOMATIC_LOCALE || normaliseLocale(value) !== null
|
|
103
114
|
}
|
|
104
115
|
|
|
116
|
+
function parseAutoWatchPreference(value: string): SubscriptionMode {
|
|
117
|
+
const mode = parseSubscriptionMode(value.trim())
|
|
118
|
+
if (mode === null) throw new ValidationError(msg('error.accounts.auto-watch-preference'))
|
|
119
|
+
return mode
|
|
120
|
+
}
|
|
121
|
+
|
|
105
122
|
export class MemberSettingsService {
|
|
106
123
|
private readonly settings: MemberSettingsRepository
|
|
107
124
|
private readonly accounts: AccountRepository
|
|
@@ -201,6 +218,8 @@ export class MemberSettingsService {
|
|
|
201
218
|
readonly postsPerPage: string
|
|
202
219
|
readonly threadsPerPage: string
|
|
203
220
|
readonly invisible: boolean
|
|
221
|
+
readonly autoWatchOwnThreads: string
|
|
222
|
+
readonly autoWatchRepliedThreads: string
|
|
204
223
|
}): Promise<void> {
|
|
205
224
|
const timezone = input.timezone.trim()
|
|
206
225
|
if (!isTimezonePreference(timezone)) {
|
|
@@ -212,6 +231,9 @@ export class MemberSettingsService {
|
|
|
212
231
|
throw new ValidationError(msg('error.accounts.language-board-recognises'))
|
|
213
232
|
}
|
|
214
233
|
|
|
234
|
+
const autoWatchOwnThreads = parseAutoWatchPreference(input.autoWatchOwnThreads)
|
|
235
|
+
const autoWatchRepliedThreads = parseAutoWatchPreference(input.autoWatchRepliedThreads)
|
|
236
|
+
|
|
215
237
|
await this.settings.saveOptions({
|
|
216
238
|
userId: input.userId,
|
|
217
239
|
timezone,
|
|
@@ -219,9 +241,23 @@ export class MemberSettingsService {
|
|
|
219
241
|
postsPerPage: parsePageSize(input.postsPerPage, 'Posts per page'),
|
|
220
242
|
threadsPerPage: parsePageSize(input.threadsPerPage, 'Threads per page'),
|
|
221
243
|
invisible: input.invisible,
|
|
244
|
+
autoWatchOwnThreads,
|
|
245
|
+
autoWatchRepliedThreads,
|
|
222
246
|
})
|
|
223
247
|
}
|
|
224
248
|
|
|
249
|
+
async saveBoardDigestCadence(input: {
|
|
250
|
+
readonly userId: number
|
|
251
|
+
readonly cadence: string
|
|
252
|
+
}): Promise<void> {
|
|
253
|
+
const cadence = input.cadence.trim()
|
|
254
|
+
if (cadence !== 'weekly' && cadence !== 'monthly') {
|
|
255
|
+
throw new ValidationError(msg('error.accounts.digest-cadence-weekly-monthly'))
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
await this.settings.saveBoardDigestCadence({ userId: input.userId, cadence })
|
|
259
|
+
}
|
|
260
|
+
|
|
225
261
|
async changePassword(input: {
|
|
226
262
|
readonly userId: number
|
|
227
263
|
readonly currentPassword: string
|
package/src/service.ts
CHANGED
|
@@ -505,6 +505,30 @@ export class IdentityService {
|
|
|
505
505
|
return this.startSession(account, this.now(), context)
|
|
506
506
|
}
|
|
507
507
|
|
|
508
|
+
async beginSessionFor(
|
|
509
|
+
account: AccountRecord,
|
|
510
|
+
context: RequestContext = {},
|
|
511
|
+
options: { readonly remember?: boolean } = {},
|
|
512
|
+
): Promise<LoginOutcome> {
|
|
513
|
+
await this.assertSignInAllowed(account)
|
|
514
|
+
|
|
515
|
+
const at = this.now()
|
|
516
|
+
const prefix = prefixOf(context)
|
|
517
|
+
if (prefix !== null) {
|
|
518
|
+
await this.store.accounts.recordLastIpPrefix(account.id, prefix)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (await this.secondFactor?.isEnrolled(account.id)) {
|
|
522
|
+
return {
|
|
523
|
+
status: 'second-factor',
|
|
524
|
+
account,
|
|
525
|
+
...(await this.holdForSecondFactor(account.id, options.remember === true, at)),
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return { status: 'signed-in', login: await this.startSession(account, at, context) }
|
|
530
|
+
}
|
|
531
|
+
|
|
508
532
|
private async assertNotFiltered(subject: BanFilterSubject): Promise<void> {
|
|
509
533
|
const match = matchBanFilter(await this.banFilters.listAll(), subject)
|
|
510
534
|
if (match) {
|