@meith/web 0.35.0 → 0.35.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.
@@ -213,7 +213,7 @@ export async function POST(request: NextRequest): Promise<Response> {
213
213
  return problem(await tr('authRoute.passkey.signInExpired'), 403)
214
214
  }
215
215
 
216
- await identity.assertSecondFactorAttemptsLeft(pending.userId)
216
+ await identity.spendSecondFactorAttempt(pending.userId)
217
217
 
218
218
  const proved = await service.proveOwnership({
219
219
  userId: pending.userId,
@@ -224,7 +224,6 @@ export async function POST(request: NextRequest): Promise<Response> {
224
224
  })
225
225
 
226
226
  if (!proved) {
227
- await identity.recordSecondFactorFailure(pending.userId)
228
227
  await recordAuthEvent({ userId: pending.userId, kind: 'second_factor_failed' })
229
228
  return problem(await tr('authRoute.passkey.notForAccount'), 403)
230
229
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meith/web",
3
- "version": "0.35.0",
3
+ "version": "0.35.1",
4
4
  "description": "The board itself: the Next.js app, and the forum-web bin that materializes it into an external board workspace.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -43,55 +43,55 @@
43
43
  "react-dom": "19.2.8",
44
44
  "tailwindcss": "^4.3.3",
45
45
  "typescript": "7.0.2",
46
- "@meith/accounts": "0.35.0",
47
- "@meith/admin": "0.35.0",
48
- "@meith/antispam": "0.35.0",
49
- "@meith/api": "0.35.0",
50
- "@meith/attachments": "0.35.0",
51
- "@meith/authorization": "0.35.0",
52
- "@meith/avatars": "0.35.0",
53
- "@meith/backup": "0.35.0",
54
- "@meith/board-digest": "0.35.0",
55
- "@meith/core": "0.35.0",
56
- "@meith/db": "0.35.0",
57
- "@meith/demo": "0.35.0",
58
- "@meith/drafts": "0.35.0",
59
- "@meith/drivers": "0.35.0",
60
- "@meith/events": "0.35.0",
61
- "@meith/forums": "0.35.0",
62
- "@meith/groups": "0.35.0",
63
- "@meith/i18n": "0.35.0",
64
- "@meith/import": "0.35.0",
65
- "@meith/install": "0.35.0",
66
- "@meith/mail": "0.35.0",
67
- "@meith/markdown": "0.35.0",
68
- "@meith/marketplace": "0.35.0",
69
- "@meith/messages": "0.35.0",
70
- "@meith/moderation": "0.35.0",
71
- "@meith/notifications": "0.35.0",
72
- "@meith/plugin-calendar": "0.35.0",
73
- "@meith/plugin-dues": "0.35.0",
74
- "@meith/plugin-kit": "0.35.0",
75
- "@meith/polls": "0.35.0",
76
- "@meith/posts": "0.35.0",
77
- "@meith/profile-fields": "0.35.0",
78
- "@meith/relations": "0.35.0",
79
- "@meith/reputation": "0.35.0",
80
- "@meith/runtime": "0.35.0",
81
- "@meith/search": "0.35.0",
82
- "@meith/settings": "0.35.0",
83
- "@meith/signatures": "0.35.0",
84
- "@meith/subscriptions": "0.35.0",
85
- "@meith/tasks": "0.35.0",
86
- "@meith/theme-clubhouse": "0.35.0",
87
- "@meith/theme-default": "0.35.0",
88
- "@meith/theme-kit": "0.35.0",
89
- "@meith/theme-midnight": "0.35.0",
90
- "@meith/theme-phasebook": "0.35.0",
91
- "@meith/theme-raidframe": "0.35.0",
92
- "@meith/threads": "0.35.0",
93
- "@meith/ui": "0.35.0",
94
- "@meith/upgrade": "0.35.0"
46
+ "@meith/accounts": "0.35.1",
47
+ "@meith/admin": "0.35.1",
48
+ "@meith/antispam": "0.35.1",
49
+ "@meith/api": "0.35.1",
50
+ "@meith/attachments": "0.35.1",
51
+ "@meith/authorization": "0.35.1",
52
+ "@meith/avatars": "0.35.1",
53
+ "@meith/backup": "0.35.1",
54
+ "@meith/board-digest": "0.35.1",
55
+ "@meith/core": "0.35.1",
56
+ "@meith/db": "0.35.1",
57
+ "@meith/demo": "0.35.1",
58
+ "@meith/drafts": "0.35.1",
59
+ "@meith/drivers": "0.35.1",
60
+ "@meith/events": "0.35.1",
61
+ "@meith/forums": "0.35.1",
62
+ "@meith/groups": "0.35.1",
63
+ "@meith/i18n": "0.35.1",
64
+ "@meith/import": "0.35.1",
65
+ "@meith/install": "0.35.1",
66
+ "@meith/mail": "0.35.1",
67
+ "@meith/markdown": "0.35.1",
68
+ "@meith/marketplace": "0.35.1",
69
+ "@meith/messages": "0.35.1",
70
+ "@meith/moderation": "0.35.1",
71
+ "@meith/notifications": "0.35.1",
72
+ "@meith/plugin-calendar": "0.35.1",
73
+ "@meith/plugin-dues": "0.35.1",
74
+ "@meith/plugin-kit": "0.35.1",
75
+ "@meith/polls": "0.35.1",
76
+ "@meith/posts": "0.35.1",
77
+ "@meith/profile-fields": "0.35.1",
78
+ "@meith/relations": "0.35.1",
79
+ "@meith/reputation": "0.35.1",
80
+ "@meith/runtime": "0.35.1",
81
+ "@meith/search": "0.35.1",
82
+ "@meith/settings": "0.35.1",
83
+ "@meith/signatures": "0.35.1",
84
+ "@meith/subscriptions": "0.35.1",
85
+ "@meith/tasks": "0.35.1",
86
+ "@meith/theme-clubhouse": "0.35.1",
87
+ "@meith/theme-default": "0.35.1",
88
+ "@meith/theme-kit": "0.35.1",
89
+ "@meith/theme-midnight": "0.35.1",
90
+ "@meith/theme-phasebook": "0.35.1",
91
+ "@meith/theme-raidframe": "0.35.1",
92
+ "@meith/threads": "0.35.1",
93
+ "@meith/ui": "0.35.1",
94
+ "@meith/upgrade": "0.35.1"
95
95
  },
96
96
  "scripts": {
97
97
  "dev": "next dev",
@@ -55,28 +55,33 @@ export async function adminSignInAction(_prev: FormState, form: FormData): Promi
55
55
  const config = await boardAuthConfig()
56
56
  const attemptBucket = await adminReauthenticationBucket(actor.userId)
57
57
  const attemptSince = new Date(Date.now() - config.lockoutMinutes * 60_000)
58
- if (
59
- config.maxLoginAttempts > 0 &&
60
- (await accountStore.loginAttempts.countFailuresSince(attemptBucket, attemptSince)) >=
61
- config.maxLoginAttempts
62
- ) {
63
- throw new ForbiddenError(msg('error.accounts.too-many-failed-attempts-please'))
58
+
59
+ let attemptId: number | null = null
60
+ if (config.maxLoginAttempts > 0) {
61
+ const spent = await accountStore.loginAttempts.recordFailureAndCount(
62
+ attemptBucket,
63
+ attemptSince,
64
+ new Date(),
65
+ )
66
+ attemptId = spent.id
67
+ if (spent.count > config.maxLoginAttempts) {
68
+ throw new ForbiddenError(msg('error.accounts.too-many-failed-attempts-please'))
69
+ }
64
70
  }
65
71
 
66
- const failAttempt = async (): Promise<void> => {
67
- await accountStore.loginAttempts.record(attemptBucket, false, new Date())
72
+ const auditFailure = async (): Promise<void> => {
68
73
  await recordAdminAction({ action: 'admin.signin_failed' })
69
74
  }
70
75
 
71
76
  const password = text(form, 'password')
72
77
  if (password === '') {
73
- await failAttempt()
78
+ await auditFailure()
74
79
  throw new ValidationError(msg('error.app.enter-password'))
75
80
  }
76
81
 
77
82
  const ok = await verifyPassword(password, account.passwordHash)
78
83
  if (!ok) {
79
- await failAttempt()
84
+ await auditFailure()
80
85
  throw new ForbiddenError(msg('error.app.password-right'))
81
86
  }
82
87
 
@@ -84,11 +89,12 @@ export async function adminSignInAction(_prev: FormState, form: FormData): Promi
84
89
  const twoFactor = twoFactorService()
85
90
 
86
91
  if (twoFactor !== null && (await twoFactor.isEnrolled(actor.userId))) {
92
+ if (attemptId !== null) await accountStore.loginAttempts.removeAttempt(attemptId)
87
93
  await holdAdminSecondFactor(actor.userId, next)
88
94
  target = '/admin'
89
95
  } else {
90
96
  if (twoFactor !== null && (await twoFactorRequiredForStaff())) {
91
- await failAttempt()
97
+ await auditFailure()
92
98
  throw new ForbiddenError(msg('adminAction.twoFactorRequired'))
93
99
  }
94
100
 
@@ -133,28 +139,31 @@ export async function adminVerifySecondFactorAction(
133
139
  const config = await boardAuthConfig()
134
140
  const attemptBucket = await adminReauthenticationBucket(actor.userId)
135
141
  const attemptSince = new Date(Date.now() - config.lockoutMinutes * 60_000)
136
- if (
137
- config.maxLoginAttempts > 0 &&
138
- (await accountStore.loginAttempts.countFailuresSince(attemptBucket, attemptSince)) >=
139
- config.maxLoginAttempts
140
- ) {
141
- throw new ForbiddenError(msg('error.accounts.too-many-failed-attempts-please'))
142
+
143
+ if (config.maxLoginAttempts > 0) {
144
+ const spent = await accountStore.loginAttempts.recordFailureAndCount(
145
+ attemptBucket,
146
+ attemptSince,
147
+ new Date(),
148
+ )
149
+ if (spent.count > config.maxLoginAttempts) {
150
+ throw new ForbiddenError(msg('error.accounts.too-many-failed-attempts-please'))
151
+ }
142
152
  }
143
153
 
144
- const failAttempt = async (): Promise<void> => {
145
- await accountStore.loginAttempts.record(attemptBucket, false, new Date())
154
+ const auditFailure = async (): Promise<void> => {
146
155
  await recordAdminAction({ action: 'admin.signin_failed' })
147
156
  }
148
157
 
149
158
  const code = text(form, 'code')
150
159
  if (code === '') {
151
- await failAttempt()
160
+ await auditFailure()
152
161
  throw new ValidationError(msg('error.app.enter-code-from-authenticator-app'))
153
162
  }
154
163
 
155
164
  const outcome = await twoFactor.verify({ userId: pending.userId, code })
156
165
  if (outcome.status !== 'ok') {
157
- await failAttempt()
166
+ await auditFailure()
158
167
  throw new ForbiddenError(
159
168
  msg(outcome.status === 'replayed' ? 'adminAction.codeReplayed' : 'adminAction.codeWrong'),
160
169
  )
@@ -1,6 +1,7 @@
1
1
  'use server'
2
2
 
3
3
  import { redirect } from 'next/navigation'
4
+ import { after } from 'next/server'
4
5
 
5
6
  import { type AuthConfig, foldIdentifier, hashToken, type LoginBucket } from '@meith/accounts'
6
7
  import { env, logger } from '@meith/core'
@@ -344,12 +345,11 @@ export async function verifySecondFactorAction(
344
345
  if (service === null) return { error: await tr('authAction.secondFactorExpired') }
345
346
 
346
347
  try {
347
- await identity.assertSecondFactorAttemptsLeft(pending.userId)
348
+ await identity.spendSecondFactorAttempt(pending.userId)
348
349
 
349
350
  const outcome = await service.verify({ userId: pending.userId, code })
350
351
 
351
352
  if (outcome.status !== 'ok') {
352
- await identity.recordSecondFactorFailure(pending.userId)
353
353
  await recordAuthEvent({ userId: pending.userId, kind: 'second_factor_failed' })
354
354
  return {
355
355
  error: await tr(
@@ -440,9 +440,10 @@ export async function requestResetAction(_prev: FormState, form: FormData): Prom
440
440
  const { token, userId } = await identity.requestPasswordReset(email)
441
441
 
442
442
  if (token !== null && userId !== null) {
443
- const account = await getContainer().accountStore.accounts.findById(userId)
444
- if (account !== null) {
443
+ const deliver = async (): Promise<void> => {
445
444
  try {
445
+ const account = await getContainer().accountStore.accounts.findById(userId)
446
+ if (account === null) return
446
447
  await sendPasswordResetEmail({
447
448
  token,
448
449
  email: account.email,
@@ -456,6 +457,12 @@ export async function requestResetAction(_prev: FormState, form: FormData): Prom
456
457
  )
457
458
  }
458
459
  }
460
+
461
+ try {
462
+ after(deliver)
463
+ } catch {
464
+ await deliver()
465
+ }
459
466
  }
460
467
 
461
468
  if (token && env.NODE_ENV === 'development') {
@@ -54,7 +54,9 @@ export async function federationProvider(id: string): Promise<IdentityProvider |
54
54
  if (!isProviderKind(id)) return null
55
55
  if (getContainer().dataSource !== 'postgres') return null
56
56
 
57
- return providerFor(id, federationOptions(await getSettingsUncached()))
57
+ return providerFor(id, federationOptions(await getSettingsUncached()), {
58
+ allowPrivateHosts: env.OIDC_ALLOW_PRIVATE_HOSTS,
59
+ })
58
60
  }
59
61
 
60
62
  export async function signInProviders(): Promise<readonly ProviderButton[]> {
@@ -17,7 +17,7 @@ import { planUpgrade, type UpgradeState, upgradeNotice } from '@meith/upgrade'
17
17
 
18
18
  import { activeDefinitions } from './plugin-host'
19
19
 
20
- export const CODE_VERSION = '0.35.0'
20
+ export const CODE_VERSION = '0.35.1'
21
21
 
22
22
  export interface UpgradeApplied {
23
23
  readonly plugins: readonly string[]