fase(9): auth module with casl rbac and session management
This commit is contained in:
8
dist/api/router.js
vendored
8
dist/api/router.js
vendored
@@ -8,8 +8,16 @@ const express_1 = require("express");
|
||||
const CrawlingController_1 = require("../modules/crawling/infrastructure/http/CrawlingController");
|
||||
const FindingsController_1 = require("../modules/findings/infrastructure/http/FindingsController");
|
||||
const FuzzingController_1 = require("../modules/fuzzing/infrastructure/http/FuzzingController");
|
||||
const AuthController_1 = require("../modules/auth/infrastructure/http/AuthController");
|
||||
const AuthMiddleware_1 = require("../modules/auth/application/middleware/AuthMiddleware");
|
||||
function createRouter(deps) {
|
||||
const router = (0, express_1.Router)();
|
||||
const { authDeps } = deps;
|
||||
// Auth routes — public (no auth middleware)
|
||||
router.use('/auth', (0, AuthController_1.createAuthController)(authDeps.registerCommand, authDeps.loginCommand, authDeps.createOrgCommand, authDeps.inviteMemberCommand, authDeps.createApiKeyCommand, authDeps.getUserQuery, authDeps.listOrgMembersQuery, authDeps.sessionRepository, authDeps.apiKeyRepository, authDeps.userRepository));
|
||||
// Apply auth middleware to all routes below
|
||||
const authMiddleware = (0, AuthMiddleware_1.createAuthMiddleware)(authDeps.userRepository, authDeps.sessionRepository, authDeps.apiKeyRepository);
|
||||
router.use(authMiddleware);
|
||||
router.use('/sessions', (0, CrawlingController_1.createCrawlingRouter)(deps.crawlingDeps));
|
||||
router.use('/findings', (0, FindingsController_1.createFindingsRouter)(deps.findingsDeps));
|
||||
router.use('/fuzz', (0, FuzzingController_1.createFuzzingRouter)(deps.fuzzingDeps));
|
||||
|
||||
4
dist/api/server.js
vendored
4
dist/api/server.js
vendored
@@ -12,6 +12,7 @@ const express_1 = __importDefault(require("express"));
|
||||
const cors_1 = __importDefault(require("cors"));
|
||||
const helmet_1 = __importDefault(require("helmet"));
|
||||
const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
|
||||
const cookie_parser_1 = __importDefault(require("cookie-parser"));
|
||||
const requestId_1 = require("./middleware/requestId");
|
||||
const notFound_1 = require("./middleware/notFound");
|
||||
const errorHandler_1 = require("./middleware/errorHandler");
|
||||
@@ -39,8 +40,9 @@ function createServer(deps) {
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
}));
|
||||
// 5. Body parsing
|
||||
// 5. Body parsing + cookies
|
||||
app.use(express_1.default.json({ limit: '10mb' }));
|
||||
app.use((0, cookie_parser_1.default)());
|
||||
// 6. Health endpoints — no auth required
|
||||
app.get('/health/live', (_req, res) => {
|
||||
res.json({ status: 'ok', uptime: process.uptime() });
|
||||
|
||||
81
dist/db/migrations/004_auth_tables.js
vendored
Normal file
81
dist/db/migrations/004_auth_tables.js
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.up = up;
|
||||
exports.down = down;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async function up(db) {
|
||||
await db.schema
|
||||
.createTable('users')
|
||||
.ifNotExists()
|
||||
.addColumn('id', 'text', (col) => col.primaryKey())
|
||||
.addColumn('email', 'text', (col) => col.notNull().unique())
|
||||
.addColumn('name', 'text', (col) => col.notNull())
|
||||
.addColumn('password_hash', 'text', (col) => col.notNull())
|
||||
.addColumn('role', 'text', (col) => col.notNull().defaultTo('member'))
|
||||
.addColumn('org_id', 'text')
|
||||
.addColumn('created_at', 'integer', (col) => col.notNull())
|
||||
.addColumn('updated_at', 'integer', (col) => col.notNull())
|
||||
.execute();
|
||||
await db.schema
|
||||
.createTable('organizations')
|
||||
.ifNotExists()
|
||||
.addColumn('id', 'text', (col) => col.primaryKey())
|
||||
.addColumn('name', 'text', (col) => col.notNull())
|
||||
.addColumn('slug', 'text', (col) => col.notNull().unique())
|
||||
.addColumn('created_at', 'integer', (col) => col.notNull())
|
||||
.execute();
|
||||
await db.schema
|
||||
.createTable('org_members')
|
||||
.ifNotExists()
|
||||
.addColumn('id', 'text', (col) => col.primaryKey())
|
||||
.addColumn('org_id', 'text', (col) => col.notNull().references('organizations.id'))
|
||||
.addColumn('user_id', 'text', (col) => col.notNull().references('users.id'))
|
||||
.addColumn('role', 'text', (col) => col.notNull().defaultTo('member'))
|
||||
.addColumn('joined_at', 'integer', (col) => col.notNull())
|
||||
.execute();
|
||||
await db.schema
|
||||
.createTable('api_keys')
|
||||
.ifNotExists()
|
||||
.addColumn('id', 'text', (col) => col.primaryKey())
|
||||
.addColumn('user_id', 'text', (col) => col.notNull().references('users.id'))
|
||||
.addColumn('org_id', 'text', (col) => col.notNull())
|
||||
.addColumn('name', 'text', (col) => col.notNull())
|
||||
.addColumn('key_hash', 'text', (col) => col.notNull().unique())
|
||||
.addColumn('key_prefix', 'text', (col) => col.notNull())
|
||||
.addColumn('permissions', 'text', (col) => col.notNull().defaultTo('["member"]'))
|
||||
.addColumn('expires_at', 'integer')
|
||||
.addColumn('last_used_at', 'integer')
|
||||
.addColumn('created_at', 'integer', (col) => col.notNull())
|
||||
.execute();
|
||||
await db.schema
|
||||
.createTable('auth_sessions')
|
||||
.ifNotExists()
|
||||
.addColumn('id', 'text', (col) => col.primaryKey())
|
||||
.addColumn('user_id', 'text', (col) => col.notNull().references('users.id'))
|
||||
.addColumn('token', 'text', (col) => col.notNull().unique())
|
||||
.addColumn('expires_at', 'integer', (col) => col.notNull())
|
||||
.addColumn('created_at', 'integer', (col) => col.notNull())
|
||||
.execute();
|
||||
await db.schema
|
||||
.createIndex('idx_auth_sessions_token')
|
||||
.ifNotExists()
|
||||
.on('auth_sessions')
|
||||
.columns(['token'])
|
||||
.execute();
|
||||
await db.schema
|
||||
.createIndex('idx_users_email')
|
||||
.ifNotExists()
|
||||
.on('users')
|
||||
.columns(['email'])
|
||||
.execute();
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async function down(db) {
|
||||
await db.schema.dropIndex('idx_users_email').ifExists().execute();
|
||||
await db.schema.dropIndex('idx_auth_sessions_token').ifExists().execute();
|
||||
await db.schema.dropTable('auth_sessions').ifExists().execute();
|
||||
await db.schema.dropTable('api_keys').ifExists().execute();
|
||||
await db.schema.dropTable('org_members').ifExists().execute();
|
||||
await db.schema.dropTable('organizations').ifExists().execute();
|
||||
await db.schema.dropTable('users').ifExists().execute();
|
||||
}
|
||||
39
dist/main.js
vendored
39
dist/main.js
vendored
@@ -36,6 +36,19 @@ const FuzzingEngineAdapter_1 = require("./modules/fuzzing/infrastructure/adapter
|
||||
const RunFuzzCommand_1 = require("./modules/fuzzing/application/commands/RunFuzzCommand");
|
||||
const OnActionExecuted_1 = require("./modules/fuzzing/application/event-handlers/OnActionExecuted");
|
||||
const InMemoryFuzzSessionRepository_1 = require("./modules/fuzzing/infrastructure/repositories/InMemoryFuzzSessionRepository");
|
||||
// Auth module
|
||||
const KyselyUserRepository_1 = require("./modules/auth/infrastructure/repositories/KyselyUserRepository");
|
||||
const KyselyOrganizationRepository_1 = require("./modules/auth/infrastructure/repositories/KyselyOrganizationRepository");
|
||||
const KyselyApiKeyRepository_1 = require("./modules/auth/infrastructure/repositories/KyselyApiKeyRepository");
|
||||
const KyselySessionRepository_1 = require("./modules/auth/infrastructure/repositories/KyselySessionRepository");
|
||||
const RegisterCommand_1 = require("./modules/auth/application/commands/RegisterCommand");
|
||||
const LoginCommand_1 = require("./modules/auth/application/commands/LoginCommand");
|
||||
const CreateOrganizationCommand_1 = require("./modules/auth/application/commands/CreateOrganizationCommand");
|
||||
const InviteMemberCommand_1 = require("./modules/auth/application/commands/InviteMemberCommand");
|
||||
const CreateApiKeyCommand_1 = require("./modules/auth/application/commands/CreateApiKeyCommand");
|
||||
const GetUserQuery_1 = require("./modules/auth/application/queries/GetUserQuery");
|
||||
const ListOrgMembersQuery_1 = require("./modules/auth/application/queries/ListOrgMembersQuery");
|
||||
const PasswordService_1 = require("./modules/auth/infrastructure/auth/PasswordService");
|
||||
// Job queue
|
||||
const SQLiteJobQueue_1 = require("./jobs/SQLiteJobQueue");
|
||||
const ExplorationWorker_1 = require("./jobs/workers/ExplorationWorker");
|
||||
@@ -83,7 +96,19 @@ async function bootstrap() {
|
||||
eventBus.subscribe('crawling.anomaly_detected', onAnomalyDetected);
|
||||
const onActionExecuted = new OnActionExecuted_1.OnActionExecuted(runFuzz);
|
||||
eventBus.subscribe('crawling.action_executed', onActionExecuted);
|
||||
// 10. HTTP server
|
||||
// 10. Auth module
|
||||
const userRepo = new KyselyUserRepository_1.KyselyUserRepository(db);
|
||||
const orgRepo = new KyselyOrganizationRepository_1.KyselyOrganizationRepository(db);
|
||||
const apiKeyRepo = new KyselyApiKeyRepository_1.KyselyApiKeyRepository(db);
|
||||
const authSessionRepo = new KyselySessionRepository_1.KyselySessionRepository(db);
|
||||
const registerCommand = new RegisterCommand_1.RegisterCommand(userRepo, eventBus, PasswordService_1.hashPassword);
|
||||
const loginCommand = new LoginCommand_1.LoginCommand(userRepo, authSessionRepo, eventBus, PasswordService_1.verifyPassword);
|
||||
const createOrgCommand = new CreateOrganizationCommand_1.CreateOrganizationCommand(orgRepo, userRepo, eventBus);
|
||||
const inviteMemberCommand = new InviteMemberCommand_1.InviteMemberCommand(orgRepo, userRepo, eventBus);
|
||||
const createApiKeyCommand = new CreateApiKeyCommand_1.CreateApiKeyCommand(apiKeyRepo, userRepo);
|
||||
const getUserQuery = new GetUserQuery_1.GetUserQuery(userRepo);
|
||||
const listOrgMembersQuery = new ListOrgMembersQuery_1.ListOrgMembersQuery(orgRepo, userRepo);
|
||||
// 11. HTTP server
|
||||
const app = (0, server_1.createServer)({
|
||||
config,
|
||||
logger,
|
||||
@@ -91,6 +116,18 @@ async function bootstrap() {
|
||||
crawlingDeps: { startCrawl, stopCrawl, getSession, listSessions },
|
||||
findingsDeps: { getFinding, listFindings, findingStats, resolveFinding, enrichFinding },
|
||||
fuzzingDeps: { runFuzz, repository: fuzzRepo },
|
||||
authDeps: {
|
||||
registerCommand,
|
||||
loginCommand,
|
||||
createOrgCommand,
|
||||
inviteMemberCommand,
|
||||
createApiKeyCommand,
|
||||
getUserQuery,
|
||||
listOrgMembersQuery,
|
||||
sessionRepository: authSessionRepo,
|
||||
apiKeyRepository: apiKeyRepo,
|
||||
userRepository: userRepo,
|
||||
},
|
||||
});
|
||||
const httpServer = http_1.default.createServer(app);
|
||||
// 11. Job queue
|
||||
|
||||
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;
|
||||
35
dist/modules/auth/domain/entities/ApiKey.js
vendored
Normal file
35
dist/modules/auth/domain/entities/ApiKey.js
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ApiKey = void 0;
|
||||
const AggregateRoot_1 = require("../../../../shared/domain/AggregateRoot");
|
||||
const UniqueId_1 = require("../../../../shared/domain/UniqueId");
|
||||
class ApiKey extends AggregateRoot_1.AggregateRoot {
|
||||
static create(props, id) {
|
||||
const keyId = id ?? UniqueId_1.UniqueId.create();
|
||||
return new ApiKey({
|
||||
...props,
|
||||
createdAt: new Date(),
|
||||
}, keyId);
|
||||
}
|
||||
static reconstitute(props, id) {
|
||||
return new ApiKey(props, id);
|
||||
}
|
||||
get userId() { return this.props.userId; }
|
||||
get orgId() { return this.props.orgId; }
|
||||
get name() { return this.props.name; }
|
||||
get keyHash() { return this.props.keyHash; }
|
||||
get keyPrefix() { return this.props.keyPrefix; }
|
||||
get permissions() { return this.props.permissions; }
|
||||
get expiresAt() { return this.props.expiresAt; }
|
||||
get lastUsedAt() { return this.props.lastUsedAt; }
|
||||
get createdAt() { return this.props.createdAt; }
|
||||
isExpired() {
|
||||
if (!this.props.expiresAt)
|
||||
return false;
|
||||
return new Date() > this.props.expiresAt;
|
||||
}
|
||||
markUsed() {
|
||||
this.props.lastUsedAt = new Date();
|
||||
}
|
||||
}
|
||||
exports.ApiKey = ApiKey;
|
||||
33
dist/modules/auth/domain/entities/Organization.js
vendored
Normal file
33
dist/modules/auth/domain/entities/Organization.js
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.Organization = void 0;
|
||||
const AggregateRoot_1 = require("../../../../shared/domain/AggregateRoot");
|
||||
const UniqueId_1 = require("../../../../shared/domain/UniqueId");
|
||||
const OrgCreated_1 = require("../events/OrgCreated");
|
||||
class Organization extends AggregateRoot_1.AggregateRoot {
|
||||
static create(props, id) {
|
||||
const orgId = id ?? UniqueId_1.UniqueId.create();
|
||||
const org = new Organization({
|
||||
...props,
|
||||
createdAt: new Date(),
|
||||
}, orgId);
|
||||
org.addDomainEvent(new OrgCreated_1.OrgCreated(orgId.toString(), {
|
||||
name: props.name,
|
||||
slug: props.slug,
|
||||
}));
|
||||
return org;
|
||||
}
|
||||
static reconstitute(props, id) {
|
||||
return new Organization(props, id);
|
||||
}
|
||||
static slugify(name) {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
}
|
||||
get name() { return this.props.name; }
|
||||
get slug() { return this.props.slug; }
|
||||
get createdAt() { return this.props.createdAt; }
|
||||
}
|
||||
exports.Organization = Organization;
|
||||
42
dist/modules/auth/domain/entities/User.js
vendored
Normal file
42
dist/modules/auth/domain/entities/User.js
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.User = void 0;
|
||||
const AggregateRoot_1 = require("../../../../shared/domain/AggregateRoot");
|
||||
const UniqueId_1 = require("../../../../shared/domain/UniqueId");
|
||||
const UserCreated_1 = require("../events/UserCreated");
|
||||
class User extends AggregateRoot_1.AggregateRoot {
|
||||
static create(props, id) {
|
||||
const userId = id ?? UniqueId_1.UniqueId.create();
|
||||
const now = new Date();
|
||||
const user = new User({
|
||||
...props,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}, userId);
|
||||
user.addDomainEvent(new UserCreated_1.UserCreated(userId.toString(), {
|
||||
email: props.email.value,
|
||||
name: props.name,
|
||||
role: props.role.value,
|
||||
}));
|
||||
return user;
|
||||
}
|
||||
static reconstitute(props, id) {
|
||||
return new User(props, id);
|
||||
}
|
||||
get email() { return this.props.email; }
|
||||
get name() { return this.props.name; }
|
||||
get passwordHash() { return this.props.passwordHash; }
|
||||
get role() { return this.props.role; }
|
||||
get orgId() { return this.props.orgId; }
|
||||
get createdAt() { return this.props.createdAt; }
|
||||
get updatedAt() { return this.props.updatedAt; }
|
||||
assignToOrg(orgId) {
|
||||
this.props.orgId = orgId;
|
||||
this.props.updatedAt = new Date();
|
||||
}
|
||||
changeRole(role) {
|
||||
this.props.role = role;
|
||||
this.props.updatedAt = new Date();
|
||||
}
|
||||
}
|
||||
exports.User = User;
|
||||
14
dist/modules/auth/domain/events/MemberInvited.js
vendored
Normal file
14
dist/modules/auth/domain/events/MemberInvited.js
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.MemberInvited = void 0;
|
||||
const crypto_1 = require("crypto");
|
||||
class MemberInvited {
|
||||
constructor(aggregateId, payload) {
|
||||
this.aggregateId = aggregateId;
|
||||
this.payload = payload;
|
||||
this.eventId = (0, crypto_1.randomUUID)();
|
||||
this.eventName = 'auth.member.invited';
|
||||
this.occurredOn = new Date();
|
||||
}
|
||||
}
|
||||
exports.MemberInvited = MemberInvited;
|
||||
14
dist/modules/auth/domain/events/OrgCreated.js
vendored
Normal file
14
dist/modules/auth/domain/events/OrgCreated.js
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.OrgCreated = void 0;
|
||||
const crypto_1 = require("crypto");
|
||||
class OrgCreated {
|
||||
constructor(aggregateId, payload) {
|
||||
this.aggregateId = aggregateId;
|
||||
this.payload = payload;
|
||||
this.eventId = (0, crypto_1.randomUUID)();
|
||||
this.eventName = 'auth.org.created';
|
||||
this.occurredOn = new Date();
|
||||
}
|
||||
}
|
||||
exports.OrgCreated = OrgCreated;
|
||||
14
dist/modules/auth/domain/events/UserCreated.js
vendored
Normal file
14
dist/modules/auth/domain/events/UserCreated.js
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.UserCreated = void 0;
|
||||
const crypto_1 = require("crypto");
|
||||
class UserCreated {
|
||||
constructor(aggregateId, payload) {
|
||||
this.aggregateId = aggregateId;
|
||||
this.payload = payload;
|
||||
this.eventId = (0, crypto_1.randomUUID)();
|
||||
this.eventName = 'auth.user.created';
|
||||
this.occurredOn = new Date();
|
||||
}
|
||||
}
|
||||
exports.UserCreated = UserCreated;
|
||||
14
dist/modules/auth/domain/events/UserLoggedIn.js
vendored
Normal file
14
dist/modules/auth/domain/events/UserLoggedIn.js
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.UserLoggedIn = void 0;
|
||||
const crypto_1 = require("crypto");
|
||||
class UserLoggedIn {
|
||||
constructor(aggregateId, payload) {
|
||||
this.aggregateId = aggregateId;
|
||||
this.payload = payload;
|
||||
this.eventId = (0, crypto_1.randomUUID)();
|
||||
this.eventName = 'auth.user.logged_in';
|
||||
this.occurredOn = new Date();
|
||||
}
|
||||
}
|
||||
exports.UserLoggedIn = UserLoggedIn;
|
||||
2
dist/modules/auth/domain/ports/IApiKeyRepository.js
vendored
Normal file
2
dist/modules/auth/domain/ports/IApiKeyRepository.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
2
dist/modules/auth/domain/ports/IOrganizationRepository.js
vendored
Normal file
2
dist/modules/auth/domain/ports/IOrganizationRepository.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
2
dist/modules/auth/domain/ports/ISessionRepository.js
vendored
Normal file
2
dist/modules/auth/domain/ports/ISessionRepository.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
2
dist/modules/auth/domain/ports/IUserRepository.js
vendored
Normal file
2
dist/modules/auth/domain/ports/IUserRepository.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
18
dist/modules/auth/domain/value-objects/Email.js
vendored
Normal file
18
dist/modules/auth/domain/value-objects/Email.js
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.Email = void 0;
|
||||
const ValueObject_1 = require("../../../../shared/domain/ValueObject");
|
||||
class Email extends ValueObject_1.ValueObject {
|
||||
static create(value) {
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (!Email.EMAIL_REGEX.test(normalized)) {
|
||||
throw new Error(`Invalid email address: ${value}`);
|
||||
}
|
||||
return new Email({ value: normalized });
|
||||
}
|
||||
get value() {
|
||||
return this.props.value;
|
||||
}
|
||||
}
|
||||
exports.Email = Email;
|
||||
Email.EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
12
dist/modules/auth/domain/value-objects/Permission.js
vendored
Normal file
12
dist/modules/auth/domain/value-objects/Permission.js
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.Permission = void 0;
|
||||
const ValueObject_1 = require("../../../../shared/domain/ValueObject");
|
||||
class Permission extends ValueObject_1.ValueObject {
|
||||
static create(action, subject) {
|
||||
return new Permission({ action, subject });
|
||||
}
|
||||
get action() { return this.props.action; }
|
||||
get subject() { return this.props.subject; }
|
||||
}
|
||||
exports.Permission = Permission;
|
||||
29
dist/modules/auth/domain/value-objects/Role.js
vendored
Normal file
29
dist/modules/auth/domain/value-objects/Role.js
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.Role = void 0;
|
||||
const ValueObject_1 = require("../../../../shared/domain/ValueObject");
|
||||
class Role extends ValueObject_1.ValueObject {
|
||||
static create(value) {
|
||||
if (!Role.VALID_ROLES.includes(value)) {
|
||||
throw new Error(`Invalid role: ${value}. Must be one of: ${Role.VALID_ROLES.join(', ')}`);
|
||||
}
|
||||
return new Role({ value: value });
|
||||
}
|
||||
static owner() { return new Role({ value: 'owner' }); }
|
||||
static admin() { return new Role({ value: 'admin' }); }
|
||||
static member() { return new Role({ value: 'member' }); }
|
||||
static viewer() { return new Role({ value: 'viewer' }); }
|
||||
get value() {
|
||||
return this.props.value;
|
||||
}
|
||||
isOwner() { return this.props.value === 'owner'; }
|
||||
isAdmin() { return this.props.value === 'admin'; }
|
||||
isMember() { return this.props.value === 'member'; }
|
||||
isViewer() { return this.props.value === 'viewer'; }
|
||||
}
|
||||
exports.Role = Role;
|
||||
Role.OWNER = 'owner';
|
||||
Role.ADMIN = 'admin';
|
||||
Role.MEMBER = 'member';
|
||||
Role.VIEWER = 'viewer';
|
||||
Role.VALID_ROLES = ['owner', 'admin', 'member', 'viewer'];
|
||||
48
dist/modules/auth/index.js
vendored
Normal file
48
dist/modules/auth/index.js
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createAuthController = exports.KyselySessionRepository = exports.KyselyApiKeyRepository = exports.KyselyOrganizationRepository = exports.KyselyUserRepository = exports.defineAbilityFor = exports.verifyPassword = exports.hashPassword = exports.requirePermission = exports.createAuthMiddleware = exports.ListOrgMembersQuery = exports.GetUserQuery = exports.CreateApiKeyCommand = exports.InviteMemberCommand = exports.CreateOrganizationCommand = exports.LoginCommand = exports.RegisterCommand = exports.Permission = exports.Role = exports.Email = exports.ApiKey = exports.Organization = exports.User = void 0;
|
||||
var User_1 = require("./domain/entities/User");
|
||||
Object.defineProperty(exports, "User", { enumerable: true, get: function () { return User_1.User; } });
|
||||
var Organization_1 = require("./domain/entities/Organization");
|
||||
Object.defineProperty(exports, "Organization", { enumerable: true, get: function () { return Organization_1.Organization; } });
|
||||
var ApiKey_1 = require("./domain/entities/ApiKey");
|
||||
Object.defineProperty(exports, "ApiKey", { enumerable: true, get: function () { return ApiKey_1.ApiKey; } });
|
||||
var Email_1 = require("./domain/value-objects/Email");
|
||||
Object.defineProperty(exports, "Email", { enumerable: true, get: function () { return Email_1.Email; } });
|
||||
var Role_1 = require("./domain/value-objects/Role");
|
||||
Object.defineProperty(exports, "Role", { enumerable: true, get: function () { return Role_1.Role; } });
|
||||
var Permission_1 = require("./domain/value-objects/Permission");
|
||||
Object.defineProperty(exports, "Permission", { enumerable: true, get: function () { return Permission_1.Permission; } });
|
||||
var RegisterCommand_1 = require("./application/commands/RegisterCommand");
|
||||
Object.defineProperty(exports, "RegisterCommand", { enumerable: true, get: function () { return RegisterCommand_1.RegisterCommand; } });
|
||||
var LoginCommand_1 = require("./application/commands/LoginCommand");
|
||||
Object.defineProperty(exports, "LoginCommand", { enumerable: true, get: function () { return LoginCommand_1.LoginCommand; } });
|
||||
var CreateOrganizationCommand_1 = require("./application/commands/CreateOrganizationCommand");
|
||||
Object.defineProperty(exports, "CreateOrganizationCommand", { enumerable: true, get: function () { return CreateOrganizationCommand_1.CreateOrganizationCommand; } });
|
||||
var InviteMemberCommand_1 = require("./application/commands/InviteMemberCommand");
|
||||
Object.defineProperty(exports, "InviteMemberCommand", { enumerable: true, get: function () { return InviteMemberCommand_1.InviteMemberCommand; } });
|
||||
var CreateApiKeyCommand_1 = require("./application/commands/CreateApiKeyCommand");
|
||||
Object.defineProperty(exports, "CreateApiKeyCommand", { enumerable: true, get: function () { return CreateApiKeyCommand_1.CreateApiKeyCommand; } });
|
||||
var GetUserQuery_1 = require("./application/queries/GetUserQuery");
|
||||
Object.defineProperty(exports, "GetUserQuery", { enumerable: true, get: function () { return GetUserQuery_1.GetUserQuery; } });
|
||||
var ListOrgMembersQuery_1 = require("./application/queries/ListOrgMembersQuery");
|
||||
Object.defineProperty(exports, "ListOrgMembersQuery", { enumerable: true, get: function () { return ListOrgMembersQuery_1.ListOrgMembersQuery; } });
|
||||
var AuthMiddleware_1 = require("./application/middleware/AuthMiddleware");
|
||||
Object.defineProperty(exports, "createAuthMiddleware", { enumerable: true, get: function () { return AuthMiddleware_1.createAuthMiddleware; } });
|
||||
var RBACMiddleware_1 = require("./application/middleware/RBACMiddleware");
|
||||
Object.defineProperty(exports, "requirePermission", { enumerable: true, get: function () { return RBACMiddleware_1.requirePermission; } });
|
||||
var PasswordService_1 = require("./infrastructure/auth/PasswordService");
|
||||
Object.defineProperty(exports, "hashPassword", { enumerable: true, get: function () { return PasswordService_1.hashPassword; } });
|
||||
Object.defineProperty(exports, "verifyPassword", { enumerable: true, get: function () { return PasswordService_1.verifyPassword; } });
|
||||
var AbilityFactory_1 = require("./infrastructure/casl/AbilityFactory");
|
||||
Object.defineProperty(exports, "defineAbilityFor", { enumerable: true, get: function () { return AbilityFactory_1.defineAbilityFor; } });
|
||||
var KyselyUserRepository_1 = require("./infrastructure/repositories/KyselyUserRepository");
|
||||
Object.defineProperty(exports, "KyselyUserRepository", { enumerable: true, get: function () { return KyselyUserRepository_1.KyselyUserRepository; } });
|
||||
var KyselyOrganizationRepository_1 = require("./infrastructure/repositories/KyselyOrganizationRepository");
|
||||
Object.defineProperty(exports, "KyselyOrganizationRepository", { enumerable: true, get: function () { return KyselyOrganizationRepository_1.KyselyOrganizationRepository; } });
|
||||
var KyselyApiKeyRepository_1 = require("./infrastructure/repositories/KyselyApiKeyRepository");
|
||||
Object.defineProperty(exports, "KyselyApiKeyRepository", { enumerable: true, get: function () { return KyselyApiKeyRepository_1.KyselyApiKeyRepository; } });
|
||||
var KyselySessionRepository_1 = require("./infrastructure/repositories/KyselySessionRepository");
|
||||
Object.defineProperty(exports, "KyselySessionRepository", { enumerable: true, get: function () { return KyselySessionRepository_1.KyselySessionRepository; } });
|
||||
var AuthController_1 = require("./infrastructure/http/AuthController");
|
||||
Object.defineProperty(exports, "createAuthController", { enumerable: true, get: function () { return AuthController_1.createAuthController; } });
|
||||
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