fase(9): auth module with casl rbac and session management

This commit is contained in:
debian
2026-03-05 09:57:49 -05:00
parent 39a5e41f75
commit 7526a5bc15
77 changed files with 3588 additions and 41 deletions

View 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;

View 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;

View 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;

View 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;

View 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;