@stjoseph/prumo 0.1.1 → 0.3.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 +50 -48
- 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 -5
- package/.prumo-templates/database/testing.md +6 -8
- package/.prumo-templates/mcp/server.md +39 -12
- 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/README.md +19 -14
- package/dist/clean.js +13 -3
- package/dist/cli.js +38 -9
- package/dist/commands.js +7 -4
- package/dist/compose.js +4 -12
- package/dist/doctor.js +31 -17
- package/dist/generate.js +56 -9
- package/dist/mcp.js +15 -8
- package/dist/names.js +42 -0
- package/dist/questions.js +32 -16
- package/package.json +2 -2
- package/templates/api/.env.example +3 -5
- package/templates/api/README.md +5 -6
- package/templates/api/docker-compose.yml +0 -1
- package/templates/api/migrations/Migration20261002210629_create_user_profile_session.ts +31 -0
- package/templates/api/migrations/Migration20261002210631_create_authorization.ts +18 -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/refresh.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/entities/session.entity.ts +76 -0
- package/templates/api/src/domain/auth/entities/user.entity.ts +17 -0
- package/templates/api/src/domain/auth/errors/email-taken.error.ts +9 -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/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/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/session.repository.ts +10 -0
- package/templates/api/src/domain/auth/repositories/user.repository.ts +9 -0
- package/templates/api/src/domain/auth/use-cases/find-user.use-case.ts +20 -0
- package/templates/api/src/domain/auth/use-cases/refresh-session.use-case.ts +92 -0
- package/templates/api/src/domain/auth/use-cases/sign-in.use-case.ts +30 -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 +46 -0
- package/templates/api/src/domain/auth/use-cases/start-session.use-case.ts +48 -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 +13 -0
- package/templates/api/src/domain/oauth/entities/authorization.entity.ts +83 -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 +14 -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 +96 -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 +75 -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 +62 -0
- package/templates/api/src/infra/config/env.ts +3 -7
- package/templates/api/src/infra/database/mikroorm/entities/authorization.schema.ts +37 -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 +33 -0
- package/templates/api/src/infra/database/mikroorm/entities/user.schema.ts +18 -0
- package/templates/api/src/infra/database/mikroorm/mikro-orm.factory.ts +9 -4
- package/templates/api/src/infra/database/mikroorm/repositories/mikroorm-authorization.repository.ts +21 -0
- package/templates/api/src/infra/database/mikroorm/repositories/mikroorm-session.repository.ts +21 -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/index.ts +4 -10
- package/templates/api/src/infra/di/oauth.di.ts +14 -0
- package/templates/api/src/infra/http/app.ts +11 -1
- package/templates/api/src/infra/http/controllers/auth.controller.ts +153 -30
- package/templates/api/src/infra/http/controllers/mcp.controller.ts +32 -27
- package/templates/api/src/infra/http/controllers/oauth.controller.ts +257 -0
- package/templates/api/src/infra/http/errors/error-handler.ts +1 -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/mcp/mcp.server.ts +10 -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/test/domain/auth/entities/session.entity.spec.ts +50 -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 +33 -0
- package/templates/api/test/domain/oauth/entities/authorization.entity.spec.ts +52 -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/http/controllers/auth.controller.spec.ts +201 -0
- package/templates/api/test/infra/http/controllers/mcp.controller.spec.ts +232 -54
- package/templates/api/test/infra/http/controllers/users.controller.spec.ts +3 -3
- 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/support/factories/user.factory.ts +9 -11
- package/templates/api/test/support/fakes/auth-fakes.ts +42 -0
- package/templates/api/test/support/fakes/fake-access-tokens.ts +19 -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-session.repository.ts +19 -0
- package/templates/api/test/support/fakes/in-memory-user.repository.ts +20 -0
- package/templates/api/test/support/setup.ts +6 -28
- package/templates/mobile/README.md +1 -1
- package/templates/mobile/jest.config.js +0 -17
- package/templates/mobile/package.json +1 -4
- package/templates/mobile/scripts/ports.mjs +1 -1
- package/templates/mobile/src/api-contract/api-error.ts +0 -11
- package/templates/mobile/src/api-contract/auth.ts +161 -0
- package/templates/mobile/src/api-contract/index.ts +10 -0
- package/templates/mobile/src/app/(app)/_layout.tsx +1 -1
- package/templates/mobile/src/app/_layout.tsx +5 -5
- package/templates/mobile/src/features/auth/sign-in-form.tsx +12 -8
- package/templates/mobile/src/features/auth/sign-out-button.tsx +4 -4
- package/templates/mobile/src/features/auth/sign-up-form.tsx +13 -9
- package/templates/mobile/src/features/auth/token-store.ts +21 -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/sign-in-form.spec.tsx +12 -11
- package/templates/mobile/test/features/auth/sign-out-button.spec.tsx +8 -9
- package/templates/mobile/test/support/memory-token-store.ts +15 -0
- package/templates/site/README.md +1 -1
- package/templates/site/package.json +1 -1
- package/templates/site/scripts/ports.mjs +1 -1
- package/templates/web/README.md +1 -1
- package/templates/web/package.json +1 -3
- package/templates/web/scripts/ports.mjs +1 -1
- package/templates/web/src/api-contract/api-error.ts +0 -11
- package/templates/web/src/api-contract/auth.ts +161 -0
- package/templates/web/src/api-contract/index.ts +10 -0
- package/templates/web/src/features/auth/sign-in-form.tsx +12 -15
- package/templates/web/src/features/auth/sign-out-button.tsx +4 -4
- package/templates/web/src/features/auth/sign-up-form.tsx +7 -9
- package/templates/web/src/features/oauth/consent-form.tsx +66 -35
- package/templates/web/src/main.tsx +6 -4
- 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/test/api-contract/auth.spec.ts +155 -0
- package/templates/web/test/app.spec.tsx +47 -22
- package/templates/web/test/features/auth/sign-in-form.spec.tsx +6 -2
- package/templates/web/test/features/oauth/consent-form.spec.tsx +83 -26
- package/templates/web/test/support/render-app.tsx +4 -4
- package/templates/workspace/README.md +1 -1
- package/templates/workspace/package.json +1 -1
- package/templates/workspace/scripts/ports.mjs +1 -1
- 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,21 @@
|
|
|
1
|
+
import { hash, verify } from '@node-rs/argon2'
|
|
2
|
+
import type { PasswordHasher } from '@/domain/auth/ports/password-hasher.port'
|
|
3
|
+
|
|
4
|
+
// The library's defaults are Argon2id with 19 MiB, two passes and one lane: OWASP's minimum, so none is restated.
|
|
5
|
+
export class Argon2PasswordHasher implements PasswordHasher {
|
|
6
|
+
// A real hash of nothing in particular, so an unknown email costs one verification like any other.
|
|
7
|
+
private readonly decoy = hash('decoy password that nobody holds')
|
|
8
|
+
|
|
9
|
+
hash(password: string): Promise<string> {
|
|
10
|
+
return hash(password)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async verify(stored: string | undefined, password: string): Promise<boolean> {
|
|
14
|
+
if (stored === undefined) {
|
|
15
|
+
await verify(await this.decoy, password)
|
|
16
|
+
return false
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return verify(stored, password)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto'
|
|
2
|
+
import type { OpaqueTokens } from '@/domain/auth/ports/opaque-tokens.port'
|
|
3
|
+
|
|
4
|
+
// 256 random bits need no slow hash: nothing can be guessed, so a plain SHA-256 is enough to keep a stolen
|
|
5
|
+
// database from handing out working tokens.
|
|
6
|
+
export class CryptoOpaqueTokens implements OpaqueTokens {
|
|
7
|
+
create(): string {
|
|
8
|
+
return randomBytes(32).toString('base64url')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
digest(token: string): string {
|
|
12
|
+
return createHash('sha256').update(token).digest('base64url')
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
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 }: 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
|
+
})
|
|
30
|
+
.setProtectedHeader({ alg: ALGORITHM })
|
|
31
|
+
.setSubject(userId)
|
|
32
|
+
.setIssuer(this.issuer)
|
|
33
|
+
.setAudience(audience)
|
|
34
|
+
.setIssuedAt()
|
|
35
|
+
.setExpirationTime(Math.floor(expiresAt.getTime() / 1000))
|
|
36
|
+
.sign(this.key)
|
|
37
|
+
|
|
38
|
+
return { token, expiresAt }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async verify(token: string, audience = this.issuer): Promise<AccessClaims | null> {
|
|
42
|
+
try {
|
|
43
|
+
const { payload } = await jwtVerify(token, this.key, {
|
|
44
|
+
algorithms: [ALGORITHM],
|
|
45
|
+
issuer: this.issuer,
|
|
46
|
+
audience,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
if (typeof payload.sub !== 'string' || typeof payload.sid !== 'string') {
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
userId: payload.sub,
|
|
55
|
+
sessionId: payload.sid,
|
|
56
|
+
...(typeof payload.client_id === 'string' && { clientId: payload.client_id }),
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -5,14 +5,10 @@ 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
|
-
MOBILE_APP_SCHEME: z
|
|
13
|
-
.string()
|
|
14
|
-
.regex(/^[a-z][a-z0-9+.-]*$/)
|
|
15
|
-
.optional(),
|
|
16
12
|
LOG_LEVEL: z.enum(['error', 'warn', 'info', 'debug']),
|
|
17
13
|
})
|
|
18
14
|
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
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
|
+
// An authorization means nothing without the user who gave it.
|
|
24
|
+
deleteRule: 'cascade',
|
|
25
|
+
},
|
|
26
|
+
codeHash: { type: 'text', nullable: true },
|
|
27
|
+
sessionId: { type: 'uuid', nullable: true },
|
|
28
|
+
expiresAt: { type: 'datetime' },
|
|
29
|
+
createdAt: { type: 'datetime', onCreate: () => new Date(), defaultRaw: 'now()' },
|
|
30
|
+
updatedAt: {
|
|
31
|
+
type: 'datetime',
|
|
32
|
+
onCreate: () => new Date(),
|
|
33
|
+
onUpdate: () => new Date(),
|
|
34
|
+
defaultRaw: 'now()',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
})
|
|
@@ -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,33 @@
|
|
|
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
|
+
previousTokenHash: { type: 'text', nullable: true },
|
|
22
|
+
rotatedAt: { type: 'datetime', nullable: true },
|
|
23
|
+
expiresAt: { type: 'datetime' },
|
|
24
|
+
revokedAt: { type: 'datetime', nullable: true },
|
|
25
|
+
createdAt: { type: 'datetime', onCreate: () => new Date(), defaultRaw: 'now()' },
|
|
26
|
+
updatedAt: {
|
|
27
|
+
type: 'datetime',
|
|
28
|
+
onCreate: () => new Date(),
|
|
29
|
+
onUpdate: () => new Date(),
|
|
30
|
+
defaultRaw: 'now()',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
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' },
|
|
10
|
+
createdAt: { type: 'datetime', onCreate: () => new Date(), defaultRaw: 'now()' },
|
|
11
|
+
updatedAt: {
|
|
12
|
+
type: 'datetime',
|
|
13
|
+
onCreate: () => new Date(),
|
|
14
|
+
onUpdate: () => new Date(),
|
|
15
|
+
defaultRaw: 'now()',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
})
|
|
@@ -1,17 +1,22 @@
|
|
|
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
5
|
import { ProfileSchema } from './entities/profile.schema'
|
|
6
|
+
import { SessionSchema } from './entities/session.schema'
|
|
7
|
+
import { UserSchema } from './entities/user.schema'
|
|
6
8
|
|
|
7
9
|
export function createOrmConfig(env: Pick<Env, 'DATABASE_URL' | 'NODE_ENV'>) {
|
|
8
10
|
return defineConfig({
|
|
9
11
|
clientUrl: env.DATABASE_URL,
|
|
10
|
-
entities: [
|
|
12
|
+
entities: [
|
|
13
|
+
UserSchema,
|
|
14
|
+
ProfileSchema,
|
|
15
|
+
SessionSchema,
|
|
16
|
+
AuthorizationSchema, // prumo:mcp
|
|
17
|
+
],
|
|
11
18
|
extensions: [Migrator],
|
|
12
19
|
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
20
|
debug: env.NODE_ENV === 'development',
|
|
16
21
|
})
|
|
17
22
|
}
|
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
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
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 { registerOauth } from './oauth.di' // prumo:mcp
|
|
8
8
|
import { registerUsers } from './users.di'
|
|
9
9
|
|
|
10
10
|
export type Dependencies = { env: Env; orm: MikroORM }
|
|
@@ -18,15 +18,9 @@ export function createContainer({ env, orm }: Dependencies): DependencyContainer
|
|
|
18
18
|
container.register(EntityManager, { useValue: orm.em })
|
|
19
19
|
container.register(TRANSACTION_MANAGER, { useClass: MikroOrmTransactionManager })
|
|
20
20
|
|
|
21
|
+
registerAuth(container)
|
|
21
22
|
registerUsers(container)
|
|
22
|
-
|
|
23
|
-
container.register(AUTH, {
|
|
24
|
-
useValue: createAuth(env, {
|
|
25
|
-
onUserCreated: async (user) => {
|
|
26
|
-
await container.resolve(CreateProfileUseCase).execute(user.id, user.name)
|
|
27
|
-
},
|
|
28
|
-
}),
|
|
29
|
-
})
|
|
23
|
+
registerOauth(container) // prumo:mcp
|
|
30
24
|
|
|
31
25
|
return container
|
|
32
26
|
}
|
|
@@ -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
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import cookie from '@fastify/cookie'
|
|
2
3
|
import cors from '@fastify/cors'
|
|
4
|
+
import rateLimit from '@fastify/rate-limit'
|
|
3
5
|
import swagger from '@fastify/swagger'
|
|
4
6
|
import swaggerUi from '@fastify/swagger-ui'
|
|
5
7
|
import { EntityManager } from '@mikro-orm/postgresql'
|
|
@@ -14,9 +16,11 @@ import { ENV, type Env } from '@/infra/config/env'
|
|
|
14
16
|
import { authController } from '@/infra/http/controllers/auth.controller'
|
|
15
17
|
import { healthController } from '@/infra/http/controllers/health.controller'
|
|
16
18
|
import { mcpController } from '@/infra/http/controllers/mcp.controller' // prumo:mcp
|
|
19
|
+
import { oauthController } from '@/infra/http/controllers/oauth.controller' // prumo:mcp
|
|
17
20
|
import { usersController } from '@/infra/http/controllers/users.controller'
|
|
18
21
|
import { registerErrorHandler } from '@/infra/http/errors/error-handler'
|
|
19
22
|
import { registerAuthHook } from '@/infra/http/hooks/auth.hook'
|
|
23
|
+
import { registerOriginCheck } from '@/infra/http/hooks/origin.hook'
|
|
20
24
|
import { registerRequestContext } from '@/infra/http/hooks/request-context.hook'
|
|
21
25
|
|
|
22
26
|
const DOCS = '/api/docs'
|
|
@@ -82,16 +86,22 @@ export async function buildApp(container: DependencyContainer): Promise<FastifyI
|
|
|
82
86
|
exposedHeaders: ['X-Request-Id'],
|
|
83
87
|
})
|
|
84
88
|
|
|
89
|
+
await app.register(cookie)
|
|
90
|
+
// Off by default: a route opts in through its config, so only the routes that take a password are limited.
|
|
91
|
+
await app.register(rateLimit, { global: false })
|
|
92
|
+
registerOriginCheck(app, env.WEB_ORIGIN)
|
|
93
|
+
|
|
85
94
|
if (env.NODE_ENV !== 'production') {
|
|
86
95
|
await registerDocs(app)
|
|
87
96
|
}
|
|
88
97
|
|
|
89
98
|
registerAuthHook(app)
|
|
90
99
|
|
|
91
|
-
await app.register(authController)
|
|
100
|
+
await app.register(authController, { prefix: '/api/auth' })
|
|
92
101
|
await app.register(healthController, { prefix: '/api/health' })
|
|
93
102
|
await app.register(usersController, { prefix: '/api/v1/users' })
|
|
94
103
|
// prumo:mcp
|
|
104
|
+
await app.register(oauthController)
|
|
95
105
|
await app.register(mcpController, { prefix: '/api/mcp' })
|
|
96
106
|
// prumo:end-mcp
|
|
97
107
|
|
|
@@ -1,36 +1,159 @@
|
|
|
1
|
-
import type { FastifyInstance } from 'fastify'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'
|
|
2
|
+
import type { ZodTypeProvider } from 'fastify-type-provider-zod'
|
|
3
|
+
import { refreshSchema } from '@/domain/auth/dto/refresh.dto'
|
|
4
|
+
import { sessionResponseSchema } from '@/domain/auth/dto/session-response.dto'
|
|
5
|
+
import { signInSchema } from '@/domain/auth/dto/sign-in.dto'
|
|
6
|
+
import { signUpSchema } from '@/domain/auth/dto/sign-up.dto'
|
|
7
|
+
import { type TokensResponseDto, tokensResponseSchema } from '@/domain/auth/dto/tokens-response.dto'
|
|
8
|
+
import { InvalidSessionError } from '@/domain/auth/errors/invalid-session.error'
|
|
9
|
+
import { FindUserUseCase } from '@/domain/auth/use-cases/find-user.use-case'
|
|
10
|
+
import { RefreshSessionUseCase } from '@/domain/auth/use-cases/refresh-session.use-case'
|
|
11
|
+
import { SignInUseCase } from '@/domain/auth/use-cases/sign-in.use-case'
|
|
12
|
+
import { SignOutUseCase } from '@/domain/auth/use-cases/sign-out.use-case'
|
|
13
|
+
import { SignUpUseCase } from '@/domain/auth/use-cases/sign-up.use-case'
|
|
14
|
+
import type { SessionTokens } from '@/domain/auth/use-cases/start-session.use-case'
|
|
15
|
+
import { ENV, type Env } from '@/infra/config/env'
|
|
16
|
+
import { ACCESS_COOKIE, currentSession, currentUser } from '@/infra/http/hooks/auth.hook'
|
|
17
|
+
|
|
18
|
+
const REFRESH_COOKIE = 'refresh_token'
|
|
19
|
+
|
|
20
|
+
// The refresh cookie travels only to the one route that reads it, never with an ordinary request.
|
|
21
|
+
const REFRESH_PATH = '/api/auth/refresh'
|
|
22
|
+
|
|
23
|
+
// Repeated guessing is slowed down per address; a refresh happens every quarter hour per tab, so it gets more room.
|
|
24
|
+
const ATTEMPTS = { rateLimit: { max: 10, timeWindow: '1 minute' } }
|
|
25
|
+
const REFRESHES = { rateLimit: { max: 60, timeWindow: '1 minute' } }
|
|
26
|
+
|
|
27
|
+
// A native client asks for its tokens in the body. The web never does: a token JavaScript can read is a token an
|
|
28
|
+
// injected script can take.
|
|
29
|
+
function wantsBearer(request: FastifyRequest): boolean {
|
|
30
|
+
return request.headers['x-auth-transport'] === 'bearer'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function asBody(tokens: SessionTokens): TokensResponseDto {
|
|
34
|
+
return {
|
|
35
|
+
accessToken: tokens.accessToken.token,
|
|
36
|
+
refreshToken: tokens.refreshToken,
|
|
37
|
+
expiresIn: Math.round((tokens.accessToken.expiresAt.getTime() - Date.now()) / 1000),
|
|
38
|
+
}
|
|
39
|
+
}
|
|
4
40
|
|
|
5
41
|
export async function authController(fastify: FastifyInstance): Promise<void> {
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
42
|
+
const app = fastify.withTypeProvider<ZodTypeProvider>()
|
|
43
|
+
const env = app.container.resolve<Env>(ENV)
|
|
44
|
+
const secure = env.API_URL.startsWith('https://')
|
|
45
|
+
const cookie = { httpOnly: true, sameSite: 'lax', secure } as const
|
|
46
|
+
|
|
47
|
+
function setCookies(reply: FastifyReply, tokens: SessionTokens): void {
|
|
48
|
+
reply.setCookie(ACCESS_COOKIE, tokens.accessToken.token, {
|
|
49
|
+
...cookie,
|
|
50
|
+
path: '/',
|
|
51
|
+
expires: tokens.accessToken.expiresAt,
|
|
52
|
+
})
|
|
53
|
+
reply.setCookie(REFRESH_COOKIE, tokens.refreshToken, {
|
|
54
|
+
...cookie,
|
|
55
|
+
path: REFRESH_PATH,
|
|
56
|
+
expires: tokens.refreshExpiresAt,
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function clearCookies(reply: FastifyReply): void {
|
|
61
|
+
reply.clearCookie(ACCESS_COOKIE, { ...cookie, path: '/' })
|
|
62
|
+
reply.clearCookie(REFRESH_COOKIE, { ...cookie, path: REFRESH_PATH })
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The web gets cookies and an empty body; a native client gets the tokens and no cookie.
|
|
66
|
+
function deliver(
|
|
67
|
+
request: FastifyRequest,
|
|
68
|
+
reply: FastifyReply,
|
|
69
|
+
tokens: SessionTokens,
|
|
70
|
+
status: number,
|
|
71
|
+
) {
|
|
72
|
+
if (wantsBearer(request)) {
|
|
73
|
+
return reply.code(status).send(asBody(tokens))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
setCookies(reply, tokens)
|
|
77
|
+
return reply.code(status === 201 ? 201 : 204).send()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
app.post(
|
|
81
|
+
'/sign-up',
|
|
82
|
+
{
|
|
83
|
+
config: { public: true, ...ATTEMPTS },
|
|
84
|
+
schema: { tags: ['auth'], body: signUpSchema, response: { 201: tokensResponseSchema } },
|
|
85
|
+
},
|
|
86
|
+
async (request, reply) =>
|
|
87
|
+
deliver(
|
|
88
|
+
request,
|
|
89
|
+
reply,
|
|
90
|
+
await app.container.resolve(SignUpUseCase).execute(request.body),
|
|
91
|
+
201,
|
|
92
|
+
),
|
|
15
93
|
)
|
|
16
|
-
// prumo:end-mcp
|
|
17
|
-
|
|
18
|
-
fastify.route({
|
|
19
|
-
...routes,
|
|
20
|
-
method: ['GET', 'POST'],
|
|
21
|
-
url: '/api/auth/*',
|
|
22
|
-
handler: async (request, reply) =>
|
|
23
|
-
sendFetchResponse(reply, await auth.handler(toFetchRequest(request))),
|
|
24
|
-
})
|
|
25
94
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
95
|
+
app.post(
|
|
96
|
+
'/sign-in',
|
|
97
|
+
{
|
|
98
|
+
config: { public: true, ...ATTEMPTS },
|
|
99
|
+
schema: { tags: ['auth'], body: signInSchema, response: { 200: tokensResponseSchema } },
|
|
100
|
+
},
|
|
101
|
+
async (request, reply) =>
|
|
102
|
+
deliver(
|
|
103
|
+
request,
|
|
104
|
+
reply,
|
|
105
|
+
await app.container.resolve(SignInUseCase).execute(request.body),
|
|
106
|
+
200,
|
|
107
|
+
),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
app.post(
|
|
111
|
+
'/refresh',
|
|
112
|
+
{
|
|
113
|
+
config: { public: true, ...REFRESHES },
|
|
114
|
+
schema: { tags: ['auth'], body: refreshSchema, response: { 200: tokensResponseSchema } },
|
|
115
|
+
},
|
|
116
|
+
async (request, reply) => {
|
|
117
|
+
const token = request.body?.refreshToken ?? request.cookies[REFRESH_COOKIE]
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
if (token === undefined) {
|
|
121
|
+
throw new InvalidSessionError()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return deliver(
|
|
125
|
+
request,
|
|
126
|
+
reply,
|
|
127
|
+
await app.container.resolve(RefreshSessionUseCase).execute(token),
|
|
128
|
+
200,
|
|
129
|
+
)
|
|
130
|
+
} catch (error) {
|
|
131
|
+
// A refresh that cannot succeed will not succeed later either: drop what the browser holds.
|
|
132
|
+
if (error instanceof InvalidSessionError && !wantsBearer(request)) {
|
|
133
|
+
clearCookies(reply)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
throw error
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
app.post('/sign-out', { schema: { tags: ['auth'] } }, async (request, reply) => {
|
|
142
|
+
await app.container
|
|
143
|
+
.resolve(SignOutUseCase)
|
|
144
|
+
.execute(currentUser(request).id, currentSession(request))
|
|
145
|
+
clearCookies(reply)
|
|
146
|
+
|
|
147
|
+
return reply.code(204).send()
|
|
34
148
|
})
|
|
35
|
-
|
|
149
|
+
|
|
150
|
+
app.get(
|
|
151
|
+
'/session',
|
|
152
|
+
{ schema: { tags: ['auth'], response: { 200: sessionResponseSchema } } },
|
|
153
|
+
async (request) => {
|
|
154
|
+
const user = await app.container.resolve(FindUserUseCase).execute(currentUser(request).id)
|
|
155
|
+
|
|
156
|
+
return { user: { id: user.id, email: user.email } }
|
|
157
|
+
},
|
|
158
|
+
)
|
|
36
159
|
}
|