Skip to content

Commit 4573fd5

Browse files
guguclaude
andauthored
ci(frontend): use playwright container, add cache + path filter (#1810)
* ci(frontend): use playwright container, add cache + path filter - Run tests in mcr.microsoft.com/playwright:v1.57.0-noble so the workflow no longer downloads browsers at runtime. - Fix yarn test (watch-mode) -> yarn test:ci. - Add yarn cache via setup-node@v4 and yarn install --immutable. - Path-filter triggers to frontend/** plus this workflow file. - Cancel superseded PR runs via concurrency group. - Bump license job Node 16 -> 24, drop unused setup-just step. - Modernize action versions (checkout@v5, setup-node@v4). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci(frontend): bump playwright image tag to v1.58.1-noble The yarn.lock resolves playwright@^1.57.0 to 1.58.1; the container tag must match exactly or Vitest's browser provider refuses to run. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(2fa): accept legacy 10-byte secrets in otplib v13 verification Users enrolled before the otplib v12 -> v13 upgrade have 10-byte secrets stored, which v13's RFC 4226 guardrail (16-byte minimum) rejects with "Secret must be at least 16 bytes". Pass a relaxed `MIN_SECRET_BYTES: 10` guardrail to verifySync so existing users can still authenticate; new secrets continue to use v13's 20-byte default. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9393201 commit 4573fd5

5 files changed

Lines changed: 66 additions & 18 deletions

File tree

.github/workflows/frontend.yml

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,56 @@
11
name: frontend
2+
23
on:
34
push:
45
branches:
5-
- main
6-
# Publish semver tags as releases
7-
tags: [ '*.*.*' ]
6+
- main
7+
tags:
8+
- '*.*.*'
9+
paths:
10+
- 'frontend/**'
11+
- '.github/workflows/frontend.yml'
812
pull_request:
13+
paths:
14+
- 'frontend/**'
15+
- '.github/workflows/frontend.yml'
16+
17+
concurrency:
18+
group: frontend-${{ github.ref }}
19+
cancel-in-progress: true
20+
21+
defaults:
22+
run:
23+
working-directory: frontend
24+
25+
# TODO: add a lint job once ESLint migration replaces the (already-removed in
26+
# @angular-devkit/build-angular v20) `tslint` builder that `yarn lint` calls.
27+
928
jobs:
1029
test:
1130
runs-on: ubuntu-latest
31+
timeout-minutes: 20
32+
container:
33+
image: mcr.microsoft.com/playwright:v1.58.1-noble
1234
steps:
13-
- uses: actions/checkout@v2
14-
- uses: actions/setup-node@v3
35+
- uses: actions/checkout@v5
36+
- uses: actions/setup-node@v4
1537
with:
1638
node-version: '24'
17-
- run: cd frontend && yarn install
18-
- name: Install Playwright browsers
19-
run: cd frontend && yarn playwright install
20-
- name: run tests
21-
run: cd frontend && yarn test
39+
cache: 'yarn'
40+
cache-dependency-path: frontend/yarn.lock
41+
- run: yarn install --immutable
42+
- name: Run unit tests
43+
run: yarn test:ci
44+
2245
license:
2346
runs-on: ubuntu-latest
47+
timeout-minutes: 10
2448
steps:
25-
- uses: actions/checkout@v2
26-
- uses: actions/setup-node@v3
49+
- uses: actions/checkout@v5
50+
- uses: actions/setup-node@v4
2751
with:
28-
node-version: '16'
29-
- uses: extractions/setup-just@v1
52+
node-version: '24'
53+
cache: 'yarn'
54+
cache-dependency-path: frontend/yarn.lock
3055
- name: license checker
31-
run: 'cd frontend && npx license-checker --onlyAllow="MIT;ISC;Python-2.0;Apache-2.0;BSD;MPL;CC;Custom: http://github.com/dscape/statsd-parser;" --excludePrivatePackages'
56+
run: 'npx --yes license-checker --onlyAllow="MIT;ISC;Python-2.0;Apache-2.0;BSD;MPL;CC;Custom: http://github.com/dscape/statsd-parser;" --excludePrivatePackages'

backend/src/entities/user/use-cases/disable-otp.use.case.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { BaseType } from '../../../common/data-injection.tokens.js';
1313
import { Messages } from '../../../exceptions/text/messages.js';
1414
import { OtpDisablingResultDS } from '../application/data-structures/otp-validation-result.ds.js';
1515
import { VerifyOtpDS } from '../application/data-structures/verify-otp.ds.js';
16+
import { legacyOtpGuardrails } from '../utils/otp-guardrails.js';
1617
import { IDisableOTP } from './user-use-cases.interfaces.js';
1718

1819
@Injectable()
@@ -41,7 +42,11 @@ export class DisableOtpUseCase extends AbstractUseCase<VerifyOtpDS, OtpDisabling
4142
throw new BadRequestException(Messages.OTP_NOT_ENABLED);
4243
}
4344
try {
44-
const isValid = verifySync({ token: otpToken, secret: otpSecretKey }).valid;
45+
const isValid = verifySync({
46+
token: otpToken,
47+
secret: otpSecretKey,
48+
guardrails: legacyOtpGuardrails,
49+
}).valid;
4550
if (isValid) {
4651
foundUser.isOTPEnabled = false;
4752
foundUser.otpSecretKey = null;

backend/src/entities/user/use-cases/otp-login-use.case.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { SignInAuditService } from '../../user-sign-in-audit/sign-in-audit.servi
1010
import { VerifyOtpDS } from '../application/data-structures/verify-otp.ds.js';
1111
import { generateGwtToken, IToken } from '../utils/generate-gwt-token.js';
1212
import { get2FaScope } from '../utils/is-jwt-scope-need.util.js';
13+
import { legacyOtpGuardrails } from '../utils/otp-guardrails.js';
1314
import { IOtpLogin } from './user-use-cases.interfaces.js';
1415

1516
@Injectable()
@@ -28,7 +29,11 @@ export class OtpLoginUseCase extends AbstractUseCase<VerifyOtpDS, IToken> implem
2829
if (!foundUser) {
2930
throw new NotFoundException(Messages.USER_NOT_FOUND);
3031
}
31-
const isValid = verifySync({ token: otpToken, secret: foundUser.otpSecretKey }).valid;
32+
const isValid = verifySync({
33+
token: otpToken,
34+
secret: foundUser.otpSecretKey,
35+
guardrails: legacyOtpGuardrails,
36+
}).valid;
3237
if (!isValid) {
3338
await this.recordSignInAudit(
3439
foundUser.email,

backend/src/entities/user/use-cases/verify-otp-use.case.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { BaseType } from '../../../common/data-injection.tokens.js';
66
import { Messages } from '../../../exceptions/text/messages.js';
77
import { OtpValidationResultDS } from '../application/data-structures/otp-validation-result.ds.js';
88
import { VerifyOtpDS } from '../application/data-structures/verify-otp.ds.js';
9+
import { legacyOtpGuardrails } from '../utils/otp-guardrails.js';
910
import { IVerifyOTP } from './user-use-cases.interfaces.js';
1011

1112
@Injectable()
@@ -48,7 +49,11 @@ export class VerifyOtpUseCase extends AbstractUseCase<VerifyOtpDS, OtpValidation
4849
);
4950
}
5051
try {
51-
const isValid = verifySync({ token: otpToken, secret: otpSecretKey }).valid;
52+
const isValid = verifySync({
53+
token: otpToken,
54+
secret: otpSecretKey,
55+
guardrails: legacyOtpGuardrails,
56+
}).valid;
5257
if (isValid) {
5358
foundUser.isOTPEnabled = true;
5459
await this._dbContext.userRepository.saveUserEntity(foundUser);
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { createGuardrails } from 'otplib';
2+
3+
// otplib v13 enforces RFC 4226's 128-bit (16-byte) minimum secret length, but
4+
// users enrolled before the v12 -> v13 upgrade have 10-byte secrets stored
5+
// (the v12 `authenticator.generateSecret()` default). Relax the guardrail at
6+
// verify time so those users can still authenticate. New secrets use v13's
7+
// 20-byte default and are RFC-compliant.
8+
export const legacyOtpGuardrails = createGuardrails({ MIN_SECRET_BYTES: 10 });

0 commit comments

Comments
 (0)