@tatara/cli 0.9.1 → 0.10.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/dist/bin.mjs +367 -93
- package/package.json +1 -1
- package/templates/features/admin/files/src/apps/admin/app.ts +1 -1
- package/templates/features/admin/files/src/modules/example/routes/admin-greeting.routes.ts +2 -2
- package/templates/features/admin/files/src/modules/example/tests/admin.test.ts +11 -4
- package/templates/features/admin/files/src/modules/staff/db/seed.ts +27 -0
- package/templates/features/admin/files/src/modules/staff/services/auth.ts +46 -0
- package/templates/features/admin/files/tests/helpers/staff.ts +8 -0
- package/templates/features/admin/files+drizzle+mysql/src/modules/staff/db/staff.schema.ts +58 -0
- package/templates/features/admin/files+drizzle+mysql/src/modules/staff/services/adapter.ts +13 -0
- package/templates/features/admin/files+drizzle+postgres/src/modules/staff/db/staff.schema.ts +57 -0
- package/templates/features/admin/files+drizzle+postgres/src/modules/staff/services/adapter.ts +13 -0
- package/templates/features/admin/files+prisma/prisma/models/staff.prisma +61 -0
- package/templates/features/admin/files+prisma+mysql/src/modules/staff/services/adapter.ts +5 -0
- package/templates/features/admin/files+prisma+postgres/src/modules/staff/services/adapter.ts +5 -0
- package/templates/features/auth-keycloak/files/keycloak/realm.json +70 -9
- package/templates/features/auth-keycloak/files/src/{core/auth → modules/users}/commands/keycloak-token.ts +3 -1
- package/templates/features/auth-keycloak/files/{src/core/auth/tests/keycloak-tokens.test.ts → tests/tatara/auth-keycloak-tokens.test.ts} +2 -2
- package/templates/features/auth-keycloak/files+auth-orgs/{src/core/auth/tests/keycloak-organizations.test.ts → tests/tatara/auth-keycloak-organizations.test.ts} +1 -1
- package/templates/features/auth-keycloak/files+authz/{src/core/auth/tests/keycloak-authz.test.ts → tests/tatara/auth-keycloak-authz.test.ts} +3 -2
- package/templates/features/auth-orgs/files+drizzle+mysql/src/{core/auth → modules/users}/db/organization.schema.ts +1 -1
- package/templates/features/auth-orgs/files+drizzle+postgres/src/{core/auth → modules/users}/db/organization.schema.ts +1 -1
- package/templates/features/authz/files/src/core/authz/system-roles.ts +1 -0
- package/templates/features/authz/files/src/{core/authz → modules/users}/routes/authorization.routes.ts +4 -1
- package/templates/features/authz/files/{src/core/authz/tests → tests/tatara}/authz.test.ts +4 -2
- package/templates/features/authz/files+drizzle/src/core/authz/index.ts +1 -0
- package/templates/features/authz/files+prisma/src/core/authz/index.ts +1 -0
- package/templates/features/base/files/src/apps/api/app.ts +1 -1
- package/templates/features/base/files/src/core/http/context.ts +2 -0
- package/templates/features/better-auth/files/src/{core/auth → modules/users}/db/seed.ts +1 -1
- package/templates/features/better-auth/files/src/modules/users/services/auth.ts +37 -0
- package/templates/features/better-auth/files/src/{core/auth → modules/users/services}/delivery.ts +1 -1
- package/templates/features/better-auth/files/tests/helpers/auth.ts +4 -3
- package/templates/features/better-auth/files+admin/tests/tatara/auth-openapi-admin.test.ts +10 -0
- package/templates/features/better-auth/files+auth-orgs/{src/core/auth/tests/principal.test.ts → tests/tatara/auth-principal.test.ts} +1 -1
- package/templates/features/better-auth/files+auth-orgs+authz/{src/core/auth/tests/principal-authz.test.ts → tests/tatara/auth-principal-authz.test.ts} +1 -1
- package/templates/features/better-auth/files+drizzle+mysql/src/{core/auth → modules/users/services}/adapter.ts +1 -1
- package/templates/features/better-auth/files+drizzle+postgres/src/{core/auth → modules/users/services}/adapter.ts +1 -1
- package/templates/versions.json +4 -4
- package/templates/features/better-auth/files/src/core/auth/auth.ts +0 -26
- package/templates/features/better-auth/files/src/core/auth/principal.ts +0 -10
- package/templates/features/better-auth/files+admin/src/core/auth/tests/openapi-admin.test.ts +0 -8
- /package/templates/features/auth/files/src/{core/auth → modules/users}/routes/me.routes.ts +0 -0
- /package/templates/features/auth/files/{src/core/auth/tests/me.test.ts → tests/tatara/auth-me.test.ts} +0 -0
- /package/templates/features/auth-keycloak/files/src/{core/auth → modules/users/services}/keycloak.ts +0 -0
- /package/templates/features/authz/files/{src/core/authz/tests/error-messages.test.ts → tests/tatara/authz-error-messages.test.ts} +0 -0
- /package/templates/features/authz/files/{src/core/authz/tests/request-body.test.ts → tests/tatara/authz-request-body.test.ts} +0 -0
- /package/templates/features/base/files/src/{core/http/app.ts → apps/http.ts} +0 -0
- /package/templates/features/base/files/{src/core/config/tests/env.test.ts → tests/tatara/config-env.test.ts} +0 -0
- /package/templates/features/better-auth/files/src/{core/auth → modules/users/services}/sms.ts +0 -0
- /package/templates/features/better-auth/files/{src/core/auth/tests/openapi.test.ts → tests/tatara/auth-openapi.test.ts} +0 -0
- /package/templates/features/better-auth/files+drizzle+mysql/src/{core/auth → modules/users}/db/auth.schema.ts +0 -0
- /package/templates/features/better-auth/files+drizzle+postgres/src/{core/auth → modules/users}/db/auth.schema.ts +0 -0
- /package/templates/features/better-auth/files+prisma+mysql/src/{core/auth → modules/users/services}/adapter.ts +0 -0
- /package/templates/features/better-auth/files+prisma+postgres/src/{core/auth → modules/users/services}/adapter.ts +0 -0
- /package/templates/features/email/files/{src/core/email/tests → tests/tatara}/email.test.ts +0 -0
- /package/templates/features/i18n/files/{src/core/i18n/tests → tests/tatara}/i18n.test.ts +0 -0
- /package/templates/features/storage/files/{src/core/storage/tests → tests/tatara}/storage.test.ts +0 -0
package/dist/bin.mjs
CHANGED
|
@@ -865,6 +865,24 @@ const authz = {
|
|
|
865
865
|
label: "Permission system",
|
|
866
866
|
dependencies: ["@tatara/authz"],
|
|
867
867
|
slots: [
|
|
868
|
+
{
|
|
869
|
+
file: "src/core/authz/index.ts",
|
|
870
|
+
slot: "options",
|
|
871
|
+
unless: ["auth-orgs"],
|
|
872
|
+
content: "singleTenant: \"default\","
|
|
873
|
+
},
|
|
874
|
+
{
|
|
875
|
+
file: "src/modules/users/routes/authorization.routes.ts",
|
|
876
|
+
slot: "route-options",
|
|
877
|
+
when: ["auth-orgs"],
|
|
878
|
+
content: "use: [requireOrg()],"
|
|
879
|
+
},
|
|
880
|
+
{
|
|
881
|
+
file: "src/modules/users/routes/authorization.routes.ts",
|
|
882
|
+
slot: "imports",
|
|
883
|
+
when: ["auth-orgs"],
|
|
884
|
+
content: "import { requireOrg } from \"#modules/users/services/auth\";"
|
|
885
|
+
},
|
|
868
886
|
{
|
|
869
887
|
file: "tests/helpers/auth.ts",
|
|
870
888
|
slot: "imports",
|
|
@@ -877,14 +895,15 @@ const authz = {
|
|
|
877
895
|
},
|
|
878
896
|
{
|
|
879
897
|
file: "tests/helpers/auth.ts",
|
|
880
|
-
slot: "
|
|
881
|
-
|
|
882
|
-
content: "
|
|
898
|
+
slot: "as-user",
|
|
899
|
+
unless: ["auth-orgs"],
|
|
900
|
+
content: "await applyTestAccess(user.userId, options);"
|
|
883
901
|
},
|
|
884
902
|
{
|
|
885
903
|
file: "tests/helpers/auth.ts",
|
|
886
904
|
slot: "as-user",
|
|
887
|
-
|
|
905
|
+
when: ["auth-orgs"],
|
|
906
|
+
content: "await applyTestAccess(user.userId, { ...options, orgId: user.orgId });"
|
|
888
907
|
},
|
|
889
908
|
{
|
|
890
909
|
file: "AGENTS.md",
|
|
@@ -1001,11 +1020,32 @@ const admin = {
|
|
|
1001
1020
|
kind: "addon",
|
|
1002
1021
|
label: "Admin API app",
|
|
1003
1022
|
scripts: { "start:admin": "node dist/apps/admin/server.mjs" },
|
|
1004
|
-
env: [
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1023
|
+
env: [
|
|
1024
|
+
{
|
|
1025
|
+
key: "ADMIN_PORT",
|
|
1026
|
+
schema: "z.coerce.number().int().default(3001)",
|
|
1027
|
+
example: "3001"
|
|
1028
|
+
},
|
|
1029
|
+
{
|
|
1030
|
+
key: "ADMIN_URL",
|
|
1031
|
+
schema: "localDefault(z.url(), \"http://localhost:3001\")",
|
|
1032
|
+
example: "http://localhost:3001",
|
|
1033
|
+
comment: "Where the admin app is served; staff sign in there"
|
|
1034
|
+
},
|
|
1035
|
+
{
|
|
1036
|
+
key: "SEED_STAFF_EMAIL",
|
|
1037
|
+
schema: "z.email().default(\"staff@example.com\")",
|
|
1038
|
+
example: "staff@example.com",
|
|
1039
|
+
comment: "Development staff account created by `db:seed` (never in production)"
|
|
1040
|
+
},
|
|
1041
|
+
{
|
|
1042
|
+
key: "SEED_STAFF_PASSWORD",
|
|
1043
|
+
schema: "z.string().min(10).optional()",
|
|
1044
|
+
example: "",
|
|
1045
|
+
secret: true,
|
|
1046
|
+
comment: "Password for the development staff account (generated by tatara new)"
|
|
1047
|
+
}
|
|
1048
|
+
],
|
|
1009
1049
|
slots: [
|
|
1010
1050
|
{
|
|
1011
1051
|
file: "src/core/http/routes.ts",
|
|
@@ -1018,15 +1058,48 @@ const admin = {
|
|
|
1018
1058
|
content: "\"apps/admin/server\": \"src/apps/admin/server.ts\","
|
|
1019
1059
|
},
|
|
1020
1060
|
{
|
|
1021
|
-
file: "src/
|
|
1061
|
+
file: "src/apps/http.ts",
|
|
1022
1062
|
slot: "imports",
|
|
1023
|
-
content:
|
|
1063
|
+
content: [
|
|
1064
|
+
"import { requireRole } from \"@tatara/auth\";",
|
|
1065
|
+
"import { except } from \"hono/combine\";",
|
|
1066
|
+
"import { attachStaffPrincipal, staffAuth } from \"#modules/staff/services/auth\";"
|
|
1067
|
+
].join("\n")
|
|
1068
|
+
},
|
|
1069
|
+
{
|
|
1070
|
+
file: "src/apps/http.ts",
|
|
1071
|
+
slot: "middleware",
|
|
1072
|
+
order: 50,
|
|
1073
|
+
content: "app.use(name === \"admin\" ? attachStaffPrincipal : attachPrincipal);"
|
|
1024
1074
|
},
|
|
1025
1075
|
{
|
|
1026
|
-
file: "src/
|
|
1076
|
+
file: "src/apps/http.ts",
|
|
1077
|
+
slot: "routes",
|
|
1078
|
+
order: 2,
|
|
1079
|
+
content: "if (name === \"admin\") app.on([\"GET\", \"POST\"], `${API_PREFIX}/auth/*`, (c) => staffAuth.handler(c.req.raw));"
|
|
1080
|
+
},
|
|
1081
|
+
{
|
|
1082
|
+
file: "src/apps/http.ts",
|
|
1027
1083
|
slot: "routes",
|
|
1028
1084
|
order: 5,
|
|
1029
|
-
content: "if (name === \"admin\") app.use(`${API_PREFIX}/*`, requireRole(\"
|
|
1085
|
+
content: "if (name === \"admin\") app.use(`${API_PREFIX}/*`, except(`${API_PREFIX}/auth/*`, requireRole(\"staff\")));"
|
|
1086
|
+
},
|
|
1087
|
+
{
|
|
1088
|
+
file: "src/core/authz/system-roles.ts",
|
|
1089
|
+
slot: "roles",
|
|
1090
|
+
content: "{ key: \"staff\", name: \"Staff\", description: \"Internal staff: platform permissions on the admin app.\" },"
|
|
1091
|
+
},
|
|
1092
|
+
{
|
|
1093
|
+
file: "src/modules/staff/services/auth.ts",
|
|
1094
|
+
slot: "resolvers",
|
|
1095
|
+
when: ["auth-keycloak"],
|
|
1096
|
+
content: "() => (c) => keycloakPrincipal(auth, c.req.header(\"authorization\")),"
|
|
1097
|
+
},
|
|
1098
|
+
{
|
|
1099
|
+
file: "src/modules/staff/services/auth.ts",
|
|
1100
|
+
slot: "imports",
|
|
1101
|
+
when: ["auth-keycloak"],
|
|
1102
|
+
content: "import { keycloakPrincipal } from \"@tatara/keycloak\";\nimport { auth } from \"#modules/users/services/auth\";"
|
|
1030
1103
|
},
|
|
1031
1104
|
{
|
|
1032
1105
|
file: "tests/helpers/api.ts",
|
|
@@ -1041,11 +1114,15 @@ const admin = {
|
|
|
1041
1114
|
{
|
|
1042
1115
|
file: "AGENTS.md",
|
|
1043
1116
|
slot: "sections",
|
|
1044
|
-
content: `## Admin app
|
|
1117
|
+
content: `## Admin app and staff
|
|
1045
1118
|
|
|
1046
1119
|
- \`apps/admin\` is a second HTTP app on \`ADMIN_PORT\`. Routes with \`defineRoutes({ app: "admin", … })\`
|
|
1047
|
-
are served only there, and every \`/api\` route on it
|
|
1048
|
-
-
|
|
1120
|
+
are served only there, and every \`/api\` route on it needs a staff principal (role \`staff\`).
|
|
1121
|
+
- Staff are separate accounts (\`src/modules/staff\`, tables \`staff_*\`), with their own better-auth
|
|
1122
|
+
mounted at \`/api/auth\` on the admin app. They never share a table or a session with users.
|
|
1123
|
+
There's no staff sign-up; the dev account comes from \`SEED_STAFF_EMAIL\`/\`SEED_STAFF_PASSWORD\`.
|
|
1124
|
+
- Staff act in the \`platform\` org, so grant platform permissions to the built-in \`staff\` role.
|
|
1125
|
+
- Test it with \`adminApi\` from \`#tests/helpers/api\` and \`asStaff()\` from \`#tests/helpers/staff\`.
|
|
1049
1126
|
`
|
|
1050
1127
|
}
|
|
1051
1128
|
]
|
|
@@ -1228,23 +1305,23 @@ const i18n = {
|
|
|
1228
1305
|
}],
|
|
1229
1306
|
slots: [
|
|
1230
1307
|
{
|
|
1231
|
-
file: "src/
|
|
1308
|
+
file: "src/apps/http.ts",
|
|
1232
1309
|
slot: "imports",
|
|
1233
1310
|
content: "import { detectLanguage } from \"#core/i18n/index\";"
|
|
1234
1311
|
},
|
|
1235
1312
|
{
|
|
1236
|
-
file: "src/
|
|
1313
|
+
file: "src/apps/http.ts",
|
|
1237
1314
|
slot: "middleware",
|
|
1238
1315
|
order: 10,
|
|
1239
1316
|
content: "app.use(detectLanguage);"
|
|
1240
1317
|
},
|
|
1241
1318
|
{
|
|
1242
|
-
file: "src/
|
|
1319
|
+
file: "src/apps/http.ts",
|
|
1243
1320
|
slot: "imports",
|
|
1244
1321
|
content: "import { t } from \"#core/i18n/index\";"
|
|
1245
1322
|
},
|
|
1246
1323
|
{
|
|
1247
|
-
file: "src/
|
|
1324
|
+
file: "src/apps/http.ts",
|
|
1248
1325
|
slot: "error-options",
|
|
1249
1326
|
content: ["message: (err, c) =>", " err.translatable ? t(c.get(\"language\"), `errors.${err.code}`, err.message) : err.message,"].join("\n")
|
|
1250
1327
|
},
|
|
@@ -1268,18 +1345,19 @@ const auth = {
|
|
|
1268
1345
|
dependencies: ["@tatara/auth"],
|
|
1269
1346
|
slots: [
|
|
1270
1347
|
{
|
|
1271
|
-
file: "src/
|
|
1348
|
+
file: "src/apps/http.ts",
|
|
1272
1349
|
slot: "imports",
|
|
1273
|
-
content: "import { attachPrincipal } from \"#
|
|
1350
|
+
content: "import { attachPrincipal } from \"#modules/users/services/auth\";"
|
|
1274
1351
|
},
|
|
1275
1352
|
{
|
|
1276
|
-
file: "src/
|
|
1353
|
+
file: "src/apps/http.ts",
|
|
1277
1354
|
slot: "middleware",
|
|
1278
1355
|
order: 50,
|
|
1356
|
+
unless: ["admin"],
|
|
1279
1357
|
content: "app.use(attachPrincipal);"
|
|
1280
1358
|
},
|
|
1281
1359
|
{
|
|
1282
|
-
file: "src/
|
|
1360
|
+
file: "src/apps/http.ts",
|
|
1283
1361
|
slot: "routes",
|
|
1284
1362
|
order: 0,
|
|
1285
1363
|
content: [
|
|
@@ -1310,7 +1388,7 @@ const BETTER_AUTH_AGENTS = `### better-auth
|
|
|
1310
1388
|
- Sessions work as cookies (browsers) and as \`Authorization: Bearer <session token>\` (mobile, APIs).
|
|
1311
1389
|
- Outside production, every email and SMS is kept in an in-memory outbox:
|
|
1312
1390
|
\`GET /api/dev/outbox?to=<address>\` returns the latest messages, including sign-in codes.
|
|
1313
|
-
- To send real SMS, implement \`SmsSender\` (from \`@tatara/auth\`) in \`src/
|
|
1391
|
+
- To send real SMS, implement \`SmsSender\` (from \`@tatara/auth\`) in \`src/modules/users/services/sms.ts\`.
|
|
1314
1392
|
`;
|
|
1315
1393
|
const drizzlePg = ["drizzle", "postgres"];
|
|
1316
1394
|
const drizzleMy = ["drizzle", "mysql"];
|
|
@@ -1343,36 +1421,48 @@ const betterAuth = {
|
|
|
1343
1421
|
],
|
|
1344
1422
|
slots: [
|
|
1345
1423
|
{
|
|
1346
|
-
file: "src/
|
|
1424
|
+
file: "src/apps/http.ts",
|
|
1347
1425
|
slot: "imports",
|
|
1348
|
-
content: "import { auth } from \"#
|
|
1426
|
+
content: "import { auth } from \"#modules/users/services/auth\";"
|
|
1349
1427
|
},
|
|
1350
1428
|
{
|
|
1351
|
-
file: "src/
|
|
1429
|
+
file: "src/apps/http.ts",
|
|
1352
1430
|
slot: "imports",
|
|
1353
|
-
content: "import { outbox } from \"#
|
|
1431
|
+
content: "import { outbox } from \"#modules/users/services/delivery\";"
|
|
1354
1432
|
},
|
|
1355
1433
|
{
|
|
1356
|
-
file: "src/
|
|
1434
|
+
file: "src/apps/http.ts",
|
|
1357
1435
|
slot: "imports",
|
|
1358
1436
|
content: "import { mergeAuthOpenApi } from \"@tatara/auth\";"
|
|
1359
1437
|
},
|
|
1360
1438
|
{
|
|
1361
|
-
file: "src/
|
|
1439
|
+
file: "src/apps/http.ts",
|
|
1362
1440
|
slot: "expose-headers",
|
|
1363
1441
|
content: "\"set-auth-token\","
|
|
1364
1442
|
},
|
|
1365
1443
|
{
|
|
1366
|
-
file: "src/
|
|
1444
|
+
file: "src/apps/http.ts",
|
|
1445
|
+
slot: "openapi-options",
|
|
1446
|
+
unless: ["admin"],
|
|
1447
|
+
content: [
|
|
1448
|
+
"async extend(document) {",
|
|
1449
|
+
" if (name === \"api\") await mergeAuthOpenApi(document, auth, `${API_PREFIX}/auth`);",
|
|
1450
|
+
"},"
|
|
1451
|
+
].join("\n")
|
|
1452
|
+
},
|
|
1453
|
+
{
|
|
1454
|
+
file: "src/apps/http.ts",
|
|
1367
1455
|
slot: "openapi-options",
|
|
1456
|
+
when: ["admin"],
|
|
1368
1457
|
content: [
|
|
1369
1458
|
"async extend(document) {",
|
|
1370
1459
|
" if (name === \"api\") await mergeAuthOpenApi(document, auth, `${API_PREFIX}/auth`);",
|
|
1460
|
+
" if (name === \"admin\") await mergeAuthOpenApi(document, staffAuth, `${API_PREFIX}/auth`);",
|
|
1371
1461
|
"},"
|
|
1372
1462
|
].join("\n")
|
|
1373
1463
|
},
|
|
1374
1464
|
{
|
|
1375
|
-
file: "src/
|
|
1465
|
+
file: "src/apps/http.ts",
|
|
1376
1466
|
slot: "routes",
|
|
1377
1467
|
order: 1,
|
|
1378
1468
|
content: [
|
|
@@ -1385,29 +1475,35 @@ const betterAuth = {
|
|
|
1385
1475
|
].join("\n")
|
|
1386
1476
|
},
|
|
1387
1477
|
{
|
|
1388
|
-
file: "src/
|
|
1478
|
+
file: "src/modules/users/services/delivery.ts",
|
|
1389
1479
|
slot: "email-delivery",
|
|
1390
1480
|
unless: ["email"],
|
|
1391
1481
|
content: "logger.info({ to: message.to, subject: message.subject }, \"email not sent: no email provider configured\");"
|
|
1392
1482
|
},
|
|
1393
1483
|
{
|
|
1394
|
-
file: "src/
|
|
1484
|
+
file: "src/modules/users/services/delivery.ts",
|
|
1395
1485
|
slot: "imports",
|
|
1396
1486
|
unless: ["email"],
|
|
1397
1487
|
content: "import { logger } from \"#core/logging/logger\";"
|
|
1398
1488
|
},
|
|
1399
1489
|
{
|
|
1400
|
-
file: "src/
|
|
1490
|
+
file: "src/modules/users/services/delivery.ts",
|
|
1401
1491
|
slot: "imports",
|
|
1402
1492
|
when: ["email"],
|
|
1403
1493
|
content: "import { sendEmail } from \"#core/email/send\";"
|
|
1404
1494
|
},
|
|
1405
1495
|
{
|
|
1406
|
-
file: "src/
|
|
1496
|
+
file: "src/modules/users/services/delivery.ts",
|
|
1407
1497
|
slot: "email-delivery",
|
|
1408
1498
|
when: ["email"],
|
|
1409
1499
|
content: "await sendEmail({ to: message.to, subject: message.subject, text: message.text });"
|
|
1410
1500
|
},
|
|
1501
|
+
{
|
|
1502
|
+
file: "tests/helpers/auth.ts",
|
|
1503
|
+
slot: "authenticated",
|
|
1504
|
+
unless: ["auth-orgs"],
|
|
1505
|
+
content: "return { userId, headers };"
|
|
1506
|
+
},
|
|
1411
1507
|
{
|
|
1412
1508
|
file: "AGENTS.md",
|
|
1413
1509
|
slot: "sections",
|
|
@@ -1421,17 +1517,17 @@ const authEmailOtp = {
|
|
|
1421
1517
|
label: "Email OTP",
|
|
1422
1518
|
slots: [
|
|
1423
1519
|
{
|
|
1424
|
-
file: "src/
|
|
1520
|
+
file: "src/modules/users/services/auth.ts",
|
|
1425
1521
|
slot: "imports",
|
|
1426
|
-
content: "import { deliverEmail } from \"#
|
|
1522
|
+
content: "import { deliverEmail } from \"#modules/users/services/delivery\";"
|
|
1427
1523
|
},
|
|
1428
1524
|
{
|
|
1429
|
-
file: "src/
|
|
1525
|
+
file: "src/modules/users/services/auth.ts",
|
|
1430
1526
|
slot: "imports",
|
|
1431
1527
|
content: "import { emailOTP } from \"better-auth/plugins\";"
|
|
1432
1528
|
},
|
|
1433
1529
|
{
|
|
1434
|
-
file: "src/
|
|
1530
|
+
file: "src/modules/users/services/auth.ts",
|
|
1435
1531
|
slot: "plugins",
|
|
1436
1532
|
content: [
|
|
1437
1533
|
"emailOTP({",
|
|
@@ -1450,13 +1546,13 @@ const authEmailOtp = {
|
|
|
1450
1546
|
};
|
|
1451
1547
|
const phoneFields = [
|
|
1452
1548
|
{
|
|
1453
|
-
file: "src/
|
|
1549
|
+
file: "src/modules/users/db/auth.schema.ts",
|
|
1454
1550
|
slot: "user-fields",
|
|
1455
1551
|
when: drizzlePg,
|
|
1456
1552
|
content: "phoneNumber: text().unique(),\nphoneNumberVerified: boolean(),"
|
|
1457
1553
|
},
|
|
1458
1554
|
{
|
|
1459
|
-
file: "src/
|
|
1555
|
+
file: "src/modules/users/db/auth.schema.ts",
|
|
1460
1556
|
slot: "user-fields",
|
|
1461
1557
|
when: drizzleMy,
|
|
1462
1558
|
content: "phoneNumber: varchar({ length: 32 }).unique(),\nphoneNumberVerified: boolean(),"
|
|
@@ -1473,17 +1569,17 @@ const authPhoneOtp = {
|
|
|
1473
1569
|
label: "Phone OTP",
|
|
1474
1570
|
slots: [
|
|
1475
1571
|
{
|
|
1476
|
-
file: "src/
|
|
1572
|
+
file: "src/modules/users/services/auth.ts",
|
|
1477
1573
|
slot: "imports",
|
|
1478
|
-
content: "import { deliverSms } from \"#
|
|
1574
|
+
content: "import { deliverSms } from \"#modules/users/services/delivery\";"
|
|
1479
1575
|
},
|
|
1480
1576
|
{
|
|
1481
|
-
file: "src/
|
|
1577
|
+
file: "src/modules/users/services/auth.ts",
|
|
1482
1578
|
slot: "imports",
|
|
1483
1579
|
content: "import { phoneNumber } from \"better-auth/plugins\";"
|
|
1484
1580
|
},
|
|
1485
1581
|
{
|
|
1486
|
-
file: "src/
|
|
1582
|
+
file: "src/modules/users/services/auth.ts",
|
|
1487
1583
|
slot: "plugins",
|
|
1488
1584
|
content: [
|
|
1489
1585
|
"phoneNumber({",
|
|
@@ -1513,12 +1609,12 @@ const authPassword = {
|
|
|
1513
1609
|
}],
|
|
1514
1610
|
slots: [
|
|
1515
1611
|
{
|
|
1516
|
-
file: "src/
|
|
1612
|
+
file: "src/modules/users/services/auth.ts",
|
|
1517
1613
|
slot: "imports",
|
|
1518
|
-
content: "import { deliverEmail } from \"#
|
|
1614
|
+
content: "import { deliverEmail } from \"#modules/users/services/delivery\";"
|
|
1519
1615
|
},
|
|
1520
1616
|
{
|
|
1521
|
-
file: "src/
|
|
1617
|
+
file: "src/modules/users/services/auth.ts",
|
|
1522
1618
|
slot: "options",
|
|
1523
1619
|
content: [
|
|
1524
1620
|
"emailAndPassword: {",
|
|
@@ -1531,7 +1627,7 @@ const authPassword = {
|
|
|
1531
1627
|
].join("\n")
|
|
1532
1628
|
},
|
|
1533
1629
|
{
|
|
1534
|
-
file: "src/
|
|
1630
|
+
file: "src/modules/users/db/seed.ts",
|
|
1535
1631
|
slot: "seed-after-user",
|
|
1536
1632
|
content: [
|
|
1537
1633
|
"if (env.SEED_ADMIN_PASSWORD) {",
|
|
@@ -1548,25 +1644,25 @@ const authPassword = {
|
|
|
1548
1644
|
};
|
|
1549
1645
|
const adminFields = [
|
|
1550
1646
|
{
|
|
1551
|
-
file: "src/
|
|
1647
|
+
file: "src/modules/users/db/auth.schema.ts",
|
|
1552
1648
|
slot: "user-fields",
|
|
1553
1649
|
when: drizzlePg,
|
|
1554
1650
|
content: "role: text(),\nbanned: boolean().default(false),\nbanReason: text(),\nbanExpires: timestamp({ withTimezone: true, mode: \"date\" }),"
|
|
1555
1651
|
},
|
|
1556
1652
|
{
|
|
1557
|
-
file: "src/
|
|
1653
|
+
file: "src/modules/users/db/auth.schema.ts",
|
|
1558
1654
|
slot: "user-fields",
|
|
1559
1655
|
when: drizzleMy,
|
|
1560
1656
|
content: "role: varchar({ length: 255 }),\nbanned: boolean().default(false),\nbanReason: text(),\nbanExpires: timestamp({ fsp: 3, mode: \"date\" }),"
|
|
1561
1657
|
},
|
|
1562
1658
|
{
|
|
1563
|
-
file: "src/
|
|
1659
|
+
file: "src/modules/users/db/auth.schema.ts",
|
|
1564
1660
|
slot: "session-fields",
|
|
1565
1661
|
when: drizzlePg,
|
|
1566
1662
|
content: "activeOrganizationId: text(),\nimpersonatedBy: text(),"
|
|
1567
1663
|
},
|
|
1568
1664
|
{
|
|
1569
|
-
file: "src/
|
|
1665
|
+
file: "src/modules/users/db/auth.schema.ts",
|
|
1570
1666
|
slot: "session-fields",
|
|
1571
1667
|
when: drizzleMy,
|
|
1572
1668
|
content: "activeOrganizationId: varchar({ length: 36 }),\nimpersonatedBy: varchar({ length: 36 }),"
|
|
@@ -1592,57 +1688,70 @@ const adminFields = [
|
|
|
1592
1688
|
content: ["activeOrganizationId String? @map(\"active_organization_id\") @db.VarChar(36)", "impersonatedBy String? @map(\"impersonated_by\") @db.VarChar(36)"].join("\n")
|
|
1593
1689
|
},
|
|
1594
1690
|
{
|
|
1595
|
-
file: "src/
|
|
1691
|
+
file: "src/modules/users/services/adapter.ts",
|
|
1596
1692
|
slot: "imports",
|
|
1597
1693
|
when: ["drizzle"],
|
|
1598
|
-
content: "import * as orgSchema from \"#
|
|
1694
|
+
content: "import * as orgSchema from \"#modules/users/db/organization.schema\";"
|
|
1599
1695
|
},
|
|
1600
1696
|
{
|
|
1601
|
-
file: "src/
|
|
1697
|
+
file: "src/modules/users/services/adapter.ts",
|
|
1602
1698
|
slot: "adapter-schema",
|
|
1603
1699
|
when: ["drizzle"],
|
|
1604
1700
|
content: "organization: orgSchema.organizations,\nmember: orgSchema.members,\ninvitation: orgSchema.invitations,"
|
|
1605
1701
|
}
|
|
1606
1702
|
];
|
|
1703
|
+
const JOIN_TEST_ORG = [
|
|
1704
|
+
"/** Makes the user a member of `orgId` (a shared test org by default), creating it on first use. */",
|
|
1705
|
+
"export async function joinTestOrg(userId: string, orgId = \"test-org\", role = \"member\"): Promise<string> {",
|
|
1706
|
+
" const { adapter } = await auth.$context;",
|
|
1707
|
+
" const org = await adapter.findOne({ model: \"organization\", where: [{ field: \"id\", value: orgId }] });",
|
|
1708
|
+
" if (!org) {",
|
|
1709
|
+
" await adapter.create({",
|
|
1710
|
+
" model: \"organization\",",
|
|
1711
|
+
" data: { id: orgId, name: orgId, slug: orgId, createdAt: new Date() },",
|
|
1712
|
+
" forceAllowId: true,",
|
|
1713
|
+
" });",
|
|
1714
|
+
" }",
|
|
1715
|
+
" await adapter.create({",
|
|
1716
|
+
" model: \"member\",",
|
|
1717
|
+
" data: { organizationId: orgId, userId, role, createdAt: new Date() },",
|
|
1718
|
+
" });",
|
|
1719
|
+
" return orgId;",
|
|
1720
|
+
"}"
|
|
1721
|
+
].join("\n");
|
|
1607
1722
|
const authOrgs = {
|
|
1608
1723
|
id: "auth-orgs",
|
|
1609
1724
|
kind: "auth-method",
|
|
1610
1725
|
label: "Organizations + roles",
|
|
1611
1726
|
slots: [
|
|
1612
1727
|
{
|
|
1613
|
-
file: "src/
|
|
1728
|
+
file: "src/modules/users/services/auth.ts",
|
|
1614
1729
|
slot: "imports",
|
|
1615
1730
|
content: "import { admin, organization } from \"better-auth/plugins\";"
|
|
1616
1731
|
},
|
|
1617
1732
|
{
|
|
1618
|
-
file: "src/
|
|
1733
|
+
file: "src/modules/users/services/auth.ts",
|
|
1619
1734
|
slot: "plugins",
|
|
1620
1735
|
unless: ["authz"],
|
|
1621
1736
|
content: "organization(),\nadmin(),",
|
|
1622
1737
|
order: 900
|
|
1623
1738
|
},
|
|
1624
1739
|
{
|
|
1625
|
-
file: "src/
|
|
1740
|
+
file: "src/modules/users/services/auth.ts",
|
|
1626
1741
|
slot: "plugins",
|
|
1627
1742
|
when: ["authz"],
|
|
1628
1743
|
content: [
|
|
1629
|
-
"
|
|
1630
|
-
"
|
|
1631
|
-
" // authz rows outlive better-auth's member row; without this, re-adding restores old roles",
|
|
1632
|
-
" afterRemoveMember: async ({ member }) => {",
|
|
1633
|
-
" await removeMemberAccess(member.organizationId, member.userId);",
|
|
1634
|
-
" },",
|
|
1635
|
-
" },",
|
|
1636
|
-
"}),",
|
|
1744
|
+
"// a removed member's authz roles and overlays go with the membership",
|
|
1745
|
+
"organization({ organizationHooks: authzOrganizationHooks(authz) }),",
|
|
1637
1746
|
"admin(),"
|
|
1638
1747
|
].join("\n"),
|
|
1639
1748
|
order: 900
|
|
1640
1749
|
},
|
|
1641
1750
|
{
|
|
1642
|
-
file: "src/
|
|
1751
|
+
file: "src/modules/users/services/auth.ts",
|
|
1643
1752
|
slot: "imports",
|
|
1644
1753
|
when: ["authz"],
|
|
1645
|
-
content: "import {
|
|
1754
|
+
content: "import { authzOrganizationHooks } from \"@tatara/authz\";\nimport { authz } from \"#core/authz/index\";"
|
|
1646
1755
|
},
|
|
1647
1756
|
{
|
|
1648
1757
|
file: "tests/helpers/auth.ts",
|
|
@@ -1650,7 +1759,22 @@ const authOrgs = {
|
|
|
1650
1759
|
content: "...(options.roles ? { role: options.roles.join(\",\") } : {}),"
|
|
1651
1760
|
},
|
|
1652
1761
|
{
|
|
1653
|
-
file: "
|
|
1762
|
+
file: "tests/helpers/auth.ts",
|
|
1763
|
+
slot: "options",
|
|
1764
|
+
content: "/** the org the user joins and acts in (default: a shared test org) */\norgId?: string;"
|
|
1765
|
+
},
|
|
1766
|
+
{
|
|
1767
|
+
file: "tests/helpers/auth.ts",
|
|
1768
|
+
slot: "authenticated",
|
|
1769
|
+
content: ["const orgId = await joinTestOrg(userId, options.orgId);", "return { userId, orgId, headers: { ...headers, \"x-org-id\": orgId } };"].join("\n")
|
|
1770
|
+
},
|
|
1771
|
+
{
|
|
1772
|
+
file: "tests/helpers/auth.ts",
|
|
1773
|
+
slot: "helpers",
|
|
1774
|
+
content: JOIN_TEST_ORG
|
|
1775
|
+
},
|
|
1776
|
+
{
|
|
1777
|
+
file: "src/modules/users/db/seed.ts",
|
|
1654
1778
|
slot: "seed-user-fields",
|
|
1655
1779
|
content: "role: \"admin\","
|
|
1656
1780
|
},
|
|
@@ -1708,41 +1832,41 @@ const authKeycloak = {
|
|
|
1708
1832
|
} },
|
|
1709
1833
|
slots: [
|
|
1710
1834
|
{
|
|
1711
|
-
file: "src/
|
|
1835
|
+
file: "src/modules/users/services/auth.ts",
|
|
1712
1836
|
slot: "imports",
|
|
1713
1837
|
content: "import { keycloakTokens } from \"@tatara/keycloak\";"
|
|
1714
1838
|
},
|
|
1715
1839
|
{
|
|
1716
|
-
file: "src/
|
|
1840
|
+
file: "src/modules/users/services/auth.ts",
|
|
1717
1841
|
slot: "imports",
|
|
1718
|
-
content: "import { keycloak } from \"#
|
|
1842
|
+
content: "import { keycloak } from \"#modules/users/services/keycloak\";"
|
|
1719
1843
|
},
|
|
1720
1844
|
{
|
|
1721
|
-
file: "src/
|
|
1845
|
+
file: "src/modules/users/services/auth.ts",
|
|
1722
1846
|
slot: "imports",
|
|
1723
1847
|
content: "import { logger } from \"#core/logging/logger\";"
|
|
1724
1848
|
},
|
|
1725
1849
|
{
|
|
1726
|
-
file: "src/
|
|
1850
|
+
file: "src/modules/users/services/auth.ts",
|
|
1727
1851
|
slot: "plugins",
|
|
1728
1852
|
unless: ["auth-orgs"],
|
|
1729
1853
|
content: "keycloakTokens({ verifier: keycloak, logger }),",
|
|
1730
1854
|
order: 800
|
|
1731
1855
|
},
|
|
1732
1856
|
{
|
|
1733
|
-
file: "src/
|
|
1857
|
+
file: "src/modules/users/services/auth.ts",
|
|
1734
1858
|
slot: "plugins",
|
|
1735
1859
|
when: ["auth-orgs"],
|
|
1736
1860
|
content: "keycloakTokens({ verifier: keycloak, logger, organizations: true }),",
|
|
1737
1861
|
order: 800
|
|
1738
1862
|
},
|
|
1739
1863
|
{
|
|
1740
|
-
file: "src/
|
|
1864
|
+
file: "src/modules/users/services/auth.ts",
|
|
1741
1865
|
slot: "resolvers",
|
|
1742
|
-
content: "(c) => keycloakPrincipal(
|
|
1866
|
+
content: "(instance) => (c) => keycloakPrincipal(instance, c.req.header(\"authorization\")),"
|
|
1743
1867
|
},
|
|
1744
1868
|
{
|
|
1745
|
-
file: "src/
|
|
1869
|
+
file: "src/modules/users/services/auth.ts",
|
|
1746
1870
|
slot: "imports",
|
|
1747
1871
|
content: "import { keycloakPrincipal } from \"@tatara/keycloak\";"
|
|
1748
1872
|
},
|
|
@@ -1754,7 +1878,7 @@ const authKeycloak = {
|
|
|
1754
1878
|
{
|
|
1755
1879
|
file: "tests/helpers/auth.ts",
|
|
1756
1880
|
slot: "imports",
|
|
1757
|
-
content: "import { keycloak } from \"#
|
|
1881
|
+
content: "import { keycloak } from \"#modules/users/services/keycloak\";"
|
|
1758
1882
|
},
|
|
1759
1883
|
{
|
|
1760
1884
|
file: "tests/helpers/auth.ts",
|
|
@@ -2213,7 +2337,6 @@ function validateOptions(input) {
|
|
|
2213
2337
|
for (const addon of options.addons) {
|
|
2214
2338
|
if ((addon === "authz" || addon === "admin") && options.auth === "none") throw new OptionsError(`The ${addon} add-on needs an auth provider.`);
|
|
2215
2339
|
if (addon === "authz" && !hasDb) throw new OptionsError("The authz add-on needs a database.");
|
|
2216
|
-
if (addon === "admin" && options.auth === "better-auth" && !options.authMethods.includes("orgs") && !options.authMethods.includes("keycloak")) throw new OptionsError("The admin add-on with better-auth needs the \"orgs\" or \"keycloak\" method (they provide user roles).");
|
|
2217
2340
|
}
|
|
2218
2341
|
return options;
|
|
2219
2342
|
}
|
|
@@ -2438,7 +2561,7 @@ function pnpmWorkspace(features) {
|
|
|
2438
2561
|
}
|
|
2439
2562
|
/** Tests of tatara's wiring, not of the app; generated apps leave them out. */
|
|
2440
2563
|
function isFrameworkTest(path) {
|
|
2441
|
-
return /^src\/core\/.+\/tests\/[^/]+\.test\.ts$/.test(path);
|
|
2564
|
+
return path.startsWith("tests/tatara/") || /^src\/core\/.+\/tests\/[^/]+\.test\.ts$/.test(path);
|
|
2442
2565
|
}
|
|
2443
2566
|
/** Folds applicable variants into each feature so later steps see one flat definition. */
|
|
2444
2567
|
function effectiveFeatures(options) {
|
|
@@ -2648,7 +2771,12 @@ function guardSlots(project, n) {
|
|
|
2648
2771
|
});
|
|
2649
2772
|
if (project.has("authz")) {
|
|
2650
2773
|
add("imports", "import { requirePermission } from \"#core/authz/index\";");
|
|
2651
|
-
|
|
2774
|
+
const orgs = project.has("auth-orgs");
|
|
2775
|
+
if (orgs) add("imports", "import { requireOrg } from \"#modules/users/services/auth\";");
|
|
2776
|
+
for (const [route, action] of Object.entries(ACTIONS)) {
|
|
2777
|
+
const guards = `${orgs ? "requireOrg(), " : ""}requirePermission("${n.kebab}:${action}")`;
|
|
2778
|
+
add(`guard-${route}`, `middleware: [${guards}] as const,\nsecurity: [{ bearerAuth: ["${n.kebab}:${action}"] }],`);
|
|
2779
|
+
}
|
|
2652
2780
|
add("auth-errors", "...errorResponses(401, 403),");
|
|
2653
2781
|
} else if (project.has("auth")) {
|
|
2654
2782
|
add("imports", "import { requireAuth } from \"@tatara/auth\";");
|
|
@@ -3156,11 +3284,16 @@ for (const [name, spec] of ${JSON.stringify(apps)}) {
|
|
|
3156
3284
|
|
|
3157
3285
|
for (const [path, item] of Object.entries(doc.paths ?? {})) {
|
|
3158
3286
|
for (const [method, op] of Object.entries(item)) {
|
|
3287
|
+
const tags = op.tags ?? [];
|
|
3159
3288
|
const security = op.security ?? [];
|
|
3160
3289
|
|
|
3161
|
-
// Parse access label based on security schemes
|
|
3290
|
+
// Parse access label based on security schemes and tags
|
|
3162
3291
|
let access = "public";
|
|
3163
|
-
|
|
3292
|
+
|
|
3293
|
+
// Routes tagged "Auth" are from better-auth's merged document
|
|
3294
|
+
if (tags.includes("Auth")) {
|
|
3295
|
+
access = "better-auth";
|
|
3296
|
+
} else if (security.length > 0) {
|
|
3164
3297
|
const labels = [];
|
|
3165
3298
|
for (const requirement of security) {
|
|
3166
3299
|
const schemes = Object.keys(requirement);
|
|
@@ -3185,7 +3318,8 @@ for (const [name, spec] of ${JSON.stringify(apps)}) {
|
|
|
3185
3318
|
access = labels.length > 1 ? labels.join(" OR ") : (labels[0] ?? "authenticated");
|
|
3186
3319
|
}
|
|
3187
3320
|
|
|
3188
|
-
|
|
3321
|
+
const summary = op.summary || op.description || "";
|
|
3322
|
+
out.push({ app: name, method: method.toUpperCase(), path, summary, access });
|
|
3189
3323
|
}
|
|
3190
3324
|
}
|
|
3191
3325
|
}
|
|
@@ -3445,7 +3579,7 @@ function importsOf(text, specifier) {
|
|
|
3445
3579
|
}
|
|
3446
3580
|
//#endregion
|
|
3447
3581
|
//#region src/upgrade/codemods/groups.ts
|
|
3448
|
-
async function read$
|
|
3582
|
+
async function read$5(ctx, path) {
|
|
3449
3583
|
return ctx.read(path).catch(() => void 0);
|
|
3450
3584
|
}
|
|
3451
3585
|
function sourceFiles(ctx) {
|
|
@@ -3458,7 +3592,7 @@ function sourceFiles(ctx) {
|
|
|
3458
3592
|
async function blockersOf$1(ctx, group, sources) {
|
|
3459
3593
|
const blockers = [];
|
|
3460
3594
|
for (const path of [...group.regenerate, ...group.remove]) {
|
|
3461
|
-
const text = await read$
|
|
3595
|
+
const text = await read$5(ctx, path);
|
|
3462
3596
|
if (text === void 0) continue;
|
|
3463
3597
|
if (!await ctx.baseline.isUnchanged(path, text)) blockers.push(`${path} is customised`);
|
|
3464
3598
|
}
|
|
@@ -3598,6 +3732,140 @@ const addonPackages = {
|
|
|
3598
3732
|
}
|
|
3599
3733
|
};
|
|
3600
3734
|
//#endregion
|
|
3735
|
+
//#region src/upgrade/moves.ts
|
|
3736
|
+
/**
|
|
3737
|
+
* Files later CLIs generate somewhere else. An older codemod that regenerates an old path gets
|
|
3738
|
+
* what this CLI generates at the new one; the codemod of the release that moved it moves it.
|
|
3739
|
+
*/
|
|
3740
|
+
const MOVED_IN_0_10 = {
|
|
3741
|
+
"src/core/http/app.ts": "src/apps/http.ts",
|
|
3742
|
+
"src/core/auth/auth.ts": "src/modules/users/services/auth.ts",
|
|
3743
|
+
"src/core/auth/adapter.ts": "src/modules/users/services/adapter.ts",
|
|
3744
|
+
"src/core/auth/delivery.ts": "src/modules/users/services/delivery.ts",
|
|
3745
|
+
"src/core/auth/sms.ts": "src/modules/users/services/sms.ts",
|
|
3746
|
+
"src/core/auth/keycloak.ts": "src/modules/users/services/keycloak.ts",
|
|
3747
|
+
"src/core/auth/db/seed.ts": "src/modules/users/db/seed.ts",
|
|
3748
|
+
"src/core/auth/db/auth.schema.ts": "src/modules/users/db/auth.schema.ts",
|
|
3749
|
+
"src/core/auth/db/organization.schema.ts": "src/modules/users/db/organization.schema.ts",
|
|
3750
|
+
"src/core/auth/routes/me.routes.ts": "src/modules/users/routes/me.routes.ts",
|
|
3751
|
+
"src/core/auth/commands/keycloak-token.ts": "src/modules/users/commands/keycloak-token.ts",
|
|
3752
|
+
"src/core/authz/routes/authorization.routes.ts": "src/modules/users/routes/authorization.routes.ts"
|
|
3753
|
+
};
|
|
3754
|
+
/** Merged into another file in 0.10: nothing is generated in its place. */
|
|
3755
|
+
const MERGED_IN_0_10 = { "src/core/auth/principal.ts": "src/modules/users/services/auth.ts" };
|
|
3756
|
+
/** Where this CLI generates what used to be at `path` (the path itself if it never moved). */
|
|
3757
|
+
function currentPath(path) {
|
|
3758
|
+
return MOVED_IN_0_10[path] ?? path;
|
|
3759
|
+
}
|
|
3760
|
+
//#endregion
|
|
3761
|
+
//#region src/upgrade/codemods/app-owned-auth.ts
|
|
3762
|
+
/** Import specifiers that moved, old → new; every name keeps working at the new place. */
|
|
3763
|
+
const SPECIFIERS = {
|
|
3764
|
+
"#core/http/app": "#apps/http",
|
|
3765
|
+
"#core/auth/auth": "#modules/users/services/auth",
|
|
3766
|
+
"#core/auth/principal": "#modules/users/services/auth",
|
|
3767
|
+
"#core/auth/adapter": "#modules/users/services/adapter",
|
|
3768
|
+
"#core/auth/delivery": "#modules/users/services/delivery",
|
|
3769
|
+
"#core/auth/sms": "#modules/users/services/sms",
|
|
3770
|
+
"#core/auth/keycloak": "#modules/users/services/keycloak",
|
|
3771
|
+
"#core/auth/db/auth.schema": "#modules/users/db/auth.schema",
|
|
3772
|
+
"#core/auth/db/organization.schema": "#modules/users/db/organization.schema"
|
|
3773
|
+
};
|
|
3774
|
+
/** Files whose content this release changes in place (new imports, orgRole, single tenant). */
|
|
3775
|
+
const REGENERATED$2 = [
|
|
3776
|
+
"src/core/http/context.ts",
|
|
3777
|
+
"tests/helpers/auth.ts",
|
|
3778
|
+
"src/core/authz/index.ts"
|
|
3779
|
+
];
|
|
3780
|
+
async function read$4(ctx, path) {
|
|
3781
|
+
return ctx.read(path).catch(() => void 0);
|
|
3782
|
+
}
|
|
3783
|
+
/** Inserts the single-tenant option into an app's own createAuthz call, if it lacks one. */
|
|
3784
|
+
function withSingleTenant(text) {
|
|
3785
|
+
if (text.includes("singleTenant")) return text;
|
|
3786
|
+
return text.replace(/(createAuthz(?:<[^>]*>)?\(\{\n)/, "$1 singleTenant: \"default\",\n");
|
|
3787
|
+
}
|
|
3788
|
+
const STAFF_FILES = [
|
|
3789
|
+
"src/modules/staff/services/auth.ts",
|
|
3790
|
+
"src/modules/staff/services/adapter.ts",
|
|
3791
|
+
"src/modules/staff/db/seed.ts",
|
|
3792
|
+
"src/modules/staff/db/staff.schema.ts",
|
|
3793
|
+
"prisma/models/staff.prisma",
|
|
3794
|
+
"tests/helpers/staff.ts"
|
|
3795
|
+
];
|
|
3796
|
+
/** The admin app now signs in staff accounts of its own; the regenerated apps/http.ts imports them. */
|
|
3797
|
+
async function addStaff(ctx) {
|
|
3798
|
+
for (const path of STAFF_FILES) {
|
|
3799
|
+
if (await read$4(ctx, path) !== void 0) continue;
|
|
3800
|
+
const text = await ctx.renderCurrent(path);
|
|
3801
|
+
if (text !== void 0) await ctx.write(path, text);
|
|
3802
|
+
}
|
|
3803
|
+
const customisedEnv = [];
|
|
3804
|
+
for (const path of [
|
|
3805
|
+
"src/core/config/env.ts",
|
|
3806
|
+
".env.example",
|
|
3807
|
+
".env.test"
|
|
3808
|
+
]) {
|
|
3809
|
+
const text = await read$4(ctx, path);
|
|
3810
|
+
if (text === void 0) continue;
|
|
3811
|
+
if (await ctx.baseline.isUnchanged(path, text)) await ctx.regenerate(path);
|
|
3812
|
+
else customisedEnv.push(path);
|
|
3813
|
+
}
|
|
3814
|
+
if (customisedEnv.length > 0) ctx.report.notes.push(`Add ADMIN_URL, SEED_STAFF_EMAIL and SEED_STAFF_PASSWORD to ${customisedEnv.join(", ")} (they are customised, so they were left as they are).`);
|
|
3815
|
+
ctx.report.notes.push("The admin app now signs in staff accounts (src/modules/staff, tables staff_*) instead of users with the admin role. Run `tatara db generate` for the new tables, add { key: \"staff\" } to src/core/authz/system-roles.ts if you use authz, and grant your platform permissions to it.");
|
|
3816
|
+
}
|
|
3817
|
+
const appOwnedAuth = {
|
|
3818
|
+
version: "0.10.0",
|
|
3819
|
+
id: "app-owned-auth",
|
|
3820
|
+
description: "Move auth into src/modules/users and HTTP wiring into src/apps; explicit org context",
|
|
3821
|
+
async run(ctx) {
|
|
3822
|
+
const { config } = ctx.project;
|
|
3823
|
+
const orgs = config.authMethods.includes("orgs");
|
|
3824
|
+
if (config.addons.includes("authz") && !orgs) {
|
|
3825
|
+
const index = await read$4(ctx, "src/core/authz/index.ts");
|
|
3826
|
+
const next = index && withSingleTenant(index);
|
|
3827
|
+
if (next && next !== index) await ctx.write("src/core/authz/index.ts", next);
|
|
3828
|
+
}
|
|
3829
|
+
const blockers = [];
|
|
3830
|
+
const moves = [];
|
|
3831
|
+
for (const [from, to] of Object.entries(MOVED_IN_0_10)) {
|
|
3832
|
+
const text = await read$4(ctx, from);
|
|
3833
|
+
if (text === void 0) continue;
|
|
3834
|
+
const there = await read$4(ctx, to);
|
|
3835
|
+
if (there !== void 0 && there !== await ctx.renderCurrent(to)) blockers.push(`${to} already exists`);
|
|
3836
|
+
else if (await ctx.baseline.isUnchanged(from, text)) moves.push([from, to]);
|
|
3837
|
+
else blockers.push(`${from} is customised`);
|
|
3838
|
+
}
|
|
3839
|
+
const merged = [];
|
|
3840
|
+
for (const from of Object.keys(MERGED_IN_0_10)) {
|
|
3841
|
+
const text = await read$4(ctx, from);
|
|
3842
|
+
if (text === void 0) continue;
|
|
3843
|
+
if (await ctx.baseline.isUnchanged(from, text)) merged.push(from);
|
|
3844
|
+
else blockers.push(`${from} is customised`);
|
|
3845
|
+
}
|
|
3846
|
+
if (blockers.length > 0) {
|
|
3847
|
+
ctx.report.notes.push(`Auth was not moved into src/modules/users: ${blockers.join("; ")}. Compare with a newly generated app (auth in src/modules/users/services/auth.ts built with createAuthInstance, HTTP wiring in src/apps/http.ts) and move it by hand.`);
|
|
3848
|
+
ctx.report.blocked.push("app-owned-auth");
|
|
3849
|
+
return;
|
|
3850
|
+
}
|
|
3851
|
+
for (const [from, to] of moves) {
|
|
3852
|
+
const next = await ctx.renderCurrent(to);
|
|
3853
|
+
if (next !== void 0) await ctx.write(to, next);
|
|
3854
|
+
await ctx.removeIfUnchanged(from);
|
|
3855
|
+
}
|
|
3856
|
+
for (const from of merged) await ctx.removeIfUnchanged(from);
|
|
3857
|
+
for (const path of REGENERATED$2) {
|
|
3858
|
+
const text = await read$4(ctx, path);
|
|
3859
|
+
if (text !== void 0 && await ctx.baseline.isUnchanged(path, text)) await ctx.regenerate(path);
|
|
3860
|
+
}
|
|
3861
|
+
for (const [from, to] of Object.entries(SPECIFIERS)) await ctx.rewriteImports(from, to);
|
|
3862
|
+
if (config.addons.includes("admin")) await addStaff(ctx);
|
|
3863
|
+
const leftovers = await glob(["src/core/auth/**/*.ts"], { cwd: ctx.project.root });
|
|
3864
|
+
if (leftovers.length > 0) ctx.report.notes.push(`Move these into src/modules/users (core must not hold auth flows): ${leftovers.join(", ")}`);
|
|
3865
|
+
if (orgs) ctx.report.notes.push("Requests that act in an organization now name it (x-org-id header, or the session's active organization); authz no longer falls back to a default org.");
|
|
3866
|
+
}
|
|
3867
|
+
};
|
|
3868
|
+
//#endregion
|
|
3601
3869
|
//#region src/upgrade/codemods/auth-package.ts
|
|
3602
3870
|
const AUTH = "@tatara/auth";
|
|
3603
3871
|
const KEYCLOAK = "@tatara/keycloak";
|
|
@@ -4232,12 +4500,17 @@ const CODEMODS = [
|
|
|
4232
4500
|
await ctx.removeIfUnchanged(path);
|
|
4233
4501
|
}
|
|
4234
4502
|
}
|
|
4235
|
-
}
|
|
4503
|
+
},
|
|
4504
|
+
appOwnedAuth
|
|
4236
4505
|
];
|
|
4237
4506
|
//#endregion
|
|
4238
4507
|
//#region src/upgrade/context.ts
|
|
4239
4508
|
const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4240
|
-
function createContext(project,
|
|
4509
|
+
function createContext(project, appBaseline, opts = {}) {
|
|
4510
|
+
const ours = /* @__PURE__ */ new Map();
|
|
4511
|
+
const baseline = { async isUnchanged(path, content) {
|
|
4512
|
+
return ours.get(path) === hashContent(content) || appBaseline.isUnchanged(path, content);
|
|
4513
|
+
} };
|
|
4241
4514
|
const report = {
|
|
4242
4515
|
ran: [],
|
|
4243
4516
|
written: [],
|
|
@@ -4257,6 +4530,7 @@ function createContext(project, baseline, opts = {}) {
|
|
|
4257
4530
|
const before = existsSync(abs(path)) ? await readFile(abs(path), "utf8") : void 0;
|
|
4258
4531
|
if (before === void 0 || await baseline.isUnchanged(path, before)) report.regenerated.push(path);
|
|
4259
4532
|
}
|
|
4533
|
+
ours.set(path, hashContent(text));
|
|
4260
4534
|
if (opts.dryRun) return;
|
|
4261
4535
|
await mkdir(dirname(abs(path)), { recursive: true });
|
|
4262
4536
|
await writeFile(abs(path), text);
|
|
@@ -4309,7 +4583,7 @@ function createContext(project, baseline, opts = {}) {
|
|
|
4309
4583
|
addons,
|
|
4310
4584
|
packageManager
|
|
4311
4585
|
});
|
|
4312
|
-
return (await rendered).get(path);
|
|
4586
|
+
return (await rendered).get(currentPath(path));
|
|
4313
4587
|
},
|
|
4314
4588
|
async regenerate(path) {
|
|
4315
4589
|
const next = await ctx.renderCurrent(path);
|