@idevconn/create-icore 0.10.2 → 0.12.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/cli.js +24 -2
- package/dist/index.cjs +23 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +23 -1
- package/package.json +1 -1
- package/templates/docker-compose.yml +14 -0
- package/templates/libs/auth-strategies/postgres/eslint.config.mjs +32 -0
- package/templates/libs/auth-strategies/postgres/package.json +23 -0
- package/templates/libs/auth-strategies/postgres/project.json +19 -0
- package/templates/libs/auth-strategies/postgres/src/index.ts +3 -0
- package/templates/libs/auth-strategies/postgres/src/lib/__tests__/postgres-auth.contract.unit.test.ts +4 -0
- package/templates/libs/auth-strategies/postgres/src/lib/__tests__/postgres-auth.module.unit.test.ts +49 -0
- package/templates/libs/auth-strategies/postgres/src/lib/postgres-auth.module.ts +39 -0
- package/templates/libs/auth-strategies/postgres/src/lib/postgres-auth.strategy.ts +210 -0
- package/templates/libs/auth-strategies/postgres/src/lib/testing/mock-postgres-auth.ts +106 -0
- package/templates/libs/auth-strategies/postgres/tsconfig.json +19 -0
- package/templates/libs/auth-strategies/postgres/tsconfig.lib.json +24 -0
- package/templates/libs/auth-strategies/postgres/tsconfig.spec.json +22 -0
- package/templates/libs/auth-strategies/postgres/vitest.config.mts +22 -0
- package/templates/libs/db-strategies/postgres/eslint.config.mjs +30 -0
- package/templates/libs/db-strategies/postgres/package.json +19 -0
- package/templates/libs/db-strategies/postgres/project.json +19 -0
- package/templates/libs/db-strategies/postgres/src/index.ts +3 -0
- package/templates/libs/db-strategies/postgres/src/lib/__tests__/postgres-db.contract.unit.test.ts +4 -0
- package/templates/libs/db-strategies/postgres/src/lib/__tests__/postgres-db.module.unit.test.ts +37 -0
- package/templates/libs/db-strategies/postgres/src/lib/postgres-db.module.ts +33 -0
- package/templates/libs/db-strategies/postgres/src/lib/postgres-db.strategy.ts +139 -0
- package/templates/libs/db-strategies/postgres/src/lib/testing/mock-postgres.ts +94 -0
- package/templates/libs/db-strategies/postgres/tsconfig.json +19 -0
- package/templates/libs/db-strategies/postgres/tsconfig.lib.json +24 -0
- package/templates/libs/db-strategies/postgres/tsconfig.spec.json +22 -0
- package/templates/libs/db-strategies/postgres/vitest.config.mts +22 -0
- package/templates/tsconfig.base.json +3 -1
package/dist/cli.js
CHANGED
|
@@ -224,6 +224,7 @@ Re-run with @latest to refresh:
|
|
|
224
224
|
{ value: "supabase", label: "Supabase" },
|
|
225
225
|
{ value: "firebase", label: "Firebase" },
|
|
226
226
|
{ value: "mongodb", label: "MongoDB (Custom Auth)" },
|
|
227
|
+
{ value: "postgres", label: "PostgreSQL (direct, postgres.js + bcrypt + JWT)" },
|
|
227
228
|
{ value: "none", label: "None \u2014 no login, open API (simple SPA)" }
|
|
228
229
|
]
|
|
229
230
|
});
|
|
@@ -233,7 +234,8 @@ Re-run with @latest to refresh:
|
|
|
233
234
|
options: [
|
|
234
235
|
{ value: "supabase", label: "Supabase Postgres" },
|
|
235
236
|
{ value: "firebase", label: "Firestore" },
|
|
236
|
-
{ value: "mongodb", label: "MongoDB" }
|
|
237
|
+
{ value: "mongodb", label: "MongoDB" },
|
|
238
|
+
{ value: "postgres", label: "PostgreSQL (direct, postgres.js)" }
|
|
237
239
|
],
|
|
238
240
|
initialValue: authProvider
|
|
239
241
|
});
|
|
@@ -410,7 +412,7 @@ async function rewriteRootPackageJson(targetDir, opts) {
|
|
|
410
412
|
const pkg = JSON.parse(raw);
|
|
411
413
|
pkg["name"] = opts.projectName;
|
|
412
414
|
pkg["version"] = "0.0.1";
|
|
413
|
-
pkg["icoreVersion"] = true ? "0.
|
|
415
|
+
pkg["icoreVersion"] = true ? "0.12.0" : "unknown";
|
|
414
416
|
pkg["private"] = true;
|
|
415
417
|
delete pkg.description;
|
|
416
418
|
const transportDeps = TRANSPORT_DEPS[opts.transport];
|
|
@@ -1476,6 +1478,16 @@ var MANIFEST = {
|
|
|
1476
1478
|
deps: { mongoose: "^9.6.3" },
|
|
1477
1479
|
tsPaths: { "@icore/auth-mongodb": ["libs/auth-strategies/mongodb/src/index.ts"] },
|
|
1478
1480
|
nestModule: { importFrom: "@icore/auth-mongodb", symbol: "MongoDbAuthModule", into: "auth" }
|
|
1481
|
+
},
|
|
1482
|
+
postgres: {
|
|
1483
|
+
libDirs: ["libs/auth-strategies/postgres"],
|
|
1484
|
+
deps: { postgres: "^3", bcrypt: "^6", jsonwebtoken: "^9" },
|
|
1485
|
+
tsPaths: { "@icore/auth-postgres": ["libs/auth-strategies/postgres/src/index.ts"] },
|
|
1486
|
+
nestModule: {
|
|
1487
|
+
importFrom: "@icore/auth-postgres",
|
|
1488
|
+
symbol: "PostgresAuthModule",
|
|
1489
|
+
into: "auth"
|
|
1490
|
+
}
|
|
1479
1491
|
}
|
|
1480
1492
|
},
|
|
1481
1493
|
storage: {
|
|
@@ -1538,6 +1550,16 @@ var MANIFEST = {
|
|
|
1538
1550
|
deps: { mongoose: "^9.6.3" },
|
|
1539
1551
|
tsPaths: { "@icore/db-mongodb": ["libs/db-strategies/mongodb/src/index.ts"] },
|
|
1540
1552
|
nestModule: { importFrom: "@icore/db-mongodb", symbol: "MongoDbDbModule", into: "notes" }
|
|
1553
|
+
},
|
|
1554
|
+
postgres: {
|
|
1555
|
+
libDirs: ["libs/db-strategies/postgres"],
|
|
1556
|
+
deps: { postgres: "^3" },
|
|
1557
|
+
tsPaths: { "@icore/db-postgres": ["libs/db-strategies/postgres/src/index.ts"] },
|
|
1558
|
+
nestModule: {
|
|
1559
|
+
importFrom: "@icore/db-postgres",
|
|
1560
|
+
symbol: "PostgresDbModule",
|
|
1561
|
+
into: "notes"
|
|
1562
|
+
}
|
|
1541
1563
|
}
|
|
1542
1564
|
},
|
|
1543
1565
|
feature: {
|
package/dist/index.cjs
CHANGED
|
@@ -1187,6 +1187,16 @@ var MANIFEST = {
|
|
|
1187
1187
|
deps: { mongoose: "^9.6.3" },
|
|
1188
1188
|
tsPaths: { "@icore/auth-mongodb": ["libs/auth-strategies/mongodb/src/index.ts"] },
|
|
1189
1189
|
nestModule: { importFrom: "@icore/auth-mongodb", symbol: "MongoDbAuthModule", into: "auth" }
|
|
1190
|
+
},
|
|
1191
|
+
postgres: {
|
|
1192
|
+
libDirs: ["libs/auth-strategies/postgres"],
|
|
1193
|
+
deps: { postgres: "^3", bcrypt: "^6", jsonwebtoken: "^9" },
|
|
1194
|
+
tsPaths: { "@icore/auth-postgres": ["libs/auth-strategies/postgres/src/index.ts"] },
|
|
1195
|
+
nestModule: {
|
|
1196
|
+
importFrom: "@icore/auth-postgres",
|
|
1197
|
+
symbol: "PostgresAuthModule",
|
|
1198
|
+
into: "auth"
|
|
1199
|
+
}
|
|
1190
1200
|
}
|
|
1191
1201
|
},
|
|
1192
1202
|
storage: {
|
|
@@ -1249,6 +1259,16 @@ var MANIFEST = {
|
|
|
1249
1259
|
deps: { mongoose: "^9.6.3" },
|
|
1250
1260
|
tsPaths: { "@icore/db-mongodb": ["libs/db-strategies/mongodb/src/index.ts"] },
|
|
1251
1261
|
nestModule: { importFrom: "@icore/db-mongodb", symbol: "MongoDbDbModule", into: "notes" }
|
|
1262
|
+
},
|
|
1263
|
+
postgres: {
|
|
1264
|
+
libDirs: ["libs/db-strategies/postgres"],
|
|
1265
|
+
deps: { postgres: "^3" },
|
|
1266
|
+
tsPaths: { "@icore/db-postgres": ["libs/db-strategies/postgres/src/index.ts"] },
|
|
1267
|
+
nestModule: {
|
|
1268
|
+
importFrom: "@icore/db-postgres",
|
|
1269
|
+
symbol: "PostgresDbModule",
|
|
1270
|
+
into: "notes"
|
|
1271
|
+
}
|
|
1252
1272
|
}
|
|
1253
1273
|
},
|
|
1254
1274
|
feature: {
|
|
@@ -2238,6 +2258,7 @@ Re-run with @latest to refresh:
|
|
|
2238
2258
|
{ value: "supabase", label: "Supabase" },
|
|
2239
2259
|
{ value: "firebase", label: "Firebase" },
|
|
2240
2260
|
{ value: "mongodb", label: "MongoDB (Custom Auth)" },
|
|
2261
|
+
{ value: "postgres", label: "PostgreSQL (direct, postgres.js + bcrypt + JWT)" },
|
|
2241
2262
|
{ value: "none", label: "None \u2014 no login, open API (simple SPA)" }
|
|
2242
2263
|
]
|
|
2243
2264
|
});
|
|
@@ -2247,7 +2268,8 @@ Re-run with @latest to refresh:
|
|
|
2247
2268
|
options: [
|
|
2248
2269
|
{ value: "supabase", label: "Supabase Postgres" },
|
|
2249
2270
|
{ value: "firebase", label: "Firestore" },
|
|
2250
|
-
{ value: "mongodb", label: "MongoDB" }
|
|
2271
|
+
{ value: "mongodb", label: "MongoDB" },
|
|
2272
|
+
{ value: "postgres", label: "PostgreSQL (direct, postgres.js)" }
|
|
2251
2273
|
],
|
|
2252
2274
|
initialValue: authProvider
|
|
2253
2275
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
type AuthBackend = 'supabase' | 'firebase' | 'mongodb';
|
|
1
|
+
type AuthBackend = 'supabase' | 'firebase' | 'mongodb' | 'postgres';
|
|
2
2
|
type AuthProvider = AuthBackend | 'none';
|
|
3
|
-
type DbProvider = 'supabase' | 'firebase' | 'mongodb' | 'none';
|
|
3
|
+
type DbProvider = 'supabase' | 'firebase' | 'mongodb' | 'postgres' | 'none';
|
|
4
4
|
type UploadProvider = 'supabase' | 'firebase' | 'cloudinary' | 'mongodb' | 'none';
|
|
5
5
|
type PaymentProvider = 'paypal' | 'none';
|
|
6
6
|
type JobsProvider = 'bullmq' | 'none';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
type AuthBackend = 'supabase' | 'firebase' | 'mongodb';
|
|
1
|
+
type AuthBackend = 'supabase' | 'firebase' | 'mongodb' | 'postgres';
|
|
2
2
|
type AuthProvider = AuthBackend | 'none';
|
|
3
|
-
type DbProvider = 'supabase' | 'firebase' | 'mongodb' | 'none';
|
|
3
|
+
type DbProvider = 'supabase' | 'firebase' | 'mongodb' | 'postgres' | 'none';
|
|
4
4
|
type UploadProvider = 'supabase' | 'firebase' | 'cloudinary' | 'mongodb' | 'none';
|
|
5
5
|
type PaymentProvider = 'paypal' | 'none';
|
|
6
6
|
type JobsProvider = 'bullmq' | 'none';
|
package/dist/index.js
CHANGED
|
@@ -1144,6 +1144,16 @@ var MANIFEST = {
|
|
|
1144
1144
|
deps: { mongoose: "^9.6.3" },
|
|
1145
1145
|
tsPaths: { "@icore/auth-mongodb": ["libs/auth-strategies/mongodb/src/index.ts"] },
|
|
1146
1146
|
nestModule: { importFrom: "@icore/auth-mongodb", symbol: "MongoDbAuthModule", into: "auth" }
|
|
1147
|
+
},
|
|
1148
|
+
postgres: {
|
|
1149
|
+
libDirs: ["libs/auth-strategies/postgres"],
|
|
1150
|
+
deps: { postgres: "^3", bcrypt: "^6", jsonwebtoken: "^9" },
|
|
1151
|
+
tsPaths: { "@icore/auth-postgres": ["libs/auth-strategies/postgres/src/index.ts"] },
|
|
1152
|
+
nestModule: {
|
|
1153
|
+
importFrom: "@icore/auth-postgres",
|
|
1154
|
+
symbol: "PostgresAuthModule",
|
|
1155
|
+
into: "auth"
|
|
1156
|
+
}
|
|
1147
1157
|
}
|
|
1148
1158
|
},
|
|
1149
1159
|
storage: {
|
|
@@ -1206,6 +1216,16 @@ var MANIFEST = {
|
|
|
1206
1216
|
deps: { mongoose: "^9.6.3" },
|
|
1207
1217
|
tsPaths: { "@icore/db-mongodb": ["libs/db-strategies/mongodb/src/index.ts"] },
|
|
1208
1218
|
nestModule: { importFrom: "@icore/db-mongodb", symbol: "MongoDbDbModule", into: "notes" }
|
|
1219
|
+
},
|
|
1220
|
+
postgres: {
|
|
1221
|
+
libDirs: ["libs/db-strategies/postgres"],
|
|
1222
|
+
deps: { postgres: "^3" },
|
|
1223
|
+
tsPaths: { "@icore/db-postgres": ["libs/db-strategies/postgres/src/index.ts"] },
|
|
1224
|
+
nestModule: {
|
|
1225
|
+
importFrom: "@icore/db-postgres",
|
|
1226
|
+
symbol: "PostgresDbModule",
|
|
1227
|
+
into: "notes"
|
|
1228
|
+
}
|
|
1209
1229
|
}
|
|
1210
1230
|
},
|
|
1211
1231
|
feature: {
|
|
@@ -2195,6 +2215,7 @@ Re-run with @latest to refresh:
|
|
|
2195
2215
|
{ value: "supabase", label: "Supabase" },
|
|
2196
2216
|
{ value: "firebase", label: "Firebase" },
|
|
2197
2217
|
{ value: "mongodb", label: "MongoDB (Custom Auth)" },
|
|
2218
|
+
{ value: "postgres", label: "PostgreSQL (direct, postgres.js + bcrypt + JWT)" },
|
|
2198
2219
|
{ value: "none", label: "None \u2014 no login, open API (simple SPA)" }
|
|
2199
2220
|
]
|
|
2200
2221
|
});
|
|
@@ -2204,7 +2225,8 @@ Re-run with @latest to refresh:
|
|
|
2204
2225
|
options: [
|
|
2205
2226
|
{ value: "supabase", label: "Supabase Postgres" },
|
|
2206
2227
|
{ value: "firebase", label: "Firestore" },
|
|
2207
|
-
{ value: "mongodb", label: "MongoDB" }
|
|
2228
|
+
{ value: "mongodb", label: "MongoDB" },
|
|
2229
|
+
{ value: "postgres", label: "PostgreSQL (direct, postgres.js)" }
|
|
2208
2230
|
],
|
|
2209
2231
|
initialValue: authProvider
|
|
2210
2232
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idevconn/create-icore",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Bootstrap a new project from the iCore scaffold (Nx + NestJS + React + Vite + shadcn/Tailwind, swappable auth + storage providers).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "iDEVconn",
|
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
services:
|
|
2
|
+
postgres:
|
|
3
|
+
image: postgres:16-alpine
|
|
4
|
+
environment:
|
|
5
|
+
POSTGRES_USER: ${POSTGRES_USER:-icore}
|
|
6
|
+
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-icore}
|
|
7
|
+
POSTGRES_DB: ${POSTGRES_DB:-icore}
|
|
8
|
+
ports:
|
|
9
|
+
- '5432:5432'
|
|
10
|
+
healthcheck:
|
|
11
|
+
test: ['CMD-SHELL', 'pg_isready -U icore']
|
|
12
|
+
interval: 5s
|
|
13
|
+
retries: 10
|
|
14
|
+
networks: [icore]
|
|
15
|
+
|
|
2
16
|
redis:
|
|
3
17
|
image: redis:7-alpine
|
|
4
18
|
healthcheck:
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import baseConfig from '../../../eslint.config.mjs';
|
|
2
|
+
|
|
3
|
+
export default [
|
|
4
|
+
...baseConfig,
|
|
5
|
+
{
|
|
6
|
+
files: ['**/*.json'],
|
|
7
|
+
rules: {
|
|
8
|
+
'@nx/dependency-checks': [
|
|
9
|
+
'error',
|
|
10
|
+
{
|
|
11
|
+
ignoredFiles: [
|
|
12
|
+
'{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}',
|
|
13
|
+
'{projectRoot}/vitest.config.{js,ts,mjs,mts}',
|
|
14
|
+
],
|
|
15
|
+
ignoredDependencies: [
|
|
16
|
+
'@icore/shared',
|
|
17
|
+
'bcrypt',
|
|
18
|
+
'jsonwebtoken',
|
|
19
|
+
'postgres',
|
|
20
|
+
'@nestjs/common',
|
|
21
|
+
'@nestjs/config',
|
|
22
|
+
'@nestjs/testing',
|
|
23
|
+
'vitest',
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
languageOptions: {
|
|
29
|
+
parser: await import('jsonc-eslint-parser'),
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@icore/auth-postgres",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"@icore/shared": "*",
|
|
10
|
+
"@nestjs/common": "^11.1.27",
|
|
11
|
+
"@nestjs/config": "^4.0.4",
|
|
12
|
+
"bcrypt": "^6.0.0",
|
|
13
|
+
"jsonwebtoken": "^9.0.3",
|
|
14
|
+
"postgres": "^3.4.5",
|
|
15
|
+
"tslib": "^2.8.1"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@nestjs/testing": "^11.1.27",
|
|
19
|
+
"@types/bcrypt": "^6.0.0",
|
|
20
|
+
"@types/jsonwebtoken": "^9.0.10",
|
|
21
|
+
"vitest": "^4.1.9"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "auth-postgres",
|
|
3
|
+
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
|
4
|
+
"sourceRoot": "libs/auth-strategies/postgres/src",
|
|
5
|
+
"projectType": "library",
|
|
6
|
+
"tags": [],
|
|
7
|
+
"targets": {
|
|
8
|
+
"build": {
|
|
9
|
+
"executor": "@nx/js:tsc",
|
|
10
|
+
"outputs": ["{options.outputPath}"],
|
|
11
|
+
"options": {
|
|
12
|
+
"outputPath": "dist/libs/auth-strategies/postgres",
|
|
13
|
+
"main": "libs/auth-strategies/postgres/src/index.ts",
|
|
14
|
+
"tsConfig": "libs/auth-strategies/postgres/tsconfig.lib.json",
|
|
15
|
+
"assets": ["libs/auth-strategies/postgres/*.md"]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
package/templates/libs/auth-strategies/postgres/src/lib/__tests__/postgres-auth.module.unit.test.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { Global, Module } from '@nestjs/common';
|
|
3
|
+
import { Test } from '@nestjs/testing';
|
|
4
|
+
import { ConfigService } from '@nestjs/config';
|
|
5
|
+
import { PostgresAuthModule, POSTGRES_AUTH_REQUIRED_ENV } from '../postgres-auth.module.js';
|
|
6
|
+
import { PostgresAuthStrategy } from '../postgres-auth.strategy.js';
|
|
7
|
+
|
|
8
|
+
let ENV: Record<string, string | undefined> = {};
|
|
9
|
+
|
|
10
|
+
@Global()
|
|
11
|
+
@Module({
|
|
12
|
+
providers: [
|
|
13
|
+
{
|
|
14
|
+
provide: ConfigService,
|
|
15
|
+
useValue: {
|
|
16
|
+
get: (k: string) => ENV[k],
|
|
17
|
+
getOrThrow: (k: string) => ENV[k],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
exports: [ConfigService],
|
|
22
|
+
})
|
|
23
|
+
class StubConfigModule {}
|
|
24
|
+
|
|
25
|
+
describe('PostgresAuthModule', () => {
|
|
26
|
+
it('declares its required env', () => {
|
|
27
|
+
expect(POSTGRES_AUTH_REQUIRED_ENV).toEqual(['POSTGRES_URL', 'JWT_SECRET']);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('provides a real PostgresAuthStrategy under AuthStrategy when env present', async () => {
|
|
31
|
+
ENV = {
|
|
32
|
+
POSTGRES_URL: 'postgresql://user:pass@localhost:5432/test',
|
|
33
|
+
JWT_SECRET: 'test-secret',
|
|
34
|
+
};
|
|
35
|
+
const ref = await Test.createTestingModule({
|
|
36
|
+
imports: [StubConfigModule, PostgresAuthModule.forRoot('.env')],
|
|
37
|
+
}).compile();
|
|
38
|
+
expect(ref.get('AuthStrategy')).toBeInstanceOf(PostgresAuthStrategy);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('provides FakeAuthStrategy when JWT_SECRET is missing', async () => {
|
|
42
|
+
ENV = { POSTGRES_URL: 'postgresql://user:pass@localhost:5432/test' };
|
|
43
|
+
const ref = await Test.createTestingModule({
|
|
44
|
+
imports: [StubConfigModule, PostgresAuthModule.forRoot('.env')],
|
|
45
|
+
}).compile();
|
|
46
|
+
const strategy = ref.get('AuthStrategy');
|
|
47
|
+
expect(strategy).not.toBeInstanceOf(PostgresAuthStrategy);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Module, DynamicModule } from '@nestjs/common';
|
|
2
|
+
import { ConfigService } from '@nestjs/config';
|
|
3
|
+
import { buildStrategyWithFallback, FakeAuthStrategy } from '@icore/shared';
|
|
4
|
+
import type { AuthStrategy } from '@icore/shared';
|
|
5
|
+
import { PostgresAuthStrategy } from './postgres-auth.strategy';
|
|
6
|
+
|
|
7
|
+
export const POSTGRES_AUTH_REQUIRED_ENV = ['POSTGRES_URL', 'JWT_SECRET'];
|
|
8
|
+
|
|
9
|
+
@Module({})
|
|
10
|
+
export class PostgresAuthModule {
|
|
11
|
+
static forRoot(envPath: string): DynamicModule {
|
|
12
|
+
return {
|
|
13
|
+
module: PostgresAuthModule,
|
|
14
|
+
providers: [
|
|
15
|
+
{
|
|
16
|
+
provide: 'AuthStrategy',
|
|
17
|
+
useFactory: (cfg: ConfigService): AuthStrategy =>
|
|
18
|
+
buildStrategyWithFallback<AuthStrategy>({
|
|
19
|
+
service: 'auth MS',
|
|
20
|
+
provider: 'postgres',
|
|
21
|
+
requiredEnv: POSTGRES_AUTH_REQUIRED_ENV,
|
|
22
|
+
cfg,
|
|
23
|
+
envPath,
|
|
24
|
+
build: () =>
|
|
25
|
+
new PostgresAuthStrategy({
|
|
26
|
+
url: cfg.getOrThrow<string>('POSTGRES_URL'),
|
|
27
|
+
jwtSecret: cfg.getOrThrow<string>('JWT_SECRET'),
|
|
28
|
+
jwtExpiresIn: cfg.get<string>('JWT_EXPIRES_IN'),
|
|
29
|
+
refreshExpiresIn: cfg.get<string>('JWT_REFRESH_EXPIRES_IN'),
|
|
30
|
+
}),
|
|
31
|
+
fake: () => new FakeAuthStrategy(),
|
|
32
|
+
}),
|
|
33
|
+
inject: [ConfigService],
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
exports: ['AuthStrategy'],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import postgres from 'postgres';
|
|
2
|
+
import * as bcrypt from 'bcrypt';
|
|
3
|
+
import * as jwt from 'jsonwebtoken';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import type {
|
|
6
|
+
AuthSession,
|
|
7
|
+
AuthStrategy,
|
|
8
|
+
MagicLinkRequest,
|
|
9
|
+
OAuthProvider,
|
|
10
|
+
OAuthStartResult,
|
|
11
|
+
VerifiedToken,
|
|
12
|
+
} from '@icore/shared';
|
|
13
|
+
|
|
14
|
+
export interface PostgresAuthStrategyOptions {
|
|
15
|
+
url: string;
|
|
16
|
+
jwtSecret: string;
|
|
17
|
+
jwtExpiresIn?: string;
|
|
18
|
+
refreshExpiresIn?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseDurationSeconds(s: string): number {
|
|
22
|
+
const m = /^(\d+)(s|m|h|d)$/.exec(s);
|
|
23
|
+
if (!m) return 900;
|
|
24
|
+
const n = parseInt(m[1] as string, 10);
|
|
25
|
+
const unit = m[2] as string;
|
|
26
|
+
if (unit === 's') return n;
|
|
27
|
+
if (unit === 'm') return n * 60;
|
|
28
|
+
if (unit === 'h') return n * 3600;
|
|
29
|
+
return n * 86400;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseDurationMs(s: string): number {
|
|
33
|
+
return parseDurationSeconds(s) * 1000;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class PostgresAuthStrategy implements AuthStrategy {
|
|
37
|
+
private readonly sql: postgres.Sql;
|
|
38
|
+
private tablesReadyPromise: Promise<void> | null = null;
|
|
39
|
+
|
|
40
|
+
constructor(private readonly opts: PostgresAuthStrategyOptions) {
|
|
41
|
+
this.sql = postgres(opts.url);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private ensureTables(): Promise<void> {
|
|
45
|
+
if (!this.tablesReadyPromise) {
|
|
46
|
+
this.tablesReadyPromise = this._createTables();
|
|
47
|
+
}
|
|
48
|
+
return this.tablesReadyPromise;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private async _createTables(): Promise<void> {
|
|
52
|
+
await this.sql`
|
|
53
|
+
CREATE TABLE IF NOT EXISTS _icore_users (
|
|
54
|
+
id TEXT PRIMARY KEY,
|
|
55
|
+
email TEXT UNIQUE NOT NULL,
|
|
56
|
+
password_hash TEXT,
|
|
57
|
+
role TEXT,
|
|
58
|
+
last_logged_in TIMESTAMPTZ,
|
|
59
|
+
created_at TIMESTAMPTZ DEFAULT now()
|
|
60
|
+
)
|
|
61
|
+
`;
|
|
62
|
+
await this.sql`
|
|
63
|
+
CREATE TABLE IF NOT EXISTS _icore_sessions (
|
|
64
|
+
id TEXT PRIMARY KEY,
|
|
65
|
+
user_id TEXT NOT NULL,
|
|
66
|
+
refresh_token TEXT UNIQUE NOT NULL,
|
|
67
|
+
expires_at TIMESTAMPTZ NOT NULL
|
|
68
|
+
)
|
|
69
|
+
`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async verifyToken(token: string): Promise<VerifiedToken> {
|
|
73
|
+
try {
|
|
74
|
+
const decoded = jwt.verify(token, this.opts.jwtSecret) as jwt.JwtPayload;
|
|
75
|
+
return {
|
|
76
|
+
uid: decoded.sub as string,
|
|
77
|
+
email: decoded['email'] as string,
|
|
78
|
+
role: decoded['role'] as string,
|
|
79
|
+
};
|
|
80
|
+
} catch (err) {
|
|
81
|
+
throw new Error('invalid_token', { cause: err });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async signIn(email: string, password: string): Promise<AuthSession> {
|
|
86
|
+
await this.ensureTables();
|
|
87
|
+
const rows = await this.sql<
|
|
88
|
+
{ id: string; email: string; password_hash: string; role: string | null }[]
|
|
89
|
+
>`
|
|
90
|
+
SELECT id, email, password_hash, role FROM _icore_users WHERE email = ${email}
|
|
91
|
+
`;
|
|
92
|
+
const user = rows[0];
|
|
93
|
+
if (!user || !user.password_hash) throw new Error('invalid_credentials');
|
|
94
|
+
const ok = await bcrypt.compare(password, user.password_hash);
|
|
95
|
+
if (!ok) throw new Error('invalid_credentials');
|
|
96
|
+
await this.sql`
|
|
97
|
+
UPDATE _icore_users SET last_logged_in = now() WHERE id = ${user.id}
|
|
98
|
+
`;
|
|
99
|
+
return this.createSession({ id: user.id, email: user.email, role: user.role ?? undefined });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async signUp(email: string, password: string): Promise<AuthSession> {
|
|
103
|
+
await this.ensureTables();
|
|
104
|
+
const id = randomUUID();
|
|
105
|
+
const passwordHash = await bcrypt.hash(password, 10);
|
|
106
|
+
try {
|
|
107
|
+
await this.sql`
|
|
108
|
+
INSERT INTO _icore_users (id, email, password_hash) VALUES (${id}, ${email}, ${passwordHash})
|
|
109
|
+
`;
|
|
110
|
+
} catch (err: unknown) {
|
|
111
|
+
if (
|
|
112
|
+
err &&
|
|
113
|
+
typeof err === 'object' &&
|
|
114
|
+
'code' in err &&
|
|
115
|
+
(err as { code: string }).code === '23505'
|
|
116
|
+
) {
|
|
117
|
+
throw new Error('user_already_exists', { cause: err });
|
|
118
|
+
}
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
121
|
+
return this.createSession({ id, email });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async refresh(refreshToken: string): Promise<AuthSession> {
|
|
125
|
+
await this.ensureTables();
|
|
126
|
+
const sessions = await this.sql<{ id: string; user_id: string; expires_at: Date }[]>`
|
|
127
|
+
SELECT id, user_id, expires_at FROM _icore_sessions WHERE refresh_token = ${refreshToken}
|
|
128
|
+
`;
|
|
129
|
+
const session = sessions[0];
|
|
130
|
+
if (!session || session.expires_at < new Date()) {
|
|
131
|
+
if (session) {
|
|
132
|
+
await this.sql`DELETE FROM _icore_sessions WHERE id = ${session.id}`;
|
|
133
|
+
}
|
|
134
|
+
throw new Error('invalid_refresh_token');
|
|
135
|
+
}
|
|
136
|
+
const users = await this.sql<{ id: string; email: string; role: string | null }[]>`
|
|
137
|
+
SELECT id, email, role FROM _icore_users WHERE id = ${session.user_id}
|
|
138
|
+
`;
|
|
139
|
+
const user = users[0];
|
|
140
|
+
if (!user) throw new Error('user_not_found');
|
|
141
|
+
await this.sql`DELETE FROM _icore_sessions WHERE id = ${session.id}`;
|
|
142
|
+
await this.sql`
|
|
143
|
+
UPDATE _icore_users SET last_logged_in = now() WHERE id = ${user.id}
|
|
144
|
+
`;
|
|
145
|
+
return this.createSession({ id: user.id, email: user.email, role: user.role ?? undefined });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async setRole(uid: string, role: string): Promise<void> {
|
|
149
|
+
await this.ensureTables();
|
|
150
|
+
await this.sql`UPDATE _icore_users SET role = ${role} WHERE id = ${uid}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async getRole(uid: string): Promise<string | null> {
|
|
154
|
+
await this.ensureTables();
|
|
155
|
+
const rows = await this.sql<{ role: string | null }[]>`
|
|
156
|
+
SELECT role FROM _icore_users WHERE id = ${uid}
|
|
157
|
+
`;
|
|
158
|
+
return rows[0]?.role ?? null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async sendMagicLink(_req: MagicLinkRequest): Promise<void> {
|
|
162
|
+
throw new Error('not_implemented');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async verifyMagicLink(_token: string): Promise<AuthSession> {
|
|
166
|
+
throw new Error('not_implemented');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async startOAuth(_provider: OAuthProvider, _callbackUrl: string): Promise<OAuthStartResult> {
|
|
170
|
+
throw new Error('not_implemented');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async completeOAuth(
|
|
174
|
+
_provider: OAuthProvider,
|
|
175
|
+
_code: string,
|
|
176
|
+
_state: string,
|
|
177
|
+
): Promise<AuthSession> {
|
|
178
|
+
throw new Error('not_implemented');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private async createSession(user: {
|
|
182
|
+
id: string;
|
|
183
|
+
email: string;
|
|
184
|
+
role?: string;
|
|
185
|
+
}): Promise<AuthSession> {
|
|
186
|
+
const expiresIn = this.opts.jwtExpiresIn ?? '15m';
|
|
187
|
+
const accessToken = jwt.sign(
|
|
188
|
+
{ sub: user.id, email: user.email, role: user.role },
|
|
189
|
+
this.opts.jwtSecret,
|
|
190
|
+
{ expiresIn: expiresIn as jwt.SignOptions['expiresIn'] },
|
|
191
|
+
);
|
|
192
|
+
const refreshToken = randomUUID();
|
|
193
|
+
const refreshMs = parseDurationMs(this.opts.refreshExpiresIn ?? '7d');
|
|
194
|
+
const expiresAt = new Date(Date.now() + refreshMs);
|
|
195
|
+
await this.sql`
|
|
196
|
+
INSERT INTO _icore_sessions (id, user_id, refresh_token, expires_at)
|
|
197
|
+
VALUES (${randomUUID()}, ${user.id}, ${refreshToken}, ${expiresAt})
|
|
198
|
+
`;
|
|
199
|
+
return {
|
|
200
|
+
accessToken,
|
|
201
|
+
refreshToken,
|
|
202
|
+
expiresIn: parseDurationSeconds(expiresIn),
|
|
203
|
+
user: { id: user.id, email: user.email },
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async end(): Promise<void> {
|
|
208
|
+
await this.sql.end();
|
|
209
|
+
}
|
|
210
|
+
}
|