fase(9): auth module with casl rbac and session management
This commit is contained in:
14
dist/modules/auth/infrastructure/auth/PasswordService.js
vendored
Normal file
14
dist/modules/auth/infrastructure/auth/PasswordService.js
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.hashPassword = hashPassword;
|
||||
exports.verifyPassword = verifyPassword;
|
||||
const argon2_1 = __importDefault(require("argon2"));
|
||||
async function hashPassword(password) {
|
||||
return argon2_1.default.hash(password);
|
||||
}
|
||||
async function verifyPassword(password, hash) {
|
||||
return argon2_1.default.verify(hash, password);
|
||||
}
|
||||
29
dist/modules/auth/infrastructure/casl/AbilityFactory.js
vendored
Normal file
29
dist/modules/auth/infrastructure/casl/AbilityFactory.js
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.defineAbilityFor = defineAbilityFor;
|
||||
const ability_1 = require("@casl/ability");
|
||||
function defineAbilityFor(role) {
|
||||
const { can, cannot, build } = new ability_1.AbilityBuilder(ability_1.createMongoAbility);
|
||||
switch (role) {
|
||||
case 'owner':
|
||||
can('manage', 'all');
|
||||
break;
|
||||
case 'admin':
|
||||
can('manage', 'all');
|
||||
cannot('delete', 'Organization');
|
||||
cannot('manage', 'License');
|
||||
can('read', 'License');
|
||||
break;
|
||||
case 'member':
|
||||
can('create', ['Session', 'Finding', 'Report']);
|
||||
can('read', 'all');
|
||||
can('update', 'Finding');
|
||||
break;
|
||||
case 'viewer':
|
||||
can('read', 'all');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return build();
|
||||
}
|
||||
170
dist/modules/auth/infrastructure/http/AuthController.js
vendored
Normal file
170
dist/modules/auth/infrastructure/http/AuthController.js
vendored
Normal file
@@ -0,0 +1,170 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createAuthController = createAuthController;
|
||||
const express_1 = require("express");
|
||||
const AuthMiddleware_1 = require("../../application/middleware/AuthMiddleware");
|
||||
function createAuthController(registerCommand, loginCommand, createOrgCommand, inviteMemberCommand, createApiKeyCommand, getUserQuery, listOrgMembersQuery, sessionRepository, apiKeyRepository, userRepository) {
|
||||
const router = (0, express_1.Router)();
|
||||
const authMiddleware = (0, AuthMiddleware_1.createAuthMiddleware)(userRepository, sessionRepository, apiKeyRepository);
|
||||
// POST /api/auth/register
|
||||
router.post('/register', async (req, res) => {
|
||||
const result = await registerCommand.execute({
|
||||
email: req.body.email,
|
||||
password: req.body.password,
|
||||
name: req.body.name,
|
||||
role: req.body.role,
|
||||
});
|
||||
if (!result.ok) {
|
||||
res.status(400).json({ error: result.error });
|
||||
return;
|
||||
}
|
||||
res.status(201).json(result.value);
|
||||
});
|
||||
// POST /api/auth/login
|
||||
router.post('/login', async (req, res) => {
|
||||
const result = await loginCommand.execute({
|
||||
email: req.body.email,
|
||||
password: req.body.password,
|
||||
});
|
||||
if (!result.ok) {
|
||||
res.status(401).json({ error: result.error });
|
||||
return;
|
||||
}
|
||||
const { sessionToken, expiresAt, ...userData } = result.value;
|
||||
res.cookie('abe_session', sessionToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env['NODE_ENV'] === 'production',
|
||||
sameSite: 'lax',
|
||||
expires: expiresAt,
|
||||
});
|
||||
res.json({ ...userData, sessionToken });
|
||||
});
|
||||
// POST /api/auth/logout
|
||||
router.post('/logout', authMiddleware, async (req, res) => {
|
||||
const token = req.cookies?.['abe_session'] ?? req.headers.authorization?.substring(7);
|
||||
if (token) {
|
||||
await sessionRepository.deleteByToken(token);
|
||||
}
|
||||
res.clearCookie('abe_session');
|
||||
res.json({ success: true });
|
||||
});
|
||||
// GET /api/auth/me
|
||||
router.get('/me', authMiddleware, async (req, res) => {
|
||||
const result = await getUserQuery.execute({ userId: req.user.id });
|
||||
if (!result.ok) {
|
||||
res.status(404).json({ error: result.error });
|
||||
return;
|
||||
}
|
||||
res.json(result.value);
|
||||
});
|
||||
// GET /api/auth/setup-required
|
||||
router.get('/setup-required', async (_req, res) => {
|
||||
const count = await userRepository.count();
|
||||
res.json({ required: count === 0 });
|
||||
});
|
||||
// POST /api/auth/setup — first-run setup
|
||||
router.post('/setup', async (req, res) => {
|
||||
const count = await userRepository.count();
|
||||
if (count > 0) {
|
||||
res.status(400).json({ error: 'Setup already completed' });
|
||||
return;
|
||||
}
|
||||
const registerResult = await registerCommand.execute({
|
||||
email: req.body.email,
|
||||
password: req.body.password,
|
||||
name: req.body.name,
|
||||
role: 'owner',
|
||||
});
|
||||
if (!registerResult.ok) {
|
||||
res.status(400).json({ error: registerResult.error });
|
||||
return;
|
||||
}
|
||||
const createOrgResult = await createOrgCommand.execute({
|
||||
name: req.body.orgName ?? 'My Organization',
|
||||
ownerId: registerResult.value.userId,
|
||||
});
|
||||
if (!createOrgResult.ok) {
|
||||
res.status(400).json({ error: createOrgResult.error });
|
||||
return;
|
||||
}
|
||||
res.status(201).json({
|
||||
user: registerResult.value,
|
||||
organization: createOrgResult.value,
|
||||
});
|
||||
});
|
||||
// POST /api/auth/organizations — create org
|
||||
router.post('/organizations', authMiddleware, async (req, res) => {
|
||||
const result = await createOrgCommand.execute({
|
||||
name: req.body.name,
|
||||
ownerId: req.user.id,
|
||||
});
|
||||
if (!result.ok) {
|
||||
res.status(400).json({ error: result.error });
|
||||
return;
|
||||
}
|
||||
res.status(201).json(result.value);
|
||||
});
|
||||
// POST /api/auth/organizations/:orgId/members — invite member
|
||||
router.post('/organizations/:orgId/members', authMiddleware, async (req, res) => {
|
||||
const result = await inviteMemberCommand.execute({
|
||||
orgId: String(req.params['orgId']),
|
||||
inviterUserId: req.user.id,
|
||||
email: req.body.email,
|
||||
role: req.body.role ?? 'member',
|
||||
});
|
||||
if (!result.ok) {
|
||||
res.status(400).json({ error: result.error });
|
||||
return;
|
||||
}
|
||||
res.status(201).json(result.value);
|
||||
});
|
||||
// GET /api/auth/organizations/:orgId/members
|
||||
router.get('/organizations/:orgId/members', authMiddleware, async (req, res) => {
|
||||
const result = await listOrgMembersQuery.execute({ orgId: String(req.params['orgId']) });
|
||||
if (!result.ok) {
|
||||
res.status(404).json({ error: result.error });
|
||||
return;
|
||||
}
|
||||
res.json(result.value);
|
||||
});
|
||||
// POST /api/auth/api-keys — create API key
|
||||
router.post('/api-keys', authMiddleware, async (req, res) => {
|
||||
const result = await createApiKeyCommand.execute({
|
||||
userId: req.user.id,
|
||||
orgId: req.user.orgId ?? 'default',
|
||||
name: req.body.name,
|
||||
permissions: req.body.permissions,
|
||||
expiresAt: req.body.expiresAt ? new Date(req.body.expiresAt) : undefined,
|
||||
});
|
||||
if (!result.ok) {
|
||||
res.status(400).json({ error: result.error });
|
||||
return;
|
||||
}
|
||||
res.status(201).json(result.value);
|
||||
});
|
||||
// GET /api/auth/api-keys — list API keys
|
||||
router.get('/api-keys', authMiddleware, async (req, res) => {
|
||||
const keys = await apiKeyRepository.listByUser(req.user.id);
|
||||
res.json(keys.map((k) => ({
|
||||
id: k.id.toString(),
|
||||
name: k.name,
|
||||
keyPrefix: k.keyPrefix,
|
||||
permissions: k.permissions,
|
||||
expiresAt: k.expiresAt,
|
||||
lastUsedAt: k.lastUsedAt,
|
||||
createdAt: k.createdAt,
|
||||
})));
|
||||
});
|
||||
// DELETE /api/auth/api-keys/:id — revoke API key
|
||||
router.delete('/api-keys/:id', authMiddleware, async (req, res) => {
|
||||
const keyId = String(req.params['id']);
|
||||
const key = await apiKeyRepository.findById(keyId);
|
||||
if (!key || key.userId !== req.user.id) {
|
||||
res.status(404).json({ error: 'API key not found' });
|
||||
return;
|
||||
}
|
||||
await apiKeyRepository.delete(keyId);
|
||||
res.json({ success: true });
|
||||
});
|
||||
return router;
|
||||
}
|
||||
75
dist/modules/auth/infrastructure/repositories/KyselyApiKeyRepository.js
vendored
Normal file
75
dist/modules/auth/infrastructure/repositories/KyselyApiKeyRepository.js
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.KyselyApiKeyRepository = void 0;
|
||||
const ApiKey_1 = require("../../domain/entities/ApiKey");
|
||||
const UniqueId_1 = require("../../../../shared/domain/UniqueId");
|
||||
class KyselyApiKeyRepository {
|
||||
constructor(db) {
|
||||
this.db = db;
|
||||
}
|
||||
async save(apiKey) {
|
||||
await this.db
|
||||
.insertInto('api_keys')
|
||||
.values({
|
||||
id: apiKey.id.toString(),
|
||||
user_id: apiKey.userId,
|
||||
org_id: apiKey.orgId,
|
||||
name: apiKey.name,
|
||||
key_hash: apiKey.keyHash,
|
||||
key_prefix: apiKey.keyPrefix,
|
||||
permissions: JSON.stringify(apiKey.permissions),
|
||||
expires_at: apiKey.expiresAt ? apiKey.expiresAt.getTime() : null,
|
||||
last_used_at: apiKey.lastUsedAt ? apiKey.lastUsedAt.getTime() : null,
|
||||
created_at: apiKey.createdAt.getTime(),
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
async findById(id) {
|
||||
const row = await this.db
|
||||
.selectFrom('api_keys')
|
||||
.selectAll()
|
||||
.where('id', '=', id)
|
||||
.executeTakeFirst();
|
||||
return row ? this.toDomain(row) : undefined;
|
||||
}
|
||||
async findByHash(keyHash) {
|
||||
const row = await this.db
|
||||
.selectFrom('api_keys')
|
||||
.selectAll()
|
||||
.where('key_hash', '=', keyHash)
|
||||
.executeTakeFirst();
|
||||
return row ? this.toDomain(row) : undefined;
|
||||
}
|
||||
async listByUser(userId) {
|
||||
const rows = await this.db
|
||||
.selectFrom('api_keys')
|
||||
.selectAll()
|
||||
.where('user_id', '=', userId)
|
||||
.execute();
|
||||
return rows.map((r) => this.toDomain(r));
|
||||
}
|
||||
async delete(id) {
|
||||
await this.db.deleteFrom('api_keys').where('id', '=', id).execute();
|
||||
}
|
||||
async updateLastUsed(id, lastUsedAt) {
|
||||
await this.db
|
||||
.updateTable('api_keys')
|
||||
.set({ last_used_at: lastUsedAt.getTime() })
|
||||
.where('id', '=', id)
|
||||
.execute();
|
||||
}
|
||||
toDomain(row) {
|
||||
return ApiKey_1.ApiKey.reconstitute({
|
||||
userId: row.user_id,
|
||||
orgId: row.org_id,
|
||||
name: row.name,
|
||||
keyHash: row.key_hash,
|
||||
keyPrefix: row.key_prefix,
|
||||
permissions: JSON.parse(row.permissions),
|
||||
expiresAt: row.expires_at ? new Date(row.expires_at) : undefined,
|
||||
lastUsedAt: row.last_used_at ? new Date(row.last_used_at) : undefined,
|
||||
createdAt: new Date(row.created_at),
|
||||
}, UniqueId_1.UniqueId.from(row.id));
|
||||
}
|
||||
}
|
||||
exports.KyselyApiKeyRepository = KyselyApiKeyRepository;
|
||||
98
dist/modules/auth/infrastructure/repositories/KyselyOrganizationRepository.js
vendored
Normal file
98
dist/modules/auth/infrastructure/repositories/KyselyOrganizationRepository.js
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.KyselyOrganizationRepository = void 0;
|
||||
const Organization_1 = require("../../domain/entities/Organization");
|
||||
const UniqueId_1 = require("../../../../shared/domain/UniqueId");
|
||||
class KyselyOrganizationRepository {
|
||||
constructor(db) {
|
||||
this.db = db;
|
||||
}
|
||||
async save(org) {
|
||||
await this.db
|
||||
.insertInto('organizations')
|
||||
.values({
|
||||
id: org.id.toString(),
|
||||
name: org.name,
|
||||
slug: org.slug,
|
||||
created_at: org.createdAt.getTime(),
|
||||
})
|
||||
.onConflict((oc) => oc.column('id').doUpdateSet({ name: org.name }))
|
||||
.execute();
|
||||
}
|
||||
async findById(id) {
|
||||
const row = await this.db
|
||||
.selectFrom('organizations')
|
||||
.selectAll()
|
||||
.where('id', '=', id)
|
||||
.executeTakeFirst();
|
||||
return row ? this.toDomain(row) : undefined;
|
||||
}
|
||||
async findBySlug(slug) {
|
||||
const row = await this.db
|
||||
.selectFrom('organizations')
|
||||
.selectAll()
|
||||
.where('slug', '=', slug)
|
||||
.executeTakeFirst();
|
||||
return row ? this.toDomain(row) : undefined;
|
||||
}
|
||||
async findAll() {
|
||||
const rows = await this.db.selectFrom('organizations').selectAll().execute();
|
||||
return rows.map((r) => this.toDomain(r));
|
||||
}
|
||||
async addMember(member) {
|
||||
await this.db
|
||||
.insertInto('org_members')
|
||||
.values({
|
||||
id: member.id,
|
||||
org_id: member.orgId,
|
||||
user_id: member.userId,
|
||||
role: member.role,
|
||||
joined_at: member.joinedAt.getTime(),
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
async getMember(orgId, userId) {
|
||||
const row = await this.db
|
||||
.selectFrom('org_members')
|
||||
.selectAll()
|
||||
.where('org_id', '=', orgId)
|
||||
.where('user_id', '=', userId)
|
||||
.executeTakeFirst();
|
||||
return row
|
||||
? { id: row.id, orgId: row.org_id, userId: row.user_id, role: row.role, joinedAt: new Date(row.joined_at) }
|
||||
: undefined;
|
||||
}
|
||||
async listMembers(orgId) {
|
||||
const rows = await this.db
|
||||
.selectFrom('org_members')
|
||||
.selectAll()
|
||||
.where('org_id', '=', orgId)
|
||||
.execute();
|
||||
return rows.map((r) => ({
|
||||
id: r.id,
|
||||
orgId: r.org_id,
|
||||
userId: r.user_id,
|
||||
role: r.role,
|
||||
joinedAt: new Date(r.joined_at),
|
||||
}));
|
||||
}
|
||||
async updateMemberRole(orgId, userId, role) {
|
||||
await this.db
|
||||
.updateTable('org_members')
|
||||
.set({ role })
|
||||
.where('org_id', '=', orgId)
|
||||
.where('user_id', '=', userId)
|
||||
.execute();
|
||||
}
|
||||
async removeMember(orgId, userId) {
|
||||
await this.db
|
||||
.deleteFrom('org_members')
|
||||
.where('org_id', '=', orgId)
|
||||
.where('user_id', '=', userId)
|
||||
.execute();
|
||||
}
|
||||
toDomain(row) {
|
||||
return Organization_1.Organization.reconstitute({ name: row.name, slug: row.slug, createdAt: new Date(row.created_at) }, UniqueId_1.UniqueId.from(row.id));
|
||||
}
|
||||
}
|
||||
exports.KyselyOrganizationRepository = KyselyOrganizationRepository;
|
||||
46
dist/modules/auth/infrastructure/repositories/KyselySessionRepository.js
vendored
Normal file
46
dist/modules/auth/infrastructure/repositories/KyselySessionRepository.js
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.KyselySessionRepository = void 0;
|
||||
class KyselySessionRepository {
|
||||
constructor(db) {
|
||||
this.db = db;
|
||||
}
|
||||
async save(session) {
|
||||
await this.db
|
||||
.insertInto('auth_sessions')
|
||||
.values({
|
||||
id: session.id,
|
||||
user_id: session.userId,
|
||||
token: session.token,
|
||||
expires_at: session.expiresAt.getTime(),
|
||||
created_at: session.createdAt.getTime(),
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
async findByToken(token) {
|
||||
const row = await this.db
|
||||
.selectFrom('auth_sessions')
|
||||
.selectAll()
|
||||
.where('token', '=', token)
|
||||
.executeTakeFirst();
|
||||
if (!row)
|
||||
return undefined;
|
||||
return {
|
||||
id: row.id,
|
||||
userId: row.user_id,
|
||||
token: row.token,
|
||||
expiresAt: new Date(row.expires_at),
|
||||
createdAt: new Date(row.created_at),
|
||||
};
|
||||
}
|
||||
async deleteByToken(token) {
|
||||
await this.db.deleteFrom('auth_sessions').where('token', '=', token).execute();
|
||||
}
|
||||
async deleteExpired() {
|
||||
await this.db
|
||||
.deleteFrom('auth_sessions')
|
||||
.where('expires_at', '<', Date.now())
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
exports.KyselySessionRepository = KyselySessionRepository;
|
||||
73
dist/modules/auth/infrastructure/repositories/KyselyUserRepository.js
vendored
Normal file
73
dist/modules/auth/infrastructure/repositories/KyselyUserRepository.js
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.KyselyUserRepository = void 0;
|
||||
const User_1 = require("../../domain/entities/User");
|
||||
const UniqueId_1 = require("../../../../shared/domain/UniqueId");
|
||||
const Email_1 = require("../../domain/value-objects/Email");
|
||||
const Role_1 = require("../../domain/value-objects/Role");
|
||||
class KyselyUserRepository {
|
||||
constructor(db) {
|
||||
this.db = db;
|
||||
}
|
||||
async save(user) {
|
||||
const row = {
|
||||
id: user.id.toString(),
|
||||
email: user.email.value,
|
||||
name: user.name,
|
||||
password_hash: user.passwordHash,
|
||||
role: user.role.value,
|
||||
org_id: user.orgId ?? null,
|
||||
created_at: user.createdAt.getTime(),
|
||||
updated_at: user.updatedAt.getTime(),
|
||||
};
|
||||
await this.db
|
||||
.insertInto('users')
|
||||
.values(row)
|
||||
.onConflict((oc) => oc.column('id').doUpdateSet({
|
||||
name: row.name,
|
||||
role: row.role,
|
||||
org_id: row.org_id,
|
||||
updated_at: row.updated_at,
|
||||
}))
|
||||
.execute();
|
||||
}
|
||||
async findById(id) {
|
||||
const row = await this.db
|
||||
.selectFrom('users')
|
||||
.selectAll()
|
||||
.where('id', '=', id)
|
||||
.executeTakeFirst();
|
||||
return row ? this.toDomain(row) : undefined;
|
||||
}
|
||||
async findByEmail(email) {
|
||||
const row = await this.db
|
||||
.selectFrom('users')
|
||||
.selectAll()
|
||||
.where('email', '=', email.toLowerCase())
|
||||
.executeTakeFirst();
|
||||
return row ? this.toDomain(row) : undefined;
|
||||
}
|
||||
async findAll() {
|
||||
const rows = await this.db.selectFrom('users').selectAll().execute();
|
||||
return rows.map((r) => this.toDomain(r));
|
||||
}
|
||||
async count() {
|
||||
const result = await this.db
|
||||
.selectFrom('users')
|
||||
.select((eb) => eb.fn.count('id').as('count'))
|
||||
.executeTakeFirstOrThrow();
|
||||
return Number(result.count);
|
||||
}
|
||||
toDomain(row) {
|
||||
return User_1.User.reconstitute({
|
||||
email: Email_1.Email.create(row.email),
|
||||
name: row.name,
|
||||
passwordHash: row.password_hash,
|
||||
role: Role_1.Role.create(row.role),
|
||||
orgId: row.org_id ?? undefined,
|
||||
createdAt: new Date(row.created_at),
|
||||
updatedAt: new Date(row.updated_at),
|
||||
}, UniqueId_1.UniqueId.from(row.id));
|
||||
}
|
||||
}
|
||||
exports.KyselyUserRepository = KyselyUserRepository;
|
||||
Reference in New Issue
Block a user