@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
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { jwtVerify, SignJWT } from 'jose'
|
|
2
|
+
import type {
|
|
3
|
+
AccessClaims,
|
|
4
|
+
AccessTokens,
|
|
5
|
+
IssuedToken,
|
|
6
|
+
} from '@/domain/auth/ports/access-tokens.port'
|
|
7
|
+
import type { Env } from '@/infra/config/env'
|
|
8
|
+
|
|
9
|
+
// One algorithm, named on both sides: a token that declares any other, `none` included, is refused.
|
|
10
|
+
const ALGORITHM = 'HS256'
|
|
11
|
+
|
|
12
|
+
export class JoseAccessTokens implements AccessTokens {
|
|
13
|
+
private readonly key: Uint8Array
|
|
14
|
+
private readonly issuer: string
|
|
15
|
+
|
|
16
|
+
constructor(env: Pick<Env, 'JWT_SECRET' | 'API_URL'>) {
|
|
17
|
+
this.key = new TextEncoder().encode(env.JWT_SECRET)
|
|
18
|
+
this.issuer = env.API_URL
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async issue(
|
|
22
|
+
{ userId, sessionId, clientId, scope }: AccessClaims,
|
|
23
|
+
expiresAt: Date,
|
|
24
|
+
audience = this.issuer,
|
|
25
|
+
): Promise<IssuedToken> {
|
|
26
|
+
const token = await new SignJWT({
|
|
27
|
+
sid: sessionId,
|
|
28
|
+
...(clientId !== undefined && { client_id: clientId }),
|
|
29
|
+
...(scope !== undefined && { scope }),
|
|
30
|
+
})
|
|
31
|
+
.setProtectedHeader({ alg: ALGORITHM })
|
|
32
|
+
.setSubject(userId)
|
|
33
|
+
.setIssuer(this.issuer)
|
|
34
|
+
.setAudience(audience)
|
|
35
|
+
.setIssuedAt()
|
|
36
|
+
.setExpirationTime(Math.floor(expiresAt.getTime() / 1000))
|
|
37
|
+
.sign(this.key)
|
|
38
|
+
|
|
39
|
+
return { token, expiresAt }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async verify(token: string, audience = this.issuer): Promise<AccessClaims | null> {
|
|
43
|
+
try {
|
|
44
|
+
const { payload } = await jwtVerify(token, this.key, {
|
|
45
|
+
algorithms: [ALGORITHM],
|
|
46
|
+
issuer: this.issuer,
|
|
47
|
+
audience,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
if (typeof payload.sub !== 'string' || typeof payload.sid !== 'string') {
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
userId: payload.sub,
|
|
56
|
+
sessionId: payload.sid,
|
|
57
|
+
...(typeof payload.client_id === 'string' && { clientId: payload.client_id }),
|
|
58
|
+
...(typeof payload.scope === 'string' && { scope: payload.scope }),
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -5,14 +5,28 @@ const envSchema = z.object({
|
|
|
5
5
|
NODE_ENV: z.enum(['development', 'test', 'production']),
|
|
6
6
|
PORT: z.coerce.number().int().min(1).max(65535),
|
|
7
7
|
DATABASE_URL: z.string().min(1),
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
JWT_SECRET: z.string().min(32),
|
|
9
|
+
// Where this API is reached from outside: the issuer of its tokens, and what decides whether cookies are Secure.
|
|
10
|
+
API_URL: z.url(),
|
|
11
11
|
WEB_ORIGIN: z.string().min(1),
|
|
12
|
+
// prumo:social
|
|
13
|
+
// Where a mobile app is sent back after signing in with a provider: its own scheme, as in app.json.
|
|
12
14
|
MOBILE_APP_SCHEME: z
|
|
13
15
|
.string()
|
|
14
16
|
.regex(/^[a-z][a-z0-9+.-]*$/)
|
|
15
17
|
.optional(),
|
|
18
|
+
// prumo:end-social
|
|
19
|
+
// prumo:google
|
|
20
|
+
GOOGLE_CLIENT_ID: z.string().min(1).optional(),
|
|
21
|
+
GOOGLE_CLIENT_SECRET: z.string().min(1).optional(),
|
|
22
|
+
// prumo:end-google
|
|
23
|
+
// prumo:apple
|
|
24
|
+
APPLE_CLIENT_ID: z.string().min(1).optional(),
|
|
25
|
+
APPLE_TEAM_ID: z.string().min(1).optional(),
|
|
26
|
+
APPLE_KEY_ID: z.string().min(1).optional(),
|
|
27
|
+
// The .p8 key's contents; on one line, with its line breaks written as \n.
|
|
28
|
+
APPLE_PRIVATE_KEY: z.string().min(1).optional(),
|
|
29
|
+
// prumo:end-apple
|
|
16
30
|
LOG_LEVEL: z.enum(['error', 'warn', 'info', 'debug']),
|
|
17
31
|
})
|
|
18
32
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { EntitySchema } from '@mikro-orm/core'
|
|
2
|
+
import { Authorization } from '@/domain/oauth/entities/authorization.entity'
|
|
3
|
+
|
|
4
|
+
export const AuthorizationSchema = new EntitySchema({
|
|
5
|
+
class: Authorization,
|
|
6
|
+
properties: {
|
|
7
|
+
id: { type: 'uuid', primary: true, defaultRaw: 'uuidv7()' },
|
|
8
|
+
clientId: { type: 'text' },
|
|
9
|
+
clientName: { type: 'text' },
|
|
10
|
+
redirectUri: { type: 'text' },
|
|
11
|
+
codeChallenge: { type: 'text' },
|
|
12
|
+
clientState: { type: 'text', nullable: true },
|
|
13
|
+
resource: { type: 'text' },
|
|
14
|
+
scope: { type: 'text' },
|
|
15
|
+
status: { type: 'text' },
|
|
16
|
+
userId: {
|
|
17
|
+
kind: 'm:1',
|
|
18
|
+
// EntitySchema cannot type a to-one relation held as its primary key on a string property.
|
|
19
|
+
entity: () => 'User' as never,
|
|
20
|
+
mapToPk: true,
|
|
21
|
+
fieldName: 'user_id',
|
|
22
|
+
nullable: true,
|
|
23
|
+
index: true,
|
|
24
|
+
// An authorization means nothing without the user who gave it.
|
|
25
|
+
deleteRule: 'cascade',
|
|
26
|
+
},
|
|
27
|
+
codeHash: { type: 'text', nullable: true },
|
|
28
|
+
sessionId: { type: 'uuid', nullable: true },
|
|
29
|
+
expiresAt: { type: 'datetime' },
|
|
30
|
+
createdAt: { type: 'datetime', onCreate: () => new Date(), defaultRaw: 'now()' },
|
|
31
|
+
updatedAt: {
|
|
32
|
+
type: 'datetime',
|
|
33
|
+
onCreate: () => new Date(),
|
|
34
|
+
onUpdate: () => new Date(),
|
|
35
|
+
defaultRaw: 'now()',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { EntitySchema } from '@mikro-orm/core'
|
|
2
|
+
import { EmailCode } from '@/domain/auth/entities/email-code.entity'
|
|
3
|
+
|
|
4
|
+
export const EmailCodeSchema = new EntitySchema({
|
|
5
|
+
class: EmailCode,
|
|
6
|
+
uniques: [{ properties: ['userId', 'purpose'] }],
|
|
7
|
+
properties: {
|
|
8
|
+
id: { type: 'uuid', primary: true, defaultRaw: 'uuidv7()' },
|
|
9
|
+
userId: {
|
|
10
|
+
kind: 'm:1',
|
|
11
|
+
// EntitySchema cannot type a to-one relation held as its primary key on a string property.
|
|
12
|
+
entity: () => 'User' as never,
|
|
13
|
+
mapToPk: true,
|
|
14
|
+
fieldName: 'user_id',
|
|
15
|
+
// A code means nothing without its user.
|
|
16
|
+
deleteRule: 'cascade',
|
|
17
|
+
},
|
|
18
|
+
purpose: { type: 'text' },
|
|
19
|
+
codeHash: { type: 'text' },
|
|
20
|
+
attemptsLeft: { type: 'integer' },
|
|
21
|
+
expiresAt: { type: 'datetime' },
|
|
22
|
+
createdAt: { type: 'datetime', onCreate: () => new Date(), defaultRaw: 'now()' },
|
|
23
|
+
updatedAt: {
|
|
24
|
+
type: 'datetime',
|
|
25
|
+
onCreate: () => new Date(),
|
|
26
|
+
onUpdate: () => new Date(),
|
|
27
|
+
defaultRaw: 'now()',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { EntitySchema } from '@mikro-orm/core'
|
|
2
|
+
import { Identity } from '@/domain/auth/entities/identity.entity'
|
|
3
|
+
|
|
4
|
+
export const IdentitySchema = new EntitySchema({
|
|
5
|
+
class: Identity,
|
|
6
|
+
uniques: [{ properties: ['provider', 'subject'] }],
|
|
7
|
+
properties: {
|
|
8
|
+
id: { type: 'uuid', primary: true, defaultRaw: 'uuidv7()' },
|
|
9
|
+
userId: {
|
|
10
|
+
kind: 'm:1',
|
|
11
|
+
// EntitySchema cannot type a to-one relation held as its primary key on a string property.
|
|
12
|
+
entity: () => 'User' as never,
|
|
13
|
+
mapToPk: true,
|
|
14
|
+
fieldName: 'user_id',
|
|
15
|
+
index: true,
|
|
16
|
+
// An identity means nothing without its user.
|
|
17
|
+
deleteRule: 'cascade',
|
|
18
|
+
},
|
|
19
|
+
provider: { type: 'text' },
|
|
20
|
+
subject: { type: 'text' },
|
|
21
|
+
createdAt: { type: 'datetime', onCreate: () => new Date(), defaultRaw: 'now()' },
|
|
22
|
+
updatedAt: {
|
|
23
|
+
type: 'datetime',
|
|
24
|
+
onCreate: () => new Date(),
|
|
25
|
+
onUpdate: () => new Date(),
|
|
26
|
+
defaultRaw: 'now()',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
})
|
|
@@ -8,7 +8,7 @@ export const ProfileSchema = new EntitySchema({
|
|
|
8
8
|
userId: {
|
|
9
9
|
kind: 'm:1',
|
|
10
10
|
// EntitySchema cannot type a to-one relation held as its primary key on a string property.
|
|
11
|
-
entity: () => '
|
|
11
|
+
entity: () => 'User' as never,
|
|
12
12
|
mapToPk: true,
|
|
13
13
|
fieldName: 'user_id',
|
|
14
14
|
unique: true,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { EntitySchema } from '@mikro-orm/core'
|
|
2
|
+
import { Session } from '@/domain/auth/entities/session.entity'
|
|
3
|
+
|
|
4
|
+
export const SessionSchema = new EntitySchema({
|
|
5
|
+
class: Session,
|
|
6
|
+
properties: {
|
|
7
|
+
id: { type: 'uuid', primary: true, defaultRaw: 'uuidv7()' },
|
|
8
|
+
userId: {
|
|
9
|
+
kind: 'm:1',
|
|
10
|
+
// EntitySchema cannot type a to-one relation held as its primary key on a string property.
|
|
11
|
+
entity: () => 'User' as never,
|
|
12
|
+
mapToPk: true,
|
|
13
|
+
fieldName: 'user_id',
|
|
14
|
+
index: true,
|
|
15
|
+
// A session means nothing without its user.
|
|
16
|
+
deleteRule: 'cascade',
|
|
17
|
+
},
|
|
18
|
+
tokenHash: { type: 'text' },
|
|
19
|
+
clientId: { type: 'text', nullable: true },
|
|
20
|
+
resource: { type: 'text', nullable: true },
|
|
21
|
+
scope: { type: 'text', nullable: true },
|
|
22
|
+
previousTokenHash: { type: 'text', nullable: true },
|
|
23
|
+
rotatedAt: { type: 'datetime', nullable: true },
|
|
24
|
+
expiresAt: { type: 'datetime' },
|
|
25
|
+
revokedAt: { type: 'datetime', nullable: true },
|
|
26
|
+
createdAt: { type: 'datetime', onCreate: () => new Date(), defaultRaw: 'now()' },
|
|
27
|
+
updatedAt: {
|
|
28
|
+
type: 'datetime',
|
|
29
|
+
onCreate: () => new Date(),
|
|
30
|
+
onUpdate: () => new Date(),
|
|
31
|
+
defaultRaw: 'now()',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { EntitySchema } from '@mikro-orm/core'
|
|
2
|
+
import { SocialSignIn } from '@/domain/auth/entities/social-sign-in.entity'
|
|
3
|
+
|
|
4
|
+
export const SocialSignInSchema = new EntitySchema({
|
|
5
|
+
class: SocialSignIn,
|
|
6
|
+
properties: {
|
|
7
|
+
id: { type: 'uuid', primary: true, defaultRaw: 'uuidv7()' },
|
|
8
|
+
provider: { type: 'text' },
|
|
9
|
+
client: { type: 'text' },
|
|
10
|
+
returnTo: { type: 'text' },
|
|
11
|
+
browserHash: { type: 'text' },
|
|
12
|
+
codeVerifier: { type: 'text', nullable: true },
|
|
13
|
+
nonce: { type: 'text' },
|
|
14
|
+
status: { type: 'text' },
|
|
15
|
+
userId: {
|
|
16
|
+
kind: 'm:1',
|
|
17
|
+
// EntitySchema cannot type a to-one relation held as its primary key on a string property.
|
|
18
|
+
entity: () => 'User' as never,
|
|
19
|
+
mapToPk: true,
|
|
20
|
+
fieldName: 'user_id',
|
|
21
|
+
nullable: true,
|
|
22
|
+
index: true,
|
|
23
|
+
deleteRule: 'cascade',
|
|
24
|
+
},
|
|
25
|
+
exchangeHash: { type: 'text', nullable: true },
|
|
26
|
+
expiresAt: { type: 'datetime' },
|
|
27
|
+
createdAt: { type: 'datetime', onCreate: () => new Date(), defaultRaw: 'now()' },
|
|
28
|
+
updatedAt: {
|
|
29
|
+
type: 'datetime',
|
|
30
|
+
onCreate: () => new Date(),
|
|
31
|
+
onUpdate: () => new Date(),
|
|
32
|
+
defaultRaw: 'now()',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { EntitySchema } from '@mikro-orm/core'
|
|
2
|
+
import { User } from '@/domain/auth/entities/user.entity'
|
|
3
|
+
|
|
4
|
+
export const UserSchema = new EntitySchema({
|
|
5
|
+
class: User,
|
|
6
|
+
properties: {
|
|
7
|
+
id: { type: 'uuid', primary: true, defaultRaw: 'uuidv7()' },
|
|
8
|
+
email: { type: 'text', unique: true },
|
|
9
|
+
passwordHash: { type: 'text', nullable: true },
|
|
10
|
+
emailVerifiedAt: { type: 'datetime', nullable: true },
|
|
11
|
+
createdAt: { type: 'datetime', onCreate: () => new Date(), defaultRaw: 'now()' },
|
|
12
|
+
updatedAt: {
|
|
13
|
+
type: 'datetime',
|
|
14
|
+
onCreate: () => new Date(),
|
|
15
|
+
onUpdate: () => new Date(),
|
|
16
|
+
defaultRaw: 'now()',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
})
|
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
import { Migrator } from '@mikro-orm/migrations'
|
|
2
2
|
import { defineConfig } from '@mikro-orm/postgresql'
|
|
3
3
|
import type { Env } from '@/infra/config/env'
|
|
4
|
-
import {
|
|
4
|
+
import { AuthorizationSchema } from './entities/authorization.schema' // prumo:mcp
|
|
5
|
+
import { EmailCodeSchema } from './entities/email-code.schema' // prumo:email
|
|
6
|
+
import { IdentitySchema } from './entities/identity.schema' // prumo:social
|
|
5
7
|
import { ProfileSchema } from './entities/profile.schema'
|
|
8
|
+
import { SessionSchema } from './entities/session.schema'
|
|
9
|
+
import { SocialSignInSchema } from './entities/social-sign-in.schema' // prumo:social
|
|
10
|
+
import { UserSchema } from './entities/user.schema'
|
|
6
11
|
|
|
7
12
|
export function createOrmConfig(env: Pick<Env, 'DATABASE_URL' | 'NODE_ENV'>) {
|
|
8
13
|
return defineConfig({
|
|
9
14
|
clientUrl: env.DATABASE_URL,
|
|
10
|
-
entities: [
|
|
15
|
+
entities: [
|
|
16
|
+
UserSchema,
|
|
17
|
+
ProfileSchema,
|
|
18
|
+
SessionSchema,
|
|
19
|
+
AuthorizationSchema, // prumo:mcp
|
|
20
|
+
EmailCodeSchema, // prumo:email
|
|
21
|
+
IdentitySchema, // prumo:social
|
|
22
|
+
SocialSignInSchema, // prumo:social
|
|
23
|
+
],
|
|
11
24
|
extensions: [Migrator],
|
|
12
25
|
migrations: { path: './migrations', snapshot: false },
|
|
13
|
-
// Better Auth migrates its own schema; without this, every generated migration drops it.
|
|
14
|
-
schemaGenerator: { ignoreSchema: ['auth'], skipTables: ['auth.user'] },
|
|
15
26
|
debug: env.NODE_ENV === 'development',
|
|
16
27
|
})
|
|
17
28
|
}
|
package/templates/api/src/infra/database/mikroorm/repositories/mikroorm-authorization.repository.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { EntityManager, LockMode } from '@mikro-orm/postgresql'
|
|
2
|
+
import { injectable } from 'tsyringe'
|
|
3
|
+
import { Authorization } from '@/domain/oauth/entities/authorization.entity'
|
|
4
|
+
import type { AuthorizationRepository } from '@/domain/oauth/repositories/authorization.repository'
|
|
5
|
+
|
|
6
|
+
@injectable()
|
|
7
|
+
export class MikroOrmAuthorizationRepository implements AuthorizationRepository {
|
|
8
|
+
constructor(private readonly em: EntityManager) {}
|
|
9
|
+
|
|
10
|
+
findById(id: string): Promise<Authorization | null> {
|
|
11
|
+
return this.em.findOne(Authorization, { id })
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
lockById(id: string): Promise<Authorization | null> {
|
|
15
|
+
return this.em.findOne(Authorization, { id }, { lockMode: LockMode.PESSIMISTIC_WRITE })
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async save(authorization: Authorization): Promise<void> {
|
|
19
|
+
await this.em.persist(authorization).flush()
|
|
20
|
+
}
|
|
21
|
+
}
|
package/templates/api/src/infra/database/mikroorm/repositories/mikroorm-email-code.repository.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { EntityManager, LockMode } from '@mikro-orm/postgresql'
|
|
2
|
+
import { injectable } from 'tsyringe'
|
|
3
|
+
import { EmailCode, type EmailCodePurpose } from '@/domain/auth/entities/email-code.entity'
|
|
4
|
+
import type { EmailCodeRepository } from '@/domain/auth/repositories/email-code.repository'
|
|
5
|
+
|
|
6
|
+
@injectable()
|
|
7
|
+
export class MikroOrmEmailCodeRepository implements EmailCodeRepository {
|
|
8
|
+
constructor(private readonly em: EntityManager) {}
|
|
9
|
+
|
|
10
|
+
lockFor(userId: string, purpose: EmailCodePurpose): Promise<EmailCode | null> {
|
|
11
|
+
return this.em.findOne(EmailCode, { userId, purpose }, { lockMode: LockMode.PESSIMISTIC_WRITE })
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async save(code: EmailCode): Promise<void> {
|
|
15
|
+
await this.em.persist(code).flush()
|
|
16
|
+
}
|
|
17
|
+
}
|
package/templates/api/src/infra/database/mikroorm/repositories/mikroorm-identity.repository.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
+
import { injectable } from 'tsyringe'
|
|
3
|
+
import { Identity } from '@/domain/auth/entities/identity.entity'
|
|
4
|
+
import type { ProviderName } from '@/domain/auth/ports/identity-providers.port'
|
|
5
|
+
import type { IdentityRepository } from '@/domain/auth/repositories/identity.repository'
|
|
6
|
+
|
|
7
|
+
@injectable()
|
|
8
|
+
export class MikroOrmIdentityRepository implements IdentityRepository {
|
|
9
|
+
constructor(private readonly em: EntityManager) {}
|
|
10
|
+
|
|
11
|
+
findBySubject(provider: ProviderName, subject: string): Promise<Identity | null> {
|
|
12
|
+
return this.em.findOne(Identity, { provider, subject })
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async save(identity: Identity): Promise<void> {
|
|
16
|
+
await this.em.persist(identity).flush()
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { EntityManager, LockMode } from '@mikro-orm/postgresql'
|
|
2
|
+
import { injectable } from 'tsyringe'
|
|
3
|
+
import { Session } from '@/domain/auth/entities/session.entity'
|
|
4
|
+
import type { SessionRepository } from '@/domain/auth/repositories/session.repository'
|
|
5
|
+
|
|
6
|
+
@injectable()
|
|
7
|
+
export class MikroOrmSessionRepository implements SessionRepository {
|
|
8
|
+
constructor(private readonly em: EntityManager) {}
|
|
9
|
+
|
|
10
|
+
findById(id: string): Promise<Session | null> {
|
|
11
|
+
return this.em.findOne(Session, { id })
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
lockById(id: string): Promise<Session | null> {
|
|
15
|
+
return this.em.findOne(Session, { id }, { lockMode: LockMode.PESSIMISTIC_WRITE })
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async save(session: Session): Promise<void> {
|
|
19
|
+
await this.em.persist(session).flush()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async revokeAllFor(userId: string, now: Date): Promise<void> {
|
|
23
|
+
await this.em.nativeUpdate(Session, { userId, revokedAt: null }, { revokedAt: now })
|
|
24
|
+
}
|
|
25
|
+
}
|
package/templates/api/src/infra/database/mikroorm/repositories/mikroorm-social-sign-in.repository.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { EntityManager, LockMode } from '@mikro-orm/postgresql'
|
|
2
|
+
import { injectable } from 'tsyringe'
|
|
3
|
+
import { SocialSignIn } from '@/domain/auth/entities/social-sign-in.entity'
|
|
4
|
+
import type { SocialSignInRepository } from '@/domain/auth/repositories/social-sign-in.repository'
|
|
5
|
+
|
|
6
|
+
@injectable()
|
|
7
|
+
export class MikroOrmSocialSignInRepository implements SocialSignInRepository {
|
|
8
|
+
constructor(private readonly em: EntityManager) {}
|
|
9
|
+
|
|
10
|
+
lockById(id: string): Promise<SocialSignIn | null> {
|
|
11
|
+
return this.em.findOne(SocialSignIn, { id }, { lockMode: LockMode.PESSIMISTIC_WRITE })
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async save(signIn: SocialSignIn): Promise<void> {
|
|
15
|
+
await this.em.persist(signIn).flush()
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { EntityManager, UniqueConstraintViolationException } from '@mikro-orm/postgresql'
|
|
2
|
+
import { injectable } from 'tsyringe'
|
|
3
|
+
import { User } from '@/domain/auth/entities/user.entity'
|
|
4
|
+
import { EmailTakenError } from '@/domain/auth/errors/email-taken.error'
|
|
5
|
+
import type { UserRepository } from '@/domain/auth/repositories/user.repository'
|
|
6
|
+
|
|
7
|
+
@injectable()
|
|
8
|
+
export class MikroOrmUserRepository implements UserRepository {
|
|
9
|
+
constructor(private readonly em: EntityManager) {}
|
|
10
|
+
|
|
11
|
+
findById(id: string): Promise<User | null> {
|
|
12
|
+
return this.em.findOne(User, { id })
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
findByEmail(email: string): Promise<User | null> {
|
|
16
|
+
return this.em.findOne(User, { email })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async save(user: User): Promise<void> {
|
|
20
|
+
try {
|
|
21
|
+
await this.em.persist(user).flush()
|
|
22
|
+
} catch (error) {
|
|
23
|
+
// Two sign-ups with one email can both pass the check; the unique index settles which one exists.
|
|
24
|
+
if (error instanceof UniqueConstraintViolationException) {
|
|
25
|
+
throw new EmailTakenError()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
throw error
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { DependencyContainer } from 'tsyringe'
|
|
2
|
+
import { ACCESS_TOKENS } from '@/domain/auth/ports/access-tokens.port'
|
|
3
|
+
import { OPAQUE_TOKENS } from '@/domain/auth/ports/opaque-tokens.port'
|
|
4
|
+
import { PASSWORD_HASHER } from '@/domain/auth/ports/password-hasher.port'
|
|
5
|
+
import { SESSION_REPOSITORY } from '@/domain/auth/repositories/session.repository'
|
|
6
|
+
import { USER_REPOSITORY } from '@/domain/auth/repositories/user.repository'
|
|
7
|
+
import { Argon2PasswordHasher } from '@/infra/auth/argon2-password-hasher.adapter'
|
|
8
|
+
import { CryptoOpaqueTokens } from '@/infra/auth/crypto-opaque-tokens.adapter'
|
|
9
|
+
import { JoseAccessTokens } from '@/infra/auth/jose-access-tokens.adapter'
|
|
10
|
+
import { ENV, type Env } from '@/infra/config/env'
|
|
11
|
+
import { MikroOrmSessionRepository } from '@/infra/database/mikroorm/repositories/mikroorm-session.repository'
|
|
12
|
+
import { MikroOrmUserRepository } from '@/infra/database/mikroorm/repositories/mikroorm-user.repository'
|
|
13
|
+
|
|
14
|
+
export function registerAuth(container: DependencyContainer): void {
|
|
15
|
+
container.register(USER_REPOSITORY, { useClass: MikroOrmUserRepository })
|
|
16
|
+
container.register(SESSION_REPOSITORY, { useClass: MikroOrmSessionRepository })
|
|
17
|
+
// One instance each: the hasher keeps its decoy hash, the signer its key.
|
|
18
|
+
container.register(PASSWORD_HASHER, { useValue: new Argon2PasswordHasher() })
|
|
19
|
+
container.register(ACCESS_TOKENS, { useValue: new JoseAccessTokens(container.resolve<Env>(ENV)) })
|
|
20
|
+
container.register(OPAQUE_TOKENS, { useValue: new CryptoOpaqueTokens() })
|
|
21
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { DependencyContainer } from 'tsyringe'
|
|
2
|
+
import { MAILER } from '@/domain/auth/ports/mailer.port'
|
|
3
|
+
import { ONE_TIME_CODES } from '@/domain/auth/ports/one-time-codes.port'
|
|
4
|
+
import { EMAIL_CODE_REPOSITORY } from '@/domain/auth/repositories/email-code.repository'
|
|
5
|
+
import { ENV, type Env } from '@/infra/config/env'
|
|
6
|
+
import { MikroOrmEmailCodeRepository } from '@/infra/database/mikroorm/repositories/mikroorm-email-code.repository'
|
|
7
|
+
import { HmacOneTimeCodes } from '@/infra/email/hmac-one-time-codes.adapter'
|
|
8
|
+
import { LogMailer } from '@/infra/email/log-mailer.adapter'
|
|
9
|
+
|
|
10
|
+
export function registerEmail(container: DependencyContainer): void {
|
|
11
|
+
const env = container.resolve<Env>(ENV)
|
|
12
|
+
|
|
13
|
+
container.register(EMAIL_CODE_REPOSITORY, { useClass: MikroOrmEmailCodeRepository })
|
|
14
|
+
container.register(ONE_TIME_CODES, { useValue: new HmacOneTimeCodes(env) })
|
|
15
|
+
// Silent under test, where a test registers its own Mailer to read what was sent.
|
|
16
|
+
container.register(MAILER, {
|
|
17
|
+
useValue: new LogMailer((line) => {
|
|
18
|
+
if (env.NODE_ENV !== 'test') {
|
|
19
|
+
console.info(line)
|
|
20
|
+
}
|
|
21
|
+
}),
|
|
22
|
+
})
|
|
23
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { EntityManager, MikroORM } from '@mikro-orm/postgresql'
|
|
2
2
|
import { type DependencyContainer, container as root } from 'tsyringe'
|
|
3
3
|
import { TRANSACTION_MANAGER } from '@/domain/shared/transactions/transaction-manager'
|
|
4
|
-
import { CreateProfileUseCase } from '@/domain/users/use-cases/create-profile.use-case'
|
|
5
|
-
import { AUTH, createAuth } from '@/infra/auth/auth.factory'
|
|
6
4
|
import { ENV, type Env } from '@/infra/config/env'
|
|
7
5
|
import { MikroOrmTransactionManager } from '@/infra/database/mikroorm/transactions/mikroorm-transaction-manager'
|
|
6
|
+
import { registerAuth } from './auth.di'
|
|
7
|
+
import { registerEmail } from './email.di' // prumo:email
|
|
8
|
+
import { registerOauth } from './oauth.di' // prumo:mcp
|
|
9
|
+
import { registerSocial } from './social.di' // prumo:social
|
|
8
10
|
import { registerUsers } from './users.di'
|
|
9
11
|
|
|
10
12
|
export type Dependencies = { env: Env; orm: MikroORM }
|
|
@@ -18,15 +20,11 @@ export function createContainer({ env, orm }: Dependencies): DependencyContainer
|
|
|
18
20
|
container.register(EntityManager, { useValue: orm.em })
|
|
19
21
|
container.register(TRANSACTION_MANAGER, { useClass: MikroOrmTransactionManager })
|
|
20
22
|
|
|
23
|
+
registerAuth(container)
|
|
21
24
|
registerUsers(container)
|
|
22
|
-
|
|
23
|
-
container
|
|
24
|
-
|
|
25
|
-
onUserCreated: async (user) => {
|
|
26
|
-
await container.resolve(CreateProfileUseCase).execute(user.id, user.name)
|
|
27
|
-
},
|
|
28
|
-
}),
|
|
29
|
-
})
|
|
25
|
+
registerOauth(container) // prumo:mcp
|
|
26
|
+
registerEmail(container) // prumo:email
|
|
27
|
+
registerSocial(container) // prumo:social
|
|
30
28
|
|
|
31
29
|
return container
|
|
32
30
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DependencyContainer } from 'tsyringe'
|
|
2
|
+
import { AUTHORIZATION_CODES } from '@/domain/oauth/ports/authorization-codes.port'
|
|
3
|
+
import { CLIENT_METADATA } from '@/domain/oauth/ports/client-metadata.port'
|
|
4
|
+
import { AUTHORIZATION_REPOSITORY } from '@/domain/oauth/repositories/authorization.repository'
|
|
5
|
+
import { MikroOrmAuthorizationRepository } from '@/infra/database/mikroorm/repositories/mikroorm-authorization.repository'
|
|
6
|
+
import { CryptoAuthorizationCodes } from '@/infra/oauth/crypto-authorization-codes.adapter'
|
|
7
|
+
import { HttpsClientMetadata } from '@/infra/oauth/https-client-metadata.adapter'
|
|
8
|
+
|
|
9
|
+
export function registerOauth(container: DependencyContainer): void {
|
|
10
|
+
container.register(AUTHORIZATION_REPOSITORY, { useClass: MikroOrmAuthorizationRepository })
|
|
11
|
+
container.register(AUTHORIZATION_CODES, { useValue: new CryptoAuthorizationCodes() })
|
|
12
|
+
// One instance, so its cache of client documents outlives a request.
|
|
13
|
+
container.register(CLIENT_METADATA, { useValue: new HttpsClientMetadata() })
|
|
14
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { DependencyContainer } from 'tsyringe'
|
|
2
|
+
import { IDENTITY_PROVIDERS } from '@/domain/auth/ports/identity-providers.port'
|
|
3
|
+
import { IDENTITY_REPOSITORY } from '@/domain/auth/repositories/identity.repository'
|
|
4
|
+
import { SOCIAL_SIGN_IN_REPOSITORY } from '@/domain/auth/repositories/social-sign-in.repository'
|
|
5
|
+
import { ENV, type Env } from '@/infra/config/env'
|
|
6
|
+
import { MikroOrmIdentityRepository } from '@/infra/database/mikroorm/repositories/mikroorm-identity.repository'
|
|
7
|
+
import { MikroOrmSocialSignInRepository } from '@/infra/database/mikroorm/repositories/mikroorm-social-sign-in.repository'
|
|
8
|
+
import { OpenIdIdentityProviders } from '@/infra/social/openid-identity-providers.adapter'
|
|
9
|
+
|
|
10
|
+
export function registerSocial(container: DependencyContainer): void {
|
|
11
|
+
container.register(IDENTITY_REPOSITORY, { useClass: MikroOrmIdentityRepository })
|
|
12
|
+
container.register(SOCIAL_SIGN_IN_REPOSITORY, { useClass: MikroOrmSocialSignInRepository })
|
|
13
|
+
// One instance, so each provider's discovery document is fetched once.
|
|
14
|
+
container.register(IDENTITY_PROVIDERS, {
|
|
15
|
+
useValue: new OpenIdIdentityProviders(container.resolve<Env>(ENV)),
|
|
16
|
+
})
|
|
17
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createHmac, randomInt } from 'node:crypto'
|
|
2
|
+
import type { OneTimeCodes } from '@/domain/auth/ports/one-time-codes.port'
|
|
3
|
+
import type { Env } from '@/infra/config/env'
|
|
4
|
+
|
|
5
|
+
export class HmacOneTimeCodes implements OneTimeCodes {
|
|
6
|
+
constructor(private readonly env: Pick<Env, 'JWT_SECRET'>) {}
|
|
7
|
+
|
|
8
|
+
create(): string {
|
|
9
|
+
return randomInt(0, 1_000_000).toString().padStart(6, '0')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// A million codes hash in a moment, so the hash is keyed by a secret the database does not hold.
|
|
13
|
+
digest(code: string): string {
|
|
14
|
+
return createHmac('sha256', this.env.JWT_SECRET).update(code).digest('base64url')
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Mail, Mailer } from '@/domain/auth/ports/mailer.port'
|
|
2
|
+
|
|
3
|
+
// Writes each mail to the log instead of sending it: enough to develop against, and nothing to sign up for. A
|
|
4
|
+
// project that sends real mail gives the Mailer port another adapter; see .prumo/email/.
|
|
5
|
+
export class LogMailer implements Mailer {
|
|
6
|
+
constructor(private readonly write: (line: string) => void) {}
|
|
7
|
+
|
|
8
|
+
async send(mail: Mail): Promise<void> {
|
|
9
|
+
this.write(`[mail] to ${mail.to}: ${mail.subject}\n${mail.text}`)
|
|
10
|
+
}
|
|
11
|
+
}
|