fase(9): auth module with casl rbac and session management
This commit is contained in:
41
dist/modules/auth/application/commands/CreateApiKeyCommand.js
vendored
Normal file
41
dist/modules/auth/application/commands/CreateApiKeyCommand.js
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.CreateApiKeyCommand = void 0;
|
||||
const Result_1 = require("../../../../shared/domain/Result");
|
||||
const ApiKey_1 = require("../../domain/entities/ApiKey");
|
||||
const crypto_1 = require("crypto");
|
||||
class CreateApiKeyCommand {
|
||||
constructor(apiKeyRepository, userRepository) {
|
||||
this.apiKeyRepository = apiKeyRepository;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
async execute(request) {
|
||||
const user = await this.userRepository.findById(request.userId);
|
||||
if (!user) {
|
||||
return (0, Result_1.Err)('User not found');
|
||||
}
|
||||
if (!request.name.trim()) {
|
||||
return (0, Result_1.Err)('API key name is required');
|
||||
}
|
||||
const rawKey = `abe_${(0, crypto_1.randomBytes)(32).toString('hex')}`;
|
||||
const keyHash = (0, crypto_1.createHash)('sha256').update(rawKey).digest('hex');
|
||||
const keyPrefix = rawKey.substring(0, 12);
|
||||
const apiKey = ApiKey_1.ApiKey.create({
|
||||
userId: request.userId,
|
||||
orgId: request.orgId,
|
||||
name: request.name.trim(),
|
||||
keyHash,
|
||||
keyPrefix,
|
||||
permissions: request.permissions ?? ['member'],
|
||||
expiresAt: request.expiresAt,
|
||||
});
|
||||
await this.apiKeyRepository.save(apiKey);
|
||||
return (0, Result_1.Ok)({
|
||||
id: apiKey.id.toString(),
|
||||
key: rawKey,
|
||||
keyPrefix,
|
||||
name: apiKey.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
exports.CreateApiKeyCommand = CreateApiKeyCommand;
|
||||
48
dist/modules/auth/application/commands/CreateOrganizationCommand.js
vendored
Normal file
48
dist/modules/auth/application/commands/CreateOrganizationCommand.js
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.CreateOrganizationCommand = void 0;
|
||||
const Result_1 = require("../../../../shared/domain/Result");
|
||||
const Organization_1 = require("../../domain/entities/Organization");
|
||||
const crypto_1 = require("crypto");
|
||||
class CreateOrganizationCommand {
|
||||
constructor(orgRepository, userRepository, eventBus) {
|
||||
this.orgRepository = orgRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.eventBus = eventBus;
|
||||
}
|
||||
async execute(request) {
|
||||
const user = await this.userRepository.findById(request.ownerId);
|
||||
if (!user) {
|
||||
return (0, Result_1.Err)('User not found');
|
||||
}
|
||||
const slug = Organization_1.Organization.slugify(request.name);
|
||||
if (!slug) {
|
||||
return (0, Result_1.Err)('Invalid organization name');
|
||||
}
|
||||
const existing = await this.orgRepository.findBySlug(slug);
|
||||
if (existing) {
|
||||
return (0, Result_1.Err)('Organization name already taken');
|
||||
}
|
||||
const org = Organization_1.Organization.create({ name: request.name, slug });
|
||||
await this.orgRepository.save(org);
|
||||
await this.orgRepository.addMember({
|
||||
id: (0, crypto_1.randomUUID)(),
|
||||
orgId: org.id.toString(),
|
||||
userId: request.ownerId,
|
||||
role: 'owner',
|
||||
joinedAt: new Date(),
|
||||
});
|
||||
user.assignToOrg(org.id.toString());
|
||||
await this.userRepository.save(user);
|
||||
for (const event of org.domainEvents) {
|
||||
await this.eventBus.publish(event);
|
||||
}
|
||||
org.clearEvents();
|
||||
return (0, Result_1.Ok)({
|
||||
orgId: org.id.toString(),
|
||||
name: org.name,
|
||||
slug: org.slug,
|
||||
});
|
||||
}
|
||||
}
|
||||
exports.CreateOrganizationCommand = CreateOrganizationCommand;
|
||||
63
dist/modules/auth/application/commands/InviteMemberCommand.js
vendored
Normal file
63
dist/modules/auth/application/commands/InviteMemberCommand.js
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.InviteMemberCommand = void 0;
|
||||
const Result_1 = require("../../../../shared/domain/Result");
|
||||
const Email_1 = require("../../domain/value-objects/Email");
|
||||
const Role_1 = require("../../domain/value-objects/Role");
|
||||
const MemberInvited_1 = require("../../domain/events/MemberInvited");
|
||||
const crypto_1 = require("crypto");
|
||||
class InviteMemberCommand {
|
||||
constructor(orgRepository, userRepository, eventBus) {
|
||||
this.orgRepository = orgRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.eventBus = eventBus;
|
||||
}
|
||||
async execute(request) {
|
||||
const org = await this.orgRepository.findById(request.orgId);
|
||||
if (!org) {
|
||||
return (0, Result_1.Err)('Organization not found');
|
||||
}
|
||||
let email;
|
||||
try {
|
||||
email = Email_1.Email.create(request.email);
|
||||
}
|
||||
catch {
|
||||
return (0, Result_1.Err)('Invalid email address');
|
||||
}
|
||||
let role;
|
||||
try {
|
||||
role = Role_1.Role.create(request.role);
|
||||
}
|
||||
catch {
|
||||
return (0, Result_1.Err)('Invalid role');
|
||||
}
|
||||
const user = await this.userRepository.findByEmail(email.value);
|
||||
if (!user) {
|
||||
return (0, Result_1.Err)('User with this email not found. They must register first.');
|
||||
}
|
||||
const existing = await this.orgRepository.getMember(request.orgId, user.id.toString());
|
||||
if (existing) {
|
||||
return (0, Result_1.Err)('User is already a member of this organization');
|
||||
}
|
||||
const memberId = (0, crypto_1.randomUUID)();
|
||||
await this.orgRepository.addMember({
|
||||
id: memberId,
|
||||
orgId: request.orgId,
|
||||
userId: user.id.toString(),
|
||||
role: role.value,
|
||||
joinedAt: new Date(),
|
||||
});
|
||||
const event = new MemberInvited_1.MemberInvited(request.orgId, {
|
||||
email: email.value,
|
||||
role: role.value,
|
||||
inviterUserId: request.inviterUserId,
|
||||
});
|
||||
await this.eventBus.publish(event);
|
||||
return (0, Result_1.Ok)({
|
||||
memberId,
|
||||
email: email.value,
|
||||
role: role.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
exports.InviteMemberCommand = InviteMemberCommand;
|
||||
56
dist/modules/auth/application/commands/LoginCommand.js
vendored
Normal file
56
dist/modules/auth/application/commands/LoginCommand.js
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.LoginCommand = void 0;
|
||||
const Result_1 = require("../../../../shared/domain/Result");
|
||||
const Email_1 = require("../../domain/value-objects/Email");
|
||||
const UserLoggedIn_1 = require("../../domain/events/UserLoggedIn");
|
||||
const crypto_1 = require("crypto");
|
||||
class LoginCommand {
|
||||
constructor(userRepository, sessionRepository, eventBus, verifyPassword, sessionMaxAgeSeconds = 7 * 24 * 60 * 60) {
|
||||
this.userRepository = userRepository;
|
||||
this.sessionRepository = sessionRepository;
|
||||
this.eventBus = eventBus;
|
||||
this.verifyPassword = verifyPassword;
|
||||
this.sessionMaxAgeSeconds = sessionMaxAgeSeconds;
|
||||
}
|
||||
async execute(request) {
|
||||
let email;
|
||||
try {
|
||||
email = Email_1.Email.create(request.email);
|
||||
}
|
||||
catch {
|
||||
return (0, Result_1.Err)('Invalid credentials');
|
||||
}
|
||||
const user = await this.userRepository.findByEmail(email.value);
|
||||
if (!user) {
|
||||
return (0, Result_1.Err)('Invalid credentials');
|
||||
}
|
||||
const valid = await this.verifyPassword(request.password, user.passwordHash);
|
||||
if (!valid) {
|
||||
return (0, Result_1.Err)('Invalid credentials');
|
||||
}
|
||||
const token = (0, crypto_1.randomUUID)();
|
||||
const expiresAt = new Date(Date.now() + this.sessionMaxAgeSeconds * 1000);
|
||||
const session = {
|
||||
id: (0, crypto_1.randomUUID)(),
|
||||
userId: user.id.toString(),
|
||||
token,
|
||||
expiresAt,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
await this.sessionRepository.save(session);
|
||||
const event = new UserLoggedIn_1.UserLoggedIn(user.id.toString(), {
|
||||
email: user.email.value,
|
||||
sessionId: session.id,
|
||||
});
|
||||
await this.eventBus.publish(event);
|
||||
return (0, Result_1.Ok)({
|
||||
userId: user.id.toString(),
|
||||
sessionToken: token,
|
||||
expiresAt,
|
||||
role: user.role.value,
|
||||
name: user.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
exports.LoginCommand = LoginCommand;
|
||||
51
dist/modules/auth/application/commands/RegisterCommand.js
vendored
Normal file
51
dist/modules/auth/application/commands/RegisterCommand.js
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.RegisterCommand = void 0;
|
||||
const Result_1 = require("../../../../shared/domain/Result");
|
||||
const User_1 = require("../../domain/entities/User");
|
||||
const Email_1 = require("../../domain/value-objects/Email");
|
||||
const Role_1 = require("../../domain/value-objects/Role");
|
||||
class RegisterCommand {
|
||||
constructor(userRepository, eventBus, hashPassword) {
|
||||
this.userRepository = userRepository;
|
||||
this.eventBus = eventBus;
|
||||
this.hashPassword = hashPassword;
|
||||
}
|
||||
async execute(request) {
|
||||
let email;
|
||||
try {
|
||||
email = Email_1.Email.create(request.email);
|
||||
}
|
||||
catch {
|
||||
return (0, Result_1.Err)('Invalid email address');
|
||||
}
|
||||
const existing = await this.userRepository.findByEmail(email.value);
|
||||
if (existing) {
|
||||
return (0, Result_1.Err)('Email already registered');
|
||||
}
|
||||
if (request.password.length < 8) {
|
||||
return (0, Result_1.Err)('Password must be at least 8 characters');
|
||||
}
|
||||
let role;
|
||||
try {
|
||||
role = request.role ? Role_1.Role.create(request.role) : Role_1.Role.member();
|
||||
}
|
||||
catch {
|
||||
return (0, Result_1.Err)('Invalid role');
|
||||
}
|
||||
const passwordHash = await this.hashPassword(request.password);
|
||||
const user = User_1.User.create({ email, name: request.name, passwordHash, role });
|
||||
await this.userRepository.save(user);
|
||||
for (const event of user.domainEvents) {
|
||||
await this.eventBus.publish(event);
|
||||
}
|
||||
user.clearEvents();
|
||||
return (0, Result_1.Ok)({
|
||||
userId: user.id.toString(),
|
||||
email: user.email.value,
|
||||
name: user.name,
|
||||
role: user.role.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
exports.RegisterCommand = RegisterCommand;
|
||||
71
dist/modules/auth/application/middleware/AuthMiddleware.js
vendored
Normal file
71
dist/modules/auth/application/middleware/AuthMiddleware.js
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createAuthMiddleware = createAuthMiddleware;
|
||||
const crypto_1 = require("crypto");
|
||||
function createAuthMiddleware(userRepository, sessionRepository, apiKeyRepository) {
|
||||
return async function authMiddleware(req, res, next) {
|
||||
try {
|
||||
// 1. Check session cookie
|
||||
const sessionToken = req.cookies?.['abe_session'];
|
||||
if (sessionToken) {
|
||||
const session = await sessionRepository.findByToken(sessionToken);
|
||||
if (session && session.expiresAt > new Date()) {
|
||||
const user = await userRepository.findById(session.userId);
|
||||
if (user) {
|
||||
req.user = {
|
||||
id: user.id.toString(),
|
||||
email: user.email.value,
|
||||
name: user.name,
|
||||
role: user.role.value,
|
||||
orgId: user.orgId,
|
||||
};
|
||||
return next();
|
||||
}
|
||||
}
|
||||
}
|
||||
// 2. Check Bearer JWT (session token in header)
|
||||
const authHeader = req.headers.authorization;
|
||||
if (authHeader?.startsWith('Bearer ')) {
|
||||
const token = authHeader.substring(7);
|
||||
const session = await sessionRepository.findByToken(token);
|
||||
if (session && session.expiresAt > new Date()) {
|
||||
const user = await userRepository.findById(session.userId);
|
||||
if (user) {
|
||||
req.user = {
|
||||
id: user.id.toString(),
|
||||
email: user.email.value,
|
||||
name: user.name,
|
||||
role: user.role.value,
|
||||
orgId: user.orgId,
|
||||
};
|
||||
return next();
|
||||
}
|
||||
}
|
||||
}
|
||||
// 3. Check API key
|
||||
const apiKeyHeader = req.headers['x-abe-api-key'];
|
||||
if (apiKeyHeader && typeof apiKeyHeader === 'string') {
|
||||
const keyHash = (0, crypto_1.createHash)('sha256').update(apiKeyHeader).digest('hex');
|
||||
const apiKey = await apiKeyRepository.findByHash(keyHash);
|
||||
if (apiKey && !apiKey.isExpired()) {
|
||||
const user = await userRepository.findById(apiKey.userId);
|
||||
if (user) {
|
||||
await apiKeyRepository.updateLastUsed(apiKey.id.toString(), new Date());
|
||||
req.user = {
|
||||
id: user.id.toString(),
|
||||
email: user.email.value,
|
||||
name: user.name,
|
||||
role: user.role.value,
|
||||
orgId: user.orgId,
|
||||
};
|
||||
return next();
|
||||
}
|
||||
}
|
||||
}
|
||||
res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
catch {
|
||||
res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
};
|
||||
}
|
||||
21
dist/modules/auth/application/middleware/RBACMiddleware.js
vendored
Normal file
21
dist/modules/auth/application/middleware/RBACMiddleware.js
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.requirePermission = requirePermission;
|
||||
const AbilityFactory_1 = require("../../infrastructure/casl/AbilityFactory");
|
||||
function requirePermission(action, subject) {
|
||||
return function rbacMiddleware(req, res, next) {
|
||||
if (!req.user) {
|
||||
res.status(401).json({ error: 'Unauthorized' });
|
||||
return;
|
||||
}
|
||||
const ability = (0, AbilityFactory_1.defineAbilityFor)(req.user.role);
|
||||
if (!ability.can(action, subject)) {
|
||||
res.status(403).json({
|
||||
error: 'Forbidden',
|
||||
message: `You do not have permission to ${action} ${subject}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
next();
|
||||
};
|
||||
}
|
||||
24
dist/modules/auth/application/queries/GetUserQuery.js
vendored
Normal file
24
dist/modules/auth/application/queries/GetUserQuery.js
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.GetUserQuery = void 0;
|
||||
const Result_1 = require("../../../../shared/domain/Result");
|
||||
class GetUserQuery {
|
||||
constructor(userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
async execute(request) {
|
||||
const user = await this.userRepository.findById(request.userId);
|
||||
if (!user) {
|
||||
return (0, Result_1.Err)('User not found');
|
||||
}
|
||||
return (0, Result_1.Ok)({
|
||||
id: user.id.toString(),
|
||||
email: user.email.value,
|
||||
name: user.name,
|
||||
role: user.role.value,
|
||||
orgId: user.orgId,
|
||||
createdAt: user.createdAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
exports.GetUserQuery = GetUserQuery;
|
||||
33
dist/modules/auth/application/queries/ListOrgMembersQuery.js
vendored
Normal file
33
dist/modules/auth/application/queries/ListOrgMembersQuery.js
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ListOrgMembersQuery = void 0;
|
||||
const Result_1 = require("../../../../shared/domain/Result");
|
||||
class ListOrgMembersQuery {
|
||||
constructor(orgRepository, userRepository) {
|
||||
this.orgRepository = orgRepository;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
async execute(request) {
|
||||
const org = await this.orgRepository.findById(request.orgId);
|
||||
if (!org) {
|
||||
return (0, Result_1.Err)('Organization not found');
|
||||
}
|
||||
const members = await this.orgRepository.listMembers(request.orgId);
|
||||
const dtos = [];
|
||||
for (const member of members) {
|
||||
const user = await this.userRepository.findById(member.userId);
|
||||
if (user) {
|
||||
dtos.push({
|
||||
id: member.id,
|
||||
userId: member.userId,
|
||||
email: user.email.value,
|
||||
name: user.name,
|
||||
role: member.role,
|
||||
joinedAt: member.joinedAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
return (0, Result_1.Ok)({ members: dtos, total: dtos.length });
|
||||
}
|
||||
}
|
||||
exports.ListOrgMembersQuery = ListOrgMembersQuery;
|
||||
Reference in New Issue
Block a user