"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AuthService = void 0;
const common_1 = require("@nestjs/common");
const jwt_1 = require("@nestjs/jwt");
const config_1 = require("@nestjs/config");
const typeorm_1 = require("@nestjs/typeorm");
const bcrypt = require("bcrypt");
const crypto = require("crypto");
const typeorm_2 = require("typeorm");
const user_entity_1 = require("../../database/entities/user.entity");
const notification_preferences_entity_1 = require("../../database/entities/notification-preferences.entity");
const password_reset_token_entity_1 = require("../../database/entities/password-reset-token.entity");
const email_verification_token_entity_1 = require("../../database/entities/email-verification-token.entity");
const groups_service_1 = require("../groups/groups.service");
const mail_service_1 = require("../mail/mail.service");
let AuthService = class AuthService {
    constructor(userRepo, notificationPrefsRepo, passwordResetTokenRepo, emailVerificationTokenRepo, dataSource, jwt, config, groupsService, mailService) {
        this.userRepo = userRepo;
        this.notificationPrefsRepo = notificationPrefsRepo;
        this.passwordResetTokenRepo = passwordResetTokenRepo;
        this.emailVerificationTokenRepo = emailVerificationTokenRepo;
        this.dataSource = dataSource;
        this.jwt = jwt;
        this.config = config;
        this.groupsService = groupsService;
        this.mailService = mailService;
    }
    async register(dto) {
        const email = dto.email.toLowerCase();
        const existing = await this.userRepo.findOne({ where: { email } });
        if (existing) {
            if (!existing.isEmailVerified && existing.passwordHash) {
                await this.createAndSendVerification(existing.id, existing.email, existing.name);
                return {
                    message: 'Check your email to verify your account before signing in',
                };
            }
            throw new common_1.ConflictException('Email already in use');
        }
        const rounds = this.config.get('app.bcryptRounds', 12);
        const passwordHash = await bcrypt.hash(dto.password, rounds);
        const user = this.userRepo.create({
            email,
            passwordHash,
            name: dto.name,
            isEmailVerified: false,
        });
        await this.userRepo.save(user);
        const prefs = this.notificationPrefsRepo.create({ userId: user.id });
        await this.notificationPrefsRepo.save(prefs);
        await this.createAndSendVerification(user.id, user.email, user.name);
        return { message: 'Check your email to verify your account before signing in' };
    }
    async login(dto, res) {
        const email = dto.email.toLowerCase();
        const user = await this.userRepo.findOne({ where: { email } });
        if (!user || !user.passwordHash) {
            await bcrypt.compare(dto.password, '$2b$12$invalidhashusedfortimingprotect');
            throw new common_1.UnauthorizedException('Invalid email or password');
        }
        const passwordMatch = await bcrypt.compare(dto.password, user.passwordHash);
        if (!passwordMatch) {
            throw new common_1.UnauthorizedException('Invalid email or password');
        }
        if (!user.isActive) {
            throw new common_1.UnauthorizedException('Account is disabled');
        }
        if (!user.isEmailVerified) {
            throw new common_1.HttpException({
                message: 'Please verify your email before signing in',
                code: 'EMAIL_NOT_VERIFIED',
            }, common_1.HttpStatus.FORBIDDEN);
        }
        await this.groupsService.acceptPendingInvitesForEmail(user.email, user.id);
        return this.issueTokens(user.id, user.email, res, this.toProfile(user));
    }
    async loginWithGoogle(profile, res) {
        let user = await this.userRepo.findOne({
            where: { googleId: profile.googleId },
        });
        if (!user) {
            user = await this.userRepo.findOne({ where: { email: profile.email } });
            if (user) {
                user.googleId = profile.googleId;
                user.avatarUrl = user.avatarUrl ?? profile.avatarUrl;
                user.isEmailVerified = true;
                user = await this.userRepo.save(user);
            }
            else {
                user = this.userRepo.create({
                    email: profile.email.toLowerCase(),
                    googleId: profile.googleId,
                    name: profile.name,
                    avatarUrl: profile.avatarUrl,
                    passwordHash: null,
                    isEmailVerified: true,
                });
                await this.userRepo.save(user);
                const prefs = this.notificationPrefsRepo.create({ userId: user.id });
                await this.notificationPrefsRepo.save(prefs);
            }
        }
        await this.groupsService.acceptPendingInvitesForEmail(user.email, user.id);
        return this.issueTokens(user.id, user.email, res, this.toProfile(user));
    }
    async refresh(userId, res) {
        const user = await this.userRepo.findOne({
            where: { id: userId },
            select: { id: true, email: true },
        });
        if (!user)
            throw new common_1.UnauthorizedException('User not found');
        const { accessToken } = await this.issueTokens(user.id, user.email, res);
        return { accessToken };
    }
    async logout(userId, res) {
        await this.userRepo.update({ id: userId }, { refreshTokenHash: null });
        this.clearRefreshCookie(res);
        return { message: 'Logged out successfully' };
    }
    async forgotPassword(email) {
        const normalized = email.toLowerCase();
        const user = await this.userRepo.findOne({ where: { email: normalized } });
        const generic = { message: 'If that email exists, a reset link has been sent' };
        if (!user?.passwordHash) {
            return generic;
        }
        await this.passwordResetTokenRepo.update({ userId: user.id, used: false }, { used: true });
        const rawToken = crypto.randomBytes(32).toString('hex');
        const tokenHash = this.hashToken(rawToken);
        const resetToken = this.passwordResetTokenRepo.create({
            userId: user.id,
            tokenHash,
            expiresAt: new Date(Date.now() + 60 * 60 * 1000),
        });
        await this.passwordResetTokenRepo.save(resetToken);
        const frontend = this.config.get('app.appOrigin', 'http://localhost:5173');
        const resetUrl = `${frontend}/reset-password?token=${encodeURIComponent(rawToken)}`;
        await this.mailService.sendPasswordReset({
            to: user.email,
            name: user.name,
            resetUrl,
        });
        return generic;
    }
    async resetPassword(token, newPassword) {
        const tokenHash = this.hashToken(token);
        const matchedToken = await this.passwordResetTokenRepo.findOne({
            where: { tokenHash },
        });
        if (!matchedToken || matchedToken.used || matchedToken.expiresAt < new Date()) {
            throw new common_1.BadRequestException('Invalid or expired reset token');
        }
        const rounds = this.config.get('app.bcryptRounds', 12);
        const passwordHash = await bcrypt.hash(newPassword, rounds);
        await this.dataSource.transaction(async (manager) => {
            await manager.update(password_reset_token_entity_1.PasswordResetToken, { id: matchedToken.id }, { used: true });
            await manager.update(user_entity_1.User, { id: matchedToken.userId }, {
                passwordHash,
                refreshTokenHash: null,
                isEmailVerified: true,
            });
        });
        return { message: 'Password reset successfully' };
    }
    async verifyEmail(token, res) {
        const tokenHash = this.hashToken(token);
        const record = await this.emailVerificationTokenRepo.findOne({
            where: { tokenHash },
            relations: { user: true },
        });
        if (!record || record.used || record.expiresAt < new Date()) {
            throw new common_1.BadRequestException('Invalid or expired verification link');
        }
        const user = await this.dataSource.transaction(async (manager) => {
            await manager.update(email_verification_token_entity_1.EmailVerificationToken, { id: record.id }, { used: true });
            await manager.update(user_entity_1.User, { id: record.userId }, { isEmailVerified: true });
            return manager.findOne(user_entity_1.User, { where: { id: record.userId } });
        });
        if (!user)
            throw new common_1.BadRequestException('Invalid or expired verification link');
        await this.groupsService.acceptPendingInvitesForEmail(user.email, user.id);
        return this.issueTokens(user.id, user.email, res, this.toProfile(user));
    }
    async resendVerification(email) {
        const normalized = email.toLowerCase();
        const user = await this.userRepo.findOne({ where: { email: normalized } });
        const generic = {
            message: 'If that account needs verification, a new email has been sent',
        };
        if (!user || user.isEmailVerified || !user.passwordHash) {
            return generic;
        }
        await this.createAndSendVerification(user.id, user.email, user.name);
        return generic;
    }
    hashToken(raw) {
        return crypto.createHash('sha256').update(raw).digest('hex');
    }
    frontendOrigin() {
        return this.config.get('app.appOrigin', 'http://localhost:5173');
    }
    async createAndSendVerification(userId, email, name) {
        await this.emailVerificationTokenRepo.update({ userId, used: false }, { used: true });
        const rawToken = crypto.randomBytes(32).toString('hex');
        const tokenHash = this.hashToken(rawToken);
        const verificationToken = this.emailVerificationTokenRepo.create({
            userId,
            tokenHash,
            expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
        });
        await this.emailVerificationTokenRepo.save(verificationToken);
        const verifyUrl = `${this.frontendOrigin()}/verify-email?token=${encodeURIComponent(rawToken)}`;
        await this.mailService.sendEmailVerification({ to: email, name, verifyUrl });
    }
    async issueTokens(userId, email, res, user) {
        const payload = { sub: userId, email };
        const accessToken = this.jwt.sign(payload, {
            secret: this.config.get('jwt.accessSecret'),
            expiresIn: this.config.get('jwt.accessExpiresIn', '15m'),
        });
        const refreshToken = this.jwt.sign(payload, {
            secret: this.config.get('jwt.refreshSecret'),
            expiresIn: this.config.get('jwt.refreshExpiresIn', '7d'),
        });
        const rounds = this.config.get('app.bcryptRounds', 12);
        const refreshTokenHash = await bcrypt.hash(refreshToken, rounds);
        await this.userRepo.update({ id: userId }, { refreshTokenHash });
        this.setRefreshCookie(res, refreshToken);
        return {
            accessToken,
            user: user ?? {},
        };
    }
    setRefreshCookie(res, token) {
        const sevenDays = 7 * 24 * 60 * 60 * 1000;
        res.cookie('refreshToken', token, {
            httpOnly: true,
            secure: this.config.get('app.nodeEnv') === 'production',
            sameSite: 'lax',
            maxAge: sevenDays,
            path: '/api/v1/auth',
        });
    }
    clearRefreshCookie(res) {
        res.clearCookie('refreshToken', { path: '/api/v1/auth' });
    }
    toProfile(user) {
        return {
            id: user.id,
            email: user.email,
            name: user.name,
            avatarUrl: user.avatarUrl,
            currency: user.currency,
            timezone: user.timezone,
            darkMode: user.darkMode,
            createdAt: user.createdAt,
        };
    }
};
exports.AuthService = AuthService;
exports.AuthService = AuthService = __decorate([
    (0, common_1.Injectable)(),
    __param(0, (0, typeorm_1.InjectRepository)(user_entity_1.User)),
    __param(1, (0, typeorm_1.InjectRepository)(notification_preferences_entity_1.NotificationPreferences)),
    __param(2, (0, typeorm_1.InjectRepository)(password_reset_token_entity_1.PasswordResetToken)),
    __param(3, (0, typeorm_1.InjectRepository)(email_verification_token_entity_1.EmailVerificationToken)),
    __metadata("design:paramtypes", [typeorm_2.Repository,
        typeorm_2.Repository,
        typeorm_2.Repository,
        typeorm_2.Repository,
        typeorm_2.DataSource,
        jwt_1.JwtService,
        config_1.ConfigService,
        groups_service_1.GroupsService,
        mail_service_1.MailService])
], AuthService);
//# sourceMappingURL=auth.service.js.map