@boopkit/cap-users 0.1.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/capability.json +50 -0
- package/dist/account-management-service.d.ts +52 -0
- package/dist/account-management-service.d.ts.map +1 -0
- package/dist/account-management-service.js +140 -0
- package/dist/account-management-service.js.map +1 -0
- package/dist/account-property-service.d.ts +18 -0
- package/dist/account-property-service.d.ts.map +1 -0
- package/dist/account-property-service.js +19 -0
- package/dist/account-property-service.js.map +1 -0
- package/dist/account-service.d.ts +84 -0
- package/dist/account-service.d.ts.map +1 -0
- package/dist/account-service.js +136 -0
- package/dist/account-service.js.map +1 -0
- package/dist/auth-middleware.d.ts +50 -0
- package/dist/auth-middleware.d.ts.map +1 -0
- package/dist/auth-middleware.js +149 -0
- package/dist/auth-middleware.js.map +1 -0
- package/dist/auth-service.d.ts +112 -0
- package/dist/auth-service.d.ts.map +1 -0
- package/dist/auth-service.js +575 -0
- package/dist/auth-service.js.map +1 -0
- package/dist/identity-service.d.ts +48 -0
- package/dist/identity-service.d.ts.map +1 -0
- package/dist/identity-service.js +98 -0
- package/dist/identity-service.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/team-invitation-service.d.ts +61 -0
- package/dist/team-invitation-service.d.ts.map +1 -0
- package/dist/team-invitation-service.js +237 -0
- package/dist/team-invitation-service.js.map +1 -0
- package/dist/types.d.ts +92 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +24 -0
- package/dist/types.js.map +1 -0
- package/dist/user-property-service.d.ts +18 -0
- package/dist/user-property-service.d.ts.map +1 -0
- package/dist/user-property-service.js +19 -0
- package/dist/user-property-service.js.map +1 -0
- package/dist/user-service.d.ts +47 -0
- package/dist/user-service.d.ts.map +1 -0
- package/dist/user-service.js +39 -0
- package/dist/user-service.js.map +1 -0
- package/package.json +31 -0
- package/scaffold/prisma/schema/accounts.prisma +47 -0
- package/scaffold/prisma/schema/users.prisma +47 -0
- package/scaffold/reference/user-auth.ref.md +237 -0
- package/scaffold/src/controllers/auth.ts +158 -0
- package/scaffold/src/middleware/auth.ts +28 -0
- package/scaffold/src/routes/auth.ts +20 -0
- package/scaffold/src/services/accounts.service.ts +53 -0
- package/scaffold/src/services/users.service.ts +46 -0
- package/scaffold/styles/auth.css +120 -0
- package/scaffold/views/login.hbs +28 -0
- package/scaffold/views/partials/auth-nav.hbs +32 -0
- package/scaffold/views/signup.hbs +29 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createPropertyService, type PropertyService, type PropertyRecord } from '@boopkit/cap-services';
|
|
2
|
+
/** Options for creating a user property service. */
|
|
3
|
+
export interface UserPropertyServiceOptions {
|
|
4
|
+
onCreated?(record: PropertyRecord): void;
|
|
5
|
+
onUpdated?(record: PropertyRecord): void;
|
|
6
|
+
onRemoved?(record: PropertyRecord): void;
|
|
7
|
+
}
|
|
8
|
+
/** The service object returned by createUserPropertyService. */
|
|
9
|
+
export type UserPropertyService = PropertyService;
|
|
10
|
+
/**
|
|
11
|
+
* Create a property service pre-configured for user properties.
|
|
12
|
+
*
|
|
13
|
+
* Thin wrapper over createPropertyService with:
|
|
14
|
+
* - parentIdField set to 'userId'
|
|
15
|
+
* - sensitiveProperties set to ['credentials']
|
|
16
|
+
*/
|
|
17
|
+
export declare function createUserPropertyService(model: Parameters<typeof createPropertyService>[0], options?: UserPropertyServiceOptions): UserPropertyService;
|
|
18
|
+
//# sourceMappingURL=user-property-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"user-property-service.d.ts","sourceRoot":"","sources":["../src/user-property-service.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,KAAK,eAAe,EACpB,KAAK,cAAc,EACpB,MAAM,uBAAuB,CAAC;AAE/B,oDAAoD;AACpD,MAAM,WAAW,0BAA0B;IACzC,SAAS,CAAC,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAAC;IACzC,SAAS,CAAC,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAAC;IACzC,SAAS,CAAC,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAAC;CAC1C;AAED,gEAAgE;AAChE,MAAM,MAAM,mBAAmB,GAAG,eAAe,CAAC;AAElD;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAC,CAAC,CAAC,EAClD,OAAO,CAAC,EAAE,0BAA0B,GACnC,mBAAmB,CAMrB"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createUserPropertyService = createUserPropertyService;
|
|
4
|
+
const cap_services_1 = require("@boopkit/cap-services");
|
|
5
|
+
/**
|
|
6
|
+
* Create a property service pre-configured for user properties.
|
|
7
|
+
*
|
|
8
|
+
* Thin wrapper over createPropertyService with:
|
|
9
|
+
* - parentIdField set to 'userId'
|
|
10
|
+
* - sensitiveProperties set to ['credentials']
|
|
11
|
+
*/
|
|
12
|
+
function createUserPropertyService(model, options) {
|
|
13
|
+
return (0, cap_services_1.createPropertyService)(model, {
|
|
14
|
+
parentIdField: 'userId',
|
|
15
|
+
sensitiveProperties: ['credentials'],
|
|
16
|
+
...options,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=user-property-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"user-property-service.js","sourceRoot":"","sources":["../src/user-property-service.ts"],"names":[],"mappings":";;AAuBA,8DASC;AAhCD,wDAI+B;AAY/B;;;;;;GAMG;AACH,SAAgB,yBAAyB,CACvC,KAAkD,EAClD,OAAoC;IAEpC,OAAO,IAAA,oCAAqB,EAAC,KAAK,EAAE;QAClC,aAAa,EAAE,QAAQ;QACvB,mBAAmB,EAAE,CAAC,aAAa,CAAC;QACpC,GAAG,OAAO;KACX,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { EntityService } from '@boopkit/cap-services';
|
|
2
|
+
import type { BaseUserInfo, User } from './types.js';
|
|
3
|
+
/** Lifecycle callbacks for user mutations. */
|
|
4
|
+
export interface UserServiceOptions<TInfo extends BaseUserInfo> {
|
|
5
|
+
onCreated?(user: User<TInfo>): void;
|
|
6
|
+
onUpdated?(user: User<TInfo>): void;
|
|
7
|
+
onRemoved?(user: User<TInfo>): void;
|
|
8
|
+
}
|
|
9
|
+
/** The service object returned by createUserService. */
|
|
10
|
+
export interface UserService<TInfo extends BaseUserInfo> extends EntityService<TInfo> {
|
|
11
|
+
findByPublicId(publicId: string): Promise<User<TInfo> | null>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Minimal delegate shape — extends the entity ModelDelegate with
|
|
15
|
+
* support for publicId lookups.
|
|
16
|
+
*/
|
|
17
|
+
interface UserModelDelegate {
|
|
18
|
+
create(args: {
|
|
19
|
+
data: Record<string, unknown>;
|
|
20
|
+
}): Promise<Record<string, unknown>>;
|
|
21
|
+
findUnique(args: {
|
|
22
|
+
where: Record<string, unknown>;
|
|
23
|
+
}): Promise<Record<string, unknown> | null>;
|
|
24
|
+
findMany(args?: {
|
|
25
|
+
skip?: number;
|
|
26
|
+
take?: number;
|
|
27
|
+
}): Promise<Record<string, unknown>[]>;
|
|
28
|
+
update(args: {
|
|
29
|
+
where: {
|
|
30
|
+
id: number;
|
|
31
|
+
};
|
|
32
|
+
data: Record<string, unknown>;
|
|
33
|
+
}): Promise<Record<string, unknown>>;
|
|
34
|
+
delete(args: {
|
|
35
|
+
where: {
|
|
36
|
+
id: number;
|
|
37
|
+
};
|
|
38
|
+
}): Promise<Record<string, unknown>>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Create a typed user service composing on top of createEntityService.
|
|
42
|
+
*
|
|
43
|
+
* Inherits CRUD from entity-service and adds `findByPublicId`.
|
|
44
|
+
*/
|
|
45
|
+
export declare function createUserService<TInfo extends BaseUserInfo = BaseUserInfo>(model: UserModelDelegate, options?: UserServiceOptions<TInfo>): UserService<TInfo>;
|
|
46
|
+
export {};
|
|
47
|
+
//# sourceMappingURL=user-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"user-service.d.ts","sourceRoot":"","sources":["../src/user-service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAErD,8CAA8C;AAC9C,MAAM,WAAW,kBAAkB,CAAC,KAAK,SAAS,YAAY;IAC5D,SAAS,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;IACpC,SAAS,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;IACpC,SAAS,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;CACrC;AAED,wDAAwD;AACxD,MAAM,WAAW,WAAW,CAAC,KAAK,SAAS,YAAY,CAAE,SAAQ,aAAa,CAAC,KAAK,CAAC;IACnF,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC;CAC/D;AAED;;;GAGG;AACH,UAAU,iBAAiB;IACzB,MAAM,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAClF,UAAU,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;IAC9F,QAAQ,CAAC,IAAI,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;IACtF,MAAM,CAAC,IAAI,EAAE;QACX,KAAK,EAAE;YAAE,EAAE,EAAE,MAAM,CAAA;SAAE,CAAC;QACtB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KAC/B,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACrC,MAAM,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE;YAAE,EAAE,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC3E;AAiBD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,SAAS,YAAY,GAAG,YAAY,EACzE,KAAK,EAAE,iBAAiB,EACxB,OAAO,CAAC,EAAE,kBAAkB,CAAC,KAAK,CAAC,GAClC,WAAW,CAAC,KAAK,CAAC,CAepB"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createUserService = createUserService;
|
|
4
|
+
const cap_services_1 = require("@boopkit/cap-services");
|
|
5
|
+
/**
|
|
6
|
+
* Map a raw delegate row to a typed User entity.
|
|
7
|
+
*/
|
|
8
|
+
function toUser(row) {
|
|
9
|
+
return {
|
|
10
|
+
id: row.id,
|
|
11
|
+
publicId: row.publicId,
|
|
12
|
+
info: row.info,
|
|
13
|
+
infoSchema: row.infoSchema,
|
|
14
|
+
revision: row.revision,
|
|
15
|
+
createdAt: row.createdAt instanceof Date ? row.createdAt : new Date(row.createdAt),
|
|
16
|
+
updatedAt: row.updatedAt instanceof Date ? row.updatedAt : new Date(row.updatedAt),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Create a typed user service composing on top of createEntityService.
|
|
21
|
+
*
|
|
22
|
+
* Inherits CRUD from entity-service and adds `findByPublicId`.
|
|
23
|
+
*/
|
|
24
|
+
function createUserService(model, options) {
|
|
25
|
+
const base = (0, cap_services_1.createEntityService)(model, {
|
|
26
|
+
infoSchema: 'user-01',
|
|
27
|
+
onCreated: options?.onCreated,
|
|
28
|
+
onUpdated: options?.onUpdated,
|
|
29
|
+
onRemoved: options?.onRemoved,
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
...base,
|
|
33
|
+
async findByPublicId(publicId) {
|
|
34
|
+
const row = await model.findUnique({ where: { publicId } });
|
|
35
|
+
return row ? toUser(row) : null;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=user-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"user-service.js","sourceRoot":"","sources":["../src/user-service.ts"],"names":[],"mappings":";;AAoDA,8CAkBC;AAtED,wDAA4D;AAgC5D;;GAEG;AACH,SAAS,MAAM,CAA6B,GAA4B;IACtE,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAY;QACpB,QAAQ,EAAE,GAAG,CAAC,QAAkB;QAChC,IAAI,EAAE,GAAG,CAAC,IAAa;QACvB,UAAU,EAAE,GAAG,CAAC,UAAoB;QACpC,QAAQ,EAAE,GAAG,CAAC,QAAkB;QAChC,SAAS,EAAE,GAAG,CAAC,SAAS,YAAY,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,SAAmB,CAAC;QAC5F,SAAS,EAAE,GAAG,CAAC,SAAS,YAAY,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,SAAmB,CAAC;KAC7F,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,SAAgB,iBAAiB,CAC/B,KAAwB,EACxB,OAAmC;IAEnC,MAAM,IAAI,GAAG,IAAA,kCAAmB,EAAQ,KAAY,EAAE;QACpD,UAAU,EAAE,SAAS;QACrB,SAAS,EAAE,OAAO,EAAE,SAAS;QAC7B,SAAS,EAAE,OAAO,EAAE,SAAS;QAC7B,SAAS,EAAE,OAAO,EAAE,SAA0D;KAC/E,CAAC,CAAC;IAEH,OAAO;QACL,GAAG,IAAI;QACP,KAAK,CAAC,cAAc,CAAC,QAAgB;YACnC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,UAAU,CAAC,EAAE,KAAK,EAAE,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC;YAC5D,OAAO,GAAG,CAAC,CAAC,CAAC,MAAM,CAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACzC,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@boopkit/cap-users",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "User data service factories — typed CRUD for users, identities, and user properties",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist/",
|
|
9
|
+
"scaffold/",
|
|
10
|
+
"capability.json"
|
|
11
|
+
],
|
|
12
|
+
"boopkit": {
|
|
13
|
+
"capability": true,
|
|
14
|
+
"scaffoldDir": "scaffold",
|
|
15
|
+
"manifest": "capability.json"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc -p tsconfig.json"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@boopkit/cap-services": "^0.1.0",
|
|
22
|
+
"bcryptjs": "^2.4.3"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"typescript": "^5.9.3"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"license": "ISC"
|
|
31
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// @boopkit/cap-users — Account models
|
|
2
|
+
// Scaffolded by boop. Modify freely — this file is developer-owned.
|
|
3
|
+
|
|
4
|
+
model Account {
|
|
5
|
+
id Int @id @default(autoincrement())
|
|
6
|
+
publicId String @unique @default(uuid()) @map("public_id") @db.VarChar(255)
|
|
7
|
+
info Json
|
|
8
|
+
infoSchema String @default("account-01") @map("info_schema") @db.VarChar(255)
|
|
9
|
+
revision Int @default(1)
|
|
10
|
+
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
|
11
|
+
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6)
|
|
12
|
+
|
|
13
|
+
accountUsers AccountUser[]
|
|
14
|
+
accountProperties AccountProperty[]
|
|
15
|
+
|
|
16
|
+
@@map("accounts")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
model AccountUser {
|
|
20
|
+
id Int @id @default(autoincrement())
|
|
21
|
+
accountId Int @map("account_id")
|
|
22
|
+
userId Int @map("user_id")
|
|
23
|
+
info Json
|
|
24
|
+
infoSchema String @default("account-user-01") @map("info_schema") @db.VarChar(255)
|
|
25
|
+
revision Int @default(1)
|
|
26
|
+
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
|
27
|
+
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6)
|
|
28
|
+
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
|
|
29
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
30
|
+
|
|
31
|
+
@@unique([accountId, userId], map: "unique_account_user")
|
|
32
|
+
@@map("account_users")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
model AccountProperty {
|
|
36
|
+
id Int @id @default(autoincrement())
|
|
37
|
+
accountId Int @map("account_id")
|
|
38
|
+
property String @db.VarChar(255)
|
|
39
|
+
info Json
|
|
40
|
+
infoSchema String @map("info_schema") @db.VarChar(255)
|
|
41
|
+
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
|
42
|
+
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6)
|
|
43
|
+
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
|
|
44
|
+
|
|
45
|
+
@@unique([accountId, property], map: "unique_account_property")
|
|
46
|
+
@@map("account_properties")
|
|
47
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// @boopkit/cap-users — User models
|
|
2
|
+
// Scaffolded by boop. Modify freely — this file is developer-owned.
|
|
3
|
+
|
|
4
|
+
model User {
|
|
5
|
+
id Int @id @default(autoincrement())
|
|
6
|
+
publicId String @unique @default(uuid()) @map("public_id") @db.VarChar(255)
|
|
7
|
+
info Json
|
|
8
|
+
infoSchema String @map("info_schema") @db.VarChar(255)
|
|
9
|
+
revision Int @default(0)
|
|
10
|
+
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
|
11
|
+
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6)
|
|
12
|
+
|
|
13
|
+
identities UserIdentity[]
|
|
14
|
+
properties UserProperty[]
|
|
15
|
+
accountUsers AccountUser[]
|
|
16
|
+
|
|
17
|
+
@@map("users")
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
model UserIdentity {
|
|
21
|
+
id Int @id @default(autoincrement())
|
|
22
|
+
userId Int @map("user_id")
|
|
23
|
+
domain String @db.VarChar(255)
|
|
24
|
+
key String @db.VarChar(255)
|
|
25
|
+
endedAt DateTime? @map("ended_at") @db.Timestamptz(6)
|
|
26
|
+
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
|
27
|
+
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6)
|
|
28
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
29
|
+
|
|
30
|
+
@@unique([domain, key], map: "unique_user_identity")
|
|
31
|
+
@@map("user_identities")
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
model UserProperty {
|
|
35
|
+
id Int @id @default(autoincrement())
|
|
36
|
+
userId Int @map("user_id")
|
|
37
|
+
property String @db.VarChar(255)
|
|
38
|
+
info Json
|
|
39
|
+
infoSchema String @map("info_schema") @db.VarChar(255)
|
|
40
|
+
revision Int @default(0)
|
|
41
|
+
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
|
42
|
+
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6)
|
|
43
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
44
|
+
|
|
45
|
+
@@unique([userId, property], map: "unique_user_property")
|
|
46
|
+
@@map("user_properties")
|
|
47
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# User & Authentication Infrastructure
|
|
2
|
+
|
|
3
|
+
How this project models users, links identities, manages credentials, and tracks session trust.
|
|
4
|
+
|
|
5
|
+
**Related capabilities**: database, services
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Four-Layer Model
|
|
10
|
+
|
|
11
|
+
Authentication is built on four independent layers. Keeping them separate prevents bugs — logout shouldn't modify which methods are accepted, disabling a method shouldn't revoke existing sessions, and changing a password shouldn't touch authorization grants.
|
|
12
|
+
|
|
13
|
+
| Layer | Question | Keyed by | Storage |
|
|
14
|
+
|-------|----------|----------|---------|
|
|
15
|
+
| **Identity** | Who is this? | domain + key | UserIdentity table (end-dated, never deleted) |
|
|
16
|
+
| **Authentication** | What methods can they use? | strategy | UserProperty `authentication` |
|
|
17
|
+
| **Credentials** | What are the secrets? | strategy | UserProperty `credentials` (sensitive) |
|
|
18
|
+
| **Authorization** | What can this device/key do? | identity_key | UserProperty `authorization` |
|
|
19
|
+
|
|
20
|
+
**Identity** maps an incoming credential (cookie, email, API key, OAuth provider ID) to a user. A user has many identities. Identities are never deleted — they're end-dated, preserving history for audit.
|
|
21
|
+
|
|
22
|
+
**Authentication** declares which login methods are accepted. Disabling a method sets `accepted: false`, which the login flow checks before verifying credentials.
|
|
23
|
+
|
|
24
|
+
**Credentials** are the secrets (bcrypt hashes, TOTP seeds). Stored separately from authentication because they're sensitive and have a different lifecycle.
|
|
25
|
+
|
|
26
|
+
**Authorization** is the active permission grant, scoped **per-device**: each browser and each API key has its own trust level. Logging out on your phone doesn't affect your laptop.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Data Model
|
|
31
|
+
|
|
32
|
+
Three tables, all following the standard boop entity shape (`id`, `publicId`, `info`, `infoSchema`, `revision`, timestamps):
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
User ──< UserIdentity (one user, many auth mechanisms)
|
|
36
|
+
User ──< UserProperty (one user, many typed properties)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### User
|
|
40
|
+
|
|
41
|
+
The core record. Domain data lives in the `info` JSON column, typed via `BaseUserInfo` (extensible with generics).
|
|
42
|
+
|
|
43
|
+
### UserIdentity
|
|
44
|
+
|
|
45
|
+
Links a user to an auth mechanism via `(domain, key)` compound unique key:
|
|
46
|
+
|
|
47
|
+
| Domain | Key | Example |
|
|
48
|
+
|--------|-----|---------|
|
|
49
|
+
| `email` | email address | `alice@example.com` |
|
|
50
|
+
| `cookie` | session token | `base64url-token` |
|
|
51
|
+
| `api-key` | hashed key | `hashed-base64url` |
|
|
52
|
+
| OAuth provider | external ID | `google-user-id` |
|
|
53
|
+
|
|
54
|
+
Soft-deleted via `endedAt` — revoked identities remain queryable with `includeEnded: true`.
|
|
55
|
+
|
|
56
|
+
### UserProperty
|
|
57
|
+
|
|
58
|
+
Key-value storage scoped to a user via `(userId, property)` compound key. Well-known properties:
|
|
59
|
+
|
|
60
|
+
| Property | Contains | Sensitive? |
|
|
61
|
+
|----------|----------|-----------|
|
|
62
|
+
| `profile` | name, email (address + verified flag) | No |
|
|
63
|
+
| `credentials` | Array of `{ strategy, secret }` — bcrypt-hashed | **Yes** |
|
|
64
|
+
| `authentication` | Array of accepted auth methods with OAuth tokens | No |
|
|
65
|
+
| `authorization` | Array of per-device/per-key grants with trust tiers | No |
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Trust Model
|
|
70
|
+
|
|
71
|
+
Authorization grants track per-device trust with three graduated tiers — a user isn't simply "logged in" or "logged out":
|
|
72
|
+
|
|
73
|
+
| Tier | Meaning | How granted | Example use |
|
|
74
|
+
|------|---------|-------------|-------------|
|
|
75
|
+
| `recognized` | We think we know who this is | Default after expiry or logout | "Welcome back, Alice" — show name, preferences |
|
|
76
|
+
| `authenticated` | They proved identity recently | `login()` or `setTrust()` | Normal session — CRUD, purchases, account access |
|
|
77
|
+
| `verified` | Extra proof on an authenticated session | `setTrust()` after TOTP/email/biometric | Change email, delete account, manage API keys |
|
|
78
|
+
|
|
79
|
+
Grants have `expires_at` — expired grants downgrade to `recognized`. Standing grants (`expires_at = 0`) never expire (used for API keys).
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Session Rotation Rules
|
|
84
|
+
|
|
85
|
+
**Rotate the session** (new cookie) when the *authentication source* changes — login, re-authentication, switching users. This prevents session fixation: an attacker who knew the pre-auth token can't ride the post-auth session.
|
|
86
|
+
|
|
87
|
+
**Upgrade the existing grant** (same cookie) when the user proves *additional* identity strength on an already-authenticated session — TOTP, biometric, password re-entry for sensitive ops. Rotating would break their session for no security benefit.
|
|
88
|
+
|
|
89
|
+
*New credential source → rotate. Stronger proof on same session → upgrade.*
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Auth Flows
|
|
94
|
+
|
|
95
|
+
### Password (local auth)
|
|
96
|
+
|
|
97
|
+
All four layers participate:
|
|
98
|
+
|
|
99
|
+
| Layer | What happens |
|
|
100
|
+
|-------|-------------|
|
|
101
|
+
| **Identity** | `domain: "email", key: "alice@example.com"` — lookup for login |
|
|
102
|
+
| **Authentication** | `{ strategy: "local", accepted: true }` — user can log in with password |
|
|
103
|
+
| **Credentials** | `{ strategy: "local", secret: "$2b$..." }` — bcrypt hash |
|
|
104
|
+
| **Authorization** | Created on login, tied to the session cookie |
|
|
105
|
+
|
|
106
|
+
**Signup:** create user → create email identity → set credentials (hash password) → add local auth method → mint session cookie → create per-device grant at `authenticated`.
|
|
107
|
+
|
|
108
|
+
**Login:** look up email identity → check `isMethodAccepted("local")` → verify bcrypt hash → rotate session (new cookie) → create per-device grant at `authenticated`.
|
|
109
|
+
|
|
110
|
+
### Cookie (session)
|
|
111
|
+
|
|
112
|
+
A cookie is an identity — `domain: "cookie", key: "a8f3...hex"`. One per browser. Each cookie has its own authorization grant:
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
Browser A: cookie aaa..., grant trust=authenticated
|
|
116
|
+
Browser B: cookie bbb..., grant trust=verified (just did TOTP)
|
|
117
|
+
Browser C: cookie ccc..., grant trust=recognized (session expired)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Session rotation:** on login (privilege-level change), the old cookie is end-dated and a new one minted. The old grant is removed, the new cookie gets a fresh grant.
|
|
121
|
+
|
|
122
|
+
**Session expiry:** when a grant's `expires_at` passes, that device's trust falls to `recognized`. The cookie still resolves the user, but they can't perform authenticated actions until they re-login.
|
|
123
|
+
|
|
124
|
+
### API Key
|
|
125
|
+
|
|
126
|
+
An API key is simultaneously an identity, a credential, and a bearer token. All four layers participate:
|
|
127
|
+
|
|
128
|
+
| Layer | What happens |
|
|
129
|
+
|-------|-------------|
|
|
130
|
+
| **Identity** | `domain: "api-key", key: "47ZHJY-..."` — lookup on each request |
|
|
131
|
+
| **Authentication** | `{ strategy: "api-key", accepted: true }` — user can use API keys |
|
|
132
|
+
| **Credentials** | `{ strategy: "api-key", secret: "$2b$..." }` — bcrypt hash of the key |
|
|
133
|
+
| **Authorization** | `{ identity_key: "47ZHJY-...", strategy: "api-key", trust: "authenticated", expires_at: 0 }` — standing per-key grant |
|
|
134
|
+
|
|
135
|
+
**Three independent levers to control access:**
|
|
136
|
+
1. **Revoke the key** — end-date the identity. Immediate.
|
|
137
|
+
2. **Disable API key auth** — set `accepted: false` on the authentication entry. All API keys stop working, but keys still exist.
|
|
138
|
+
3. **Expire the grant** — set `expires_at` on the per-key grant. Can be re-enabled without rotating the key.
|
|
139
|
+
|
|
140
|
+
**Validation flow:** identity lookup → `isMethodAccepted("api-key")` → bcrypt compare → check per-key grant → return user.
|
|
141
|
+
|
|
142
|
+
The plaintext key is returned exactly once at creation. Only the hash is stored.
|
|
143
|
+
|
|
144
|
+
### OAuth (as client)
|
|
145
|
+
|
|
146
|
+
When a user logs in via Google (or other provider), the app is an OAuth *client*. The provider proves identity; boop stores the result.
|
|
147
|
+
|
|
148
|
+
| Layer | What happens |
|
|
149
|
+
|-------|-------------|
|
|
150
|
+
| **Identity** | `domain: "google", key: "google-sub-id-12345"` — maps provider ID to user |
|
|
151
|
+
| **Authentication** | `{ strategy: "oauth-google", accepted: true, refresh_token: "rt_...", access_token: "at_...", ... }` — delegated credentials stored |
|
|
152
|
+
| **Credentials** | None — OAuth credentials are delegated, stored in the authentication entry |
|
|
153
|
+
| **Authorization** | Created on login, tied to the session cookie — same as password login |
|
|
154
|
+
|
|
155
|
+
OAuth tokens (`refresh_token`, `access_token`) are **delegated credentials** — tokens held from the provider to act on the user's behalf. They are NOT how the app recognizes the user on subsequent requests. Users are recognized via cookies, same as password login.
|
|
156
|
+
|
|
157
|
+
**OAuth login flow:** provider callback with auth code → exchange for tokens → look up or create identity (`domain: "google"`) → store tokens in authentication entry → rotate session → create per-device grant.
|
|
158
|
+
|
|
159
|
+
### TOTP / Second Factor
|
|
160
|
+
|
|
161
|
+
TOTP upgrades trust on the current device. It does NOT rotate the session — the user already proved identity at login (which rotated the session). TOTP is additional proof on the same session, not a new authentication source.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Logout vs. Forget
|
|
166
|
+
|
|
167
|
+
| User action | Function | Cookie | Grant | Other devices |
|
|
168
|
+
|-------------|----------|--------|-------|---------------|
|
|
169
|
+
| "Log out" | `logout(userId, token)` | Survives | trust → `recognized` | Unaffected |
|
|
170
|
+
| "Log out everywhere" | `logoutAll(userId)` | Survive | All session grants → `recognized` | All downgraded |
|
|
171
|
+
| "Forget me" | `forgetDevice(userId, token)` | End-dated | Removed | Unaffected |
|
|
172
|
+
| "Sign out everywhere" | `forgetAllDevices(userId)` | All end-dated | All session grants removed | All forgotten |
|
|
173
|
+
|
|
174
|
+
API key grants are not affected by any of these — managed separately via `revokeApiKey`.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Services
|
|
179
|
+
|
|
180
|
+
All services compose from `@boopkit/cap-services` primitives:
|
|
181
|
+
|
|
182
|
+
| Service | Factory | Purpose |
|
|
183
|
+
|---------|---------|---------|
|
|
184
|
+
| `userService` | `createUserService<TInfo>()` | CRUD on User with typed `info` |
|
|
185
|
+
| `identityService` | `createIdentityService()` | Link/unlink auth mechanisms |
|
|
186
|
+
| `userPropertyService` | `createUserPropertyService()` | Read/write user properties (credentials gated) |
|
|
187
|
+
| `authService` | `createAuthService(deps, opts)` | Orchestrates signup, login, sessions, API keys |
|
|
188
|
+
|
|
189
|
+
### AuthService key operations
|
|
190
|
+
|
|
191
|
+
**Signup**: `signup({ email, password, name })` — creates user, email identity, profile, credentials, auth method, session token, and authorization grant in one call. Each step is also available individually for graduated control.
|
|
192
|
+
|
|
193
|
+
**Login**: `login(email, password, currentSessionToken?)` — validates credentials, rotates session (ends old token if provided), grants `authenticated` trust.
|
|
194
|
+
|
|
195
|
+
**Session management**:
|
|
196
|
+
- `logout(userId, token)` — downgrades trust to `recognized` (device remembers user but loses auth)
|
|
197
|
+
- `forgetDevice(userId, token)` — end-dates cookie identity and removes grant (irreversible)
|
|
198
|
+
- `logoutAll` / `forgetAllDevices` — bulk variants
|
|
199
|
+
|
|
200
|
+
**API keys**: `createApiKey(userId)` / `validateApiKey(key)` / `revokeApiKey` / `rotateApiKey` — standing grants that never expire.
|
|
201
|
+
|
|
202
|
+
**Trust queries**: `getTrustTier(userId, identityKey)`, `isAuthorized(userId, identityKey, requiredTrust?)`.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Extension
|
|
207
|
+
|
|
208
|
+
Define project-specific user data via generics:
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
interface MyUserInfo extends BaseUserInfo {
|
|
212
|
+
department: string;
|
|
213
|
+
role: 'admin' | 'member';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const userService = createUserService<MyUserInfo>(db.user);
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The type propagates through all service operations.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Environment
|
|
224
|
+
|
|
225
|
+
| Variable | Default | Purpose |
|
|
226
|
+
|----------|---------|---------|
|
|
227
|
+
| `SESSION_DURATION` | `604800` (7 days) | Session token lifetime in seconds |
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Key Files
|
|
232
|
+
|
|
233
|
+
| File | Purpose |
|
|
234
|
+
|------|---------|
|
|
235
|
+
| `prisma/users.prisma` | User, UserIdentity, UserProperty table definitions |
|
|
236
|
+
| `src/services/users.service.ts` | Service wiring — instantiates all user services |
|
|
237
|
+
| `@boopkit/cap-users` | Service factories (user, identity, property, auth) |
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { Response } from 'express';
|
|
2
|
+
import type { AuthService } from '@boopkit/cap-users';
|
|
3
|
+
import { AuthValidationError } from '@boopkit/cap-users';
|
|
4
|
+
|
|
5
|
+
/** Extended Express Request with auth fields set by ensureVisitor middleware. */
|
|
6
|
+
interface AuthRequest {
|
|
7
|
+
body: Record<string, any>;
|
|
8
|
+
query: Record<string, any>;
|
|
9
|
+
cookies?: Record<string, string>;
|
|
10
|
+
user?: { id: number; [key: string]: any };
|
|
11
|
+
sessionToken?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1000;
|
|
15
|
+
const COOKIE_NAME = 'auth_id';
|
|
16
|
+
|
|
17
|
+
/** Map AuthValidationError field names to view-friendly error keys. */
|
|
18
|
+
const FIELD_ERROR_MAP: Record<string, string> = {
|
|
19
|
+
email: 'email_invalid',
|
|
20
|
+
password: 'password_weak',
|
|
21
|
+
passwordConfirmation: 'password_mismatch',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export interface AuthControllerDeps {
|
|
25
|
+
authService: AuthService;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createAuthController(deps: AuthControllerDeps) {
|
|
29
|
+
const { authService } = deps;
|
|
30
|
+
|
|
31
|
+
// ---- Login ----
|
|
32
|
+
|
|
33
|
+
async function showLogin(req: AuthRequest, res: Response): Promise<void> {
|
|
34
|
+
// Already authenticated? Redirect away.
|
|
35
|
+
if (res.locals.auth?.trustTier === 'authenticated' || res.locals.auth?.trustTier === 'verified') {
|
|
36
|
+
const next = req.query.next as string | undefined;
|
|
37
|
+
const redirectTo = (typeof next === 'string' && next.startsWith('/')) ? next : '/';
|
|
38
|
+
res.redirect(redirectTo);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const next = req.query.next as string | undefined;
|
|
43
|
+
res.render('login', {
|
|
44
|
+
layout: 'main',
|
|
45
|
+
action: '/login',
|
|
46
|
+
next,
|
|
47
|
+
error: req.query.error as string | undefined,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function handleLogin(req: AuthRequest, res: Response): Promise<void> {
|
|
52
|
+
const { email, password } = req.body;
|
|
53
|
+
const next = req.body.next || req.query.next as string | undefined;
|
|
54
|
+
const currentSessionToken = req.cookies?.[COOKIE_NAME] as string | undefined;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const result = await authService.login(email, password, currentSessionToken);
|
|
58
|
+
|
|
59
|
+
// Set rotated session cookie
|
|
60
|
+
res.cookie(COOKIE_NAME, result.sessionToken, {
|
|
61
|
+
httpOnly: true,
|
|
62
|
+
maxAge: ONE_YEAR_MS,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Redirect to ?next= or home
|
|
66
|
+
const redirectTo = (typeof next === 'string' && next.startsWith('/')) ? next : '/';
|
|
67
|
+
res.redirect(redirectTo);
|
|
68
|
+
} catch {
|
|
69
|
+
// Generic error — same message for invalid password and nonexistent email (no enumeration)
|
|
70
|
+
res.render('login', {
|
|
71
|
+
layout: 'main',
|
|
72
|
+
action: '/login',
|
|
73
|
+
next,
|
|
74
|
+
email,
|
|
75
|
+
errorMessage: 'Invalid email or password',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---- Signup ----
|
|
81
|
+
|
|
82
|
+
async function showSignup(req: AuthRequest, res: Response): Promise<void> {
|
|
83
|
+
// Already authenticated? Redirect away.
|
|
84
|
+
if (res.locals.auth?.trustTier === 'authenticated' || res.locals.auth?.trustTier === 'verified') {
|
|
85
|
+
const next = req.query.next as string | undefined;
|
|
86
|
+
const redirectTo = (typeof next === 'string' && next.startsWith('/')) ? next : '/';
|
|
87
|
+
res.redirect(redirectTo);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const next = req.query.next as string | undefined;
|
|
92
|
+
res.render('signup', {
|
|
93
|
+
layout: 'main',
|
|
94
|
+
action: '/signup',
|
|
95
|
+
next,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function handleSignup(req: AuthRequest, res: Response): Promise<void> {
|
|
100
|
+
const { email, password, passwordConfirmation } = req.body;
|
|
101
|
+
const next = req.body.next || req.query.next as string | undefined;
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const result = await authService.signup({ email, password, passwordConfirmation });
|
|
105
|
+
|
|
106
|
+
// Set session cookie
|
|
107
|
+
res.cookie(COOKIE_NAME, result.sessionToken, {
|
|
108
|
+
httpOnly: true,
|
|
109
|
+
maxAge: ONE_YEAR_MS,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Redirect to ?next= or home
|
|
113
|
+
const redirectTo = (typeof next === 'string' && next.startsWith('/')) ? next : '/';
|
|
114
|
+
res.redirect(redirectTo);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
// Build per-field error flags for the view
|
|
117
|
+
const errors: Record<string, boolean> = {};
|
|
118
|
+
let errorMessage = 'Signup failed';
|
|
119
|
+
|
|
120
|
+
if (err instanceof AuthValidationError) {
|
|
121
|
+
const key = FIELD_ERROR_MAP[err.field] ?? err.field;
|
|
122
|
+
errors[key] = true;
|
|
123
|
+
errorMessage = err.message;
|
|
124
|
+
} else if (err instanceof Error && err.message.includes('Unique constraint')) {
|
|
125
|
+
errors.user_exists = true;
|
|
126
|
+
errorMessage = 'An account with this email already exists';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
res.render('signup', {
|
|
130
|
+
layout: 'main',
|
|
131
|
+
action: '/signup',
|
|
132
|
+
next,
|
|
133
|
+
email,
|
|
134
|
+
errors,
|
|
135
|
+
errorMessage,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---- Logout ----
|
|
141
|
+
|
|
142
|
+
async function handleLogout(req: AuthRequest, res: Response): Promise<void> {
|
|
143
|
+
if (req.user && req.sessionToken) {
|
|
144
|
+
await authService.logout(req.user.id, req.sessionToken);
|
|
145
|
+
}
|
|
146
|
+
res.redirect('/');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function handleForgetDevice(req: AuthRequest, res: Response): Promise<void> {
|
|
150
|
+
if (req.user && req.sessionToken) {
|
|
151
|
+
await authService.forgetDevice(req.user.id, req.sessionToken);
|
|
152
|
+
}
|
|
153
|
+
res.clearCookie(COOKIE_NAME);
|
|
154
|
+
res.redirect('/');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { showLogin, handleLogin, showSignup, handleSignup, handleLogout, handleForgetDevice };
|
|
158
|
+
}
|