feat build
This commit is contained in:
+925
-30
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,38 @@
|
||||
-- Revocable admin sessions.
|
||||
--
|
||||
-- Until now an admin-panel token was purely self-contained: once signed it was
|
||||
-- valid for its full 12h and nothing could cut it short — not deactivating the
|
||||
-- account, not a central "sign out everywhere". Connecting the central IAM made
|
||||
-- that gap concrete, because an IAM session revocation had no effect on a token
|
||||
-- this API had already issued.
|
||||
--
|
||||
-- Each issued token now carries a `jti` recorded here, and JwtStrategy rejects a
|
||||
-- token whose row is revoked. Rows are only written (and only enforced) while
|
||||
-- session tracking is enabled, so a deployment that leaves it off keeps exactly
|
||||
-- the previous behaviour and the previous query count.
|
||||
CREATE TABLE "AdminSession" (
|
||||
"id" TEXT NOT NULL,
|
||||
"jti" TEXT NOT NULL,
|
||||
"adminUserId" TEXT NOT NULL,
|
||||
"iamSessionId" TEXT,
|
||||
"iamSub" TEXT,
|
||||
"ipAddress" TEXT,
|
||||
"userAgent" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"expiresAt" TIMESTAMP(3),
|
||||
"lastSeenAt" TIMESTAMP(3),
|
||||
"revokedAt" TIMESTAMP(3),
|
||||
"revokedReason" TEXT,
|
||||
|
||||
CONSTRAINT "AdminSession_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "AdminSession_jti_key" ON "AdminSession"("jti");
|
||||
CREATE INDEX "AdminSession_adminUserId_idx" ON "AdminSession"("adminUserId");
|
||||
CREATE INDEX "AdminSession_iamSessionId_idx" ON "AdminSession"("iamSessionId");
|
||||
CREATE INDEX "AdminSession_iamSub_idx" ON "AdminSession"("iamSub");
|
||||
CREATE INDEX "AdminSession_expiresAt_idx" ON "AdminSession"("expiresAt");
|
||||
|
||||
ALTER TABLE "AdminSession"
|
||||
ADD CONSTRAINT "AdminSession_adminUserId_fkey"
|
||||
FOREIGN KEY ("adminUserId") REFERENCES "AdminUser"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,33 @@
|
||||
-- Revocable app sessions (customer app and sales-expert app).
|
||||
--
|
||||
-- The admin panel got this in 20260925120000_admin_session; the app token family
|
||||
-- had the same gap: a 30-day self-contained token that nothing could end early.
|
||||
-- That matters more here, because the app's login is OTP-only — there is no
|
||||
-- password an operator could change to cut access.
|
||||
--
|
||||
-- No foreign key on purpose: an app token's subject is a Customer for a customer
|
||||
-- and an AdminUser for a sales expert (D21), which one FK cannot express.
|
||||
CREATE TABLE "AppSession" (
|
||||
"id" TEXT NOT NULL,
|
||||
"jti" TEXT NOT NULL,
|
||||
"subjectId" TEXT NOT NULL,
|
||||
"subjectType" TEXT NOT NULL,
|
||||
"mobile" TEXT,
|
||||
"iamSessionId" TEXT,
|
||||
"iamSub" TEXT,
|
||||
"ipAddress" TEXT,
|
||||
"userAgent" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"expiresAt" TIMESTAMP(3),
|
||||
"lastSeenAt" TIMESTAMP(3),
|
||||
"revokedAt" TIMESTAMP(3),
|
||||
"revokedReason" TEXT,
|
||||
|
||||
CONSTRAINT "AppSession_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "AppSession_jti_key" ON "AppSession"("jti");
|
||||
CREATE INDEX "AppSession_subjectId_idx" ON "AppSession"("subjectId");
|
||||
CREATE INDEX "AppSession_iamSessionId_idx" ON "AppSession"("iamSessionId");
|
||||
CREATE INDEX "AppSession_iamSub_idx" ON "AppSession"("iamSub");
|
||||
CREATE INDEX "AppSession_expiresAt_idx" ON "AppSession"("expiresAt");
|
||||
@@ -1,9 +0,0 @@
|
||||
class PrismaClient {
|
||||
constructor() {
|
||||
throw new Error(
|
||||
'@prisma/client/deno/edge did not initialize yet. Please run "prisma generate" and try to import it again.',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export { PrismaClient }
|
||||
+39
-5
File diff suppressed because one or more lines are too long
@@ -150,6 +150,38 @@ exports.Prisma.AdminUserScalarFieldEnum = {
|
||||
memberCode: 'memberCode'
|
||||
};
|
||||
|
||||
exports.Prisma.AppSessionScalarFieldEnum = {
|
||||
id: 'id',
|
||||
jti: 'jti',
|
||||
subjectId: 'subjectId',
|
||||
subjectType: 'subjectType',
|
||||
mobile: 'mobile',
|
||||
iamSessionId: 'iamSessionId',
|
||||
iamSub: 'iamSub',
|
||||
ipAddress: 'ipAddress',
|
||||
userAgent: 'userAgent',
|
||||
createdAt: 'createdAt',
|
||||
expiresAt: 'expiresAt',
|
||||
lastSeenAt: 'lastSeenAt',
|
||||
revokedAt: 'revokedAt',
|
||||
revokedReason: 'revokedReason'
|
||||
};
|
||||
|
||||
exports.Prisma.AdminSessionScalarFieldEnum = {
|
||||
id: 'id',
|
||||
jti: 'jti',
|
||||
adminUserId: 'adminUserId',
|
||||
iamSessionId: 'iamSessionId',
|
||||
iamSub: 'iamSub',
|
||||
ipAddress: 'ipAddress',
|
||||
userAgent: 'userAgent',
|
||||
createdAt: 'createdAt',
|
||||
expiresAt: 'expiresAt',
|
||||
lastSeenAt: 'lastSeenAt',
|
||||
revokedAt: 'revokedAt',
|
||||
revokedReason: 'revokedReason'
|
||||
};
|
||||
|
||||
exports.Prisma.SalesExpertProfileScalarFieldEnum = {
|
||||
id: 'id',
|
||||
adminUserId: 'adminUserId',
|
||||
@@ -657,6 +689,8 @@ exports.MessageAudience = exports.$Enums.MessageAudience = {
|
||||
exports.Prisma.ModelName = {
|
||||
City: 'City',
|
||||
AdminUser: 'AdminUser',
|
||||
AppSession: 'AppSession',
|
||||
AdminSession: 'AdminSession',
|
||||
SalesExpertProfile: 'SalesExpertProfile',
|
||||
Customer: 'Customer',
|
||||
CustomerAddress: 'CustomerAddress',
|
||||
|
||||
+3254
-60
File diff suppressed because it is too large
Load Diff
+39
-5
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "prisma-client-4b01ea5ca305ee68796b97b40684b681824ae0788fb6c2d4d029c05f8b265ccc",
|
||||
"name": "prisma-client-e72c5c915e96796902913fde6ec160a337ad8fec683314696d8bd7a12ac26468",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"browser": "index-browser.js",
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -184,6 +184,11 @@ model AdminUser {
|
||||
/// Only ever set for role = SALES_EXPERT (D17).
|
||||
salesExpertProfile SalesExpertProfile?
|
||||
|
||||
/// Issued admin-panel tokens, recorded only while session tracking is on
|
||||
/// (AUTH_SESSION_TRACKING, or implicitly whenever the IAM is enabled) so a
|
||||
/// token can be revoked before it expires. See AdminSession.
|
||||
sessions AdminSession[]
|
||||
|
||||
@@index([role])
|
||||
@@index([userTypeId])
|
||||
@@index([avatarFileId])
|
||||
@@ -191,6 +196,94 @@ model AdminUser {
|
||||
@@index([lastName])
|
||||
}
|
||||
|
||||
/// An issued APP token (customer app / sales-expert app), so it can be revoked
|
||||
/// before it expires — the counterpart of AdminSession for the other token family.
|
||||
///
|
||||
/// WHY A SEPARATE TABLE AND NO FOREIGN KEY
|
||||
/// ---------------------------------------
|
||||
/// An app token's `sub` is a Customer id for a customer and an **AdminUser id**
|
||||
/// for a sales expert (D21). One FK cannot point at two tables, so the subject is
|
||||
/// stored as a plain id plus its type. The rows are short-lived operational data,
|
||||
/// not part of the accounting model, so the missing FK costs nothing here.
|
||||
model AppSession {
|
||||
id String @id @default(cuid())
|
||||
|
||||
/// The token's `jti` claim. Unique: one row per issued token.
|
||||
jti String @unique
|
||||
|
||||
/// Customer.id, or AdminUser.id when subjectType is "sales-expert".
|
||||
subjectId String
|
||||
subjectType String
|
||||
|
||||
mobile String?
|
||||
|
||||
/// Set when the login was confirmed by the central IAM: its session id (`sid`)
|
||||
/// and stable user id (`sub`), which is what a back-channel logout arrives with.
|
||||
iamSessionId String?
|
||||
iamSub String?
|
||||
|
||||
ipAddress String?
|
||||
userAgent String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
expiresAt DateTime?
|
||||
lastSeenAt DateTime?
|
||||
|
||||
revokedAt DateTime?
|
||||
revokedReason String?
|
||||
|
||||
@@index([subjectId])
|
||||
@@index([iamSessionId])
|
||||
@@index([iamSub])
|
||||
@@index([expiresAt])
|
||||
}
|
||||
|
||||
/// An issued admin-panel JWT, so it can be revoked before it expires.
|
||||
///
|
||||
/// WHY THIS EXISTS
|
||||
/// ---------------
|
||||
/// The admin token is self-contained and valid for 12h, which means nothing —
|
||||
/// not disabling the account, not a central "sign out everywhere" — could cut a
|
||||
/// session short: the token kept working until it expired. With the central IAM
|
||||
/// in the loop that gap became visible (an IAM session revocation had no effect
|
||||
/// here), so every issued token now carries a `jti` recorded on this table and
|
||||
/// JwtStrategy refuses a token whose row is revoked.
|
||||
///
|
||||
/// Rows are written ONLY while tracking is enabled, and JwtStrategy only
|
||||
/// enforces them while it is enabled, so a deployment that does not want the
|
||||
/// extra lookup behaves exactly as before.
|
||||
model AdminSession {
|
||||
id String @id @default(cuid())
|
||||
|
||||
/// The token's `jti` claim. Unique: one row per issued token.
|
||||
jti String @unique
|
||||
|
||||
adminUserId String
|
||||
adminUser AdminUser @relation(fields: [adminUserId], references: [id], onDelete: Cascade)
|
||||
|
||||
/// Set when the login was verified by the IAM: its session id (`sid`) and the
|
||||
/// stable user id (`sub`) there. Back-channel logout arrives keyed by these —
|
||||
/// deliberately here rather than as columns on AdminUser, so enabling the
|
||||
/// feature needs no change to an existing table.
|
||||
iamSessionId String?
|
||||
iamSub String?
|
||||
|
||||
ipAddress String?
|
||||
userAgent String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
expiresAt DateTime?
|
||||
lastSeenAt DateTime?
|
||||
|
||||
revokedAt DateTime?
|
||||
revokedReason String?
|
||||
|
||||
@@index([adminUserId])
|
||||
@@index([iamSessionId])
|
||||
@@index([iamSub])
|
||||
@@index([expiresAt])
|
||||
}
|
||||
|
||||
/// The sales-expert fields legacy kept on the `customers` row (type=1) and that
|
||||
/// were lost when a sales expert became an AdminUser (D17).
|
||||
///
|
||||
|
||||
+34
@@ -150,6 +150,38 @@ exports.Prisma.AdminUserScalarFieldEnum = {
|
||||
memberCode: 'memberCode'
|
||||
};
|
||||
|
||||
exports.Prisma.AppSessionScalarFieldEnum = {
|
||||
id: 'id',
|
||||
jti: 'jti',
|
||||
subjectId: 'subjectId',
|
||||
subjectType: 'subjectType',
|
||||
mobile: 'mobile',
|
||||
iamSessionId: 'iamSessionId',
|
||||
iamSub: 'iamSub',
|
||||
ipAddress: 'ipAddress',
|
||||
userAgent: 'userAgent',
|
||||
createdAt: 'createdAt',
|
||||
expiresAt: 'expiresAt',
|
||||
lastSeenAt: 'lastSeenAt',
|
||||
revokedAt: 'revokedAt',
|
||||
revokedReason: 'revokedReason'
|
||||
};
|
||||
|
||||
exports.Prisma.AdminSessionScalarFieldEnum = {
|
||||
id: 'id',
|
||||
jti: 'jti',
|
||||
adminUserId: 'adminUserId',
|
||||
iamSessionId: 'iamSessionId',
|
||||
iamSub: 'iamSub',
|
||||
ipAddress: 'ipAddress',
|
||||
userAgent: 'userAgent',
|
||||
createdAt: 'createdAt',
|
||||
expiresAt: 'expiresAt',
|
||||
lastSeenAt: 'lastSeenAt',
|
||||
revokedAt: 'revokedAt',
|
||||
revokedReason: 'revokedReason'
|
||||
};
|
||||
|
||||
exports.Prisma.SalesExpertProfileScalarFieldEnum = {
|
||||
id: 'id',
|
||||
adminUserId: 'adminUserId',
|
||||
@@ -657,6 +689,8 @@ exports.MessageAudience = exports.$Enums.MessageAudience = {
|
||||
exports.Prisma.ModelName = {
|
||||
City: 'City',
|
||||
AdminUser: 'AdminUser',
|
||||
AppSession: 'AppSession',
|
||||
AdminSession: 'AdminSession',
|
||||
SalesExpertProfile: 'SalesExpertProfile',
|
||||
Customer: 'Customer',
|
||||
CustomerAddress: 'CustomerAddress',
|
||||
|
||||
Reference in New Issue
Block a user