@stjoseph/prumo 0.2.0 → 0.4.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/.prumo-templates/api/auth.md +51 -60
- package/.prumo-templates/api/config.md +2 -2
- package/.prumo-templates/api/routes.md +6 -4
- package/.prumo-templates/api/structure.md +4 -1
- package/.prumo-templates/client/data.md +16 -18
- package/.prumo-templates/client/forms.md +2 -2
- package/.prumo-templates/core/testing.md +4 -4
- package/.prumo-templates/database/testing.md +6 -8
- package/.prumo-templates/email/codes.md +62 -0
- package/.prumo-templates/email/mailer.md +47 -0
- package/.prumo-templates/mcp/server.md +55 -14
- package/.prumo-templates/mobile/routing.md +1 -1
- package/.prumo-templates/mobile/storage.md +7 -6
- package/.prumo-templates/monorepo/layout.md +4 -4
- package/.prumo-templates/multi-tenancy/client.md +6 -6
- package/.prumo-templates/multi-tenancy/isolation.md +17 -10
- package/.prumo-templates/social/sign-in.md +77 -0
- package/README.md +8 -6
- package/dist/asker.js +13 -0
- package/dist/clean.js +2 -2
- package/dist/cli.js +16 -2
- package/dist/commands.js +7 -0
- package/dist/compose.js +3 -11
- package/dist/context.js +4 -0
- package/dist/features.js +230 -0
- package/dist/generate.js +20 -7
- package/dist/questions.js +64 -1
- package/package.json +1 -1
- package/templates/api/.env.example +21 -5
- package/templates/api/README.md +4 -5
- package/templates/api/docker-compose.yml +0 -1
- package/templates/api/migrations/Migration20261005111545_create_user_profile_session.ts +31 -0
- package/templates/api/migrations/Migration20261005111547_create_authorization.ts +18 -0
- package/templates/api/migrations/Migration20261005111549_create_email_code.ts +18 -0
- package/templates/api/migrations/Migration20261005111550_create_identity_social_sign_in.ts +25 -0
- package/templates/api/package.json +7 -8
- package/templates/api/scripts/database.mjs +3 -11
- package/templates/api/scripts/ports.mjs +1 -1
- package/templates/api/src/domain/auth/dto/email-request.dto.ts +5 -0
- package/templates/api/src/domain/auth/dto/refresh.dto.ts +9 -0
- package/templates/api/src/domain/auth/dto/reset-password.dto.ts +9 -0
- package/templates/api/src/domain/auth/dto/session-response.dto.ts +7 -0
- package/templates/api/src/domain/auth/dto/sign-in.dto.ts +8 -0
- package/templates/api/src/domain/auth/dto/sign-up.dto.ts +9 -0
- package/templates/api/src/domain/auth/dto/tokens-response.dto.ts +10 -0
- package/templates/api/src/domain/auth/dto/verify-email.dto.ts +8 -0
- package/templates/api/src/domain/auth/entities/email-code.entity.ts +46 -0
- package/templates/api/src/domain/auth/entities/identity.entity.ts +18 -0
- package/templates/api/src/domain/auth/entities/session.entity.ts +78 -0
- package/templates/api/src/domain/auth/entities/social-sign-in.entity.ts +82 -0
- package/templates/api/src/domain/auth/entities/user.entity.ts +34 -0
- package/templates/api/src/domain/auth/errors/email-not-verified.error.ts +10 -0
- package/templates/api/src/domain/auth/errors/email-taken.error.ts +9 -0
- package/templates/api/src/domain/auth/errors/invalid-code.error.ts +11 -0
- package/templates/api/src/domain/auth/errors/invalid-credentials.error.ts +10 -0
- package/templates/api/src/domain/auth/errors/invalid-session.error.ts +9 -0
- package/templates/api/src/domain/auth/errors/provider-email-unverified.error.ts +11 -0
- package/templates/api/src/domain/auth/errors/provider-not-configured.error.ts +10 -0
- package/templates/api/src/domain/auth/errors/social-sign-in-failed.error.ts +11 -0
- package/templates/api/src/domain/auth/errors/superseded-token.error.ts +11 -0
- package/templates/api/src/domain/auth/ports/access-tokens.port.ts +15 -0
- package/templates/api/src/domain/auth/ports/identity-providers.port.ts +29 -0
- package/templates/api/src/domain/auth/ports/mailer.port.ts +8 -0
- package/templates/api/src/domain/auth/ports/one-time-codes.port.ts +7 -0
- package/templates/api/src/domain/auth/ports/opaque-tokens.port.ts +7 -0
- package/templates/api/src/domain/auth/ports/password-hasher.port.ts +8 -0
- package/templates/api/src/domain/auth/repositories/email-code.repository.ts +9 -0
- package/templates/api/src/domain/auth/repositories/identity.repository.ts +9 -0
- package/templates/api/src/domain/auth/repositories/session.repository.ts +12 -0
- package/templates/api/src/domain/auth/repositories/social-sign-in.repository.ts +9 -0
- package/templates/api/src/domain/auth/repositories/user.repository.ts +9 -0
- package/templates/api/src/domain/auth/use-cases/complete-social-sign-in.use-case.ts +103 -0
- package/templates/api/src/domain/auth/use-cases/exchange-social-code.use-case.ts +56 -0
- package/templates/api/src/domain/auth/use-cases/find-user.use-case.ts +20 -0
- package/templates/api/src/domain/auth/use-cases/issue-email-code.use-case.ts +55 -0
- package/templates/api/src/domain/auth/use-cases/link-identity.use-case.ts +77 -0
- package/templates/api/src/domain/auth/use-cases/refresh-session.use-case.ts +94 -0
- package/templates/api/src/domain/auth/use-cases/request-email-verification.use-case.ts +21 -0
- package/templates/api/src/domain/auth/use-cases/request-password-reset.use-case.ts +21 -0
- package/templates/api/src/domain/auth/use-cases/reset-password.use-case.ts +73 -0
- package/templates/api/src/domain/auth/use-cases/sign-in.use-case.ts +38 -0
- package/templates/api/src/domain/auth/use-cases/sign-out.use-case.ts +22 -0
- package/templates/api/src/domain/auth/use-cases/sign-up.use-case.ts +57 -0
- package/templates/api/src/domain/auth/use-cases/start-session.use-case.ts +55 -0
- package/templates/api/src/domain/auth/use-cases/start-social-sign-in.use-case.ts +54 -0
- package/templates/api/src/domain/auth/use-cases/verify-email.use-case.ts +62 -0
- package/templates/api/src/domain/oauth/dto/authorization-decision.dto.ts +7 -0
- package/templates/api/src/domain/oauth/dto/authorization-view.dto.ts +14 -0
- package/templates/api/src/domain/oauth/entities/authorization.entity.ts +87 -0
- package/templates/api/src/domain/oauth/errors/authorization-not-found.error.ts +9 -0
- package/templates/api/src/domain/oauth/errors/authorization-request.error.ts +18 -0
- package/templates/api/src/domain/oauth/errors/invalid-client.error.ts +6 -0
- package/templates/api/src/domain/oauth/errors/invalid-grant.error.ts +6 -0
- package/templates/api/src/domain/oauth/ports/authorization-codes.port.ts +8 -0
- package/templates/api/src/domain/oauth/ports/client-metadata.port.ts +9 -0
- package/templates/api/src/domain/oauth/repositories/authorization.repository.ts +10 -0
- package/templates/api/src/domain/oauth/use-cases/decide-authorization.use-case.ts +53 -0
- package/templates/api/src/domain/oauth/use-cases/exchange-code.use-case.ts +97 -0
- package/templates/api/src/domain/oauth/use-cases/find-authorization.use-case.ts +24 -0
- package/templates/api/src/domain/oauth/use-cases/start-authorization.use-case.ts +89 -0
- package/templates/api/src/domain/shared/errors/domain-error.ts +2 -0
- package/templates/api/src/infra/auth/argon2-password-hasher.adapter.ts +21 -0
- package/templates/api/src/infra/auth/crypto-opaque-tokens.adapter.ts +14 -0
- package/templates/api/src/infra/auth/jose-access-tokens.adapter.ts +64 -0
- package/templates/api/src/infra/config/env.ts +17 -3
- package/templates/api/src/infra/database/mikroorm/entities/authorization.schema.ts +38 -0
- package/templates/api/src/infra/database/mikroorm/entities/email-code.schema.ts +30 -0
- package/templates/api/src/infra/database/mikroorm/entities/identity.schema.ts +29 -0
- package/templates/api/src/infra/database/mikroorm/entities/profile.schema.ts +1 -1
- package/templates/api/src/infra/database/mikroorm/entities/session.schema.ts +34 -0
- package/templates/api/src/infra/database/mikroorm/entities/social-sign-in.schema.ts +35 -0
- package/templates/api/src/infra/database/mikroorm/entities/user.schema.ts +19 -0
- package/templates/api/src/infra/database/mikroorm/mikro-orm.factory.ts +15 -4
- package/templates/api/src/infra/database/mikroorm/repositories/mikroorm-authorization.repository.ts +21 -0
- package/templates/api/src/infra/database/mikroorm/repositories/mikroorm-email-code.repository.ts +17 -0
- package/templates/api/src/infra/database/mikroorm/repositories/mikroorm-identity.repository.ts +18 -0
- package/templates/api/src/infra/database/mikroorm/repositories/mikroorm-session.repository.ts +25 -0
- package/templates/api/src/infra/database/mikroorm/repositories/mikroorm-social-sign-in.repository.ts +17 -0
- package/templates/api/src/infra/database/mikroorm/repositories/mikroorm-user.repository.ts +31 -0
- package/templates/api/src/infra/di/auth.di.ts +21 -0
- package/templates/api/src/infra/di/email.di.ts +23 -0
- package/templates/api/src/infra/di/index.ts +8 -10
- package/templates/api/src/infra/di/oauth.di.ts +14 -0
- package/templates/api/src/infra/di/social.di.ts +17 -0
- package/templates/api/src/infra/email/hmac-one-time-codes.adapter.ts +16 -0
- package/templates/api/src/infra/email/log-mailer.adapter.ts +11 -0
- package/templates/api/src/infra/http/app.ts +15 -1
- package/templates/api/src/infra/http/controllers/auth-email.controller.ts +73 -0
- package/templates/api/src/infra/http/controllers/auth.controller.ts +108 -29
- package/templates/api/src/infra/http/controllers/mcp.controller.ts +46 -27
- package/templates/api/src/infra/http/controllers/oauth.controller.ts +267 -0
- package/templates/api/src/infra/http/controllers/social.controller.ts +192 -0
- package/templates/api/src/infra/http/errors/error-handler.ts +5 -0
- package/templates/api/src/infra/http/fastify.d.ts +2 -3
- package/templates/api/src/infra/http/fetch-bridge.ts +17 -5
- package/templates/api/src/infra/http/hooks/auth.hook.ts +25 -8
- package/templates/api/src/infra/http/hooks/origin.hook.ts +22 -0
- package/templates/api/src/infra/http/server.ts +1 -4
- package/templates/api/src/infra/http/session-delivery.ts +69 -0
- package/templates/api/src/infra/mcp/mcp.scopes.ts +27 -0
- package/templates/api/src/infra/mcp/mcp.server.ts +10 -0
- package/templates/api/src/infra/mcp/tools/users.tool.ts +6 -0
- package/templates/api/src/infra/oauth/crypto-authorization-codes.adapter.ts +21 -0
- package/templates/api/src/infra/oauth/https-client-metadata.adapter.ts +231 -0
- package/templates/api/src/infra/social/openid-identity-providers.adapter.ts +199 -0
- package/templates/api/test/domain/auth/entities/session.entity.spec.ts +50 -0
- package/templates/api/test/domain/auth/use-cases/email.use-case.spec.ts +87 -0
- package/templates/api/test/domain/auth/use-cases/refresh-session.use-case.spec.ts +66 -0
- package/templates/api/test/domain/auth/use-cases/sign-in.use-case.spec.ts +29 -0
- package/templates/api/test/domain/auth/use-cases/sign-up.use-case.spec.ts +41 -0
- package/templates/api/test/domain/auth/use-cases/social.use-case.spec.ts +148 -0
- package/templates/api/test/domain/oauth/entities/authorization.entity.spec.ts +53 -0
- package/templates/api/test/infra/auth/argon2-password-hasher.adapter.spec.ts +17 -0
- package/templates/api/test/infra/auth/jose-access-tokens.adapter.spec.ts +57 -0
- package/templates/api/test/infra/database/mikroorm/mikro-orm.factory.spec.ts +1 -1
- package/templates/api/test/infra/database/mikroorm/repositories/mikroorm-user.repository.spec.ts +28 -0
- package/templates/api/test/infra/email/hmac-one-time-codes.adapter.spec.ts +15 -0
- package/templates/api/test/infra/http/controllers/auth-email.controller.spec.ts +92 -0
- package/templates/api/test/infra/http/controllers/auth.controller.spec.ts +168 -0
- package/templates/api/test/infra/http/controllers/mcp.controller.spec.ts +309 -67
- package/templates/api/test/infra/http/controllers/social.controller.spec.ts +108 -0
- package/templates/api/test/infra/http/controllers/users.controller.spec.ts +13 -35
- package/templates/api/test/infra/oauth/crypto-authorization-codes.adapter.spec.ts +18 -0
- package/templates/api/test/infra/oauth/https-client-metadata.adapter.spec.ts +58 -0
- package/templates/api/test/infra/social/openid-identity-providers.adapter.spec.ts +154 -0
- package/templates/api/test/support/factories/user.factory.ts +9 -11
- package/templates/api/test/support/fake-oidc-server.ts +96 -0
- package/templates/api/test/support/fakes/auth-fakes.ts +94 -0
- package/templates/api/test/support/fakes/fake-access-tokens.ts +19 -0
- package/templates/api/test/support/fakes/fake-mailer.ts +21 -0
- package/templates/api/test/support/fakes/fake-one-time-codes.ts +14 -0
- package/templates/api/test/support/fakes/fake-opaque-tokens.ts +14 -0
- package/templates/api/test/support/fakes/fake-password-hasher.ts +15 -0
- package/templates/api/test/support/fakes/immediate-transaction-manager.ts +8 -0
- package/templates/api/test/support/fakes/in-memory-email-code.repository.ts +15 -0
- package/templates/api/test/support/fakes/in-memory-session.repository.ts +27 -0
- package/templates/api/test/support/fakes/in-memory-user.repository.ts +20 -0
- package/templates/api/test/support/fakes/social-fakes.ts +112 -0
- package/templates/api/test/support/setup.ts +6 -28
- package/templates/api/test/support/test-app.ts +65 -0
- package/templates/mobile/README.md +1 -1
- package/templates/mobile/jest.config.js +0 -17
- package/templates/mobile/package.json +0 -4
- package/templates/mobile/scripts/ports.mjs +1 -1
- package/templates/mobile/src/api-contract/api-error.ts +4 -11
- package/templates/mobile/src/api-contract/auth.ts +221 -0
- package/templates/mobile/src/api-contract/index.ts +14 -0
- package/templates/mobile/src/app/(app)/_layout.tsx +1 -1
- package/templates/mobile/src/app/(auth)/forgot-password.tsx +20 -0
- package/templates/mobile/src/app/(auth)/reset-password.tsx +18 -0
- package/templates/mobile/src/app/(auth)/sign-in.tsx +9 -0
- package/templates/mobile/src/app/(auth)/sign-up.tsx +4 -0
- package/templates/mobile/src/app/(auth)/verify-email.tsx +18 -0
- package/templates/mobile/src/app/_layout.tsx +5 -5
- package/templates/mobile/src/features/auth/forgot-password-form.tsx +67 -0
- package/templates/mobile/src/features/auth/reset-password-form.tsx +93 -0
- package/templates/mobile/src/features/auth/sign-in-form.tsx +21 -7
- package/templates/mobile/src/features/auth/sign-out-button.tsx +4 -4
- package/templates/mobile/src/features/auth/sign-up-form.tsx +24 -8
- package/templates/mobile/src/features/auth/social-buttons.tsx +79 -0
- package/templates/mobile/src/features/auth/token-store.ts +21 -0
- package/templates/mobile/src/features/auth/verify-email-form.tsx +81 -0
- package/templates/mobile/src/features/clients/clients-context.tsx +1 -2
- package/templates/mobile/test/features/auth/authenticated-layout.spec.tsx +16 -14
- package/templates/mobile/test/features/auth/email-flows.spec.tsx +86 -0
- package/templates/mobile/test/features/auth/sign-in-form.spec.tsx +12 -11
- package/templates/mobile/test/features/auth/sign-out-button.spec.tsx +8 -9
- package/templates/mobile/test/features/auth/social-buttons.spec.tsx +77 -0
- package/templates/mobile/test/support/memory-token-store.ts +15 -0
- package/templates/site/scripts/ports.mjs +1 -1
- package/templates/web/package.json +0 -3
- package/templates/web/scripts/ports.mjs +1 -1
- package/templates/web/src/api-contract/api-error.ts +4 -11
- package/templates/web/src/api-contract/auth.ts +221 -0
- package/templates/web/src/api-contract/index.ts +14 -0
- package/templates/web/src/features/auth/forgot-password-form.tsx +52 -0
- package/templates/web/src/features/auth/reset-password-form.tsx +82 -0
- package/templates/web/src/features/auth/sign-in-form.tsx +24 -14
- package/templates/web/src/features/auth/sign-out-button.tsx +4 -4
- package/templates/web/src/features/auth/sign-up-form.tsx +16 -8
- package/templates/web/src/features/auth/social-buttons.tsx +45 -0
- package/templates/web/src/features/auth/verify-email-form.tsx +84 -0
- package/templates/web/src/features/oauth/consent-form.tsx +72 -35
- package/templates/web/src/main.tsx +6 -4
- package/templates/web/src/routeTree.gen.ts +73 -2
- package/templates/web/src/router.tsx +1 -2
- package/templates/web/src/routes/_authenticated/consent.tsx +5 -4
- package/templates/web/src/routes/_authenticated.tsx +1 -1
- package/templates/web/src/routes/forgot-password.tsx +17 -0
- package/templates/web/src/routes/reset-password.tsx +20 -0
- package/templates/web/src/routes/sign-in.tsx +10 -3
- package/templates/web/src/routes/sign-up.tsx +4 -0
- package/templates/web/src/routes/verify-email.tsx +20 -0
- package/templates/web/test/api-contract/auth.spec.ts +155 -0
- package/templates/web/test/app.spec.tsx +47 -22
- package/templates/web/test/features/auth/email-flows.spec.tsx +121 -0
- package/templates/web/test/features/auth/sign-in-form.spec.tsx +6 -2
- package/templates/web/test/features/auth/social-buttons.spec.tsx +35 -0
- package/templates/web/test/features/oauth/consent-form.spec.tsx +90 -26
- package/templates/web/test/support/render-app.tsx +4 -4
- package/templates/workspace/scripts/ports.mjs +1 -1
- package/dist/mcp.js +0 -100
- package/templates/api/docker/init/01-auth-schema.sql +0 -1
- package/templates/api/migrations/Migration20260914123239_create_profile.ts +0 -13
- package/templates/api/src/infra/auth/auth.cli.ts +0 -5
- package/templates/api/src/infra/auth/auth.factory.ts +0 -77
- package/templates/api/src/infra/database/mikroorm/entities/auth-user.schema.ts +0 -16
- package/templates/api/test/infra/auth/auth.factory.spec.ts +0 -22
- package/templates/mobile/src/features/auth/auth-client.ts +0 -27
- package/templates/mobile/src/features/auth/session.ts +0 -17
- package/templates/web/src/features/auth/auth-client.ts +0 -17
- package/templates/web/src/features/auth/session.ts +0 -17
|
@@ -10,8 +10,11 @@
|
|
|
10
10
|
|
|
11
11
|
import { Route as rootRouteImport } from './routes/__root'
|
|
12
12
|
import { Route as AuthenticatedRouteImport } from './routes/_authenticated'
|
|
13
|
+
import { Route as ForgotPasswordRouteImport } from './routes/forgot-password'
|
|
14
|
+
import { Route as ResetPasswordRouteImport } from './routes/reset-password'
|
|
13
15
|
import { Route as SignInRouteImport } from './routes/sign-in'
|
|
14
16
|
import { Route as SignUpRouteImport } from './routes/sign-up'
|
|
17
|
+
import { Route as VerifyEmailRouteImport } from './routes/verify-email'
|
|
15
18
|
import { Route as AuthenticatedIndexRouteImport } from './routes/_authenticated/index'
|
|
16
19
|
import { Route as AuthenticatedConsentRouteImport } from './routes/_authenticated/consent'
|
|
17
20
|
|
|
@@ -19,6 +22,16 @@ const AuthenticatedRoute = AuthenticatedRouteImport.update({
|
|
|
19
22
|
id: '/_authenticated',
|
|
20
23
|
getParentRoute: () => rootRouteImport,
|
|
21
24
|
} as any)
|
|
25
|
+
const ForgotPasswordRoute = ForgotPasswordRouteImport.update({
|
|
26
|
+
id: '/forgot-password',
|
|
27
|
+
path: '/forgot-password',
|
|
28
|
+
getParentRoute: () => rootRouteImport,
|
|
29
|
+
} as any)
|
|
30
|
+
const ResetPasswordRoute = ResetPasswordRouteImport.update({
|
|
31
|
+
id: '/reset-password',
|
|
32
|
+
path: '/reset-password',
|
|
33
|
+
getParentRoute: () => rootRouteImport,
|
|
34
|
+
} as any)
|
|
22
35
|
const SignInRoute = SignInRouteImport.update({
|
|
23
36
|
id: '/sign-in',
|
|
24
37
|
path: '/sign-in',
|
|
@@ -29,6 +42,11 @@ const SignUpRoute = SignUpRouteImport.update({
|
|
|
29
42
|
path: '/sign-up',
|
|
30
43
|
getParentRoute: () => rootRouteImport,
|
|
31
44
|
} as any)
|
|
45
|
+
const VerifyEmailRoute = VerifyEmailRouteImport.update({
|
|
46
|
+
id: '/verify-email',
|
|
47
|
+
path: '/verify-email',
|
|
48
|
+
getParentRoute: () => rootRouteImport,
|
|
49
|
+
} as any)
|
|
32
50
|
const AuthenticatedIndexRoute = AuthenticatedIndexRouteImport.update({
|
|
33
51
|
id: '/',
|
|
34
52
|
path: '/',
|
|
@@ -42,42 +60,71 @@ const AuthenticatedConsentRoute = AuthenticatedConsentRouteImport.update({
|
|
|
42
60
|
|
|
43
61
|
export interface FileRoutesByFullPath {
|
|
44
62
|
'/': typeof AuthenticatedIndexRoute
|
|
63
|
+
'/forgot-password': typeof ForgotPasswordRoute
|
|
64
|
+
'/reset-password': typeof ResetPasswordRoute
|
|
45
65
|
'/sign-in': typeof SignInRoute
|
|
46
66
|
'/sign-up': typeof SignUpRoute
|
|
67
|
+
'/verify-email': typeof VerifyEmailRoute
|
|
47
68
|
'/consent': typeof AuthenticatedConsentRoute
|
|
48
69
|
}
|
|
49
70
|
export interface FileRoutesByTo {
|
|
71
|
+
'/forgot-password': typeof ForgotPasswordRoute
|
|
72
|
+
'/reset-password': typeof ResetPasswordRoute
|
|
50
73
|
'/sign-in': typeof SignInRoute
|
|
51
74
|
'/sign-up': typeof SignUpRoute
|
|
75
|
+
'/verify-email': typeof VerifyEmailRoute
|
|
52
76
|
'/consent': typeof AuthenticatedConsentRoute
|
|
53
77
|
'/': typeof AuthenticatedIndexRoute
|
|
54
78
|
}
|
|
55
79
|
export interface FileRoutesById {
|
|
56
80
|
__root__: typeof rootRouteImport
|
|
57
81
|
'/_authenticated': typeof AuthenticatedRouteWithChildren
|
|
82
|
+
'/forgot-password': typeof ForgotPasswordRoute
|
|
83
|
+
'/reset-password': typeof ResetPasswordRoute
|
|
58
84
|
'/sign-in': typeof SignInRoute
|
|
59
85
|
'/sign-up': typeof SignUpRoute
|
|
86
|
+
'/verify-email': typeof VerifyEmailRoute
|
|
60
87
|
'/_authenticated/consent': typeof AuthenticatedConsentRoute
|
|
61
88
|
'/_authenticated/': typeof AuthenticatedIndexRoute
|
|
62
89
|
}
|
|
63
90
|
export interface FileRouteTypes {
|
|
64
91
|
fileRoutesByFullPath: FileRoutesByFullPath
|
|
65
|
-
fullPaths:
|
|
92
|
+
fullPaths:
|
|
93
|
+
| '/'
|
|
94
|
+
| '/forgot-password'
|
|
95
|
+
| '/reset-password'
|
|
96
|
+
| '/sign-in'
|
|
97
|
+
| '/sign-up'
|
|
98
|
+
| '/verify-email'
|
|
99
|
+
| '/consent'
|
|
66
100
|
fileRoutesByTo: FileRoutesByTo
|
|
67
|
-
to:
|
|
101
|
+
to:
|
|
102
|
+
| '/forgot-password'
|
|
103
|
+
| '/reset-password'
|
|
104
|
+
| '/sign-in'
|
|
105
|
+
| '/sign-up'
|
|
106
|
+
| '/verify-email'
|
|
107
|
+
| '/consent'
|
|
108
|
+
| '/'
|
|
68
109
|
id:
|
|
69
110
|
| '__root__'
|
|
70
111
|
| '/_authenticated'
|
|
112
|
+
| '/forgot-password'
|
|
113
|
+
| '/reset-password'
|
|
71
114
|
| '/sign-in'
|
|
72
115
|
| '/sign-up'
|
|
116
|
+
| '/verify-email'
|
|
73
117
|
| '/_authenticated/consent'
|
|
74
118
|
| '/_authenticated/'
|
|
75
119
|
fileRoutesById: FileRoutesById
|
|
76
120
|
}
|
|
77
121
|
export interface RootRouteChildren {
|
|
78
122
|
AuthenticatedRoute: typeof AuthenticatedRouteWithChildren
|
|
123
|
+
ForgotPasswordRoute: typeof ForgotPasswordRoute
|
|
124
|
+
ResetPasswordRoute: typeof ResetPasswordRoute
|
|
79
125
|
SignInRoute: typeof SignInRoute
|
|
80
126
|
SignUpRoute: typeof SignUpRoute
|
|
127
|
+
VerifyEmailRoute: typeof VerifyEmailRoute
|
|
81
128
|
}
|
|
82
129
|
|
|
83
130
|
declare module '@tanstack/react-router' {
|
|
@@ -89,6 +136,20 @@ declare module '@tanstack/react-router' {
|
|
|
89
136
|
preLoaderRoute: typeof AuthenticatedRouteImport
|
|
90
137
|
parentRoute: typeof rootRouteImport
|
|
91
138
|
}
|
|
139
|
+
'/forgot-password': {
|
|
140
|
+
id: '/forgot-password'
|
|
141
|
+
path: '/forgot-password'
|
|
142
|
+
fullPath: '/forgot-password'
|
|
143
|
+
preLoaderRoute: typeof ForgotPasswordRouteImport
|
|
144
|
+
parentRoute: typeof rootRouteImport
|
|
145
|
+
}
|
|
146
|
+
'/reset-password': {
|
|
147
|
+
id: '/reset-password'
|
|
148
|
+
path: '/reset-password'
|
|
149
|
+
fullPath: '/reset-password'
|
|
150
|
+
preLoaderRoute: typeof ResetPasswordRouteImport
|
|
151
|
+
parentRoute: typeof rootRouteImport
|
|
152
|
+
}
|
|
92
153
|
'/sign-in': {
|
|
93
154
|
id: '/sign-in'
|
|
94
155
|
path: '/sign-in'
|
|
@@ -103,6 +164,13 @@ declare module '@tanstack/react-router' {
|
|
|
103
164
|
preLoaderRoute: typeof SignUpRouteImport
|
|
104
165
|
parentRoute: typeof rootRouteImport
|
|
105
166
|
}
|
|
167
|
+
'/verify-email': {
|
|
168
|
+
id: '/verify-email'
|
|
169
|
+
path: '/verify-email'
|
|
170
|
+
fullPath: '/verify-email'
|
|
171
|
+
preLoaderRoute: typeof VerifyEmailRouteImport
|
|
172
|
+
parentRoute: typeof rootRouteImport
|
|
173
|
+
}
|
|
106
174
|
'/_authenticated/': {
|
|
107
175
|
id: '/_authenticated/'
|
|
108
176
|
path: '/'
|
|
@@ -136,8 +204,11 @@ const AuthenticatedRouteWithChildren = AuthenticatedRoute._addFileChildren(
|
|
|
136
204
|
|
|
137
205
|
const rootRouteChildren: RootRouteChildren = {
|
|
138
206
|
AuthenticatedRoute: AuthenticatedRouteWithChildren,
|
|
207
|
+
ForgotPasswordRoute: ForgotPasswordRoute,
|
|
208
|
+
ResetPasswordRoute: ResetPasswordRoute,
|
|
139
209
|
SignInRoute: SignInRoute,
|
|
140
210
|
SignUpRoute: SignUpRoute,
|
|
211
|
+
VerifyEmailRoute: VerifyEmailRoute,
|
|
141
212
|
}
|
|
142
213
|
export const routeTree = rootRouteImport
|
|
143
214
|
._addFileChildren(rootRouteChildren)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { QueryClient } from '@tanstack/react-query'
|
|
2
2
|
import { createRouter, type RouterHistory } from '@tanstack/react-router'
|
|
3
|
-
import type { ApiClient } from '@/api-contract'
|
|
4
|
-
import type { AuthClient } from '@/features/auth/auth-client'
|
|
3
|
+
import type { ApiClient, AuthClient } from '@/api-contract'
|
|
5
4
|
import { routeTree } from './routeTree.gen'
|
|
6
5
|
|
|
7
6
|
export type RouterContext = {
|
|
@@ -2,20 +2,21 @@ import { createFileRoute } from '@tanstack/react-router'
|
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
import { ConsentForm } from '@/features/oauth/consent-form'
|
|
4
4
|
|
|
5
|
-
//
|
|
5
|
+
// The API sends the browser here with the id of the authorization an MCP client asked for. Signing in first, when
|
|
6
|
+
// needed, comes back to this same address.
|
|
6
7
|
export const Route = createFileRoute('/_authenticated/consent')({
|
|
7
|
-
validateSearch: z.
|
|
8
|
+
validateSearch: z.object({ request: z.uuid() }),
|
|
8
9
|
component: ConsentPage,
|
|
9
10
|
})
|
|
10
11
|
|
|
11
12
|
function ConsentPage() {
|
|
12
13
|
const { auth } = Route.useRouteContext()
|
|
13
|
-
const {
|
|
14
|
+
const { request } = Route.useSearch()
|
|
14
15
|
|
|
15
16
|
return (
|
|
16
17
|
<>
|
|
17
18
|
<h1 className="text-2xl font-semibold">Allow access</h1>
|
|
18
|
-
<ConsentForm auth={auth}
|
|
19
|
+
<ConsentForm auth={auth} requestId={request} leave={(url) => window.location.assign(url)} />
|
|
19
20
|
</>
|
|
20
21
|
)
|
|
21
22
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
|
|
2
|
-
import { sessionQuery } from '@/
|
|
2
|
+
import { sessionQuery } from '@/api-contract'
|
|
3
3
|
|
|
4
4
|
export const Route = createFileRoute('/_authenticated')({
|
|
5
5
|
beforeLoad: async ({ context, location }) => {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
import { ForgotPasswordForm } from '@/features/auth/forgot-password-form'
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute('/forgot-password')({
|
|
5
|
+
component: ForgotPasswordPage,
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
function ForgotPasswordPage() {
|
|
9
|
+
const { auth } = Route.useRouteContext()
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<>
|
|
13
|
+
<h1 className="text-2xl font-semibold">Reset your password</h1>
|
|
14
|
+
<ForgotPasswordForm auth={auth} />
|
|
15
|
+
</>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { ResetPasswordForm } from '@/features/auth/reset-password-form'
|
|
4
|
+
|
|
5
|
+
export const Route = createFileRoute('/reset-password')({
|
|
6
|
+
validateSearch: z.object({ email: z.email() }),
|
|
7
|
+
component: ResetPasswordPage,
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
function ResetPasswordPage() {
|
|
11
|
+
const { auth } = Route.useRouteContext()
|
|
12
|
+
const { email } = Route.useSearch()
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<>
|
|
16
|
+
<h1 className="text-2xl font-semibold">Choose a new password</h1>
|
|
17
|
+
<ResetPasswordForm auth={auth} email={email} />
|
|
18
|
+
</>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import { createFileRoute } from '@tanstack/react-router'
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
import { SignInForm } from '@/features/auth/sign-in-form'
|
|
4
|
+
import { SocialButtons } from '@/features/auth/social-buttons' // prumo:social
|
|
4
5
|
|
|
5
6
|
export const Route = createFileRoute('/sign-in')({
|
|
6
|
-
validateSearch: z.object({
|
|
7
|
+
validateSearch: z.object({
|
|
8
|
+
redirect: z.string().optional(),
|
|
9
|
+
error: z.string().optional(), // prumo:social
|
|
10
|
+
}),
|
|
7
11
|
component: SignInPage,
|
|
8
12
|
})
|
|
9
13
|
|
|
10
14
|
function SignInPage() {
|
|
11
15
|
const { auth } = Route.useRouteContext()
|
|
12
|
-
const
|
|
16
|
+
const search = Route.useSearch()
|
|
13
17
|
|
|
14
18
|
return (
|
|
15
19
|
<>
|
|
16
20
|
<h1 className="text-2xl font-semibold">Sign in</h1>
|
|
17
|
-
|
|
21
|
+
{/* prumo:social */}
|
|
22
|
+
<SocialButtons auth={auth} returnTo={search.redirect} error={search.error} />
|
|
23
|
+
{/* prumo:end-social */}
|
|
24
|
+
<SignInForm auth={auth} redirect={search.redirect} />
|
|
18
25
|
</>
|
|
19
26
|
)
|
|
20
27
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createFileRoute } from '@tanstack/react-router'
|
|
2
2
|
import { SignUpForm } from '@/features/auth/sign-up-form'
|
|
3
|
+
import { SocialButtons } from '@/features/auth/social-buttons' // prumo:social
|
|
3
4
|
|
|
4
5
|
export const Route = createFileRoute('/sign-up')({
|
|
5
6
|
component: SignUpPage,
|
|
@@ -11,6 +12,9 @@ function SignUpPage() {
|
|
|
11
12
|
return (
|
|
12
13
|
<>
|
|
13
14
|
<h1 className="text-2xl font-semibold">Create an account</h1>
|
|
15
|
+
{/* prumo:social */}
|
|
16
|
+
<SocialButtons auth={auth} returnTo={undefined} error={undefined} />
|
|
17
|
+
{/* prumo:end-social */}
|
|
14
18
|
<SignUpForm auth={auth} />
|
|
15
19
|
</>
|
|
16
20
|
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { VerifyEmailForm } from '@/features/auth/verify-email-form'
|
|
4
|
+
|
|
5
|
+
export const Route = createFileRoute('/verify-email')({
|
|
6
|
+
validateSearch: z.object({ email: z.email() }),
|
|
7
|
+
component: VerifyEmailPage,
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
function VerifyEmailPage() {
|
|
11
|
+
const { auth } = Route.useRouteContext()
|
|
12
|
+
const { email } = Route.useSearch()
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<>
|
|
16
|
+
<h1 className="text-2xl font-semibold">Confirm your email</h1>
|
|
17
|
+
<VerifyEmailForm auth={auth} email={email} />
|
|
18
|
+
</>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { API_URL, fakeTransport, json } from '@test/support/fake-transport'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { createAuthClient, type TokenStore, type Tokens } from '@/api-contract'
|
|
4
|
+
|
|
5
|
+
const TOKENS: Tokens = { accessToken: 'access-1', refreshToken: 'refresh-1', expiresIn: 900 }
|
|
6
|
+
|
|
7
|
+
function memoryStore(initial: Tokens | null = null): TokenStore & { current: Tokens | null } {
|
|
8
|
+
return {
|
|
9
|
+
current: initial,
|
|
10
|
+
async read() {
|
|
11
|
+
return this.current
|
|
12
|
+
},
|
|
13
|
+
async write(tokens) {
|
|
14
|
+
this.current = tokens
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('createAuthClient on the web', () => {
|
|
20
|
+
it('refreshes once on a 401, repeats the request, and asks nothing of the body', async () => {
|
|
21
|
+
const calls: string[] = []
|
|
22
|
+
let valid = false
|
|
23
|
+
const auth = createAuthClient({
|
|
24
|
+
baseUrl: API_URL,
|
|
25
|
+
fetch: fakeTransport({
|
|
26
|
+
'GET /api/v1/things': () => {
|
|
27
|
+
calls.push('things')
|
|
28
|
+
return valid ? json(200, []) : json(401, {})
|
|
29
|
+
},
|
|
30
|
+
'POST /api/auth/refresh': (request) => {
|
|
31
|
+
calls.push(`refresh:${request.headers.get('x-auth-transport')}`)
|
|
32
|
+
valid = true
|
|
33
|
+
return new Response(null, { status: 204 })
|
|
34
|
+
},
|
|
35
|
+
}),
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const response = await auth.fetch(`${API_URL}/api/v1/things`)
|
|
39
|
+
|
|
40
|
+
expect(response.status).toBe(200)
|
|
41
|
+
expect(calls).toEqual(['things', 'refresh:null', 'things'])
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('makes one refresh for every request refused at once', async () => {
|
|
45
|
+
let refreshes = 0
|
|
46
|
+
let valid = false
|
|
47
|
+
const auth = createAuthClient({
|
|
48
|
+
baseUrl: API_URL,
|
|
49
|
+
fetch: fakeTransport({
|
|
50
|
+
'GET /api/v1/things': () => (valid ? json(200, []) : json(401, {})),
|
|
51
|
+
'POST /api/auth/refresh': async () => {
|
|
52
|
+
refreshes += 1
|
|
53
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
54
|
+
valid = true
|
|
55
|
+
return new Response(null, { status: 204 })
|
|
56
|
+
},
|
|
57
|
+
}),
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const responses = await Promise.all([1, 2, 3].map(() => auth.fetch(`${API_URL}/api/v1/things`)))
|
|
61
|
+
|
|
62
|
+
expect(responses.map((response) => response.status)).toEqual([200, 200, 200])
|
|
63
|
+
expect(refreshes).toBe(1)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('repeats the request when another tab rotated the token first', async () => {
|
|
67
|
+
let valid = false
|
|
68
|
+
const auth = createAuthClient({
|
|
69
|
+
baseUrl: API_URL,
|
|
70
|
+
fetch: fakeTransport({
|
|
71
|
+
'GET /api/v1/things': () => {
|
|
72
|
+
const response = valid ? json(200, []) : json(401, {})
|
|
73
|
+
valid = true
|
|
74
|
+
return response
|
|
75
|
+
},
|
|
76
|
+
'POST /api/auth/refresh': () => json(409, {}),
|
|
77
|
+
}),
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
expect((await auth.fetch(`${API_URL}/api/v1/things`)).status).toBe(200)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('answers a visitor without a session with null', async () => {
|
|
84
|
+
const auth = createAuthClient({
|
|
85
|
+
baseUrl: API_URL,
|
|
86
|
+
fetch: fakeTransport({
|
|
87
|
+
'GET /api/auth/session': () => json(401, {}),
|
|
88
|
+
'POST /api/auth/refresh': () => json(401, {}),
|
|
89
|
+
}),
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
expect(await auth.getSession()).toBeNull()
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
describe('createAuthClient for a native app', () => {
|
|
97
|
+
it('keeps the tokens it signs in with, and sends them as a bearer', async () => {
|
|
98
|
+
const tokens = memoryStore()
|
|
99
|
+
let authorization: string | null = null
|
|
100
|
+
const auth = createAuthClient({
|
|
101
|
+
baseUrl: API_URL,
|
|
102
|
+
tokens,
|
|
103
|
+
fetch: fakeTransport({
|
|
104
|
+
'POST /api/auth/sign-in': (request) =>
|
|
105
|
+
request.headers.get('x-auth-transport') === 'bearer' ? json(200, TOKENS) : json(400, {}),
|
|
106
|
+
'GET /api/auth/session': (request) => {
|
|
107
|
+
authorization = request.headers.get('authorization')
|
|
108
|
+
return json(200, { user: { id: 'user-1', email: 'ana@example.com' } })
|
|
109
|
+
},
|
|
110
|
+
}),
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
await auth.signIn({ email: 'ana@example.com', password: 'correct-horse-battery' })
|
|
114
|
+
await auth.getSession()
|
|
115
|
+
|
|
116
|
+
expect(tokens.current).toEqual(TOKENS)
|
|
117
|
+
expect(authorization).toBe('Bearer access-1')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('refreshes with the stored token, and forgets both when that fails', async () => {
|
|
121
|
+
const tokens = memoryStore(TOKENS)
|
|
122
|
+
let sent: unknown
|
|
123
|
+
const auth = createAuthClient({
|
|
124
|
+
baseUrl: API_URL,
|
|
125
|
+
tokens,
|
|
126
|
+
fetch: fakeTransport({
|
|
127
|
+
'GET /api/auth/session': () => json(401, {}),
|
|
128
|
+
'POST /api/auth/refresh': async (request) => {
|
|
129
|
+
sent = await request.json()
|
|
130
|
+
return json(401, {})
|
|
131
|
+
},
|
|
132
|
+
}),
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
expect(await auth.getSession()).toBeNull()
|
|
136
|
+
expect(sent).toEqual({ refreshToken: 'refresh-1' })
|
|
137
|
+
expect(tokens.current).toBeNull()
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('forgets the tokens on sign-out even when the request never arrives', async () => {
|
|
141
|
+
const tokens = memoryStore(TOKENS)
|
|
142
|
+
const auth = createAuthClient({
|
|
143
|
+
baseUrl: API_URL,
|
|
144
|
+
tokens,
|
|
145
|
+
fetch: fakeTransport({
|
|
146
|
+
'POST /api/auth/sign-out': () => {
|
|
147
|
+
throw new TypeError('Network request failed')
|
|
148
|
+
},
|
|
149
|
+
}),
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
await expect(auth.signOut()).rejects.toThrow('Network request failed')
|
|
153
|
+
expect(tokens.current).toBeNull()
|
|
154
|
+
})
|
|
155
|
+
})
|
|
@@ -5,9 +5,17 @@ import userEvent from '@testing-library/user-event'
|
|
|
5
5
|
import { describe, expect, it } from 'vitest'
|
|
6
6
|
import type { Profile } from '@/api-contract'
|
|
7
7
|
|
|
8
|
-
const user = { id: 'user-1', email: 'ana@example.com'
|
|
8
|
+
const user = { id: 'user-1', email: 'ana@example.com' }
|
|
9
9
|
|
|
10
|
-
const session = {
|
|
10
|
+
const session = { user }
|
|
11
|
+
|
|
12
|
+
const empty = (status: number) => new Response(null, { status })
|
|
13
|
+
|
|
14
|
+
// What the API answers a visitor without cookies: no session, and nothing to refresh it with.
|
|
15
|
+
const signedOut = {
|
|
16
|
+
'GET /api/auth/session': () => json(401, {}),
|
|
17
|
+
'POST /api/auth/refresh': () => json(401, {}),
|
|
18
|
+
}
|
|
11
19
|
|
|
12
20
|
const profile: Profile = {
|
|
13
21
|
id: 'profile-1',
|
|
@@ -21,7 +29,7 @@ const profile: Profile = {
|
|
|
21
29
|
|
|
22
30
|
describe('App', () => {
|
|
23
31
|
it('sends a visitor without a session to sign in', async () => {
|
|
24
|
-
renderApp('/', fakeTransport(
|
|
32
|
+
renderApp('/', fakeTransport(signedOut))
|
|
25
33
|
|
|
26
34
|
expect(await screen.findByRole('heading', { name: 'Sign in' })).toBeInTheDocument()
|
|
27
35
|
})
|
|
@@ -35,10 +43,11 @@ describe('App', () => {
|
|
|
35
43
|
renderApp(
|
|
36
44
|
'/',
|
|
37
45
|
fakeTransport({
|
|
38
|
-
|
|
39
|
-
'
|
|
46
|
+
...signedOut,
|
|
47
|
+
'GET /api/auth/session': () => (signedIn ? json(200, session) : json(401, {})),
|
|
48
|
+
'POST /api/auth/sign-in': () => {
|
|
40
49
|
signedIn = true
|
|
41
|
-
return
|
|
50
|
+
return empty(204)
|
|
42
51
|
},
|
|
43
52
|
'GET /api/v1/users/me': () => json(200, profile),
|
|
44
53
|
}),
|
|
@@ -59,17 +68,18 @@ describe('App', () => {
|
|
|
59
68
|
renderApp(
|
|
60
69
|
'/',
|
|
61
70
|
fakeTransport({
|
|
62
|
-
|
|
63
|
-
'
|
|
71
|
+
...signedOut,
|
|
72
|
+
'GET /api/auth/session': () => (registered ? json(200, session) : json(401, {})),
|
|
73
|
+
'POST /api/auth/sign-up': () => {
|
|
64
74
|
registered = true
|
|
65
|
-
return
|
|
75
|
+
return empty(201)
|
|
66
76
|
},
|
|
67
77
|
'GET /api/v1/users/me': () => json(200, profile),
|
|
68
78
|
}),
|
|
69
79
|
)
|
|
70
80
|
|
|
71
81
|
await visitor.click(await screen.findByRole('link', { name: 'Sign up' }))
|
|
72
|
-
await visitor.type(await screen.findByLabelText('Name'),
|
|
82
|
+
await visitor.type(await screen.findByLabelText('Name'), 'Ana')
|
|
73
83
|
await visitor.type(screen.getByLabelText('Email'), user.email)
|
|
74
84
|
await visitor.type(screen.getByLabelText('Password'), 'correct-horse-battery')
|
|
75
85
|
await visitor.click(screen.getByRole('button', { name: 'Create account' }))
|
|
@@ -82,7 +92,7 @@ describe('App', () => {
|
|
|
82
92
|
const people = {
|
|
83
93
|
ana: { user, profile },
|
|
84
94
|
bruno: {
|
|
85
|
-
user: { id: 'user-2', email: 'bruno@example.com'
|
|
95
|
+
user: { id: 'user-2', email: 'bruno@example.com' },
|
|
86
96
|
profile: { ...profile, id: 'profile-2', userId: 'user-2', displayName: 'Bruno' },
|
|
87
97
|
},
|
|
88
98
|
}
|
|
@@ -91,20 +101,16 @@ describe('App', () => {
|
|
|
91
101
|
renderApp(
|
|
92
102
|
'/',
|
|
93
103
|
fakeTransport({
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
current === null
|
|
98
|
-
? null
|
|
99
|
-
: { session: { id: `session-${current}` }, user: people[current].user },
|
|
100
|
-
),
|
|
104
|
+
...signedOut,
|
|
105
|
+
'GET /api/auth/session': () =>
|
|
106
|
+
current === null ? json(401, {}) : json(200, { user: people[current].user }),
|
|
101
107
|
'POST /api/auth/sign-out': () => {
|
|
102
108
|
current = null
|
|
103
|
-
return
|
|
109
|
+
return empty(204)
|
|
104
110
|
},
|
|
105
|
-
'POST /api/auth/sign-in
|
|
111
|
+
'POST /api/auth/sign-in': () => {
|
|
106
112
|
current = 'bruno'
|
|
107
|
-
return
|
|
113
|
+
return empty(204)
|
|
108
114
|
},
|
|
109
115
|
'GET /api/v1/users/me': () => json(200, current === null ? null : people[current].profile),
|
|
110
116
|
}),
|
|
@@ -121,13 +127,32 @@ describe('App', () => {
|
|
|
121
127
|
expect(screen.getByLabelText('Display name')).toHaveValue('Bruno')
|
|
122
128
|
})
|
|
123
129
|
|
|
130
|
+
// Fifteen minutes in, the access cookie has expired: the next request is refused once, refreshed, and repeated.
|
|
131
|
+
it('keeps a visitor signed in across an expired access token', async () => {
|
|
132
|
+
let refreshed = false
|
|
133
|
+
|
|
134
|
+
renderApp(
|
|
135
|
+
'/',
|
|
136
|
+
fakeTransport({
|
|
137
|
+
'GET /api/auth/session': () => (refreshed ? json(200, session) : json(401, {})),
|
|
138
|
+
'POST /api/auth/refresh': () => {
|
|
139
|
+
refreshed = true
|
|
140
|
+
return empty(204)
|
|
141
|
+
},
|
|
142
|
+
'GET /api/v1/users/me': () => json(200, profile),
|
|
143
|
+
}),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
expect(await screen.findByRole('heading', { name: 'Your profile' })).toBeInTheDocument()
|
|
147
|
+
})
|
|
148
|
+
|
|
124
149
|
it('signs a visitor out of this tab even when the sign-out request never arrives', async () => {
|
|
125
150
|
const visitor = userEvent.setup()
|
|
126
151
|
|
|
127
152
|
renderApp(
|
|
128
153
|
'/',
|
|
129
154
|
fakeTransport({
|
|
130
|
-
'GET /api/auth/
|
|
155
|
+
'GET /api/auth/session': () => json(200, session),
|
|
131
156
|
'GET /api/v1/users/me': () => json(200, profile),
|
|
132
157
|
'POST /api/auth/sign-out': () => {
|
|
133
158
|
throw new TypeError('Failed to fetch')
|