feat: remove OTP gate from transactions, fix categories auth, add implementation plan

- Remove OtpGateGuard from transactions controller (OTP verified at login)
- Fix categories controller to use authenticated user instead of TEMP_USER_ID
- Add comprehensive implementation plan document
- Update .env.example with WEB_APP_URL
- Prepare for admin dashboard development
This commit is contained in:
dwindown
2025-10-11 14:00:11 +07:00
parent 0da6071eb3
commit 249f3a9d7d
159 changed files with 13748 additions and 3369 deletions

View File

@@ -1,16 +1,22 @@
# Database Configuration
DATABASE_URL="postgresql://username:password@localhost:5432/tabungin_dev"
SHADOW_DATABASE_URL="postgresql://username:password@localhost:5432/tabungin_shadow"
DATABASE_URL="postgresql://user:password@localhost:5432/tabungin?schema=public"
DATABASE_URL_SHADOW="postgresql://user:password@localhost:5432/tabungin_shadow?schema=public"
# Firebase Admin SDK Configuration
# Get these from Firebase Console > Project Settings > Service Accounts
FIREBASE_PROJECT_ID=your_project_id
FIREBASE_CLIENT_EMAIL=firebase-adminsdk-xxxxx@your_project_id.iam.gserviceaccount.com
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nYOUR_PRIVATE_KEY_HERE\n-----END PRIVATE KEY-----\n"
# JWT Authentication (generate a random 32+ character string for production)
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
# API Configuration
PORT=3000
WEB_APP_URL=http://localhost:5173
# Exchange Rate API
EXCHANGE_RATE_URL=https://api.exchangerate-api.com/v4/latest/IDR
# Development User ID (run seed script to create this user)
TEMP_USER_ID=16b74848-daa3-4dc9-8de2-3cf59e08f8e3
# Google OAuth (for "Continue with Google")
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
GOOGLE_CALLBACK_URL=http://localhost:3001/api/auth/google/callback
# OTP Webhook URLs (n8n)
OTP_SEND_WEBHOOK_URL=https://your-n8n-instance.com/webhook/send-otp
OTP_SEND_WEBHOOK_URL_TEST=https://your-n8n-instance.com/webhook-test/send-otp
# App Configuration
PORT=3001
WEB_APP_URL=http://localhost:5174

View File

@@ -50,6 +50,7 @@ const users_module_1 = require("./users/users.module");
const wallets_module_1 = require("./wallets/wallets.module");
const transactions_module_1 = require("./transactions/transactions.module");
const categories_module_1 = require("./categories/categories.module");
const otp_module_1 = require("./otp/otp.module");
let AppModule = class AppModule {
};
exports.AppModule = AppModule;
@@ -69,6 +70,7 @@ exports.AppModule = AppModule = __decorate([
wallets_module_1.WalletsModule,
transactions_module_1.TransactionsModule,
categories_module_1.CategoriesModule,
otp_module_1.OtpModule,
],
controllers: [health_controller_1.HealthController],
providers: [],

View File

@@ -1 +1 @@
{"version":3,"file":"app.module.js","sourceRoot":"","sources":["../src/app.module.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,2CAAwC;AACxC,2CAA8C;AAC9C,2CAA6B;AAC7B,0DAAsD;AACtD,oDAAgD;AAChD,kEAA8D;AAC9D,uDAAmD;AACnD,6DAAyD;AACzD,4EAAwE;AACxE,sEAAkE;AAqB3D,IAAM,SAAS,GAAf,MAAM,SAAS;CAAG,CAAA;AAAZ,8BAAS;oBAAT,SAAS;IAnBrB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,qBAAY,CAAC,OAAO,CAAC;gBACnB,QAAQ,EAAE,IAAI;gBACd,WAAW,EAAE;oBACX,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,MAAM,CAAC;oBACnC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,YAAY,CAAC;iBAC1C;aACF,CAAC;YACF,4BAAY;YACZ,wBAAU;YACV,0BAAW;YACX,8BAAa;YACb,wCAAkB;YAClB,oCAAgB;SACjB;QACD,WAAW,EAAE,CAAC,oCAAgB,CAAC;QAC/B,SAAS,EAAE,EAAE;KACd,CAAC;GACW,SAAS,CAAG"}
{"version":3,"file":"app.module.js","sourceRoot":"","sources":["../src/app.module.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,2CAAwC;AACxC,2CAA8C;AAC9C,2CAA6B;AAC7B,0DAAsD;AACtD,oDAAgD;AAChD,kEAA8D;AAC9D,uDAAmD;AACnD,6DAAyD;AACzD,4EAAwE;AACxE,sEAAkE;AAClE,iDAA6C;AAsBtC,IAAM,SAAS,GAAf,MAAM,SAAS;CAAG,CAAA;AAAZ,8BAAS;oBAAT,SAAS;IApBrB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,qBAAY,CAAC,OAAO,CAAC;gBACnB,QAAQ,EAAE,IAAI;gBACd,WAAW,EAAE;oBACX,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,MAAM,CAAC;oBACnC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,YAAY,CAAC;iBAC1C;aACF,CAAC;YACF,4BAAY;YACZ,wBAAU;YACV,0BAAW;YACX,8BAAa;YACb,wCAAkB;YAClB,oCAAgB;YAChB,sBAAS;SACV;QACD,WAAW,EAAE,CAAC,oCAAgB,CAAC;QAC/B,SAAS,EAAE,EAAE;KACd,CAAC;GACW,SAAS,CAAG"}

83
apps/api/dist/auth/auth.controller.d.ts vendored Normal file
View File

@@ -0,0 +1,83 @@
import { AuthService } from './auth.service';
import type { Response } from 'express';
interface RequestWithUser {
user: {
userId: string;
email: string;
};
}
export declare class AuthController {
private authService;
constructor(authService: AuthService);
register(body: {
email: string;
password: string;
name?: string;
}): Promise<{
user: {
id: string;
email: string;
name: string | null;
avatarUrl: string | null;
emailVerified: boolean;
};
token: string;
}>;
login(body: {
email: string;
password: string;
}): Promise<{
requiresOtp: boolean;
availableMethods: {
email: boolean;
whatsapp: boolean;
totp: boolean;
};
tempToken: string;
user?: undefined;
token?: undefined;
} | {
user: {
id: string;
email: string;
name: string | null;
avatarUrl: string | null;
emailVerified: boolean;
};
token: string;
requiresOtp?: undefined;
availableMethods?: undefined;
tempToken?: undefined;
}>;
verifyOtp(body: {
tempToken: string;
otpCode: string;
method: 'email' | 'totp';
}): Promise<{
user: {
id: string;
email: string;
name: string | null;
avatarUrl: string | null;
emailVerified: boolean;
};
token: string;
}>;
googleAuth(): Promise<void>;
googleAuthCallback(req: any, res: Response): Promise<void>;
getProfile(req: RequestWithUser): Promise<{
id: string;
email: string;
emailVerified: boolean;
name: string | null;
avatarUrl: string | null;
}>;
changePassword(req: RequestWithUser, body: {
currentPassword: string;
newPassword: string;
isSettingPassword?: boolean;
}): Promise<{
message: string;
}>;
}
export {};

112
apps/api/dist/auth/auth.controller.js vendored Normal file
View File

@@ -0,0 +1,112 @@
"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.AuthController = void 0;
const common_1 = require("@nestjs/common");
const auth_guard_1 = require("./auth.guard");
const passport_1 = require("@nestjs/passport");
const auth_service_1 = require("./auth.service");
let AuthController = class AuthController {
authService;
constructor(authService) {
this.authService = authService;
}
async register(body) {
return this.authService.register(body.email, body.password, body.name);
}
async login(body) {
return this.authService.login(body.email, body.password);
}
async verifyOtp(body) {
return this.authService.verifyOtpAndLogin(body.tempToken, body.otpCode, body.method);
}
async googleAuth() {
}
async googleAuthCallback(req, res) {
const result = await this.authService.googleLogin(req.user);
const frontendUrl = process.env.WEB_APP_URL || 'http://localhost:5174';
if (result.requiresOtp) {
res.redirect(`${frontendUrl}/auth/otp?token=${result.tempToken}&methods=${JSON.stringify(result.availableMethods)}`);
}
else {
res.redirect(`${frontendUrl}/auth/callback?token=${result.token}`);
}
}
async getProfile(req) {
return this.authService.getUserProfile(req.user.userId);
}
async changePassword(req, body) {
return this.authService.changePassword(req.user.userId, body.currentPassword, body.newPassword, body.isSettingPassword);
}
};
exports.AuthController = AuthController;
__decorate([
(0, common_1.Post)('register'),
__param(0, (0, common_1.Body)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], AuthController.prototype, "register", null);
__decorate([
(0, common_1.Post)('login'),
__param(0, (0, common_1.Body)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], AuthController.prototype, "login", null);
__decorate([
(0, common_1.Post)('verify-otp'),
__param(0, (0, common_1.Body)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], AuthController.prototype, "verifyOtp", null);
__decorate([
(0, common_1.Get)('google'),
(0, common_1.UseGuards)((0, passport_1.AuthGuard)('google')),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], AuthController.prototype, "googleAuth", null);
__decorate([
(0, common_1.Get)('google/callback'),
(0, common_1.UseGuards)((0, passport_1.AuthGuard)('google')),
__param(0, (0, common_1.Req)()),
__param(1, (0, common_1.Res)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object]),
__metadata("design:returntype", Promise)
], AuthController.prototype, "googleAuthCallback", null);
__decorate([
(0, common_1.Get)('me'),
(0, common_1.UseGuards)(auth_guard_1.AuthGuard),
__param(0, (0, common_1.Req)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], AuthController.prototype, "getProfile", null);
__decorate([
(0, common_1.Post)('change-password'),
(0, common_1.UseGuards)(auth_guard_1.AuthGuard),
__param(0, (0, common_1.Req)()),
__param(1, (0, common_1.Body)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object]),
__metadata("design:returntype", Promise)
], AuthController.prototype, "changePassword", null);
exports.AuthController = AuthController = __decorate([
(0, common_1.Controller)('auth'),
__metadata("design:paramtypes", [auth_service_1.AuthService])
], AuthController);
//# sourceMappingURL=auth.controller.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"auth.controller.js","sourceRoot":"","sources":["../../src/auth/auth.controller.ts"],"names":[],"mappings":"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"}

View File

@@ -1,8 +1,9 @@
import { CanActivate, ExecutionContext } from '@nestjs/common';
import { FirebaseService } from './firebase.service';
export declare class AuthGuard implements CanActivate {
private firebaseService;
constructor(firebaseService: FirebaseService);
canActivate(context: ExecutionContext): Promise<boolean>;
private extractTokenFromHeader;
import { ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
declare const AuthGuard_base: import("@nestjs/passport").Type<import("@nestjs/passport").IAuthGuard>;
export declare class AuthGuard extends AuthGuard_base {
private reflector;
constructor(reflector: Reflector);
canActivate(context: ExecutionContext): boolean | Promise<boolean> | import("rxjs").Observable<boolean>;
}
export {};

View File

@@ -11,39 +11,28 @@ var __metadata = (this && this.__metadata) || function (k, v) {
Object.defineProperty(exports, "__esModule", { value: true });
exports.AuthGuard = void 0;
const common_1 = require("@nestjs/common");
const firebase_service_1 = require("./firebase.service");
let AuthGuard = class AuthGuard {
firebaseService;
constructor(firebaseService) {
this.firebaseService = firebaseService;
const core_1 = require("@nestjs/core");
const passport_1 = require("@nestjs/passport");
let AuthGuard = class AuthGuard extends (0, passport_1.AuthGuard)('jwt') {
reflector;
constructor(reflector) {
super();
this.reflector = reflector;
}
async canActivate(context) {
const request = context.switchToHttp().getRequest();
if (!this.firebaseService.isFirebaseConfigured()) {
console.warn('⚠️ Firebase not configured - allowing request without auth');
canActivate(context) {
const isPublic = this.reflector.getAllAndOverride('isPublic', [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new common_1.UnauthorizedException('No token provided');
}
try {
const decodedToken = await this.firebaseService.verifyIdToken(token);
request.user = decodedToken;
return true;
}
catch (error) {
throw new common_1.UnauthorizedException('Invalid token');
}
}
extractTokenFromHeader(request) {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
return super.canActivate(context);
}
};
exports.AuthGuard = AuthGuard;
exports.AuthGuard = AuthGuard = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [firebase_service_1.FirebaseService])
__metadata("design:paramtypes", [core_1.Reflector])
], AuthGuard);
//# sourceMappingURL=auth.guard.js.map

View File

@@ -1 +1 @@
{"version":3,"file":"auth.guard.js","sourceRoot":"","sources":["../../src/auth/auth.guard.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAAkG;AAClG,yDAAqD;AAG9C,IAAM,SAAS,GAAf,MAAM,SAAS;IACA;IAApB,YAAoB,eAAgC;QAAhC,oBAAe,GAAf,eAAe,CAAiB;IAAG,CAAC;IAExD,KAAK,CAAC,WAAW,CAAC,OAAyB;QACzC,MAAM,OAAO,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,UAAU,EAAE,CAAC;QAGpD,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,oBAAoB,EAAE,EAAE,CAAC;YACjD,OAAO,CAAC,IAAI,CAAC,4DAA4D,CAAC,CAAC;YAC3E,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,KAAK,GAAG,IAAI,CAAC,sBAAsB,CAAC,OAAO,CAAC,CAAC;QAEnD,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,MAAM,IAAI,8BAAqB,CAAC,mBAAmB,CAAC,CAAC;QACvD,CAAC;QAED,IAAI,CAAC;YACH,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,eAAe,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;YACrE,OAAO,CAAC,IAAI,GAAG,YAAY,CAAC;YAC5B,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,8BAAqB,CAAC,eAAe,CAAC,CAAC;QACnD,CAAC;IACH,CAAC;IAEO,sBAAsB,CAAC,OAAY;QACzC,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,aAAa,EAAE,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;QACtE,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;IAC/C,CAAC;CACF,CAAA;AA/BY,8BAAS;oBAAT,SAAS;IADrB,IAAA,mBAAU,GAAE;qCAE0B,kCAAe;GADzC,SAAS,CA+BrB"}
{"version":3,"file":"auth.guard.js","sourceRoot":"","sources":["../../src/auth/auth.guard.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAA8D;AAC9D,uCAAyC;AACzC,+CAAkE;AAG3D,IAAM,SAAS,GAAf,MAAM,SAAU,SAAQ,IAAA,oBAAiB,EAAC,KAAK,CAAC;IACjC;IAApB,YAAoB,SAAoB;QACtC,KAAK,EAAE,CAAC;QADU,cAAS,GAAT,SAAS,CAAW;IAExC,CAAC;IAED,WAAW,CAAC,OAAyB;QAEnC,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,iBAAiB,CAAU,UAAU,EAAE;YACrE,OAAO,CAAC,UAAU,EAAE;YACpB,OAAO,CAAC,QAAQ,EAAE;SACnB,CAAC,CAAC;QAEH,IAAI,QAAQ,EAAE,CAAC;YACb,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO,KAAK,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IACpC,CAAC;CACF,CAAA;AAlBY,8BAAS;oBAAT,SAAS;IADrB,IAAA,mBAAU,GAAE;qCAEoB,gBAAS;GAD7B,SAAS,CAkBrB"}

View File

@@ -8,15 +8,31 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
Object.defineProperty(exports, "__esModule", { value: true });
exports.AuthModule = void 0;
const common_1 = require("@nestjs/common");
const firebase_service_1 = require("./firebase.service");
const auth_guard_1 = require("./auth.guard");
const jwt_1 = require("@nestjs/jwt");
const passport_1 = require("@nestjs/passport");
const auth_controller_1 = require("./auth.controller");
const auth_service_1 = require("./auth.service");
const jwt_strategy_1 = require("./jwt.strategy");
const google_strategy_1 = require("./google.strategy");
const prisma_module_1 = require("../prisma/prisma.module");
const otp_module_1 = require("../otp/otp.module");
let AuthModule = class AuthModule {
};
exports.AuthModule = AuthModule;
exports.AuthModule = AuthModule = __decorate([
(0, common_1.Module)({
providers: [firebase_service_1.FirebaseService, auth_guard_1.AuthGuard],
exports: [firebase_service_1.FirebaseService, auth_guard_1.AuthGuard],
imports: [
prisma_module_1.PrismaModule,
passport_1.PassportModule,
(0, common_1.forwardRef)(() => otp_module_1.OtpModule),
jwt_1.JwtModule.register({
secret: process.env.JWT_SECRET || 'your-secret-key',
signOptions: { expiresIn: '7d' },
}),
],
controllers: [auth_controller_1.AuthController],
providers: [auth_service_1.AuthService, jwt_strategy_1.JwtStrategy, google_strategy_1.GoogleStrategy],
exports: [auth_service_1.AuthService],
})
], AuthModule);
//# sourceMappingURL=auth.module.js.map

View File

@@ -1 +1 @@
{"version":3,"file":"auth.module.js","sourceRoot":"","sources":["../../src/auth/auth.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,yDAAqD;AACrD,6CAAyC;AAMlC,IAAM,UAAU,GAAhB,MAAM,UAAU;CAAG,CAAA;AAAb,gCAAU;qBAAV,UAAU;IAJtB,IAAA,eAAM,EAAC;QACN,SAAS,EAAE,CAAC,kCAAe,EAAE,sBAAS,CAAC;QACvC,OAAO,EAAE,CAAC,kCAAe,EAAE,sBAAS,CAAC;KACtC,CAAC;GACW,UAAU,CAAG"}
{"version":3,"file":"auth.module.js","sourceRoot":"","sources":["../../src/auth/auth.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAoD;AACpD,qCAAwC;AACxC,+CAAkD;AAClD,uDAAmD;AACnD,iDAA6C;AAC7C,iDAA6C;AAC7C,uDAAmD;AACnD,2DAAuD;AACvD,kDAA8C;AAgBvC,IAAM,UAAU,GAAhB,MAAM,UAAU;CAAG,CAAA;AAAb,gCAAU;qBAAV,UAAU;IAdtB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,4BAAY;YACZ,yBAAc;YACd,IAAA,mBAAU,EAAC,GAAG,EAAE,CAAC,sBAAS,CAAC;YAC3B,eAAS,CAAC,QAAQ,CAAC;gBACjB,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,iBAAiB;gBACnD,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE;aACjC,CAAC;SACH;QACD,WAAW,EAAE,CAAC,gCAAc,CAAC;QAC7B,SAAS,EAAE,CAAC,0BAAW,EAAE,0BAAW,EAAE,gCAAc,CAAC;QACrD,OAAO,EAAE,CAAC,0BAAW,CAAC;KACvB,CAAC;GACW,UAAU,CAAG"}

93
apps/api/dist/auth/auth.service.d.ts vendored Normal file
View File

@@ -0,0 +1,93 @@
import { JwtService } from '@nestjs/jwt';
import { PrismaService } from '../prisma/prisma.service';
import { OtpService } from '../otp/otp.service';
export declare class AuthService {
private readonly prisma;
private readonly jwtService;
private readonly otpService;
constructor(prisma: PrismaService, jwtService: JwtService, otpService: OtpService);
register(email: string, password: string, name?: string): Promise<{
user: {
id: string;
email: string;
name: string | null;
avatarUrl: string | null;
emailVerified: boolean;
};
token: string;
}>;
login(email: string, password: string): Promise<{
requiresOtp: boolean;
availableMethods: {
email: boolean;
whatsapp: boolean;
totp: boolean;
};
tempToken: string;
user?: undefined;
token?: undefined;
} | {
user: {
id: string;
email: string;
name: string | null;
avatarUrl: string | null;
emailVerified: boolean;
};
token: string;
requiresOtp?: undefined;
availableMethods?: undefined;
tempToken?: undefined;
}>;
googleLogin(googleProfile: {
googleId: string;
email: string;
name: string;
avatarUrl?: string;
}): Promise<{
requiresOtp: boolean;
availableMethods: {
email: boolean;
whatsapp: boolean;
totp: boolean;
};
tempToken: string;
user?: undefined;
token?: undefined;
} | {
user: {
id: string;
email: string;
name: string | null;
avatarUrl: string | null;
emailVerified: boolean;
};
token: string;
requiresOtp?: undefined;
availableMethods?: undefined;
tempToken?: undefined;
}>;
verifyOtpAndLogin(tempToken: string, otpCode: string, method: 'email' | 'whatsapp' | 'totp'): Promise<{
user: {
id: string;
email: string;
name: string | null;
avatarUrl: string | null;
emailVerified: boolean;
};
token: string;
}>;
private generateToken;
private generateTempToken;
getUserProfile(userId: string): Promise<{
id: string;
email: string;
emailVerified: boolean;
name: string | null;
avatarUrl: string | null;
}>;
changePassword(userId: string, currentPassword: string, newPassword: string, isSettingPassword?: boolean): Promise<{
message: string;
}>;
private downloadAndStoreAvatar;
}

404
apps/api/dist/auth/auth.service.js vendored Normal file
View File

@@ -0,0 +1,404 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
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 __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
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); }
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AuthService = void 0;
const common_1 = require("@nestjs/common");
const jwt_1 = require("@nestjs/jwt");
const prisma_service_1 = require("../prisma/prisma.service");
const otp_service_1 = require("../otp/otp.service");
const bcrypt = __importStar(require("bcrypt"));
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const axios_1 = __importDefault(require("axios"));
let AuthService = class AuthService {
prisma;
jwtService;
otpService;
constructor(prisma, jwtService, otpService) {
this.prisma = prisma;
this.jwtService = jwtService;
this.otpService = otpService;
}
async register(email, password, name) {
const existing = await this.prisma.user.findUnique({ where: { email } });
if (existing) {
throw new common_1.ConflictException('Email already registered');
}
const passwordHash = await bcrypt.hash(password, 10);
const user = await this.prisma.user.create({
data: {
email,
passwordHash,
name,
emailVerified: false,
},
});
const token = this.generateToken(user.id, user.email);
return {
user: {
id: user.id,
email: user.email,
name: user.name,
avatarUrl: user.avatarUrl,
emailVerified: user.emailVerified,
},
token,
};
}
async login(email, password) {
const user = await this.prisma.user.findUnique({
where: { email },
select: {
id: true,
email: true,
passwordHash: true,
name: true,
avatarUrl: true,
emailVerified: true,
otpEmailEnabled: true,
otpWhatsappEnabled: true,
otpTotpEnabled: true,
},
});
if (!user || !user.passwordHash) {
throw new common_1.UnauthorizedException('Invalid credentials');
}
const isValid = await bcrypt.compare(password, user.passwordHash);
if (!isValid) {
throw new common_1.UnauthorizedException('Invalid credentials');
}
const requiresOtp = user.otpEmailEnabled || user.otpWhatsappEnabled || user.otpTotpEnabled;
if (requiresOtp) {
if (user.otpEmailEnabled) {
try {
await this.otpService.sendEmailOtp(user.id);
}
catch (error) {
console.error('Failed to send email OTP during login:', error);
}
}
if (user.otpWhatsappEnabled) {
try {
await this.otpService.sendWhatsappOtp(user.id, 'live');
}
catch (error) {
console.error('Failed to send WhatsApp OTP during login:', error);
}
}
return {
requiresOtp: true,
availableMethods: {
email: user.otpEmailEnabled,
whatsapp: user.otpWhatsappEnabled,
totp: user.otpTotpEnabled,
},
tempToken: this.generateTempToken(user.id, user.email),
};
}
const token = this.generateToken(user.id, user.email);
return {
user: {
id: user.id,
email: user.email,
name: user.name,
avatarUrl: user.avatarUrl,
emailVerified: user.emailVerified,
},
token,
};
}
async googleLogin(googleProfile) {
let user = await this.prisma.user.findUnique({
where: { email: googleProfile.email },
});
if (!user) {
user = await this.prisma.user.create({
data: {
email: googleProfile.email,
name: googleProfile.name,
avatarUrl: googleProfile.avatarUrl,
emailVerified: true,
authAccounts: {
create: {
provider: 'google',
issuer: 'google.com',
subject: googleProfile.googleId,
},
},
},
});
}
else {
const existingAuth = await this.prisma.authAccount.findUnique({
where: {
issuer_subject: {
issuer: 'google.com',
subject: googleProfile.googleId,
},
},
});
if (!existingAuth) {
await this.prisma.authAccount.create({
data: {
userId: user.id,
provider: 'google',
issuer: 'google.com',
subject: googleProfile.googleId,
},
});
}
console.log('Updating user with Google profile:', {
name: googleProfile.name,
avatarUrl: googleProfile.avatarUrl,
});
let avatarUrl = user.avatarUrl;
if (googleProfile.avatarUrl) {
try {
avatarUrl = await this.downloadAndStoreAvatar(googleProfile.avatarUrl, user.id);
}
catch (error) {
console.error('Failed to download avatar:', error);
avatarUrl = googleProfile.avatarUrl;
}
}
user = await this.prisma.user.update({
where: { id: user.id },
data: {
name: googleProfile.name || user.name,
avatarUrl: avatarUrl || user.avatarUrl,
emailVerified: true,
},
});
console.log('User updated, avatar:', user.avatarUrl);
}
const requiresOtp = user.otpEmailEnabled || user.otpWhatsappEnabled || user.otpTotpEnabled;
if (requiresOtp) {
if (user.otpEmailEnabled) {
try {
await this.otpService.sendEmailOtp(user.id);
}
catch (error) {
console.error('Failed to send email OTP during Google login:', error);
}
}
if (user.otpWhatsappEnabled) {
try {
await this.otpService.sendWhatsappOtp(user.id, 'live');
}
catch (error) {
console.error('Failed to send WhatsApp OTP during Google login:', error);
}
}
return {
requiresOtp: true,
availableMethods: {
email: user.otpEmailEnabled,
whatsapp: user.otpWhatsappEnabled,
totp: user.otpTotpEnabled,
},
tempToken: this.generateTempToken(user.id, user.email),
};
}
const token = this.generateToken(user.id, user.email);
return {
user: {
id: user.id,
email: user.email,
name: user.name,
avatarUrl: user.avatarUrl,
emailVerified: user.emailVerified,
},
token,
};
}
async verifyOtpAndLogin(tempToken, otpCode, method) {
let payload;
try {
payload = this.jwtService.verify(tempToken);
}
catch {
throw new common_1.UnauthorizedException('Invalid or expired token');
}
if (!payload.temp) {
throw new common_1.UnauthorizedException('Invalid token type');
}
const userId = payload.userId || payload.sub;
const email = payload.email;
if (!userId || !email) {
throw new common_1.UnauthorizedException('Invalid token payload');
}
const user = await this.prisma.user.findUnique({
where: { id: userId },
});
if (!user) {
throw new common_1.UnauthorizedException('User not found');
}
if (method === 'email') {
const isValid = this.otpService.verifyEmailOtpForLogin(userId, otpCode);
if (!isValid) {
throw new common_1.UnauthorizedException('Invalid or expired email OTP code');
}
}
else if (method === 'whatsapp') {
const isValid = this.otpService.verifyWhatsappOtpForLogin(userId, otpCode);
if (!isValid) {
throw new common_1.UnauthorizedException('Invalid or expired WhatsApp OTP code');
}
}
else if (method === 'totp') {
if (!user.otpTotpSecret) {
throw new common_1.UnauthorizedException('TOTP not set up');
}
const { authenticator } = await import('otplib');
const isValid = authenticator.verify({
token: otpCode,
secret: user.otpTotpSecret,
});
if (!isValid) {
throw new common_1.UnauthorizedException('Invalid TOTP code');
}
}
const token = this.generateToken(userId, email);
return {
user: {
id: user.id,
email: user.email,
name: user.name,
avatarUrl: user.avatarUrl,
emailVerified: user.emailVerified,
},
token,
};
}
generateToken(userId, email) {
return this.jwtService.sign({
sub: userId,
email,
});
}
generateTempToken(userId, email) {
return this.jwtService.sign({ userId, email, temp: true }, { expiresIn: '5m' });
}
async getUserProfile(userId) {
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
email: true,
name: true,
avatarUrl: true,
emailVerified: true,
},
});
if (!user) {
throw new common_1.UnauthorizedException('User not found');
}
return user;
}
async changePassword(userId, currentPassword, newPassword, isSettingPassword) {
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: { passwordHash: true },
});
if (!user) {
throw new common_1.BadRequestException('User not found');
}
if (isSettingPassword && !user.passwordHash) {
const newPasswordHash = await bcrypt.hash(newPassword, 10);
await this.prisma.user.update({
where: { id: userId },
data: { passwordHash: newPasswordHash },
});
return { message: 'Password set successfully' };
}
if (!user.passwordHash) {
throw new common_1.BadRequestException('Cannot change password for this account');
}
const isValid = await bcrypt.compare(currentPassword, user.passwordHash);
if (!isValid) {
throw new common_1.UnauthorizedException('Current password is incorrect');
}
const newPasswordHash = await bcrypt.hash(newPassword, 10);
await this.prisma.user.update({
where: { id: userId },
data: { passwordHash: newPasswordHash },
});
return { message: 'Password changed successfully' };
}
async downloadAndStoreAvatar(avatarUrl, userId) {
try {
const uploadsDir = path.join(process.cwd(), 'public', 'avatars');
if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir, { recursive: true });
}
const response = await axios_1.default.get(avatarUrl, {
responseType: 'arraybuffer',
});
const ext = 'jpg';
const filename = `${userId}.${ext}`;
const filepath = path.join(uploadsDir, filename);
fs.writeFileSync(filepath, response.data);
return `/avatars/${filename}`;
}
catch (error) {
console.error('Error downloading avatar:', error);
throw error;
}
}
};
exports.AuthService = AuthService;
exports.AuthService = AuthService = __decorate([
(0, common_1.Injectable)(),
__param(2, (0, common_1.Inject)((0, common_1.forwardRef)(() => otp_service_1.OtpService))),
__metadata("design:paramtypes", [prisma_service_1.PrismaService,
jwt_1.JwtService,
otp_service_1.OtpService])
], AuthService);
//# sourceMappingURL=auth.service.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,9 +0,0 @@
import * as admin from 'firebase-admin';
export declare class FirebaseService {
private app;
private isConfigured;
constructor();
verifyIdToken(idToken: string): Promise<admin.auth.DecodedIdToken>;
getUser(uid: string): Promise<admin.auth.UserRecord>;
isFirebaseConfigured(): boolean;
}

View File

@@ -1,113 +0,0 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
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 __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FirebaseService = void 0;
const common_1 = require("@nestjs/common");
const admin = __importStar(require("firebase-admin"));
let FirebaseService = class FirebaseService {
app = null;
isConfigured = false;
constructor() {
const projectId = process.env.FIREBASE_PROJECT_ID;
const clientEmail = process.env.FIREBASE_CLIENT_EMAIL;
const privateKey = process.env.FIREBASE_PRIVATE_KEY;
if (projectId && clientEmail && privateKey) {
try {
if (!admin.apps.length) {
this.app = admin.initializeApp({
credential: admin.credential.cert({
projectId,
clientEmail,
privateKey: privateKey.replace(/\\n/g, '\n'),
}),
});
}
else {
this.app = admin.app();
}
this.isConfigured = true;
console.log('✅ Firebase Admin initialized successfully');
}
catch (error) {
console.warn('⚠️ Firebase Admin initialization failed:', error.message);
this.isConfigured = false;
}
}
else {
console.warn('⚠️ Firebase credentials not found. Auth will use fallback mode.');
this.isConfigured = false;
}
}
async verifyIdToken(idToken) {
if (!this.isConfigured || !this.app) {
throw new Error('Firebase not configured');
}
try {
return await admin.auth().verifyIdToken(idToken);
}
catch (error) {
throw new Error('Invalid token');
}
}
async getUser(uid) {
if (!this.isConfigured || !this.app) {
throw new Error('Firebase not configured');
}
try {
return await admin.auth().getUser(uid);
}
catch (error) {
throw new Error('User not found');
}
}
isFirebaseConfigured() {
return this.isConfigured;
}
};
exports.FirebaseService = FirebaseService;
exports.FirebaseService = FirebaseService = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [])
], FirebaseService);
//# sourceMappingURL=firebase.service.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"firebase.service.js","sourceRoot":"","sources":["../../src/auth/firebase.service.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,2CAA4C;AAC5C,sDAAwC;AAGjC,IAAM,eAAe,GAArB,MAAM,eAAe;IAClB,GAAG,GAAyB,IAAI,CAAC;IACjC,YAAY,GAAY,KAAK,CAAC;IAEtC;QAEE,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC;QAClD,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC;QACtD,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC;QAEpD,IAAI,SAAS,IAAI,WAAW,IAAI,UAAU,EAAE,CAAC;YAC3C,IAAI,CAAC;gBACH,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;oBACvB,IAAI,CAAC,GAAG,GAAG,KAAK,CAAC,aAAa,CAAC;wBAC7B,UAAU,EAAE,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC;4BAChC,SAAS;4BACT,WAAW;4BACX,UAAU,EAAE,UAAU,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC;yBAC7C,CAAC;qBACH,CAAC,CAAC;gBACL,CAAC;qBAAM,CAAC;oBACN,IAAI,CAAC,GAAG,GAAG,KAAK,CAAC,GAAG,EAAE,CAAC;gBACzB,CAAC;gBACD,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;gBACzB,OAAO,CAAC,GAAG,CAAC,2CAA2C,CAAC,CAAC;YAC3D,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,CAAC,IAAI,CAAC,0CAA0C,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;gBACxE,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC;YAC5B,CAAC;QACH,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,IAAI,CAAC,iEAAiE,CAAC,CAAC;YAChF,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC;QAC5B,CAAC;IACH,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,OAAe;QACjC,IAAI,CAAC,IAAI,CAAC,YAAY,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;YACpC,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;QAC7C,CAAC;QACD,IAAI,CAAC;YACH,OAAO,MAAM,KAAK,CAAC,IAAI,EAAE,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QACnD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CAAC,eAAe,CAAC,CAAC;QACnC,CAAC;IACH,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,GAAW;QACvB,IAAI,CAAC,IAAI,CAAC,YAAY,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;YACpC,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;QAC7C,CAAC;QACD,IAAI,CAAC;YACH,OAAO,MAAM,KAAK,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACzC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC;QACpC,CAAC;IACH,CAAC;IAED,oBAAoB;QAClB,OAAO,IAAI,CAAC,YAAY,CAAC;IAC3B,CAAC;CACF,CAAA;AA5DY,0CAAe;0BAAf,eAAe;IAD3B,IAAA,mBAAU,GAAE;;GACA,eAAe,CA4D3B"}

View File

@@ -0,0 +1,9 @@
import { Strategy, VerifyCallback } from 'passport-google-oauth20';
declare const GoogleStrategy_base: new (...args: [options: import("passport-google-oauth20").StrategyOptionsWithRequest] | [options: import("passport-google-oauth20").StrategyOptions] | [options: import("passport-google-oauth20").StrategyOptions] | [options: import("passport-google-oauth20").StrategyOptionsWithRequest]) => Strategy & {
validate(...args: any[]): unknown;
};
export declare class GoogleStrategy extends GoogleStrategy_base {
constructor();
validate(accessToken: string, refreshToken: string, profile: any, done: VerifyCallback): Promise<any>;
}
export {};

42
apps/api/dist/auth/google.strategy.js vendored Normal file
View File

@@ -0,0 +1,42 @@
"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);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GoogleStrategy = void 0;
const common_1 = require("@nestjs/common");
const passport_1 = require("@nestjs/passport");
const passport_google_oauth20_1 = require("passport-google-oauth20");
let GoogleStrategy = class GoogleStrategy extends (0, passport_1.PassportStrategy)(passport_google_oauth20_1.Strategy, 'google') {
constructor() {
super({
clientID: process.env.GOOGLE_CLIENT_ID || '',
clientSecret: process.env.GOOGLE_CLIENT_SECRET || '',
callbackURL: process.env.GOOGLE_CALLBACK_URL ||
'http://localhost:3001/api/auth/google/callback',
scope: ['email', 'profile'],
});
}
async validate(accessToken, refreshToken, profile, done) {
const { id, name, emails, photos } = profile;
const user = {
googleId: id,
email: emails[0].value,
name: name.givenName + ' ' + name.familyName,
avatarUrl: photos[0]?.value,
};
done(null, user);
}
};
exports.GoogleStrategy = GoogleStrategy;
exports.GoogleStrategy = GoogleStrategy = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [])
], GoogleStrategy);
//# sourceMappingURL=google.strategy.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"google.strategy.js","sourceRoot":"","sources":["../../src/auth/google.strategy.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAA4C;AAC5C,+CAAoD;AACpD,qEAAmE;AAG5D,IAAM,cAAc,GAApB,MAAM,cAAe,SAAQ,IAAA,2BAAgB,EAAC,kCAAQ,EAAE,QAAQ,CAAC;IACtE;QACE,KAAK,CAAC;YACJ,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,EAAE;YAC5C,YAAY,EAAE,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,EAAE;YACpD,WAAW,EACT,OAAO,CAAC,GAAG,CAAC,mBAAmB;gBAC/B,gDAAgD;YAClD,KAAK,EAAE,CAAC,OAAO,EAAE,SAAS,CAAC;SAC5B,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,QAAQ,CACZ,WAAmB,EACnB,YAAoB,EACpB,OAAY,EACZ,IAAoB;QAEpB,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;QAE7C,MAAM,IAAI,GAAG;YACX,QAAQ,EAAE,EAAE;YACZ,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK;YACtB,IAAI,EAAE,IAAI,CAAC,SAAS,GAAG,GAAG,GAAG,IAAI,CAAC,UAAU;YAC5C,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK;SAC5B,CAAC;QAEF,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACnB,CAAC;CACF,CAAA;AA7BY,wCAAc;yBAAd,cAAc;IAD1B,IAAA,mBAAU,GAAE;;GACA,cAAc,CA6B1B"}

18
apps/api/dist/auth/jwt.strategy.d.ts vendored Normal file
View File

@@ -0,0 +1,18 @@
import { Strategy } from 'passport-jwt';
export interface JwtPayload {
sub: string;
email: string;
iat?: number;
exp?: number;
}
declare const JwtStrategy_base: new (...args: [opt: import("passport-jwt").StrategyOptionsWithRequest] | [opt: import("passport-jwt").StrategyOptionsWithoutRequest]) => Strategy & {
validate(...args: any[]): unknown;
};
export declare class JwtStrategy extends JwtStrategy_base {
constructor();
validate(payload: JwtPayload): Promise<{
userId: string;
email: string;
}>;
}
export {};

33
apps/api/dist/auth/jwt.strategy.js vendored Normal file
View File

@@ -0,0 +1,33 @@
"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);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.JwtStrategy = void 0;
const common_1 = require("@nestjs/common");
const passport_1 = require("@nestjs/passport");
const passport_jwt_1 = require("passport-jwt");
let JwtStrategy = class JwtStrategy extends (0, passport_1.PassportStrategy)(passport_jwt_1.Strategy) {
constructor() {
super({
jwtFromRequest: passport_jwt_1.ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.JWT_SECRET || 'your-secret-key-change-this',
});
}
async validate(payload) {
return { userId: payload.sub, email: payload.email };
}
};
exports.JwtStrategy = JwtStrategy;
exports.JwtStrategy = JwtStrategy = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [])
], JwtStrategy);
//# sourceMappingURL=jwt.strategy.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"jwt.strategy.js","sourceRoot":"","sources":["../../src/auth/jwt.strategy.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAA4C;AAC5C,+CAAoD;AACpD,+CAAoD;AAU7C,IAAM,WAAW,GAAjB,MAAM,WAAY,SAAQ,IAAA,2BAAgB,EAAC,uBAAQ,CAAC;IACzD;QACE,KAAK,CAAC;YACJ,cAAc,EAAE,yBAAU,CAAC,2BAA2B,EAAE;YACxD,gBAAgB,EAAE,KAAK;YACvB,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,6BAA6B;SACrE,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,OAAmB;QAChC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,GAAG,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC;IACvD,CAAC;CACF,CAAA;AAZY,kCAAW;sBAAX,WAAW;IADvB,IAAA,mBAAU,GAAE;;GACA,WAAW,CAYvB"}

View File

@@ -1,24 +1,28 @@
import { CategoriesService } from '../categories/categories.service';
import { CreateCategoryDto } from '../categories/dto/create-category.dto';
interface RequestWithUser {
user: {
userId: string;
};
}
export declare class CategoriesController {
private readonly categoriesService;
constructor(categoriesService: CategoriesService);
private userId;
create(createCategoryDto: CreateCategoryDto): Promise<{
create(req: RequestWithUser, createCategoryDto: CreateCategoryDto): Promise<{
id: string;
createdAt: Date;
updatedAt: Date;
name: string;
userId: string;
}>;
findAll(): Promise<{
findAll(req: RequestWithUser): Promise<{
id: string;
createdAt: Date;
updatedAt: Date;
name: string;
userId: string;
}[]>;
remove(id: string): Promise<{
remove(req: RequestWithUser, id: string): Promise<{
id: string;
createdAt: Date;
updatedAt: Date;
@@ -26,3 +30,4 @@ export declare class CategoriesController {
userId: string;
}>;
}
export {};

View File

@@ -16,51 +16,52 @@ exports.CategoriesController = void 0;
const common_1 = require("@nestjs/common");
const categories_service_1 = require("../categories/categories.service");
const create_category_dto_1 = require("../categories/dto/create-category.dto");
const user_util_1 = require("../common/user.util");
const auth_guard_1 = require("../auth/auth.guard");
let CategoriesController = class CategoriesController {
categoriesService;
constructor(categoriesService) {
this.categoriesService = categoriesService;
}
userId() {
return (0, user_util_1.getTempUserId)();
}
create(createCategoryDto) {
create(req, createCategoryDto) {
return this.categoriesService.create({
...createCategoryDto,
userId: this.userId(),
userId: req.user.userId,
});
}
findAll() {
return this.categoriesService.findAll(this.userId());
findAll(req) {
return this.categoriesService.findAll(req.user.userId);
}
remove(id) {
return this.categoriesService.remove(id, this.userId());
remove(req, id) {
return this.categoriesService.remove(id, req.user.userId);
}
};
exports.CategoriesController = CategoriesController;
__decorate([
(0, common_1.Post)(),
__param(0, (0, common_1.Body)()),
__param(0, (0, common_1.Req)()),
__param(1, (0, common_1.Body)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [create_category_dto_1.CreateCategoryDto]),
__metadata("design:paramtypes", [Object, create_category_dto_1.CreateCategoryDto]),
__metadata("design:returntype", void 0)
], CategoriesController.prototype, "create", null);
__decorate([
(0, common_1.Get)(),
__param(0, (0, common_1.Req)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", void 0)
], CategoriesController.prototype, "findAll", null);
__decorate([
(0, common_1.Delete)(':id'),
__param(0, (0, common_1.Param)('id')),
__param(0, (0, common_1.Req)()),
__param(1, (0, common_1.Param)('id')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:paramtypes", [Object, String]),
__metadata("design:returntype", void 0)
], CategoriesController.prototype, "remove", null);
exports.CategoriesController = CategoriesController = __decorate([
(0, common_1.Controller)('categories'),
(0, common_1.UseGuards)(auth_guard_1.AuthGuard),
__metadata("design:paramtypes", [categories_service_1.CategoriesService])
], CategoriesController);
//# sourceMappingURL=categories.controller.js.map

View File

@@ -1 +1 @@
{"version":3,"file":"categories.controller.js","sourceRoot":"","sources":["../../src/categories/categories.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAOwB;AACxB,yEAAqE;AACrE,+EAA0E;AAC1E,mDAAoD;AAG7C,IAAM,oBAAoB,GAA1B,MAAM,oBAAoB;IACF;IAA7B,YAA6B,iBAAoC;QAApC,sBAAiB,GAAjB,iBAAiB,CAAmB;IAAG,CAAC;IAE7D,MAAM;QACZ,OAAO,IAAA,yBAAa,GAAE,CAAC;IACzB,CAAC;IAGD,MAAM,CAAS,iBAAoC;QACjD,OAAO,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC;YACnC,GAAG,iBAAiB;YACpB,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE;SACtB,CAAC,CAAC;IACL,CAAC;IAGD,OAAO;QACL,OAAO,IAAI,CAAC,iBAAiB,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IACvD,CAAC;IAGD,MAAM,CAAc,EAAU;QAC5B,OAAO,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IAC1D,CAAC;CACF,CAAA;AAxBY,oDAAoB;AAQ/B;IADC,IAAA,aAAI,GAAE;IACC,WAAA,IAAA,aAAI,GAAE,CAAA;;qCAAoB,uCAAiB;;kDAKlD;AAGD;IADC,IAAA,YAAG,GAAE;;;;mDAGL;AAGD;IADC,IAAA,eAAM,EAAC,KAAK,CAAC;IACN,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;;;;kDAElB;+BAvBU,oBAAoB;IADhC,IAAA,mBAAU,EAAC,YAAY,CAAC;qCAEyB,sCAAiB;GADtD,oBAAoB,CAwBhC"}
{"version":3,"file":"categories.controller.js","sourceRoot":"","sources":["../../src/categories/categories.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAA4F;AAC5F,yEAAqE;AACrE,+EAA0E;AAC1E,mDAA+C;AAUxC,IAAM,oBAAoB,GAA1B,MAAM,oBAAoB;IACF;IAA7B,YAA6B,iBAAoC;QAApC,sBAAiB,GAAjB,iBAAiB,CAAmB;IAAG,CAAC;IAGrE,MAAM,CAAQ,GAAoB,EAAU,iBAAoC;QAC9E,OAAO,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC;YACnC,GAAG,iBAAiB;YACpB,MAAM,EAAE,GAAG,CAAC,IAAI,CAAC,MAAM;SACxB,CAAC,CAAC;IACL,CAAC;IAGD,OAAO,CAAQ,GAAoB;QACjC,OAAO,IAAI,CAAC,iBAAiB,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACzD,CAAC;IAGD,MAAM,CAAQ,GAAoB,EAAe,EAAU;QACzD,OAAO,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,EAAE,EAAE,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC5D,CAAC;CACF,CAAA;AApBY,oDAAoB;AAI/B;IADC,IAAA,aAAI,GAAE;IACC,WAAA,IAAA,YAAG,GAAE,CAAA;IAAwB,WAAA,IAAA,aAAI,GAAE,CAAA;;6CAAoB,uCAAiB;;kDAK/E;AAGD;IADC,IAAA,YAAG,GAAE;IACG,WAAA,IAAA,YAAG,GAAE,CAAA;;;;mDAEb;AAGD;IADC,IAAA,eAAM,EAAC,KAAK,CAAC;IACN,WAAA,IAAA,YAAG,GAAE,CAAA;IAAwB,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;;;;kDAE/C;+BAnBU,oBAAoB;IAFhC,IAAA,mBAAU,EAAC,YAAY,CAAC;IACxB,IAAA,kBAAS,EAAC,sBAAS,CAAC;qCAE6B,sCAAiB;GADtD,oBAAoB,CAoBhC"}

View File

@@ -1 +1 @@
{"version":3,"file":"categories.service.js","sourceRoot":"","sources":["../../src/categories/categories.service.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAAkF;AAClF,6DAAyD;AAIlD,IAAM,iBAAiB,GAAvB,MAAM,iBAAiB;IACR;IAApB,YAAoB,MAAqB;QAArB,WAAM,GAAN,MAAM,CAAe;IAAG,CAAC;IAE7C,KAAK,CAAC,MAAM,CAAC,IAA4C;QACvD,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;gBACvC,IAAI,EAAE;oBACJ,IAAI,EAAE,IAAI,CAAC,IAAI;oBACf,MAAM,EAAE,IAAI,CAAC,MAAM;iBACpB;aACF,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;gBAC3B,MAAM,IAAI,0BAAiB,CAAC,yBAAyB,CAAC,CAAC;YACzD,CAAC;YACD,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,MAAc;QAC1B,OAAO,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;YACnC,KAAK,EAAE,EAAE,MAAM,EAAE;YACjB,OAAO,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE;SACzB,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,EAAU,EAAE,MAAc;QACrC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC;YACpD,KAAK,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE;SACtB,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,IAAI,0BAAiB,CAAC,oBAAoB,CAAC,CAAC;QACpD,CAAC;QAED,OAAO,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;YACjC,KAAK,EAAE,EAAE,EAAE,EAAE;SACd,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,KAAe,EAAE,MAAc;QAChD,MAAM,UAAU,GAAU,EAAE,CAAC;QAE7B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC;gBAClD,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE;aACxB,CAAC,CAAC;YAEH,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;oBAC3C,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE;iBACvB,CAAC,CAAC;YACL,CAAC;YAED,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC5B,CAAC;QAED,OAAO,UAAU,CAAC;IACpB,CAAC;CACF,CAAA;AA3DY,8CAAiB;4BAAjB,iBAAiB;IAD7B,IAAA,mBAAU,GAAE;qCAEiB,8BAAa;GAD9B,iBAAiB,CA2D7B"}
{"version":3,"file":"categories.service.js","sourceRoot":"","sources":["../../src/categories/categories.service.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAIwB;AACxB,6DAAyD;AAIlD,IAAM,iBAAiB,GAAvB,MAAM,iBAAiB;IACR;IAApB,YAAoB,MAAqB;QAArB,WAAM,GAAN,MAAM,CAAe;IAAG,CAAC;IAE7C,KAAK,CAAC,MAAM,CAAC,IAA4C;QACvD,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;gBACvC,IAAI,EAAE;oBACJ,IAAI,EAAE,IAAI,CAAC,IAAI;oBACf,MAAM,EAAE,IAAI,CAAC,MAAM;iBACpB;aACF,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;gBAC3B,MAAM,IAAI,0BAAiB,CAAC,yBAAyB,CAAC,CAAC;YACzD,CAAC;YACD,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,MAAc;QAC1B,OAAO,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;YACnC,KAAK,EAAE,EAAE,MAAM,EAAE;YACjB,OAAO,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE;SACzB,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,EAAU,EAAE,MAAc;QACrC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC;YACpD,KAAK,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE;SACtB,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,IAAI,0BAAiB,CAAC,oBAAoB,CAAC,CAAC;QACpD,CAAC;QAED,OAAO,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;YACjC,KAAK,EAAE,EAAE,EAAE,EAAE;SACd,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,KAAe,EAAE,MAAc;QAChD,MAAM,UAAU,GAAU,EAAE,CAAC;QAE7B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC;gBAClD,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE;aACxB,CAAC,CAAC;YAEH,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;oBAC3C,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE;iBACvB,CAAC,CAAC;YACL,CAAC;YAED,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC5B,CAAC;QAED,OAAO,UAAU,CAAC;IACpB,CAAC;CACF,CAAA;AA3DY,8CAAiB;4BAAjB,iBAAiB;IAD7B,IAAA,mBAAU,GAAE;qCAEiB,8BAAa;GAD9B,iBAAiB,CA2D7B"}

View File

@@ -1 +1 @@
{"version":3,"file":"user.util.js","sourceRoot":"","sources":["../../src/common/user.util.ts"],"names":[],"mappings":";;AAAA,sCAMG;AAEH,oDAQC;AAED,kDAKC;AAvBD,SAAgB,aAAa;IACzB,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE,IAAI,EAAE,CAAC;IAC5C,IAAI,CAAC,EAAE,EAAE,CAAC;QACR,MAAM,IAAI,KAAK,CAAC,mEAAmE,CAAC,CAAC;IACvF,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAEH,SAAgB,oBAAoB,CAAC,OAAY;IAE/C,IAAI,OAAO,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC;QACtB,OAAO,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC;IAC1B,CAAC;IAGD,OAAO,aAAa,EAAE,CAAC;AACzB,CAAC;AAED,SAAgB,mBAAmB;IACjC,OAAO,CAAC,MAAW,EAAE,WAAmB,EAAE,UAA8B,EAAE,EAAE;IAG5E,CAAC,CAAC;AACJ,CAAC"}
{"version":3,"file":"user.util.js","sourceRoot":"","sources":["../../src/common/user.util.ts"],"names":[],"mappings":";;AAAA,sCAQC;AAED,oDAQC;AAED,kDAKC;AAzBD,SAAgB,aAAa;IAC3B,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE,IAAI,EAAE,CAAC;IAC5C,IAAI,CAAC,EAAE,EAAE,CAAC;QACR,MAAM,IAAI,KAAK,CACb,mEAAmE,CACpE,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,SAAgB,oBAAoB,CAAC,OAAY;IAE/C,IAAI,OAAO,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC;QACtB,OAAO,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC;IAC1B,CAAC;IAGD,OAAO,aAAa,EAAE,CAAC;AACzB,CAAC;AAED,SAAgB,mBAAmB;IACjC,OAAO,CAAC,MAAW,EAAE,WAAmB,EAAE,UAA8B,EAAE,EAAE;IAG5E,CAAC,CAAC;AACJ,CAAC"}

View File

@@ -2,8 +2,10 @@
Object.defineProperty(exports, "__esModule", { value: true });
const core_1 = require("@nestjs/core");
const app_module_1 = require("./app.module");
const path_1 = require("path");
async function bootstrap() {
const app = await core_1.NestFactory.create(app_module_1.AppModule);
app.useStaticAssets((0, path_1.join)(__dirname, '..', 'public'));
const webOrigin = process.env.WEB_APP_URL ?? 'http://localhost:5173';
app.enableCors({
origin: webOrigin,
@@ -12,7 +14,7 @@ async function bootstrap() {
app.setGlobalPrefix('api');
const port = process.env.PORT ? Number(process.env.PORT) : 3000;
await app.listen(port);
console.log(`API listening on http://localhost:${port}`);
console.log(`API listening on ${await app.getUrl()}`);
}
bootstrap();
void bootstrap();
//# sourceMappingURL=main.js.map

View File

@@ -1 +1 @@
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";;AAAA,uCAA2C;AAC3C,6CAAyC;AAEzC,KAAK,UAAU,SAAS;IACtB,MAAM,GAAG,GAAG,MAAM,kBAAW,CAAC,MAAM,CAAC,sBAAS,CAAC,CAAC;IAGhD,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,uBAAuB,CAAC;IACrE,GAAG,CAAC,UAAU,CAAC;QACb,MAAM,EAAE,SAAS;QACjB,WAAW,EAAE,IAAI;KAClB,CAAC,CAAC;IAGH,GAAG,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;IAE3B,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAChE,MAAM,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAEvB,OAAO,CAAC,GAAG,CAAC,qCAAqC,IAAI,EAAE,CAAC,CAAC;AAC3D,CAAC;AACD,SAAS,EAAE,CAAC"}
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";;AAAA,uCAA2C;AAC3C,6CAAyC;AAEzC,+BAA4B;AAE5B,KAAK,UAAU,SAAS;IACtB,MAAM,GAAG,GAAG,MAAM,kBAAW,CAAC,MAAM,CAAyB,sBAAS,CAAC,CAAC;IAGxE,GAAG,CAAC,eAAe,CAAC,IAAA,WAAI,EAAC,SAAS,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC;IAGrD,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,uBAAuB,CAAC;IACrE,GAAG,CAAC,UAAU,CAAC;QACb,MAAM,EAAE,SAAS;QACjB,WAAW,EAAE,IAAI;KAClB,CAAC,CAAC;IAGH,GAAG,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;IAE3B,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAChE,MAAM,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAEvB,OAAO,CAAC,GAAG,CAAC,oBAAoB,MAAM,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AACxD,CAAC;AAED,KAAK,SAAS,EAAE,CAAC"}

7
apps/api/dist/otp/otp-gate.guard.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
import { CanActivate, ExecutionContext } from '@nestjs/common';
import { OtpService } from './otp.service';
export declare class OtpGateGuard implements CanActivate {
private otpService;
constructor(otpService: OtpService);
canActivate(context: ExecutionContext): Promise<boolean>;
}

56
apps/api/dist/otp/otp-gate.guard.js vendored Normal file
View File

@@ -0,0 +1,56 @@
"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);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.OtpGateGuard = void 0;
const common_1 = require("@nestjs/common");
const otp_service_1 = require("./otp.service");
let OtpGateGuard = class OtpGateGuard {
otpService;
constructor(otpService) {
this.otpService = otpService;
}
async canActivate(context) {
const request = context.switchToHttp().getRequest();
const userId = request.user?.userId;
if (!userId) {
return true;
}
const status = await this.otpService.getStatus(userId);
if (!status.emailEnabled && !status.totpEnabled) {
return true;
}
const otpCode = request.headers['x-otp-code'] || request.body?.otpCode;
const otpMethod = (request.headers['x-otp-method'] ||
request.body?.otpMethod ||
'totp');
if (!otpCode) {
throw new common_1.UnauthorizedException({
message: 'OTP verification required',
requiresOtp: true,
availableMethods: {
email: status.emailEnabled,
totp: status.totpEnabled,
},
});
}
const isValid = await this.otpService.verifyOtpGate(userId, otpCode, otpMethod);
if (!isValid) {
throw new common_1.UnauthorizedException('Invalid OTP code');
}
return true;
}
};
exports.OtpGateGuard = OtpGateGuard;
exports.OtpGateGuard = OtpGateGuard = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [otp_service_1.OtpService])
], OtpGateGuard);
//# sourceMappingURL=otp-gate.guard.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"otp-gate.guard.js","sourceRoot":"","sources":["../../src/otp/otp-gate.guard.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAKwB;AACxB,+CAA2C;AAcpC,IAAM,YAAY,GAAlB,MAAM,YAAY;IACH;IAApB,YAAoB,UAAsB;QAAtB,eAAU,GAAV,UAAU,CAAY;IAAG,CAAC;IAE9C,KAAK,CAAC,WAAW,CAAC,OAAyB;QACzC,MAAM,OAAO,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,UAAU,EAAmB,CAAC;QAGrE,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;QACpC,IAAI,CAAC,MAAM,EAAE,CAAC;YAEZ,OAAO,IAAI,CAAC;QACd,CAAC;QAGD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QAGvD,IAAI,CAAC,MAAM,CAAC,YAAY,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;YAChD,OAAO,IAAI,CAAC;QACd,CAAC;QAGD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;QACvE,MAAM,SAAS,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,cAAc,CAAC;YAChD,OAAO,CAAC,IAAI,EAAE,SAAS;YACvB,MAAM,CAAqB,CAAC;QAE9B,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,IAAI,8BAAqB,CAAC;gBAC9B,OAAO,EAAE,2BAA2B;gBACpC,WAAW,EAAE,IAAI;gBACjB,gBAAgB,EAAE;oBAChB,KAAK,EAAE,MAAM,CAAC,YAAY;oBAC1B,IAAI,EAAE,MAAM,CAAC,WAAW;iBACzB;aACF,CAAC,CAAC;QACL,CAAC;QAGD,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,aAAa,CACjD,MAAM,EACN,OAAO,EACP,SAAS,CACV,CAAC;QAEF,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,IAAI,8BAAqB,CAAC,kBAAkB,CAAC,CAAC;QACtD,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;CACF,CAAA;AAnDY,oCAAY;uBAAZ,YAAY;IADxB,IAAA,mBAAU,GAAE;qCAEqB,wBAAU;GAD/B,YAAY,CAmDxB"}

92
apps/api/dist/otp/otp.controller.d.ts vendored Normal file
View File

@@ -0,0 +1,92 @@
import { JwtService } from '@nestjs/jwt';
import { OtpService } from './otp.service';
export declare const IS_PUBLIC_KEY = "isPublic";
export declare const Public: () => import("@nestjs/common").CustomDecorator<string>;
interface RequestWithUser extends Request {
user: {
userId: string;
email: string;
};
}
export declare class OtpController {
private readonly otpService;
private readonly jwtService;
constructor(otpService: OtpService, jwtService: JwtService);
getStatus(req: RequestWithUser): Promise<{
emailEnabled: boolean;
whatsappEnabled: boolean;
totpEnabled: boolean;
phone?: undefined;
totpSecret?: undefined;
} | {
phone: string | null;
emailEnabled: boolean;
whatsappEnabled: boolean;
totpEnabled: boolean;
totpSecret: string | null;
}>;
sendEmailOtp(req: RequestWithUser): Promise<{
success: boolean;
message: string;
}>;
verifyEmailOtp(req: RequestWithUser, body: {
code: string;
}): Promise<{
success: boolean;
message: string;
}>;
disableEmailOtp(req: RequestWithUser): Promise<{
success: boolean;
message: string;
}>;
setupTotp(req: RequestWithUser): Promise<{
secret: string;
qrCode: string;
}>;
verifyTotp(req: RequestWithUser, body: {
code: string;
}): Promise<{
success: boolean;
message: string;
}>;
disableTotp(req: RequestWithUser): Promise<{
success: boolean;
message: string;
}>;
sendWhatsappOtp(req: RequestWithUser, body: {
mode?: 'test' | 'live';
}): Promise<{
success: boolean;
message: string;
}>;
verifyWhatsappOtp(req: RequestWithUser, body: {
code: string;
}): Promise<{
success: boolean;
message: string;
}>;
disableWhatsappOtp(req: RequestWithUser): Promise<{
success: boolean;
message: string;
}>;
checkWhatsappNumber(body: {
phone: string;
}): Promise<{
success: boolean;
isRegistered: boolean;
message: string;
}>;
resendEmailOtp(body: {
tempToken: string;
}): Promise<{
success: boolean;
message: string;
}>;
resendWhatsappOtp(body: {
tempToken: string;
}): Promise<{
success: boolean;
message: string;
}>;
}
export {};

200
apps/api/dist/otp/otp.controller.js vendored Normal file
View File

@@ -0,0 +1,200 @@
"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.OtpController = exports.Public = exports.IS_PUBLIC_KEY = void 0;
const common_1 = require("@nestjs/common");
const jwt_1 = require("@nestjs/jwt");
const auth_guard_1 = require("../auth/auth.guard");
const otp_service_1 = require("./otp.service");
exports.IS_PUBLIC_KEY = 'isPublic';
const Public = () => (0, common_1.SetMetadata)(exports.IS_PUBLIC_KEY, true);
exports.Public = Public;
let OtpController = class OtpController {
otpService;
jwtService;
constructor(otpService, jwtService) {
this.otpService = otpService;
this.jwtService = jwtService;
}
async getStatus(req) {
return this.otpService.getStatus(req.user.userId);
}
async sendEmailOtp(req) {
return this.otpService.sendEmailOtp(req.user.userId);
}
async verifyEmailOtp(req, body) {
return this.otpService.verifyEmailOtp(req.user.userId, body.code);
}
async disableEmailOtp(req) {
return this.otpService.disableEmailOtp(req.user.userId);
}
async setupTotp(req) {
return this.otpService.setupTotp(req.user.userId);
}
async verifyTotp(req, body) {
return this.otpService.verifyTotp(req.user.userId, body.code);
}
async disableTotp(req) {
return this.otpService.disableTotp(req.user.userId);
}
async sendWhatsappOtp(req, body) {
return this.otpService.sendWhatsappOtp(req.user.userId, body.mode || 'test');
}
async verifyWhatsappOtp(req, body) {
return this.otpService.verifyWhatsappOtp(req.user.userId, body.code);
}
async disableWhatsappOtp(req) {
return this.otpService.disableWhatsappOtp(req.user.userId);
}
async checkWhatsappNumber(body) {
return this.otpService.checkWhatsappNumber(body.phone);
}
async resendEmailOtp(body) {
try {
const payload = this.jwtService.verify(body.tempToken);
if (!payload.temp) {
throw new common_1.UnauthorizedException('Invalid token type');
}
const userId = payload.userId || payload.sub;
if (!userId) {
throw new common_1.UnauthorizedException('Invalid token payload');
}
return this.otpService.sendEmailOtp(userId);
}
catch {
throw new common_1.UnauthorizedException('Invalid or expired token');
}
}
async resendWhatsappOtp(body) {
try {
const payload = this.jwtService.verify(body.tempToken);
if (!payload.temp) {
throw new common_1.UnauthorizedException('Invalid token type');
}
const userId = payload.userId || payload.sub;
if (!userId) {
throw new common_1.UnauthorizedException('Invalid token payload');
}
return this.otpService.sendWhatsappOtp(userId, 'live');
}
catch {
throw new common_1.UnauthorizedException('Invalid or expired token');
}
}
};
exports.OtpController = OtpController;
__decorate([
(0, common_1.Get)('status'),
__param(0, (0, common_1.Req)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], OtpController.prototype, "getStatus", null);
__decorate([
(0, common_1.Post)('email/send'),
__param(0, (0, common_1.Req)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], OtpController.prototype, "sendEmailOtp", null);
__decorate([
(0, common_1.Post)('email/verify'),
__param(0, (0, common_1.Req)()),
__param(1, (0, common_1.Body)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object]),
__metadata("design:returntype", Promise)
], OtpController.prototype, "verifyEmailOtp", null);
__decorate([
(0, common_1.Post)('email/disable'),
__param(0, (0, common_1.Req)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], OtpController.prototype, "disableEmailOtp", null);
__decorate([
(0, common_1.Post)('totp/setup'),
__param(0, (0, common_1.Req)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], OtpController.prototype, "setupTotp", null);
__decorate([
(0, common_1.Post)('totp/verify'),
__param(0, (0, common_1.Req)()),
__param(1, (0, common_1.Body)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object]),
__metadata("design:returntype", Promise)
], OtpController.prototype, "verifyTotp", null);
__decorate([
(0, common_1.Post)('totp/disable'),
__param(0, (0, common_1.Req)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], OtpController.prototype, "disableTotp", null);
__decorate([
(0, common_1.Post)('whatsapp/send'),
__param(0, (0, common_1.Req)()),
__param(1, (0, common_1.Body)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object]),
__metadata("design:returntype", Promise)
], OtpController.prototype, "sendWhatsappOtp", null);
__decorate([
(0, common_1.Post)('whatsapp/verify'),
__param(0, (0, common_1.Req)()),
__param(1, (0, common_1.Body)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object]),
__metadata("design:returntype", Promise)
], OtpController.prototype, "verifyWhatsappOtp", null);
__decorate([
(0, common_1.Post)('whatsapp/disable'),
__param(0, (0, common_1.Req)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], OtpController.prototype, "disableWhatsappOtp", null);
__decorate([
(0, common_1.Post)('whatsapp/check'),
__param(0, (0, common_1.Body)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], OtpController.prototype, "checkWhatsappNumber", null);
__decorate([
(0, exports.Public)(),
(0, common_1.Post)('email/resend'),
__param(0, (0, common_1.Body)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], OtpController.prototype, "resendEmailOtp", null);
__decorate([
(0, exports.Public)(),
(0, common_1.Post)('whatsapp/resend'),
__param(0, (0, common_1.Body)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], OtpController.prototype, "resendWhatsappOtp", null);
exports.OtpController = OtpController = __decorate([
(0, common_1.Controller)('otp'),
(0, common_1.UseGuards)(auth_guard_1.AuthGuard),
__metadata("design:paramtypes", [otp_service_1.OtpService,
jwt_1.JwtService])
], OtpController);
//# sourceMappingURL=otp.controller.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"otp.controller.js","sourceRoot":"","sources":["../../src/otp/otp.controller.ts"],"names":[],"mappings":"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"}

2
apps/api/dist/otp/otp.module.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
export declare class OtpModule {
}

35
apps/api/dist/otp/otp.module.js vendored Normal file
View File

@@ -0,0 +1,35 @@
"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;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.OtpModule = void 0;
const common_1 = require("@nestjs/common");
const jwt_1 = require("@nestjs/jwt");
const otp_controller_1 = require("./otp.controller");
const otp_service_1 = require("./otp.service");
const otp_gate_guard_1 = require("./otp-gate.guard");
const auth_module_1 = require("../auth/auth.module");
const prisma_module_1 = require("../prisma/prisma.module");
let OtpModule = class OtpModule {
};
exports.OtpModule = OtpModule;
exports.OtpModule = OtpModule = __decorate([
(0, common_1.Module)({
imports: [
(0, common_1.forwardRef)(() => auth_module_1.AuthModule),
prisma_module_1.PrismaModule,
jwt_1.JwtModule.register({
secret: process.env.JWT_SECRET || 'your-secret-key',
signOptions: { expiresIn: '7d' },
}),
],
controllers: [otp_controller_1.OtpController],
providers: [otp_service_1.OtpService, otp_gate_guard_1.OtpGateGuard],
exports: [otp_service_1.OtpService, otp_gate_guard_1.OtpGateGuard],
})
], OtpModule);
//# sourceMappingURL=otp.module.js.map

1
apps/api/dist/otp/otp.module.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"otp.module.js","sourceRoot":"","sources":["../../src/otp/otp.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAoD;AACpD,qCAAwC;AACxC,qDAAiD;AACjD,+CAA2C;AAC3C,qDAAgD;AAChD,qDAAiD;AACjD,2DAAuD;AAehD,IAAM,SAAS,GAAf,MAAM,SAAS;CAAG,CAAA;AAAZ,8BAAS;oBAAT,SAAS;IAbrB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,IAAA,mBAAU,EAAC,GAAG,EAAE,CAAC,wBAAU,CAAC;YAC5B,4BAAY;YACZ,eAAS,CAAC,QAAQ,CAAC;gBACjB,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,iBAAiB;gBACnD,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE;aACjC,CAAC;SACH;QACD,WAAW,EAAE,CAAC,8BAAa,CAAC;QAC5B,SAAS,EAAE,CAAC,wBAAU,EAAE,6BAAY,CAAC;QACrC,OAAO,EAAE,CAAC,wBAAU,EAAE,6BAAY,CAAC;KACpC,CAAC;GACW,SAAS,CAAG"}

67
apps/api/dist/otp/otp.service.d.ts vendored Normal file
View File

@@ -0,0 +1,67 @@
import { PrismaService } from '../prisma/prisma.service';
export declare class OtpService {
private prisma;
private emailOtpStore;
private whatsappOtpStore;
constructor(prisma: PrismaService);
sendEmailOtp(userId: string): Promise<{
success: boolean;
message: string;
}>;
verifyEmailOtpForLogin(userId: string, code: string): boolean;
verifyEmailOtp(userId: string, code: string): Promise<{
success: boolean;
message: string;
}>;
disableEmailOtp(userId: string): Promise<{
success: boolean;
message: string;
}>;
setupTotp(userId: string): Promise<{
secret: string;
qrCode: string;
}>;
verifyTotp(userId: string, code: string): Promise<{
success: boolean;
message: string;
}>;
disableTotp(userId: string): Promise<{
success: boolean;
message: string;
}>;
getStatus(userId: string): Promise<{
emailEnabled: boolean;
whatsappEnabled: boolean;
totpEnabled: boolean;
phone?: undefined;
totpSecret?: undefined;
} | {
phone: string | null;
emailEnabled: boolean;
whatsappEnabled: boolean;
totpEnabled: boolean;
totpSecret: string | null;
}>;
verifyOtpGate(userId: string, code: string, method: 'email' | 'totp'): Promise<boolean>;
private generateOtpCode;
private sendOtpViaWebhook;
sendWhatsappOtp(userId: string, mode?: 'test' | 'live'): Promise<{
success: boolean;
message: string;
}>;
verifyWhatsappOtp(userId: string, code: string): Promise<{
success: boolean;
message: string;
}>;
verifyWhatsappOtpForLogin(userId: string, code: string): boolean;
disableWhatsappOtp(userId: string): Promise<{
success: boolean;
message: string;
}>;
checkWhatsappNumber(phone: string): Promise<{
success: boolean;
isRegistered: boolean;
message: string;
}>;
private sendWhatsappOtpViaWebhook;
}

351
apps/api/dist/otp/otp.service.js vendored Normal file
View File

@@ -0,0 +1,351 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
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 __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.OtpService = void 0;
const common_1 = require("@nestjs/common");
const otplib_1 = require("otplib");
const prisma_service_1 = require("../prisma/prisma.service");
const axios_1 = __importDefault(require("axios"));
const QRCode = __importStar(require("qrcode"));
let OtpService = class OtpService {
prisma;
emailOtpStore = new Map();
whatsappOtpStore = new Map();
constructor(prisma) {
this.prisma = prisma;
}
async sendEmailOtp(userId) {
const user = await this.prisma.user.findUnique({ where: { id: userId } });
if (!user) {
throw new common_1.BadRequestException('User not found');
}
const code = this.generateOtpCode();
const expiresAt = new Date(Date.now() + 10 * 60 * 1000);
this.emailOtpStore.set(userId, { code, expiresAt });
try {
await this.sendOtpViaWebhook(user.email, code);
return { success: true, message: 'OTP sent to your email' };
}
catch (error) {
console.error('Failed to send OTP via webhook:', error);
console.log(`📧 OTP Code for ${user.email}: ${code}`);
return {
success: true,
message: 'OTP sent (check console for dev code)',
};
}
}
verifyEmailOtpForLogin(userId, code) {
const stored = this.emailOtpStore.get(userId);
if (!stored) {
return false;
}
if (new Date() > stored.expiresAt) {
this.emailOtpStore.delete(userId);
return false;
}
if (stored.code !== code) {
return false;
}
this.emailOtpStore.delete(userId);
return true;
}
async verifyEmailOtp(userId, code) {
const stored = this.emailOtpStore.get(userId);
if (!stored) {
throw new common_1.BadRequestException('No OTP found. Please request a new one.');
}
if (new Date() > stored.expiresAt) {
this.emailOtpStore.delete(userId);
throw new common_1.BadRequestException('OTP has expired. Please request a new one.');
}
if (stored.code !== code) {
throw new common_1.BadRequestException('Invalid OTP code.');
}
await this.prisma.user.update({
where: { id: userId },
data: { otpEmailEnabled: true },
});
this.emailOtpStore.delete(userId);
return { success: true, message: 'Email OTP enabled successfully' };
}
async disableEmailOtp(userId) {
await this.prisma.user.update({
where: { id: userId },
data: { otpEmailEnabled: false },
});
return { success: true, message: 'Email OTP disabled' };
}
async setupTotp(userId) {
const user = await this.prisma.user.findUnique({ where: { id: userId } });
if (!user) {
throw new common_1.BadRequestException('User not found');
}
const secret = otplib_1.authenticator.generateSecret();
await this.prisma.user.update({
where: { id: userId },
data: { otpTotpSecret: secret },
});
const serviceName = 'Tabungin';
const accountName = user.email;
const otpauthUrl = otplib_1.authenticator.keyuri(accountName, serviceName, secret);
const qrCodeDataUrl = await QRCode.toDataURL(otpauthUrl);
return {
secret,
qrCode: qrCodeDataUrl,
};
}
async verifyTotp(userId, code) {
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: { otpTotpSecret: true },
});
if (!user?.otpTotpSecret) {
throw new common_1.BadRequestException('No TOTP setup found. Please setup TOTP first.');
}
const isValid = otplib_1.authenticator.verify({
token: code,
secret: user.otpTotpSecret,
});
if (!isValid) {
throw new common_1.BadRequestException('Invalid TOTP code.');
}
await this.prisma.user.update({
where: { id: userId },
data: { otpTotpEnabled: true },
});
return { success: true, message: 'TOTP enabled successfully' };
}
async disableTotp(userId) {
await this.prisma.user.update({
where: { id: userId },
data: {
otpTotpEnabled: false,
otpTotpSecret: null,
},
});
return { success: true, message: 'TOTP disabled' };
}
async getStatus(userId) {
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: {
phone: true,
otpEmailEnabled: true,
otpWhatsappEnabled: true,
otpTotpEnabled: true,
otpTotpSecret: true,
},
});
if (!user) {
return {
emailEnabled: false,
whatsappEnabled: false,
totpEnabled: false,
};
}
return {
phone: user.phone,
emailEnabled: user.otpEmailEnabled,
whatsappEnabled: user.otpWhatsappEnabled,
totpEnabled: user.otpTotpEnabled,
totpSecret: user.otpTotpSecret,
};
}
async verifyOtpGate(userId, code, method) {
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: {
otpEmailEnabled: true,
otpTotpEnabled: true,
otpTotpSecret: true,
},
});
if (!user) {
return false;
}
if (method === 'email' && user.otpEmailEnabled) {
const stored = this.emailOtpStore.get(userId);
if (stored && new Date() <= stored.expiresAt && stored.code === code) {
return true;
}
}
if (method === 'totp' && user.otpTotpEnabled && user.otpTotpSecret) {
return otplib_1.authenticator.verify({ token: code, secret: user.otpTotpSecret });
}
return false;
}
generateOtpCode() {
return Math.floor(100000 + Math.random() * 900000).toString();
}
async sendOtpViaWebhook(email, code, mode = 'test') {
const webhookUrl = process.env.OTP_SEND_WEBHOOK_URL_TEST || process.env.OTP_SEND_WEBHOOK_URL;
if (!webhookUrl) {
throw new Error('OTP_SEND_WEBHOOK_URL or OTP_SEND_WEBHOOK_URL_TEST not configured');
}
await axios_1.default.post(webhookUrl, {
method: 'email',
mode,
to: email,
subject: 'Tabungin - Your OTP Code',
message: `Your OTP code is: ${code}. This code will expire in 10 minutes.`,
code,
});
}
async sendWhatsappOtp(userId, mode = 'test') {
const user = await this.prisma.user.findUnique({ where: { id: userId } });
if (!user) {
throw new common_1.BadRequestException('User not found');
}
if (!user.phone) {
throw new common_1.BadRequestException('Phone number not set');
}
const code = this.generateOtpCode();
const expiresAt = new Date(Date.now() + 10 * 60 * 1000);
this.whatsappOtpStore.set(userId, { code, expiresAt });
try {
await this.sendWhatsappOtpViaWebhook(user.phone, code, mode);
return { success: true, message: 'OTP sent to your WhatsApp' };
}
catch (error) {
console.error('Failed to send WhatsApp OTP via webhook:', error);
console.log(`📱 WhatsApp OTP Code for ${user.phone}: ${code}`);
return {
success: true,
message: 'OTP sent (check console for dev code)',
};
}
}
async verifyWhatsappOtp(userId, code) {
const stored = this.whatsappOtpStore.get(userId);
if (!stored) {
throw new common_1.BadRequestException('No OTP found. Please request a new one.');
}
if (new Date() > stored.expiresAt) {
this.whatsappOtpStore.delete(userId);
throw new common_1.BadRequestException('OTP has expired. Please request a new one.');
}
if (stored.code !== code) {
throw new common_1.BadRequestException('Invalid OTP code');
}
await this.prisma.user.update({
where: { id: userId },
data: { otpWhatsappEnabled: true },
});
this.whatsappOtpStore.delete(userId);
return { success: true, message: 'WhatsApp OTP enabled successfully' };
}
verifyWhatsappOtpForLogin(userId, code) {
const stored = this.whatsappOtpStore.get(userId);
if (!stored) {
return false;
}
if (new Date() > stored.expiresAt) {
this.whatsappOtpStore.delete(userId);
return false;
}
if (stored.code !== code) {
return false;
}
this.whatsappOtpStore.delete(userId);
return true;
}
async disableWhatsappOtp(userId) {
await this.prisma.user.update({
where: { id: userId },
data: { otpWhatsappEnabled: false },
});
return { success: true, message: 'WhatsApp OTP disabled' };
}
async checkWhatsappNumber(phone) {
try {
const webhookUrl = process.env.OTP_SEND_WEBHOOK_URL_TEST ||
process.env.OTP_SEND_WEBHOOK_URL;
if (!webhookUrl) {
throw new Error('Webhook URL not configured');
}
const response = await axios_1.default.post(webhookUrl, {
method: 'whatsapp',
mode: 'checknumber',
phone,
});
return {
success: true,
isRegistered: response.data?.isRegistered || false,
message: response.data?.message || 'Number checked',
};
}
catch (error) {
console.error('Failed to check WhatsApp number:', error);
console.log(`📱 Checking WhatsApp number: ${phone} - Assumed valid`);
return {
success: true,
isRegistered: true,
message: 'Number is valid (dev mode)',
};
}
}
async sendWhatsappOtpViaWebhook(phone, code, mode = 'test') {
const webhookUrl = process.env.OTP_SEND_WEBHOOK_URL_TEST || process.env.OTP_SEND_WEBHOOK_URL;
if (!webhookUrl) {
throw new Error('Webhook URL not configured');
}
await axios_1.default.post(webhookUrl, {
method: 'whatsapp',
mode,
phone,
message: `Your Tabungin OTP code is: ${code}. This code will expire in 10 minutes.`,
code,
});
}
};
exports.OtpService = OtpService;
exports.OtpService = OtpService = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
], OtpService);
//# sourceMappingURL=otp.service.js.map

1
apps/api/dist/otp/otp.service.js.map vendored Normal file

File diff suppressed because one or more lines are too long

15
apps/api/dist/seed.js vendored
View File

@@ -2,18 +2,21 @@
Object.defineProperty(exports, "__esModule", { value: true });
const client_1 = require("@prisma/client");
const prisma = new client_1.PrismaClient();
const adminSeeder = {
email: 'dwindi.ramadhana@gmail.com',
password: 'tabungin2k25!@#',
};
const TEMP_USER_ID = process.env.TEMP_USER_ID || '16b74848-daa3-4dc9-8de2-3cf59e08f8e3';
async function main() {
const userId = '16b74848-daa3-4dc9-8de2-3cf59e08f8e3';
const user = await prisma.user.upsert({
where: { id: userId },
where: { id: TEMP_USER_ID },
update: {},
create: {
id: userId,
id: TEMP_USER_ID,
email: 'temp@example.com',
},
});
const existing = await prisma.wallet.findFirst({
where: { userId: user.id, kind: 'money' },
});
const existing = await prisma.wallet.findFirst({});
if (!existing) {
await prisma.wallet.create({
data: {

View File

@@ -1 +1 @@
{"version":3,"file":"seed.js","sourceRoot":"","sources":["../src/seed.ts"],"names":[],"mappings":";;AAAA,2CAA8C;AAE9C,MAAM,MAAM,GAAG,IAAI,qBAAY,EAAE,CAAC;AAElC,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,sCAAsC,CAAC;IACtD,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;QACpC,KAAK,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE;QACrB,MAAM,EAAE,EAAE;QACV,MAAM,EAAE;YACN,EAAE,EAAE,MAAM;SACX;KACF,CAAC,CAAC;IAGH,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC;QAC7C,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE;KAC1C,CAAC,CAAC;IAEH,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC;YACzB,IAAI,EAAE;gBACJ,MAAM,EAAE,IAAI,CAAC,EAAE;gBACf,IAAI,EAAE,OAAO;gBACb,IAAI,EAAE,MAAM;gBACZ,QAAQ,EAAE,KAAK;aAChB;SACF,CAAC,CAAC;IACL,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,8BAA8B,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;AACvD,CAAC;AAED,IAAI,EAAE;KACH,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE;IACX,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACjB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC;KACD,OAAO,CAAC,KAAK,IAAI,EAAE;IAClB,MAAM,MAAM,CAAC,WAAW,EAAE,CAAC;AAC7B,CAAC,CAAC,CAAC"}
{"version":3,"file":"seed.js","sourceRoot":"","sources":["../src/seed.ts"],"names":[],"mappings":";;AAAA,2CAA8C;AAC9C,MAAM,MAAM,GAAG,IAAI,qBAAY,EAAE,CAAC;AAElC,MAAM,WAAW,GAAG;IAClB,KAAK,EAAE,4BAA4B;IACnC,QAAQ,EAAE,iBAAiB;CAC5B,CAAA;AAED,MAAM,YAAY,GAChB,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,sCAAsC,CAAC;AAErE,KAAK,UAAU,IAAI;IACjB,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;QACpC,KAAK,EAAE,EAAE,EAAE,EAAE,YAAY,EAAE;QAC3B,MAAM,EAAE,EAAE;QACV,MAAM,EAAE;YACN,EAAE,EAAE,YAAY;YAChB,KAAK,EAAE,kBAAkB;SAC1B;KACF,CAAC,CAAC;IAGH,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IAEnD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC;YACzB,IAAI,EAAE;gBACJ,MAAM,EAAE,IAAI,CAAC,EAAE;gBACf,IAAI,EAAE,OAAO;gBACb,IAAI,EAAE,MAAM;gBACZ,QAAQ,EAAE,KAAK;aAChB;SACF,CAAC,CAAC;IACL,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,8BAA8B,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;AACvD,CAAC;AAED,IAAI,EAAE;KACH,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE;IACX,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACjB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC;KACD,OAAO,CAAC,KAAK,IAAI,EAAE;IAClB,MAAM,MAAM,CAAC,WAAW,EAAE,CAAC;AAC7B,CAAC,CAAC,CAAC"}

View File

@@ -1 +1 @@
{"version":3,"file":"transaction.dto.js","sourceRoot":"","sources":["../../src/transactions/transaction.dto.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AAEX,QAAA,uBAAuB,GAAG,OAAC,CAAC,MAAM,CAAC;IAC9C,MAAM,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IACxC,SAAS,EAAE,OAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,EAAE;IAC1C,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IACtC,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IACjD,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;CAC9C,CAAC,CAAC"}
{"version":3,"file":"transaction.dto.js","sourceRoot":"","sources":["../../src/transactions/transaction.dto.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AAEX,QAAA,uBAAuB,GAAG,OAAC,CAAC,MAAM,CAAC;IAC9C,MAAM,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IACxC,SAAS,EAAE,OAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,QAAQ,EAAE;IAC3C,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IACtC,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IACjD,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;CAC9C,CAAC,CAAC"}

View File

@@ -1,9 +1,14 @@
import type { Response } from 'express';
import { TransactionsService } from './transactions.service';
interface RequestWithUser {
user: {
userId: string;
};
}
export declare class TransactionsController {
private readonly tx;
constructor(tx: TransactionsService);
list(walletId: string): import("@prisma/client").Prisma.PrismaPromise<{
list(req: RequestWithUser, walletId: string): import("@prisma/client").Prisma.PrismaPromise<{
category: string | null;
id: string;
createdAt: Date;
@@ -15,7 +20,7 @@ export declare class TransactionsController {
walletId: string;
recurrenceId: string | null;
}[]>;
create(walletId: string, body: {
create(req: RequestWithUser, walletId: string, body: {
amount: number | string;
direction: 'in' | 'out';
date?: string;
@@ -33,8 +38,8 @@ export declare class TransactionsController {
walletId: string;
recurrenceId: string | null;
}>;
exportCsv(walletId: string, from: string | undefined, to: string | undefined, category: string | undefined, direction: 'in' | 'out' | undefined, res: Response): Promise<void>;
update(walletId: string, id: string, body: unknown): Promise<{
exportCsv(req: RequestWithUser, walletId: string, from: string | undefined, to: string | undefined, category: string | undefined, direction: 'in' | 'out' | undefined, res: Response): Promise<void>;
update(req: RequestWithUser, walletId: string, id: string, body: unknown): Promise<{
category: string | null;
id: string;
createdAt: Date;
@@ -46,7 +51,7 @@ export declare class TransactionsController {
walletId: string;
recurrenceId: string | null;
}>;
delete(walletId: string, id: string): Promise<{
delete(req: RequestWithUser, walletId: string, id: string): Promise<{
category: string | null;
id: string;
createdAt: Date;
@@ -59,3 +64,4 @@ export declare class TransactionsController {
recurrenceId: string | null;
}>;
}
export {};

View File

@@ -14,6 +14,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
Object.defineProperty(exports, "__esModule", { value: true });
exports.TransactionsController = void 0;
const common_1 = require("@nestjs/common");
const auth_guard_1 = require("../auth/auth.guard");
const transactions_service_1 = require("./transactions.service");
const transaction_dto_1 = require("./transaction.dto");
let TransactionsController = class TransactionsController {
@@ -21,14 +22,19 @@ let TransactionsController = class TransactionsController {
constructor(tx) {
this.tx = tx;
}
list(walletId) {
return this.tx.list(walletId);
list(req, walletId) {
return this.tx.list(req.user.userId, walletId);
}
create(walletId, body) {
return this.tx.create(walletId, body);
create(req, walletId, body) {
return this.tx.create(req.user.userId, walletId, body);
}
async exportCsv(walletId, from, to, category, direction, res) {
const rows = await this.tx.listWithFilters(walletId, { from, to, category, direction });
async exportCsv(req, walletId, from, to, category, direction, res) {
const rows = await this.tx.listWithFilters(req.user.userId, walletId, {
from,
to,
category,
direction,
});
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader('Content-Disposition', `attachment; filename="transactions_${walletId}.csv"`);
res.write(`date,category,memo,direction,amount\n`);
@@ -50,66 +56,73 @@ let TransactionsController = class TransactionsController {
}
res.end();
}
async update(walletId, id, body) {
async update(req, walletId, id, body) {
try {
const parsed = transaction_dto_1.TransactionUpdateSchema.parse(body);
return this.tx.update(walletId, id, parsed);
return this.tx.update(req.user.userId, walletId, id, parsed);
}
catch (e) {
throw new common_1.BadRequestException(e?.errors ?? 'Invalid payload');
const error = e;
throw new common_1.BadRequestException(error?.errors ?? 'Invalid payload');
}
}
delete(walletId, id) {
return this.tx.delete(walletId, id);
delete(req, walletId, id) {
return this.tx.delete(req.user.userId, walletId, id);
}
};
exports.TransactionsController = TransactionsController;
__decorate([
(0, common_1.Get)(),
__param(0, (0, common_1.Param)('walletId')),
__param(0, (0, common_1.Req)()),
__param(1, (0, common_1.Param)('walletId')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:paramtypes", [Object, String]),
__metadata("design:returntype", void 0)
], TransactionsController.prototype, "list", null);
__decorate([
(0, common_1.Post)(),
__param(0, (0, common_1.Param)('walletId')),
__param(1, (0, common_1.Body)()),
__param(0, (0, common_1.Req)()),
__param(1, (0, common_1.Param)('walletId')),
__param(2, (0, common_1.Body)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, Object]),
__metadata("design:paramtypes", [Object, String, Object]),
__metadata("design:returntype", void 0)
], TransactionsController.prototype, "create", null);
__decorate([
(0, common_1.Get)('export.csv'),
__param(0, (0, common_1.Param)('walletId')),
__param(1, (0, common_1.Query)('from')),
__param(2, (0, common_1.Query)('to')),
__param(3, (0, common_1.Query)('category')),
__param(4, (0, common_1.Query)('direction')),
__param(5, (0, common_1.Res)()),
__param(0, (0, common_1.Req)()),
__param(1, (0, common_1.Param)('walletId')),
__param(2, (0, common_1.Query)('from')),
__param(3, (0, common_1.Query)('to')),
__param(4, (0, common_1.Query)('category')),
__param(5, (0, common_1.Query)('direction')),
__param(6, (0, common_1.Res)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, Object, Object, Object, Object, Object]),
__metadata("design:paramtypes", [Object, String, Object, Object, Object, Object, Object]),
__metadata("design:returntype", Promise)
], TransactionsController.prototype, "exportCsv", null);
__decorate([
(0, common_1.Put)(':id'),
__param(0, (0, common_1.Param)('walletId')),
__param(1, (0, common_1.Param)('id')),
__param(2, (0, common_1.Body)()),
__param(0, (0, common_1.Req)()),
__param(1, (0, common_1.Param)('walletId')),
__param(2, (0, common_1.Param)('id')),
__param(3, (0, common_1.Body)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, String, Object]),
__metadata("design:paramtypes", [Object, String, String, Object]),
__metadata("design:returntype", Promise)
], TransactionsController.prototype, "update", null);
__decorate([
(0, common_1.Delete)(':id'),
__param(0, (0, common_1.Param)('walletId')),
__param(1, (0, common_1.Param)('id')),
__param(0, (0, common_1.Req)()),
__param(1, (0, common_1.Param)('walletId')),
__param(2, (0, common_1.Param)('id')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, String]),
__metadata("design:paramtypes", [Object, String, String]),
__metadata("design:returntype", void 0)
], TransactionsController.prototype, "delete", null);
exports.TransactionsController = TransactionsController = __decorate([
(0, common_1.Controller)('wallets/:walletId/transactions'),
(0, common_1.UseGuards)(auth_guard_1.AuthGuard),
__metadata("design:paramtypes", [transactions_service_1.TransactionsService])
], TransactionsController);
//# sourceMappingURL=transactions.controller.js.map

View File

@@ -1 +1 @@
{"version":3,"file":"transactions.controller.js","sourceRoot":"","sources":["../../src/transactions/transactions.controller.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"transactions.controller.js","sourceRoot":"","sources":["../../src/transactions/transactions.controller.ts"],"names":[],"mappings":"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"}

View File

@@ -11,12 +11,13 @@ const common_1 = require("@nestjs/common");
const transactions_service_1 = require("./transactions.service");
const transactions_controller_1 = require("./transactions.controller");
const prisma_module_1 = require("../prisma/prisma.module");
const otp_module_1 = require("../otp/otp.module");
let TransactionsModule = class TransactionsModule {
};
exports.TransactionsModule = TransactionsModule;
exports.TransactionsModule = TransactionsModule = __decorate([
(0, common_1.Module)({
imports: [prisma_module_1.PrismaModule],
imports: [prisma_module_1.PrismaModule, otp_module_1.OtpModule],
providers: [transactions_service_1.TransactionsService],
controllers: [transactions_controller_1.TransactionsController],
exports: [transactions_service_1.TransactionsService],

View File

@@ -1 +1 @@
{"version":3,"file":"transactions.module.js","sourceRoot":"","sources":["../../src/transactions/transactions.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,iEAA6D;AAC7D,uEAAmE;AACnE,2DAAuD;AAQhD,IAAM,kBAAkB,GAAxB,MAAM,kBAAkB;CAAG,CAAA;AAArB,gDAAkB;6BAAlB,kBAAkB;IAN9B,IAAA,eAAM,EAAC;QACN,OAAO,EAAE,CAAC,4BAAY,CAAC;QACvB,SAAS,EAAE,CAAC,0CAAmB,CAAC;QAChC,WAAW,EAAE,CAAC,gDAAsB,CAAC;QACrC,OAAO,EAAE,CAAC,0CAAmB,CAAC;KAC/B,CAAC;GACW,kBAAkB,CAAG"}
{"version":3,"file":"transactions.module.js","sourceRoot":"","sources":["../../src/transactions/transactions.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,iEAA6D;AAC7D,uEAAmE;AACnE,2DAAuD;AACvD,kDAA8C;AAQvC,IAAM,kBAAkB,GAAxB,MAAM,kBAAkB;CAAG,CAAA;AAArB,gDAAkB;6BAAlB,kBAAkB;IAN9B,IAAA,eAAM,EAAC;QACN,OAAO,EAAE,CAAC,4BAAY,EAAE,sBAAS,CAAC;QAClC,SAAS,EAAE,CAAC,0CAAmB,CAAC;QAChC,WAAW,EAAE,CAAC,gDAAsB,CAAC;QACrC,OAAO,EAAE,CAAC,0CAAmB,CAAC;KAC/B,CAAC;GACW,kBAAkB,CAAG"}

View File

@@ -4,8 +4,7 @@ import type { TransactionUpdateDto } from './transaction.dto';
export declare class TransactionsService {
private prisma;
constructor(prisma: PrismaService);
private userId;
list(walletId: string): Prisma.PrismaPromise<{
list(userId: string, walletId: string): Prisma.PrismaPromise<{
category: string | null;
id: string;
createdAt: Date;
@@ -17,7 +16,7 @@ export declare class TransactionsService {
walletId: string;
recurrenceId: string | null;
}[]>;
listAll(): Prisma.PrismaPromise<{
listAll(userId: string): Prisma.PrismaPromise<{
category: string | null;
id: string;
createdAt: Date;
@@ -29,7 +28,7 @@ export declare class TransactionsService {
walletId: string;
recurrenceId: string | null;
}[]>;
listWithFilters(walletId: string, filters: {
listWithFilters(userId: string, walletId: string, filters: {
from?: string;
to?: string;
category?: string;
@@ -46,7 +45,7 @@ export declare class TransactionsService {
walletId: string;
recurrenceId: string | null;
}[]>;
create(walletId: string, input: {
create(userId: string, walletId: string, input: {
amount: string | number;
direction: 'in' | 'out';
date?: string;
@@ -64,7 +63,7 @@ export declare class TransactionsService {
walletId: string;
recurrenceId: string | null;
}>;
update(walletId: string, id: string, dto: TransactionUpdateDto): Promise<{
update(userId: string, walletId: string, id: string, dto: TransactionUpdateDto): Promise<{
category: string | null;
id: string;
createdAt: Date;
@@ -76,7 +75,7 @@ export declare class TransactionsService {
walletId: string;
recurrenceId: string | null;
}>;
delete(walletId: string, id: string): Promise<{
delete(userId: string, walletId: string, id: string): Promise<{
category: string | null;
id: string;
createdAt: Date;

View File

@@ -12,32 +12,28 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.TransactionsService = void 0;
const common_1 = require("@nestjs/common");
const prisma_service_1 = require("../prisma/prisma.service");
const user_util_1 = require("../common/user.util");
let TransactionsService = class TransactionsService {
prisma;
constructor(prisma) {
this.prisma = prisma;
}
userId() {
return (0, user_util_1.getTempUserId)();
}
list(walletId) {
list(userId, walletId) {
return this.prisma.transaction.findMany({
where: { userId: this.userId(), walletId },
where: { userId, walletId },
orderBy: { date: 'desc' },
take: 200,
});
}
listAll() {
listAll(userId) {
return this.prisma.transaction.findMany({
where: { userId: this.userId() },
where: { userId },
orderBy: { date: 'desc' },
take: 1000,
});
}
listWithFilters(walletId, filters) {
listWithFilters(userId, walletId, filters) {
const where = {
userId: (0, user_util_1.getTempUserId)(),
userId,
walletId,
};
if (filters.direction)
@@ -56,20 +52,20 @@ let TransactionsService = class TransactionsService {
orderBy: { date: 'desc' },
});
}
async create(walletId, input) {
async create(userId, walletId, input) {
const amountNum = typeof input.amount === 'string' ? Number(input.amount) : input.amount;
if (!Number.isFinite(amountNum))
throw new Error('amount must be a number');
const date = input.date ? new Date(input.date) : new Date();
const wallet = await this.prisma.wallet.findFirst({
where: { id: walletId, userId: this.userId(), deletedAt: null },
where: { id: walletId, userId, deletedAt: null },
select: { id: true },
});
if (!wallet)
throw new Error('wallet not found');
return this.prisma.transaction.create({
data: {
userId: this.userId(),
userId,
walletId,
amount: amountNum,
direction: input.direction,
@@ -79,9 +75,9 @@ let TransactionsService = class TransactionsService {
},
});
}
async update(walletId, id, dto) {
async update(userId, walletId, id, dto) {
const existing = await this.prisma.transaction.findFirst({
where: { id, walletId, userId: this.userId() },
where: { id, walletId, userId },
});
if (!existing)
throw new Error('transaction not found');
@@ -101,9 +97,9 @@ let TransactionsService = class TransactionsService {
data,
});
}
async delete(walletId, id) {
async delete(userId, walletId, id) {
const existing = await this.prisma.transaction.findFirst({
where: { id, walletId, userId: this.userId() },
where: { id, walletId, userId },
});
if (!existing)
throw new Error('transaction not found');

View File

@@ -1 +1 @@
{"version":3,"file":"transactions.service.js","sourceRoot":"","sources":["../../src/transactions/transactions.service.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"transactions.service.js","sourceRoot":"","sources":["../../src/transactions/transactions.service.ts"],"names":[],"mappings":"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"}

File diff suppressed because one or more lines are too long

View File

@@ -1,16 +1,54 @@
import { UsersService } from './users.service';
interface RequestWithUser extends Request {
user: {
userId: string;
email: string;
};
}
export declare class UsersController {
private readonly users;
constructor(users: UsersService);
me(): Promise<{
id: string;
email: string | null;
email: string;
phone: string | null;
createdAt: Date;
updatedAt: Date;
status: string;
emailVerified: boolean;
passwordHash: string | null;
name: string | null;
avatarUrl: string | null;
defaultCurrency: string | null;
timeZone: string | null;
otpEmailEnabled: boolean;
otpWhatsappEnabled: boolean;
otpTotpEnabled: boolean;
otpTotpSecret: string | null;
} | null>;
updateProfile(req: RequestWithUser, body: {
name?: string;
phone?: string;
}): Promise<{
success: boolean;
message: string;
user: {
id: string;
email: string;
phone: string | null;
name: string | null;
avatarUrl: string | null;
};
}>;
getAuthInfo(req: RequestWithUser): Promise<{
hasGoogleAuth: boolean;
hasPassword: boolean;
}>;
deleteAccount(req: RequestWithUser, body: {
password: string;
}): Promise<{
success: boolean;
message: string;
}>;
}
export {};

View File

@@ -8,9 +8,13 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
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.UsersController = void 0;
const common_1 = require("@nestjs/common");
const auth_guard_1 = require("../auth/auth.guard");
const users_service_1 = require("./users.service");
let UsersController = class UsersController {
users;
@@ -20,6 +24,15 @@ let UsersController = class UsersController {
me() {
return this.users.me();
}
async updateProfile(req, body) {
return this.users.updateProfile(req.user.userId, body);
}
async getAuthInfo(req) {
return this.users.getAuthInfo(req.user.userId);
}
async deleteAccount(req, body) {
return this.users.deleteAccount(req.user.userId, body.password);
}
};
exports.UsersController = UsersController;
__decorate([
@@ -28,8 +41,32 @@ __decorate([
__metadata("design:paramtypes", []),
__metadata("design:returntype", void 0)
], UsersController.prototype, "me", null);
__decorate([
(0, common_1.Put)('profile'),
__param(0, (0, common_1.Req)()),
__param(1, (0, common_1.Body)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object]),
__metadata("design:returntype", Promise)
], UsersController.prototype, "updateProfile", null);
__decorate([
(0, common_1.Get)('auth-info'),
__param(0, (0, common_1.Req)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], UsersController.prototype, "getAuthInfo", null);
__decorate([
(0, common_1.Delete)('account'),
__param(0, (0, common_1.Req)()),
__param(1, (0, common_1.Body)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object, Object]),
__metadata("design:returntype", Promise)
], UsersController.prototype, "deleteAccount", null);
exports.UsersController = UsersController = __decorate([
(0, common_1.Controller)('users'),
(0, common_1.UseGuards)(auth_guard_1.AuthGuard),
__metadata("design:paramtypes", [users_service_1.UsersService])
], UsersController);
//# sourceMappingURL=users.controller.js.map

View File

@@ -1 +1 @@
{"version":3,"file":"users.controller.js","sourceRoot":"","sources":["../../src/users/users.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAAiD;AACjD,mDAA+C;AAGxC,IAAM,eAAe,GAArB,MAAM,eAAe;IACG;IAA7B,YAA6B,KAAmB;QAAnB,UAAK,GAAL,KAAK,CAAc;IAAG,CAAC;IAGpD,EAAE;QACA,OAAO,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC;IACzB,CAAC;CACF,CAAA;AAPY,0CAAe;AAI1B;IADC,IAAA,YAAG,EAAC,IAAI,CAAC;;;;yCAGT;0BANU,eAAe;IAD3B,IAAA,mBAAU,EAAC,OAAO,CAAC;qCAEkB,4BAAY;GADrC,eAAe,CAO3B"}
{"version":3,"file":"users.controller.js","sourceRoot":"","sources":["../../src/users/users.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAAoF;AACpF,mDAA+C;AAC/C,mDAA+C;AAWxC,IAAM,eAAe,GAArB,MAAM,eAAe;IACG;IAA7B,YAA6B,KAAmB;QAAnB,UAAK,GAAL,KAAK,CAAc;IAAG,CAAC;IAGpD,EAAE;QACA,OAAO,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC;IACzB,CAAC;IAGK,AAAN,KAAK,CAAC,aAAa,CACV,GAAoB,EACnB,IAAuC;QAE/C,OAAO,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACzD,CAAC;IAGK,AAAN,KAAK,CAAC,WAAW,CAAQ,GAAoB;QAC3C,OAAO,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACjD,CAAC;IAGK,AAAN,KAAK,CAAC,aAAa,CACV,GAAoB,EACnB,IAA0B;QAElC,OAAO,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IAClE,CAAC;CACF,CAAA;AA5BY,0CAAe;AAI1B;IADC,IAAA,YAAG,EAAC,IAAI,CAAC;;;;yCAGT;AAGK;IADL,IAAA,YAAG,EAAC,SAAS,CAAC;IAEZ,WAAA,IAAA,YAAG,GAAE,CAAA;IACL,WAAA,IAAA,aAAI,GAAE,CAAA;;;;oDAGR;AAGK;IADL,IAAA,YAAG,EAAC,WAAW,CAAC;IACE,WAAA,IAAA,YAAG,GAAE,CAAA;;;;kDAEvB;AAGK;IADL,IAAA,eAAM,EAAC,SAAS,CAAC;IAEf,WAAA,IAAA,YAAG,GAAE,CAAA;IACL,WAAA,IAAA,aAAI,GAAE,CAAA;;;;oDAGR;0BA3BU,eAAe;IAF3B,IAAA,mBAAU,EAAC,OAAO,CAAC;IACnB,IAAA,kBAAS,EAAC,sBAAS,CAAC;qCAEiB,4BAAY;GADrC,eAAe,CA4B3B"}

View File

@@ -4,13 +4,42 @@ export declare class UsersService {
constructor(prisma: PrismaService);
me(): Promise<{
id: string;
email: string | null;
email: string;
phone: string | null;
createdAt: Date;
updatedAt: Date;
status: string;
emailVerified: boolean;
passwordHash: string | null;
name: string | null;
avatarUrl: string | null;
defaultCurrency: string | null;
timeZone: string | null;
otpEmailEnabled: boolean;
otpWhatsappEnabled: boolean;
otpTotpEnabled: boolean;
otpTotpSecret: string | null;
} | null>;
updateProfile(userId: string, data: {
name?: string;
phone?: string;
}): Promise<{
success: boolean;
message: string;
user: {
id: string;
email: string;
phone: string | null;
name: string | null;
avatarUrl: string | null;
};
}>;
getAuthInfo(userId: string): Promise<{
hasGoogleAuth: boolean;
hasPassword: boolean;
}>;
deleteAccount(userId: string, password: string): Promise<{
success: boolean;
message: string;
}>;
}

View File

@@ -1,10 +1,43 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
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 __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
@@ -13,6 +46,7 @@ exports.UsersService = void 0;
const common_1 = require("@nestjs/common");
const prisma_service_1 = require("../prisma/prisma.service");
const user_util_1 = require("../common/user.util");
const bcrypt = __importStar(require("bcrypt"));
let UsersService = class UsersService {
prisma;
constructor(prisma) {
@@ -22,6 +56,79 @@ let UsersService = class UsersService {
const userId = (0, user_util_1.getTempUserId)();
return this.prisma.user.findUnique({ where: { id: userId } });
}
async updateProfile(userId, data) {
try {
const user = await this.prisma.user.update({
where: { id: userId },
data: {
...(data.name !== undefined && { name: data.name }),
...(data.phone !== undefined && { phone: data.phone }),
},
select: {
id: true,
email: true,
name: true,
phone: true,
avatarUrl: true,
},
});
return {
success: true,
message: 'Profile updated successfully',
user,
};
}
catch (error) {
if (error.code === 'P2002') {
throw new common_1.BadRequestException('Phone number already in use');
}
throw error;
}
}
async getAuthInfo(userId) {
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: {
passwordHash: true,
avatarUrl: true,
},
});
const hasGoogleAuth = user?.avatarUrl?.includes('googleusercontent.com') ||
user?.avatarUrl?.startsWith('/avatars/') ||
false;
return {
hasGoogleAuth,
hasPassword: user?.passwordHash !== null,
};
}
async deleteAccount(userId, password) {
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: {
passwordHash: true,
},
});
if (!user) {
throw new common_1.BadRequestException('User not found');
}
if (!user.passwordHash) {
throw new common_1.BadRequestException('Cannot delete account without password. Please set a password first.');
}
const isValid = await bcrypt.compare(password, user.passwordHash);
if (!isValid) {
throw new common_1.UnauthorizedException('Incorrect password');
}
await this.prisma.authAccount.deleteMany({
where: { userId: userId },
});
await this.prisma.user.delete({
where: { id: userId },
});
return {
success: true,
message: 'Account deleted successfully',
};
}
};
exports.UsersService = UsersService;
exports.UsersService = UsersService = __decorate([

View File

@@ -1 +1 @@
{"version":3,"file":"users.service.js","sourceRoot":"","sources":["../../src/users/users.service.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAA4C;AAC5C,6DAAyD;AACzD,mDAAoD;AAG7C,IAAM,YAAY,GAAlB,MAAM,YAAY;IACH;IAApB,YAAoB,MAAqB;QAArB,WAAM,GAAN,MAAM,CAAe;IAAG,CAAC;IAE7C,KAAK,CAAC,EAAE;QACN,MAAM,MAAM,GAAG,IAAA,yBAAa,GAAE,CAAC;QAC/B,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC;IAChE,CAAC;CACF,CAAA;AAPY,oCAAY;uBAAZ,YAAY;IADxB,IAAA,mBAAU,GAAE;qCAEiB,8BAAa;GAD9B,YAAY,CAOxB"}
{"version":3,"file":"users.service.js","sourceRoot":"","sources":["../../src/users/users.service.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,2CAAwF;AACxF,6DAAyD;AACzD,mDAAoD;AACpD,+CAAiC;AAG1B,IAAM,YAAY,GAAlB,MAAM,YAAY;IACH;IAApB,YAAoB,MAAqB;QAArB,WAAM,GAAN,MAAM,CAAe;IAAG,CAAC;IAE7C,KAAK,CAAC,EAAE;QACN,MAAM,MAAM,GAAG,IAAA,yBAAa,GAAE,CAAC;QAC/B,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC;IAChE,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,MAAc,EAAE,IAAuC;QACzE,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;gBACzC,KAAK,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE;gBACrB,IAAI,EAAE;oBACJ,GAAG,CAAC,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC;oBACnD,GAAG,CAAC,IAAI,CAAC,KAAK,KAAK,SAAS,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC;iBACvD;gBACD,MAAM,EAAE;oBACN,EAAE,EAAE,IAAI;oBACR,KAAK,EAAE,IAAI;oBACX,IAAI,EAAE,IAAI;oBACV,KAAK,EAAE,IAAI;oBACX,SAAS,EAAE,IAAI;iBAChB;aACF,CAAC,CAAC;YAEH,OAAO;gBACL,OAAO,EAAE,IAAI;gBACb,OAAO,EAAE,8BAA8B;gBACvC,IAAI;aACL,CAAC;QACJ,CAAC;QAAC,OAAO,KAAU,EAAE,CAAC;YACpB,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;gBAC3B,MAAM,IAAI,4BAAmB,CAAC,6BAA6B,CAAC,CAAC;YAC/D,CAAC;YACD,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,MAAc;QAE9B,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC;YAC7C,KAAK,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE;YACrB,MAAM,EAAE;gBACN,YAAY,EAAE,IAAI;gBAClB,SAAS,EAAE,IAAI;aAChB;SACF,CAAC,CAAC;QAGH,MAAM,aAAa,GACjB,IAAI,EAAE,SAAS,EAAE,QAAQ,CAAC,uBAAuB,CAAC;YAClD,IAAI,EAAE,SAAS,EAAE,UAAU,CAAC,WAAW,CAAC;YACxC,KAAK,CAAC;QAER,OAAO;YACL,aAAa;YACb,WAAW,EAAE,IAAI,EAAE,YAAY,KAAK,IAAI;SACzC,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,MAAc,EAAE,QAAgB;QAElD,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC;YAC7C,KAAK,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE;YACrB,MAAM,EAAE;gBACN,YAAY,EAAE,IAAI;aACnB;SACF,CAAC,CAAC;QAEH,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,MAAM,IAAI,4BAAmB,CAAC,gBAAgB,CAAC,CAAC;QAClD,CAAC;QAED,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;YACvB,MAAM,IAAI,4BAAmB,CAC3B,sEAAsE,CACvE,CAAC;QACJ,CAAC;QAGD,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;QAClE,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,IAAI,8BAAqB,CAAC,oBAAoB,CAAC,CAAC;QACxD,CAAC;QAID,MAAM,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,UAAU,CAAC;YACvC,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE;SAC1B,CAAC,CAAC;QAMH,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;YAC5B,KAAK,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE;SACtB,CAAC,CAAC;QAEH,OAAO;YACL,OAAO,EAAE,IAAI;YACb,OAAO,EAAE,8BAA8B;SACxC,CAAC;IACJ,CAAC;CACF,CAAA;AAxGY,oCAAY;uBAAZ,YAAY;IADxB,IAAA,mBAAU,GAAE;qCAEiB,8BAAa;GAD9B,YAAY,CAwGxB"}

View File

@@ -1,10 +1,15 @@
import { WalletsService } from './wallets.service';
import { TransactionsService } from '../transactions/transactions.service';
interface RequestWithUser {
user: {
userId: string;
};
}
export declare class WalletsController {
private readonly wallets;
private readonly transactions;
constructor(wallets: WalletsService, transactions: TransactionsService);
list(): import("@prisma/client").Prisma.PrismaPromise<{
list(req: RequestWithUser): import("@prisma/client").Prisma.PrismaPromise<{
id: string;
createdAt: Date;
updatedAt: Date;
@@ -17,7 +22,7 @@ export declare class WalletsController {
pricePerUnit: import("@prisma/client/runtime/library").Decimal | null;
deletedAt: Date | null;
}[]>;
getAllTransactions(): Promise<{
getAllTransactions(req: RequestWithUser): Promise<{
category: string | null;
id: string;
createdAt: Date;
@@ -29,7 +34,7 @@ export declare class WalletsController {
walletId: string;
recurrenceId: string | null;
}[]>;
create(body: {
create(req: RequestWithUser, body: {
name: string;
currency?: string;
kind?: 'money' | 'asset';
@@ -51,7 +56,7 @@ export declare class WalletsController {
}, never, import("@prisma/client/runtime/library").DefaultArgs, import("@prisma/client").Prisma.PrismaClientOptions> | {
error: string;
};
update(id: string, body: {
update(req: RequestWithUser, id: string, body: {
name?: string;
currency?: string;
kind?: 'money' | 'asset';
@@ -71,7 +76,7 @@ export declare class WalletsController {
pricePerUnit: import("@prisma/client/runtime/library").Decimal | null;
deletedAt: Date | null;
}, never, import("@prisma/client/runtime/library").DefaultArgs, import("@prisma/client").Prisma.PrismaClientOptions>;
delete(id: string): import("@prisma/client").Prisma.Prisma__WalletClient<{
delete(req: RequestWithUser, id: string): import("@prisma/client").Prisma.Prisma__WalletClient<{
id: string;
createdAt: Date;
updatedAt: Date;
@@ -85,3 +90,4 @@ export declare class WalletsController {
deletedAt: Date | null;
}, never, import("@prisma/client/runtime/library").DefaultArgs, import("@prisma/client").Prisma.PrismaClientOptions>;
}
export {};

View File

@@ -16,6 +16,7 @@ exports.WalletsController = void 0;
const common_1 = require("@nestjs/common");
const wallets_service_1 = require("./wallets.service");
const transactions_service_1 = require("../transactions/transactions.service");
const auth_guard_1 = require("../auth/auth.guard");
let WalletsController = class WalletsController {
wallets;
transactions;
@@ -23,62 +24,68 @@ let WalletsController = class WalletsController {
this.wallets = wallets;
this.transactions = transactions;
}
list() {
return this.wallets.list();
list(req) {
return this.wallets.list(req.user.userId);
}
async getAllTransactions() {
return this.transactions.listAll();
async getAllTransactions(req) {
return this.transactions.listAll(req.user.userId);
}
create(body) {
create(req, body) {
if (!body?.name) {
return { error: 'name is required' };
}
return this.wallets.create(body);
return this.wallets.create(req.user.userId, body);
}
update(id, body) {
return this.wallets.update(id, body);
update(req, id, body) {
return this.wallets.update(req.user.userId, id, body);
}
delete(id) {
return this.wallets.delete(id);
delete(req, id) {
return this.wallets.delete(req.user.userId, id);
}
};
exports.WalletsController = WalletsController;
__decorate([
(0, common_1.Get)(),
__param(0, (0, common_1.Req)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", void 0)
], WalletsController.prototype, "list", null);
__decorate([
(0, common_1.Get)('transactions'),
__param(0, (0, common_1.Req)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], WalletsController.prototype, "getAllTransactions", null);
__decorate([
(0, common_1.Post)(),
__param(0, (0, common_1.Body)()),
__param(0, (0, common_1.Req)()),
__param(1, (0, common_1.Body)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:paramtypes", [Object, Object]),
__metadata("design:returntype", void 0)
], WalletsController.prototype, "create", null);
__decorate([
(0, common_1.Put)(':id'),
__param(0, (0, common_1.Param)('id')),
__param(1, (0, common_1.Body)()),
__param(0, (0, common_1.Req)()),
__param(1, (0, common_1.Param)('id')),
__param(2, (0, common_1.Body)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, Object]),
__metadata("design:paramtypes", [Object, String, Object]),
__metadata("design:returntype", void 0)
], WalletsController.prototype, "update", null);
__decorate([
(0, common_1.Delete)(':id'),
__param(0, (0, common_1.Param)('id')),
__param(0, (0, common_1.Req)()),
__param(1, (0, common_1.Param)('id')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:paramtypes", [Object, String]),
__metadata("design:returntype", void 0)
], WalletsController.prototype, "delete", null);
exports.WalletsController = WalletsController = __decorate([
(0, common_1.Controller)('wallets'),
(0, common_1.UseGuards)(auth_guard_1.AuthGuard),
__metadata("design:paramtypes", [wallets_service_1.WalletsService,
transactions_service_1.TransactionsService])
], WalletsController);

View File

@@ -1 +1 @@
{"version":3,"file":"wallets.controller.js","sourceRoot":"","sources":["../../src/wallets/wallets.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAAiF;AACjF,uDAAmD;AACnD,+EAA2E;AAGpE,IAAM,iBAAiB,GAAvB,MAAM,iBAAiB;IAET;IACA;IAFnB,YACmB,OAAuB,EACvB,YAAiC;QADjC,YAAO,GAAP,OAAO,CAAgB;QACvB,iBAAY,GAAZ,YAAY,CAAqB;IACjD,CAAC;IAGJ,IAAI;QACF,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;IAC7B,CAAC;IAGK,AAAN,KAAK,CAAC,kBAAkB;QACtB,OAAO,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,CAAC;IACrC,CAAC;IAGD,MAAM,CAAS,IAAiI;QAC9I,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC;YAChB,OAAO,EAAE,KAAK,EAAE,kBAAkB,EAAE,CAAC;QACvC,CAAC;QACD,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACnC,CAAC;IAGD,MAAM,CAAc,EAAU,EAAU,IAAkI;QACxK,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;IACvC,CAAC;IAGD,MAAM,CAAc,EAAU;QAC5B,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACjC,CAAC;CACF,CAAA;AAjCY,8CAAiB;AAO5B;IADC,IAAA,YAAG,GAAE;;;;6CAGL;AAGK;IADL,IAAA,YAAG,EAAC,cAAc,CAAC;;;;2DAGnB;AAGD;IADC,IAAA,aAAI,GAAE;IACC,WAAA,IAAA,aAAI,GAAE,CAAA;;;;+CAKb;AAGD;IADC,IAAA,YAAG,EAAC,KAAK,CAAC;IACH,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;IAAc,WAAA,IAAA,aAAI,GAAE,CAAA;;;;+CAEtC;AAGD;IADC,IAAA,eAAM,EAAC,KAAK,CAAC;IACN,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;;;;+CAElB;4BAhCU,iBAAiB;IAD7B,IAAA,mBAAU,EAAC,SAAS,CAAC;qCAGQ,gCAAc;QACT,0CAAmB;GAHzC,iBAAiB,CAiC7B"}
{"version":3,"file":"wallets.controller.js","sourceRoot":"","sources":["../../src/wallets/wallets.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAUwB;AACxB,uDAAmD;AACnD,+EAA2E;AAC3E,mDAA+C;AAUxC,IAAM,iBAAiB,GAAvB,MAAM,iBAAiB;IAET;IACA;IAFnB,YACmB,OAAuB,EACvB,YAAiC;QADjC,YAAO,GAAP,OAAO,CAAgB;QACvB,iBAAY,GAAZ,YAAY,CAAqB;IACjD,CAAC;IAGJ,IAAI,CAAQ,GAAoB;QAC9B,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC5C,CAAC;IAGK,AAAN,KAAK,CAAC,kBAAkB,CAAQ,GAAoB;QAClD,OAAO,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACpD,CAAC;IAGD,MAAM,CACG,GAAoB,EAE3B,IAOC;QAED,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC;YAChB,OAAO,EAAE,KAAK,EAAE,kBAAkB,EAAE,CAAC;QACvC,CAAC;QACD,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACpD,CAAC;IAGD,MAAM,CACG,GAAoB,EACd,EAAU,EAEvB,IAOC;QAED,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;IACxD,CAAC;IAGD,MAAM,CAAQ,GAAoB,EAAe,EAAU;QACzD,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAClD,CAAC;CACF,CAAA;AAxDY,8CAAiB;AAO5B;IADC,IAAA,YAAG,GAAE;IACA,WAAA,IAAA,YAAG,GAAE,CAAA;;;;6CAEV;AAGK;IADL,IAAA,YAAG,EAAC,cAAc,CAAC;IACM,WAAA,IAAA,YAAG,GAAE,CAAA;;;;2DAE9B;AAGD;IADC,IAAA,aAAI,GAAE;IAEJ,WAAA,IAAA,YAAG,GAAE,CAAA;IACL,WAAA,IAAA,aAAI,GAAE,CAAA;;;;+CAcR;AAGD;IADC,IAAA,YAAG,EAAC,KAAK,CAAC;IAER,WAAA,IAAA,YAAG,GAAE,CAAA;IACL,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;IACX,WAAA,IAAA,aAAI,GAAE,CAAA;;;;+CAWR;AAGD;IADC,IAAA,eAAM,EAAC,KAAK,CAAC;IACN,WAAA,IAAA,YAAG,GAAE,CAAA;IAAwB,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;;;;+CAE/C;4BAvDU,iBAAiB;IAF7B,IAAA,mBAAU,EAAC,SAAS,CAAC;IACrB,IAAA,kBAAS,EAAC,sBAAS,CAAC;qCAGS,gCAAc;QACT,0CAAmB;GAHzC,iBAAiB,CAwD7B"}

View File

@@ -2,8 +2,7 @@ import { PrismaService } from '../prisma/prisma.service';
export declare class WalletsService {
private prisma;
constructor(prisma: PrismaService);
private userId;
list(): import("@prisma/client").Prisma.PrismaPromise<{
list(userId: string): import("@prisma/client").Prisma.PrismaPromise<{
id: string;
createdAt: Date;
updatedAt: Date;
@@ -16,7 +15,7 @@ export declare class WalletsService {
pricePerUnit: import("@prisma/client/runtime/library").Decimal | null;
deletedAt: Date | null;
}[]>;
create(input: {
create(userId: string, input: {
name: string;
currency?: string;
kind?: 'money' | 'asset';
@@ -36,7 +35,7 @@ export declare class WalletsService {
pricePerUnit: import("@prisma/client/runtime/library").Decimal | null;
deletedAt: Date | null;
}, never, import("@prisma/client/runtime/library").DefaultArgs, import("@prisma/client").Prisma.PrismaClientOptions>;
update(id: string, input: {
update(userId: string, id: string, input: {
name?: string;
currency?: string;
kind?: 'money' | 'asset';
@@ -56,7 +55,7 @@ export declare class WalletsService {
pricePerUnit: import("@prisma/client/runtime/library").Decimal | null;
deletedAt: Date | null;
}, never, import("@prisma/client/runtime/library").DefaultArgs, import("@prisma/client").Prisma.PrismaClientOptions>;
delete(id: string): import("@prisma/client").Prisma.Prisma__WalletClient<{
delete(userId: string, id: string): import("@prisma/client").Prisma.Prisma__WalletClient<{
id: string;
createdAt: Date;
updatedAt: Date;

View File

@@ -12,36 +12,32 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.WalletsService = void 0;
const common_1 = require("@nestjs/common");
const prisma_service_1 = require("../prisma/prisma.service");
const user_util_1 = require("../common/user.util");
let WalletsService = class WalletsService {
prisma;
constructor(prisma) {
this.prisma = prisma;
}
userId() {
return (0, user_util_1.getTempUserId)();
}
list() {
list(userId) {
return this.prisma.wallet.findMany({
where: { userId: this.userId(), deletedAt: null },
where: { userId, deletedAt: null },
orderBy: { createdAt: 'asc' },
});
}
create(input) {
create(userId, input) {
const kind = input.kind ?? 'money';
return this.prisma.wallet.create({
data: {
userId: this.userId(),
userId,
name: input.name,
kind,
currency: kind === 'money' ? (input.currency ?? 'IDR') : null,
unit: kind === 'asset' ? (input.unit ?? null) : null,
initialAmount: input.initialAmount || null,
pricePerUnit: kind === 'asset' ? (input.pricePerUnit || null) : null,
pricePerUnit: kind === 'asset' ? input.pricePerUnit || null : null,
},
});
}
update(id, input) {
update(userId, id, input) {
const updateData = {};
if (input.name !== undefined)
updateData.name = input.name;
@@ -67,13 +63,13 @@ let WalletsService = class WalletsService {
if (input.pricePerUnit !== undefined)
updateData.pricePerUnit = input.pricePerUnit || null;
return this.prisma.wallet.update({
where: { id, userId: this.userId() },
where: { id, userId },
data: updateData,
});
}
delete(id) {
delete(userId, id) {
return this.prisma.wallet.update({
where: { id, userId: this.userId() },
where: { id, userId },
data: { deletedAt: new Date() },
});
}

View File

@@ -1 +1 @@
{"version":3,"file":"wallets.service.js","sourceRoot":"","sources":["../../src/wallets/wallets.service.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"wallets.service.js","sourceRoot":"","sources":["../../src/wallets/wallets.service.ts"],"names":[],"mappings":"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"}

File diff suppressed because it is too large Load Diff

View File

@@ -27,15 +27,23 @@
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/jwt": "^11.0.0",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/swagger": "^11.2.0",
"@prisma/client": "^6.17.0",
"axios": "^1.12.2",
"bcrypt": "^6.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"cookie-parser": "^1.4.7",
"firebase-admin": "^13.5.0",
"jose": "^6.0.12",
"otplib": "^12.0.1",
"passport": "^0.7.0",
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1",
"pg": "^8.16.3",
"qrcode": "^1.5.4",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"swagger-ui-express": "^5.0.1",
@@ -47,9 +55,13 @@
"@nestjs/cli": "^11.0.10",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@types/bcrypt": "^6.0.0",
"@types/express": "^5.0.0",
"@types/jest": "^30.0.0",
"@types/node": "^22.10.7",
"@types/passport-google-oauth20": "^2.0.16",
"@types/passport-jwt": "^4.0.1",
"@types/qrcode": "^1.5.5",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",

View File

@@ -0,0 +1,37 @@
/*
Warnings:
- Made the column `email` on table `User` required. This step will fail if there are existing NULL values in that column.
*/
-- AlterTable
ALTER TABLE "public"."User" ADD COLUMN "emailVerified" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "otpEmailEnabled" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "otpTotpEnabled" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "otpTotpSecret" TEXT,
ADD COLUMN "passwordHash" TEXT,
ALTER COLUMN "email" SET NOT NULL;
-- AlterTable
ALTER TABLE "public"."Wallet" ADD COLUMN "initialAmount" DECIMAL(18,2),
ADD COLUMN "pricePerUnit" DECIMAL(18,2);
-- CreateTable
CREATE TABLE "public"."Category" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Category_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "Category_userId_idx" ON "public"."Category"("userId");
-- CreateIndex
CREATE UNIQUE INDEX "Category_userId_name_key" ON "public"."Category"("userId", "name");
-- AddForeignKey
ALTER TABLE "public"."Category" ADD CONSTRAINT "Category_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -0,0 +1,12 @@
/*
Warnings:
- A unique constraint covering the columns `[phone]` on the table `User` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "public"."User" ADD COLUMN "otpWhatsappEnabled" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "phone" TEXT;
-- CreateIndex
CREATE UNIQUE INDEX "User_phone_key" ON "public"."User"("phone");

View File

@@ -5,7 +5,7 @@ generator client {
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
shadowDatabaseUrl = env("DATABASE_URL_SHADOW")
}
model User {
@@ -13,11 +13,19 @@ model User {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
status String @default("active")
email String? @unique
email String @unique
emailVerified Boolean @default(false)
passwordHash String?
name String?
avatarUrl String?
phone String? @unique
defaultCurrency String?
timeZone String?
// OTP/MFA fields
otpEmailEnabled Boolean @default(false)
otpWhatsappEnabled Boolean @default(false)
otpTotpEnabled Boolean @default(false)
otpTotpSecret String?
authAccounts AuthAccount[]
categories Category[]
Recurrence Recurrence[]

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -8,6 +8,7 @@ import { UsersModule } from './users/users.module';
import { WalletsModule } from './wallets/wallets.module';
import { TransactionsModule } from './transactions/transactions.module';
import { CategoriesModule } from './categories/categories.module';
import { OtpModule } from './otp/otp.module';
@Module({
imports: [
@@ -24,8 +25,9 @@ import { CategoriesModule } from './categories/categories.module';
WalletsModule,
TransactionsModule,
CategoriesModule,
OtpModule,
],
controllers: [HealthController],
providers: [],
})
export class AppModule {}
export class AppModule {}

View File

@@ -0,0 +1,104 @@
import {
Controller,
Post,
Get,
Body,
UseGuards,
Req,
Res,
} from '@nestjs/common';
import { AuthGuard as JwtAuthGuard } from './auth.guard';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';
import type { Response } from 'express';
interface RequestWithUser {
user: {
userId: string;
email: string;
};
}
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('register')
async register(
@Body() body: { email: string; password: string; name?: string },
) {
return this.authService.register(body.email, body.password, body.name);
}
@Post('login')
async login(@Body() body: { email: string; password: string }) {
return this.authService.login(body.email, body.password);
}
@Post('verify-otp')
async verifyOtp(
@Body()
body: {
tempToken: string;
otpCode: string;
method: 'email' | 'totp';
},
) {
return this.authService.verifyOtpAndLogin(
body.tempToken,
body.otpCode,
body.method,
);
}
@Get('google')
@UseGuards(AuthGuard('google'))
async googleAuth() {
// Initiates Google OAuth flow
}
@Get('google/callback')
@UseGuards(AuthGuard('google'))
async googleAuthCallback(@Req() req: any, @Res() res: Response) {
// Handle Google OAuth callback
const result = await this.authService.googleLogin(req.user);
// Redirect to frontend with token or OTP requirement
const frontendUrl = process.env.WEB_APP_URL || 'http://localhost:5174';
if (result.requiresOtp) {
// Redirect to OTP page with temp token
res.redirect(
`${frontendUrl}/auth/otp?token=${result.tempToken}&methods=${JSON.stringify(result.availableMethods)}`,
);
} else {
// Redirect to app with full token
res.redirect(`${frontendUrl}/auth/callback?token=${result.token}`);
}
}
@Get('me')
@UseGuards(JwtAuthGuard)
async getProfile(@Req() req: RequestWithUser) {
return this.authService.getUserProfile(req.user.userId);
}
@Post('change-password')
@UseGuards(JwtAuthGuard)
async changePassword(
@Req() req: RequestWithUser,
@Body()
body: {
currentPassword: string;
newPassword: string;
isSettingPassword?: boolean;
},
) {
return this.authService.changePassword(
req.user.userId,
body.currentPassword,
body.newPassword,
body.isSettingPassword,
);
}
}

View File

@@ -1,36 +1,24 @@
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { FirebaseService } from './firebase.service';
import { Injectable, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard as PassportAuthGuard } from '@nestjs/passport';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private firebaseService: FirebaseService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
// If Firebase is not configured, allow all requests (development mode)
if (!this.firebaseService.isFirebaseConfigured()) {
console.warn('⚠️ Firebase not configured - allowing request without auth');
return true;
}
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException('No token provided');
}
try {
const decodedToken = await this.firebaseService.verifyIdToken(token);
request.user = decodedToken;
return true;
} catch (error) {
throw new UnauthorizedException('Invalid token');
}
export class AuthGuard extends PassportAuthGuard('jwt') {
constructor(private reflector: Reflector) {
super();
}
private extractTokenFromHeader(request: any): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
canActivate(context: ExecutionContext) {
// Check if route is marked as public
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
return super.canActivate(context);
}
}

View File

@@ -1,9 +1,25 @@
import { Module } from '@nestjs/common';
import { FirebaseService } from './firebase.service';
import { AuthGuard } from './auth.guard';
import { Module, forwardRef } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtStrategy } from './jwt.strategy';
import { GoogleStrategy } from './google.strategy';
import { PrismaModule } from '../prisma/prisma.module';
import { OtpModule } from '../otp/otp.module';
@Module({
providers: [FirebaseService, AuthGuard],
exports: [FirebaseService, AuthGuard],
imports: [
PrismaModule,
PassportModule,
forwardRef(() => OtpModule),
JwtModule.register({
secret: process.env.JWT_SECRET || 'your-secret-key',
signOptions: { expiresIn: '7d' },
}),
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy, GoogleStrategy],
exports: [AuthService],
})
export class AuthModule {}

View File

@@ -0,0 +1,479 @@
import {
Injectable,
UnauthorizedException,
BadRequestException,
ConflictException,
Inject,
forwardRef,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { PrismaService } from '../prisma/prisma.service';
import { OtpService } from '../otp/otp.service';
import * as bcrypt from 'bcrypt';
import * as fs from 'fs';
import * as path from 'path';
import axios from 'axios';
@Injectable()
export class AuthService {
constructor(
private readonly prisma: PrismaService,
private readonly jwtService: JwtService,
@Inject(forwardRef(() => OtpService))
private readonly otpService: OtpService,
) {}
async register(email: string, password: string, name?: string) {
// Check if user already exists
const existing = await this.prisma.user.findUnique({ where: { email } });
if (existing) {
throw new ConflictException('Email already registered');
}
// Hash password
const passwordHash = await bcrypt.hash(password, 10);
// Create user
const user = await this.prisma.user.create({
data: {
email,
passwordHash,
name,
emailVerified: false, // Will be verified via OTP
},
});
// Generate JWT token
const token = this.generateToken(user.id, user.email);
return {
user: {
id: user.id,
email: user.email,
name: user.name,
avatarUrl: user.avatarUrl,
emailVerified: user.emailVerified,
},
token,
};
}
async login(email: string, password: string) {
// Find user
const user = await this.prisma.user.findUnique({
where: { email },
select: {
id: true,
email: true,
passwordHash: true,
name: true,
avatarUrl: true,
emailVerified: true,
otpEmailEnabled: true,
otpWhatsappEnabled: true,
otpTotpEnabled: true,
},
});
if (!user || !user.passwordHash) {
throw new UnauthorizedException('Invalid credentials');
}
// Verify password
const isValid = await bcrypt.compare(password, user.passwordHash);
if (!isValid) {
throw new UnauthorizedException('Invalid credentials');
}
// Check if OTP is required
const requiresOtp =
user.otpEmailEnabled || user.otpWhatsappEnabled || user.otpTotpEnabled;
if (requiresOtp) {
// Send email OTP if enabled
if (user.otpEmailEnabled) {
try {
await this.otpService.sendEmailOtp(user.id);
} catch (error) {
console.error('Failed to send email OTP during login:', error);
// Continue anyway - user can request resend
}
}
// Send WhatsApp OTP if enabled (use 'live' mode for login)
if (user.otpWhatsappEnabled) {
try {
await this.otpService.sendWhatsappOtp(user.id, 'live');
} catch (error) {
console.error('Failed to send WhatsApp OTP during login:', error);
// Continue anyway - user can request resend
}
}
// Return temporary token that requires OTP verification
return {
requiresOtp: true,
availableMethods: {
email: user.otpEmailEnabled,
whatsapp: user.otpWhatsappEnabled,
totp: user.otpTotpEnabled,
},
tempToken: this.generateTempToken(user.id, user.email),
};
}
// Generate full JWT token
const token = this.generateToken(user.id, user.email);
return {
user: {
id: user.id,
email: user.email,
name: user.name,
avatarUrl: user.avatarUrl,
emailVerified: user.emailVerified,
},
token,
};
}
async googleLogin(googleProfile: {
googleId: string;
email: string;
name: string;
avatarUrl?: string;
}) {
// Find or create user
let user = await this.prisma.user.findUnique({
where: { email: googleProfile.email },
});
if (!user) {
// Create new user from Google profile
user = await this.prisma.user.create({
data: {
email: googleProfile.email,
name: googleProfile.name,
avatarUrl: googleProfile.avatarUrl,
emailVerified: true, // Google emails are pre-verified
authAccounts: {
create: {
provider: 'google',
issuer: 'google.com',
subject: googleProfile.googleId,
},
},
},
});
} else {
// Update existing user with Google account if not already linked
const existingAuth = await this.prisma.authAccount.findUnique({
where: {
issuer_subject: {
issuer: 'google.com',
subject: googleProfile.googleId,
},
},
});
if (!existingAuth) {
await this.prisma.authAccount.create({
data: {
userId: user.id,
provider: 'google',
issuer: 'google.com',
subject: googleProfile.googleId,
},
});
}
// Update user info from Google (always update to get latest avatar)
console.log('Updating user with Google profile:', {
name: googleProfile.name,
avatarUrl: googleProfile.avatarUrl,
});
// Download and store avatar locally to avoid Google rate limits
let avatarUrl = user.avatarUrl;
if (googleProfile.avatarUrl) {
try {
avatarUrl = await this.downloadAndStoreAvatar(
googleProfile.avatarUrl,
user.id,
);
} catch (error) {
console.error('Failed to download avatar:', error);
// Fallback to Google URL
avatarUrl = googleProfile.avatarUrl;
}
}
user = await this.prisma.user.update({
where: { id: user.id },
data: {
name: googleProfile.name || user.name,
avatarUrl: avatarUrl || user.avatarUrl,
emailVerified: true,
},
});
console.log('User updated, avatar:', user.avatarUrl);
}
// Check if OTP is required
const requiresOtp =
user.otpEmailEnabled || user.otpWhatsappEnabled || user.otpTotpEnabled;
if (requiresOtp) {
// Send email OTP if enabled
if (user.otpEmailEnabled) {
try {
await this.otpService.sendEmailOtp(user.id);
} catch (error) {
console.error('Failed to send email OTP during Google login:', error);
// Continue anyway - user can request resend
}
}
// Send WhatsApp OTP if enabled (use 'live' mode for login)
if (user.otpWhatsappEnabled) {
try {
await this.otpService.sendWhatsappOtp(user.id, 'live');
} catch (error) {
console.error(
'Failed to send WhatsApp OTP during Google login:',
error,
);
// Continue anyway - user can request resend
}
}
return {
requiresOtp: true,
availableMethods: {
email: user.otpEmailEnabled,
whatsapp: user.otpWhatsappEnabled,
totp: user.otpTotpEnabled,
},
tempToken: this.generateTempToken(user.id, user.email),
};
}
// Generate JWT token
const token = this.generateToken(user.id, user.email);
return {
user: {
id: user.id,
email: user.email,
name: user.name,
avatarUrl: user.avatarUrl,
emailVerified: user.emailVerified,
},
token,
};
}
async verifyOtpAndLogin(
tempToken: string,
otpCode: string,
method: 'email' | 'whatsapp' | 'totp',
) {
// Verify temp token
let payload: {
temp?: boolean;
userId?: string;
sub?: string;
email?: string;
};
try {
payload = this.jwtService.verify(tempToken);
} catch {
throw new UnauthorizedException('Invalid or expired token');
}
if (!payload.temp) {
throw new UnauthorizedException('Invalid token type');
}
const userId = payload.userId || payload.sub;
const email = payload.email;
if (!userId || !email) {
throw new UnauthorizedException('Invalid token payload');
}
// Get user to verify OTP
const user = await this.prisma.user.findUnique({
where: { id: userId },
});
if (!user) {
throw new UnauthorizedException('User not found');
}
// Verify OTP code based on method
if (method === 'email') {
// Verify email OTP using OTP service
const isValid = this.otpService.verifyEmailOtpForLogin(userId, otpCode);
if (!isValid) {
throw new UnauthorizedException('Invalid or expired email OTP code');
}
} else if (method === 'whatsapp') {
// Verify WhatsApp OTP using OTP service
const isValid = this.otpService.verifyWhatsappOtpForLogin(
userId,
otpCode,
);
if (!isValid) {
throw new UnauthorizedException('Invalid or expired WhatsApp OTP code');
}
} else if (method === 'totp') {
// Verify TOTP
if (!user.otpTotpSecret) {
throw new UnauthorizedException('TOTP not set up');
}
const { authenticator } = await import('otplib');
const isValid = authenticator.verify({
token: otpCode,
secret: user.otpTotpSecret,
});
if (!isValid) {
throw new UnauthorizedException('Invalid TOTP code');
}
}
// Generate full JWT token
const token = this.generateToken(userId, email);
return {
user: {
id: user.id,
email: user.email,
name: user.name,
avatarUrl: user.avatarUrl,
emailVerified: user.emailVerified,
},
token,
};
}
private generateToken(userId: string, email: string): string {
return this.jwtService.sign({
sub: userId,
email,
});
}
private generateTempToken(userId: string, email: string): string {
return this.jwtService.sign(
{ userId, email, temp: true },
{ expiresIn: '5m' }, // Temp token expires in 5 minutes
);
}
async getUserProfile(userId: string) {
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
email: true,
name: true,
avatarUrl: true,
emailVerified: true,
},
});
if (!user) {
throw new UnauthorizedException('User not found');
}
return user;
}
async changePassword(
userId: string,
currentPassword: string,
newPassword: string,
isSettingPassword?: boolean,
) {
// Get user with password hash
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: { passwordHash: true },
});
if (!user) {
throw new BadRequestException('User not found');
}
// If setting password for Google user (no existing password)
if (isSettingPassword && !user.passwordHash) {
// Hash new password
const newPasswordHash = await bcrypt.hash(newPassword, 10);
// Set password
await this.prisma.user.update({
where: { id: userId },
data: { passwordHash: newPasswordHash },
});
return { message: 'Password set successfully' };
}
// Otherwise, changing existing password
if (!user.passwordHash) {
throw new BadRequestException('Cannot change password for this account');
}
// Verify current password
const isValid = await bcrypt.compare(currentPassword, user.passwordHash);
if (!isValid) {
throw new UnauthorizedException('Current password is incorrect');
}
// Hash new password
const newPasswordHash = await bcrypt.hash(newPassword, 10);
// Update password
await this.prisma.user.update({
where: { id: userId },
data: { passwordHash: newPasswordHash },
});
return { message: 'Password changed successfully' };
}
private async downloadAndStoreAvatar(
avatarUrl: string,
userId: string,
): Promise<string> {
try {
// Create uploads directory if it doesn't exist
const uploadsDir = path.join(process.cwd(), 'public', 'avatars');
if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir, { recursive: true });
}
// Download image
const response = await axios.get(avatarUrl, {
responseType: 'arraybuffer',
});
// Generate filename
const ext = 'jpg'; // Google avatars are usually JPG
const filename = `${userId}.${ext}`;
const filepath = path.join(uploadsDir, filename);
// Save file
fs.writeFileSync(filepath, response.data);
// Return public URL
return `/avatars/${filename}`;
} catch (error) {
console.error('Error downloading avatar:', error);
throw error;
}
}
}

View File

@@ -1,65 +0,0 @@
import { Injectable } from '@nestjs/common';
import * as admin from 'firebase-admin';
@Injectable()
export class FirebaseService {
private app: admin.app.App | null = null;
private isConfigured: boolean = false;
constructor() {
// Only initialize Firebase if credentials are available
const projectId = process.env.FIREBASE_PROJECT_ID;
const clientEmail = process.env.FIREBASE_CLIENT_EMAIL;
const privateKey = process.env.FIREBASE_PRIVATE_KEY;
if (projectId && clientEmail && privateKey) {
try {
if (!admin.apps.length) {
this.app = admin.initializeApp({
credential: admin.credential.cert({
projectId,
clientEmail,
privateKey: privateKey.replace(/\\n/g, '\n'),
}),
});
} else {
this.app = admin.app();
}
this.isConfigured = true;
console.log('✅ Firebase Admin initialized successfully');
} catch (error) {
console.warn('⚠️ Firebase Admin initialization failed:', error.message);
this.isConfigured = false;
}
} else {
console.warn('⚠️ Firebase credentials not found. Auth will use fallback mode.');
this.isConfigured = false;
}
}
async verifyIdToken(idToken: string): Promise<admin.auth.DecodedIdToken> {
if (!this.isConfigured || !this.app) {
throw new Error('Firebase not configured');
}
try {
return await admin.auth().verifyIdToken(idToken);
} catch (error) {
throw new Error('Invalid token');
}
}
async getUser(uid: string): Promise<admin.auth.UserRecord> {
if (!this.isConfigured || !this.app) {
throw new Error('Firebase not configured');
}
try {
return await admin.auth().getUser(uid);
} catch (error) {
throw new Error('User not found');
}
}
isFirebaseConfigured(): boolean {
return this.isConfigured;
}
}

View File

@@ -0,0 +1,35 @@
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, VerifyCallback } from 'passport-google-oauth20';
@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
constructor() {
super({
clientID: process.env.GOOGLE_CLIENT_ID || '',
clientSecret: process.env.GOOGLE_CLIENT_SECRET || '',
callbackURL:
process.env.GOOGLE_CALLBACK_URL ||
'http://localhost:3001/api/auth/google/callback',
scope: ['email', 'profile'],
});
}
async validate(
accessToken: string,
refreshToken: string,
profile: any,
done: VerifyCallback,
): Promise<any> {
const { id, name, emails, photos } = profile;
const user = {
googleId: id,
email: emails[0].value,
name: name.givenName + ' ' + name.familyName,
avatarUrl: photos[0]?.value,
};
done(null, user);
}
}

View File

@@ -0,0 +1,25 @@
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
export interface JwtPayload {
sub: string; // user ID
email: string;
iat?: number;
exp?: number;
}
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.JWT_SECRET || 'your-secret-key-change-this',
});
}
async validate(payload: JwtPayload) {
return { userId: payload.sub, email: payload.email };
}
}

View File

@@ -1,38 +1,34 @@
import {
Controller,
Get,
Post,
Body,
Param,
Delete,
} from '@nestjs/common';
import { Controller, Get, Post, Body, Param, Delete, Req, UseGuards } from '@nestjs/common';
import { CategoriesService } from '../categories/categories.service';
import { CreateCategoryDto } from '../categories/dto/create-category.dto';
import { getTempUserId } from '../common/user.util';
import { AuthGuard } from '../auth/auth.guard';
interface RequestWithUser {
user: {
userId: string;
};
}
@Controller('categories')
@UseGuards(AuthGuard)
export class CategoriesController {
constructor(private readonly categoriesService: CategoriesService) {}
private userId(): string {
return getTempUserId();
}
@Post()
create(@Body() createCategoryDto: CreateCategoryDto) {
create(@Req() req: RequestWithUser, @Body() createCategoryDto: CreateCategoryDto) {
return this.categoriesService.create({
...createCategoryDto,
userId: this.userId(),
userId: req.user.userId,
});
}
@Get()
findAll() {
return this.categoriesService.findAll(this.userId());
findAll(@Req() req: RequestWithUser) {
return this.categoriesService.findAll(req.user.userId);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.categoriesService.remove(id, this.userId());
remove(@Req() req: RequestWithUser, @Param('id') id: string) {
return this.categoriesService.remove(id, req.user.userId);
}
}
}

View File

@@ -1,4 +1,8 @@
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import {
Injectable,
NotFoundException,
ConflictException,
} from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateCategoryDto } from './dto/create-category.dto';
@@ -45,7 +49,7 @@ export class CategoriesService {
async findOrCreate(names: string[], userId: string) {
const categories: any[] = [];
for (const name of names) {
let category = await this.prisma.category.findFirst({
where: { name, userId },

View File

@@ -1,17 +1,19 @@
export function getTempUserId(): string {
const id = process.env.TEMP_USER_ID?.trim();
if (!id) {
throw new Error('TEMP_USER_ID is not set. Run the seed and set it in apps/api/.env');
}
return id;
const id = process.env.TEMP_USER_ID?.trim();
if (!id) {
throw new Error(
'TEMP_USER_ID is not set. Run the seed and set it in apps/api/.env',
);
}
return id;
}
export function getUserIdFromRequest(request: any): string {
// If Firebase user is authenticated, use their UID
if (request.user?.uid) {
return request.user.uid;
}
// Fallback to temp user for development
return getTempUserId();
}
@@ -21,4 +23,4 @@ export function createUserDecorator() {
// This is a placeholder for a proper decorator implementation
// In a real app, you'd create a proper parameter decorator
};
}
}

View File

@@ -16,4 +16,4 @@ export class HealthController {
await this.prisma.$queryRaw`SELECT 1`;
return { db: 'connected' };
}
}
}

View File

@@ -1,8 +1,13 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const app = await NestFactory.create<NestExpressApplication>(AppModule);
// Serve static files from public directory
app.useStaticAssets(join(__dirname, '..', 'public'));
// Allow web app to call API in dev
const webOrigin = process.env.WEB_APP_URL ?? 'http://localhost:5173';
@@ -16,7 +21,8 @@ async function bootstrap() {
const port = process.env.PORT ? Number(process.env.PORT) : 3000;
await app.listen(port);
// eslint-disable-next-line no-console
console.log(`API listening on http://localhost:${port}`);
console.log(`API listening on ${await app.getUrl()}`);
}
bootstrap();
void bootstrap();

View File

@@ -0,0 +1,72 @@
import {
Injectable,
CanActivate,
ExecutionContext,
UnauthorizedException,
} from '@nestjs/common';
import { OtpService } from './otp.service';
interface RequestWithUser {
user: {
userId: string;
};
headers: Record<string, string>;
body?: {
otpCode?: string;
otpMethod?: string;
};
}
@Injectable()
export class OtpGateGuard implements CanActivate {
constructor(private otpService: OtpService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<RequestWithUser>();
// Get userId from JWT (set by AuthGuard)
const userId = request.user?.userId;
if (!userId) {
// If no user, let AuthGuard handle it
return true;
}
// Check if user has OTP enabled
const status = await this.otpService.getStatus(userId);
// If no OTP methods are enabled, allow access
if (!status.emailEnabled && !status.totpEnabled) {
return true;
}
// Check for OTP verification in headers or body
const otpCode = request.headers['x-otp-code'] || request.body?.otpCode;
const otpMethod = (request.headers['x-otp-method'] ||
request.body?.otpMethod ||
'totp') as 'email' | 'totp';
if (!otpCode) {
throw new UnauthorizedException({
message: 'OTP verification required',
requiresOtp: true,
availableMethods: {
email: status.emailEnabled,
totp: status.totpEnabled,
},
});
}
// Verify the OTP
const isValid = await this.otpService.verifyOtpGate(
userId,
otpCode,
otpMethod,
);
if (!isValid) {
throw new UnauthorizedException('Invalid OTP code');
}
return true;
}
}

View File

@@ -0,0 +1,150 @@
import {
Controller,
Get,
Post,
Body,
UseGuards,
Req,
UnauthorizedException,
SetMetadata,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { AuthGuard } from '../auth/auth.guard';
import { OtpService } from './otp.service';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
interface RequestWithUser extends Request {
user: {
userId: string;
email: string;
};
}
@Controller('otp')
@UseGuards(AuthGuard)
export class OtpController {
constructor(
private readonly otpService: OtpService,
private readonly jwtService: JwtService,
) {}
@Get('status')
async getStatus(@Req() req: RequestWithUser) {
return this.otpService.getStatus(req.user.userId);
}
@Post('email/send')
async sendEmailOtp(@Req() req: RequestWithUser) {
return this.otpService.sendEmailOtp(req.user.userId);
}
@Post('email/verify')
async verifyEmailOtp(
@Req() req: RequestWithUser,
@Body() body: { code: string },
) {
return this.otpService.verifyEmailOtp(req.user.userId, body.code);
}
@Post('email/disable')
async disableEmailOtp(@Req() req: RequestWithUser) {
return this.otpService.disableEmailOtp(req.user.userId);
}
@Post('totp/setup')
async setupTotp(@Req() req: RequestWithUser) {
return this.otpService.setupTotp(req.user.userId);
}
@Post('totp/verify')
async verifyTotp(
@Req() req: RequestWithUser,
@Body() body: { code: string },
) {
return this.otpService.verifyTotp(req.user.userId, body.code);
}
@Post('totp/disable')
async disableTotp(@Req() req: RequestWithUser) {
return this.otpService.disableTotp(req.user.userId);
}
@Post('whatsapp/send')
async sendWhatsappOtp(
@Req() req: RequestWithUser,
@Body() body: { mode?: 'test' | 'live' },
) {
return this.otpService.sendWhatsappOtp(
req.user.userId,
body.mode || 'test',
);
}
@Post('whatsapp/verify')
async verifyWhatsappOtp(
@Req() req: RequestWithUser,
@Body() body: { code: string },
) {
return this.otpService.verifyWhatsappOtp(req.user.userId, body.code);
}
@Post('whatsapp/disable')
async disableWhatsappOtp(@Req() req: RequestWithUser) {
return this.otpService.disableWhatsappOtp(req.user.userId);
}
@Post('whatsapp/check')
async checkWhatsappNumber(@Body() body: { phone: string }) {
return this.otpService.checkWhatsappNumber(body.phone);
}
@Public()
@Post('email/resend')
async resendEmailOtp(@Body() body: { tempToken: string }) {
try {
// Verify temp token
const payload = this.jwtService.verify(body.tempToken);
if (!payload.temp) {
throw new UnauthorizedException('Invalid token type');
}
const userId = payload.userId || payload.sub;
if (!userId) {
throw new UnauthorizedException('Invalid token payload');
}
// Send OTP
return this.otpService.sendEmailOtp(userId);
} catch {
throw new UnauthorizedException('Invalid or expired token');
}
}
@Public()
@Post('whatsapp/resend')
async resendWhatsappOtp(@Body() body: { tempToken: string }) {
try {
// Verify temp token
const payload = this.jwtService.verify(body.tempToken);
if (!payload.temp) {
throw new UnauthorizedException('Invalid token type');
}
const userId = payload.userId || payload.sub;
if (!userId) {
throw new UnauthorizedException('Invalid token payload');
}
// Send WhatsApp OTP (live mode for login)
return this.otpService.sendWhatsappOtp(userId, 'live');
} catch {
throw new UnauthorizedException('Invalid or expired token');
}
}
}

View File

@@ -0,0 +1,22 @@
import { Module, forwardRef } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { OtpController } from './otp.controller';
import { OtpService } from './otp.service';
import { OtpGateGuard } from './otp-gate.guard';
import { AuthModule } from '../auth/auth.module';
import { PrismaModule } from '../prisma/prisma.module';
@Module({
imports: [
forwardRef(() => AuthModule),
PrismaModule,
JwtModule.register({
secret: process.env.JWT_SECRET || 'your-secret-key',
signOptions: { expiresIn: '7d' },
}),
],
controllers: [OtpController],
providers: [OtpService, OtpGateGuard],
exports: [OtpService, OtpGateGuard],
})
export class OtpModule {}

View File

@@ -0,0 +1,436 @@
import { Injectable, BadRequestException } from '@nestjs/common';
import { authenticator } from 'otplib';
import { PrismaService } from '../prisma/prisma.service';
import axios from 'axios';
import * as QRCode from 'qrcode';
@Injectable()
export class OtpService {
private emailOtpStore = new Map<string, { code: string; expiresAt: Date }>();
private whatsappOtpStore = new Map<
string,
{ code: string; expiresAt: Date }
>();
constructor(private prisma: PrismaService) {}
async sendEmailOtp(
userId: string,
): Promise<{ success: boolean; message: string }> {
const user = await this.prisma.user.findUnique({ where: { id: userId } });
if (!user) {
throw new BadRequestException('User not found');
}
const code = this.generateOtpCode();
const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes
// Store the code
this.emailOtpStore.set(userId, { code, expiresAt });
// Send via webhook (you'll handle the actual email sending)
try {
await this.sendOtpViaWebhook(user.email, code);
return { success: true, message: 'OTP sent to your email' };
} catch (error: unknown) {
console.error('Failed to send OTP via webhook:', error);
// For development, log the code
console.log(`📧 OTP Code for ${user.email}: ${code}`);
return {
success: true,
message: 'OTP sent (check console for dev code)',
};
}
}
// Verify email OTP for login (doesn't enable the feature)
verifyEmailOtpForLogin(userId: string, code: string): boolean {
const stored = this.emailOtpStore.get(userId);
if (!stored) {
return false;
}
if (new Date() > stored.expiresAt) {
this.emailOtpStore.delete(userId);
return false;
}
if (stored.code !== code) {
return false;
}
// Clean up
this.emailOtpStore.delete(userId);
return true;
}
// Verify and enable email OTP (for setup)
async verifyEmailOtp(
userId: string,
code: string,
): Promise<{ success: boolean; message: string }> {
const stored = this.emailOtpStore.get(userId);
if (!stored) {
throw new BadRequestException('No OTP found. Please request a new one.');
}
if (new Date() > stored.expiresAt) {
this.emailOtpStore.delete(userId);
throw new BadRequestException(
'OTP has expired. Please request a new one.',
);
}
if (stored.code !== code) {
throw new BadRequestException('Invalid OTP code.');
}
// Enable email OTP in database
await this.prisma.user.update({
where: { id: userId },
data: { otpEmailEnabled: true },
});
// Clean up
this.emailOtpStore.delete(userId);
return { success: true, message: 'Email OTP enabled successfully' };
}
async disableEmailOtp(
userId: string,
): Promise<{ success: boolean; message: string }> {
await this.prisma.user.update({
where: { id: userId },
data: { otpEmailEnabled: false },
});
return { success: true, message: 'Email OTP disabled' };
}
async setupTotp(userId: string): Promise<{ secret: string; qrCode: string }> {
const user = await this.prisma.user.findUnique({ where: { id: userId } });
if (!user) {
throw new BadRequestException('User not found');
}
const secret = authenticator.generateSecret();
// Store the secret in database (not yet enabled)
await this.prisma.user.update({
where: { id: userId },
data: { otpTotpSecret: secret },
});
// Generate QR code URL for Google Authenticator
const serviceName = 'Tabungin';
const accountName = user.email;
const otpauthUrl = authenticator.keyuri(accountName, serviceName, secret);
// Generate QR code as data URL
const qrCodeDataUrl = await QRCode.toDataURL(otpauthUrl);
return {
secret,
qrCode: qrCodeDataUrl,
};
}
async verifyTotp(
userId: string,
code: string,
): Promise<{ success: boolean; message: string }> {
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: { otpTotpSecret: true },
});
if (!user?.otpTotpSecret) {
throw new BadRequestException(
'No TOTP setup found. Please setup TOTP first.',
);
}
const isValid = authenticator.verify({
token: code,
secret: user.otpTotpSecret,
});
if (!isValid) {
throw new BadRequestException('Invalid TOTP code.');
}
// Enable TOTP in database
await this.prisma.user.update({
where: { id: userId },
data: { otpTotpEnabled: true },
});
return { success: true, message: 'TOTP enabled successfully' };
}
async disableTotp(
userId: string,
): Promise<{ success: boolean; message: string }> {
await this.prisma.user.update({
where: { id: userId },
data: {
otpTotpEnabled: false,
otpTotpSecret: null,
},
});
return { success: true, message: 'TOTP disabled' };
}
async getStatus(userId: string) {
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: {
phone: true,
otpEmailEnabled: true,
otpWhatsappEnabled: true,
otpTotpEnabled: true,
otpTotpSecret: true,
},
});
if (!user) {
return {
emailEnabled: false,
whatsappEnabled: false,
totpEnabled: false,
};
}
return {
phone: user.phone,
emailEnabled: user.otpEmailEnabled,
whatsappEnabled: user.otpWhatsappEnabled,
totpEnabled: user.otpTotpEnabled,
totpSecret: user.otpTotpSecret,
};
}
// OTP Gate - verify user's OTP during login
async verifyOtpGate(
userId: string,
code: string,
method: 'email' | 'totp',
): Promise<boolean> {
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: {
otpEmailEnabled: true,
otpTotpEnabled: true,
otpTotpSecret: true,
},
});
if (!user) {
return false;
}
if (method === 'email' && user.otpEmailEnabled) {
// For login, we'd need to send a fresh OTP first
const stored = this.emailOtpStore.get(userId);
if (stored && new Date() <= stored.expiresAt && stored.code === code) {
return true;
}
}
if (method === 'totp' && user.otpTotpEnabled && user.otpTotpSecret) {
return authenticator.verify({ token: code, secret: user.otpTotpSecret });
}
return false;
}
private generateOtpCode(): string {
return Math.floor(100000 + Math.random() * 900000).toString();
}
private async sendOtpViaWebhook(
email: string,
code: string,
mode: 'test' | 'live' = 'test',
): Promise<void> {
// Use test webhook if available, otherwise use production webhook
const webhookUrl =
process.env.OTP_SEND_WEBHOOK_URL_TEST || process.env.OTP_SEND_WEBHOOK_URL;
if (!webhookUrl) {
throw new Error(
'OTP_SEND_WEBHOOK_URL or OTP_SEND_WEBHOOK_URL_TEST not configured',
);
}
await axios.post(webhookUrl, {
method: 'email',
mode, // 'test' or 'live'
to: email,
subject: 'Tabungin - Your OTP Code',
message: `Your OTP code is: ${code}. This code will expire in 10 minutes.`,
code,
});
}
// WhatsApp OTP methods
async sendWhatsappOtp(
userId: string,
mode: 'test' | 'live' = 'test',
): Promise<{ success: boolean; message: string }> {
const user = await this.prisma.user.findUnique({ where: { id: userId } });
if (!user) {
throw new BadRequestException('User not found');
}
if (!user.phone) {
throw new BadRequestException('Phone number not set');
}
const code = this.generateOtpCode();
const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes
// Store the code
this.whatsappOtpStore.set(userId, { code, expiresAt });
// Send via webhook
try {
await this.sendWhatsappOtpViaWebhook(user.phone, code, mode);
return { success: true, message: 'OTP sent to your WhatsApp' };
} catch (error: unknown) {
console.error('Failed to send WhatsApp OTP via webhook:', error);
// For development, log the code
console.log(`📱 WhatsApp OTP Code for ${user.phone}: ${code}`);
return {
success: true,
message: 'OTP sent (check console for dev code)',
};
}
}
async verifyWhatsappOtp(
userId: string,
code: string,
): Promise<{ success: boolean; message: string }> {
const stored = this.whatsappOtpStore.get(userId);
if (!stored) {
throw new BadRequestException('No OTP found. Please request a new one.');
}
if (new Date() > stored.expiresAt) {
this.whatsappOtpStore.delete(userId);
throw new BadRequestException(
'OTP has expired. Please request a new one.',
);
}
if (stored.code !== code) {
throw new BadRequestException('Invalid OTP code');
}
// OTP is valid, enable WhatsApp OTP
await this.prisma.user.update({
where: { id: userId },
data: { otpWhatsappEnabled: true },
});
// Clear the OTP
this.whatsappOtpStore.delete(userId);
return { success: true, message: 'WhatsApp OTP enabled successfully' };
}
verifyWhatsappOtpForLogin(userId: string, code: string): boolean {
const stored = this.whatsappOtpStore.get(userId);
if (!stored) {
return false;
}
if (new Date() > stored.expiresAt) {
this.whatsappOtpStore.delete(userId);
return false;
}
if (stored.code !== code) {
return false;
}
// Clear the OTP
this.whatsappOtpStore.delete(userId);
return true;
}
async disableWhatsappOtp(
userId: string,
): Promise<{ success: boolean; message: string }> {
await this.prisma.user.update({
where: { id: userId },
data: { otpWhatsappEnabled: false },
});
return { success: true, message: 'WhatsApp OTP disabled' };
}
async checkWhatsappNumber(
phone: string,
): Promise<{ success: boolean; isRegistered: boolean; message: string }> {
// Send check request to webhook
try {
const webhookUrl =
process.env.OTP_SEND_WEBHOOK_URL_TEST ||
process.env.OTP_SEND_WEBHOOK_URL;
if (!webhookUrl) {
throw new Error('Webhook URL not configured');
}
const response = await axios.post(webhookUrl, {
method: 'whatsapp',
mode: 'checknumber',
phone,
});
return {
success: true,
isRegistered: response.data?.isRegistered || false,
message: response.data?.message || 'Number checked',
};
} catch (error: unknown) {
console.error('Failed to check WhatsApp number:', error);
// For development, assume number is valid
console.log(`📱 Checking WhatsApp number: ${phone} - Assumed valid`);
return {
success: true,
isRegistered: true,
message: 'Number is valid (dev mode)',
};
}
}
private async sendWhatsappOtpViaWebhook(
phone: string,
code: string,
mode: 'test' | 'live' = 'test',
): Promise<void> {
const webhookUrl =
process.env.OTP_SEND_WEBHOOK_URL_TEST || process.env.OTP_SEND_WEBHOOK_URL;
if (!webhookUrl) {
throw new Error('Webhook URL not configured');
}
await axios.post(webhookUrl, {
method: 'whatsapp',
mode, // 'test' or 'live'
phone,
message: `Your Tabungin OTP code is: ${code}. This code will expire in 10 minutes.`,
code,
});
}
}

View File

@@ -6,4 +6,4 @@ import { PrismaService } from './prisma.service';
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}
export class PrismaModule {}

View File

@@ -13,4 +13,4 @@ export class PrismaService extends PrismaClient implements OnModuleInit {
await app.close();
});
}
}
}

View File

@@ -1,21 +1,26 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
const adminSeeder = {
email: 'dwindi.ramadhana@gmail.com',
password: 'tabungin2k25!@#',
}
const TEMP_USER_ID =
process.env.TEMP_USER_ID || '16b74848-daa3-4dc9-8de2-3cf59e08f8e3';
async function main() {
const userId = '16b74848-daa3-4dc9-8de2-3cf59e08f8e3';
const user = await prisma.user.upsert({
where: { id: userId },
where: { id: TEMP_USER_ID },
update: {},
create: {
id: userId,
id: TEMP_USER_ID,
email: 'temp@example.com',
},
});
// create a sample money wallet if none
const existing = await prisma.wallet.findFirst({
where: { userId: user.id, kind: 'money' },
});
const existing = await prisma.wallet.findFirst({});
if (!existing) {
await prisma.wallet.create({
@@ -38,4 +43,4 @@ main()
})
.finally(async () => {
await prisma.$disconnect();
});
});

View File

@@ -2,9 +2,9 @@ import { z } from 'zod';
export const TransactionUpdateSchema = z.object({
amount: z.number().positive().optional(),
direction: z.enum(['in','out']).optional(),
date: z.string().datetime().optional(), // ISO string
direction: z.enum(['in', 'out']).optional(),
date: z.string().datetime().optional(), // ISO string
category: z.string().min(1).nullable().optional(),
memo: z.string().min(1).nullable().optional(),
});
export type TransactionUpdateDto = z.infer<typeof TransactionUpdateSchema>;
export type TransactionUpdateDto = z.infer<typeof TransactionUpdateSchema>;

View File

@@ -1,39 +1,77 @@
import { BadRequestException, Body, Controller, Get, Param, Post, Query, Res, Put, Delete } from '@nestjs/common';
import {
BadRequestException,
Body,
Controller,
Get,
Post,
Put,
Param,
Delete,
UseGuards,
Query,
Res,
Req,
} from '@nestjs/common';
import type { Response } from 'express';
import { AuthGuard } from '../auth/auth.guard';
import { TransactionsService } from './transactions.service';
import { TransactionUpdateSchema } from './transaction.dto';
interface RequestWithUser {
user: {
userId: string;
};
}
@Controller('wallets/:walletId/transactions')
@UseGuards(AuthGuard)
export class TransactionsController {
constructor(private readonly tx: TransactionsService) {}
@Get()
list(@Param('walletId') walletId: string) {
return this.tx.list(walletId);
list(@Req() req: RequestWithUser, @Param('walletId') walletId: string) {
return this.tx.list(req.user.userId, walletId);
}
@Post()
create(
@Req() req: RequestWithUser,
@Param('walletId') walletId: string,
@Body() body: { amount: number | string; direction: 'in' | 'out'; date?: string; category?: string; memo?: string }
@Body()
body: {
amount: number | string;
direction: 'in' | 'out';
date?: string;
category?: string;
memo?: string;
},
) {
return this.tx.create(walletId, body);
return this.tx.create(req.user.userId, walletId, body);
}
@Get('export.csv')
async exportCsv(
@Req() req: RequestWithUser,
@Param('walletId') walletId: string,
@Query('from') from: string | undefined,
@Query('to') to: string | undefined,
@Query('category') category: string | undefined,
@Query('direction') direction: 'in' | 'out' | undefined,
@Res() res: Response
@Res() res: Response,
) {
const rows = await this.tx.listWithFilters(walletId, { from, to, category, direction });
const rows = await this.tx.listWithFilters(req.user.userId, walletId, {
from,
to,
category,
direction,
});
// CSV headers
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader('Content-Disposition', `attachment; filename="transactions_${walletId}.csv"`);
res.setHeader(
'Content-Disposition',
`attachment; filename="transactions_${walletId}.csv"`,
);
// Write CSV header row
res.write(`date,category,memo,direction,amount\n`);
@@ -60,17 +98,27 @@ export class TransactionsController {
}
@Put(':id')
async update(@Param('walletId') walletId: string, @Param('id') id: string, @Body() body: unknown) {
async update(
@Req() req: RequestWithUser,
@Param('walletId') walletId: string,
@Param('id') id: string,
@Body() body: unknown,
) {
try {
const parsed = TransactionUpdateSchema.parse(body);
return this.tx.update(walletId, id, parsed);
} catch (e: any) {
throw new BadRequestException(e?.errors ?? 'Invalid payload');
return this.tx.update(req.user.userId, walletId, id, parsed);
} catch (e) {
const error = e as { errors?: unknown };
throw new BadRequestException(error?.errors ?? 'Invalid payload');
}
}
@Delete(':id')
delete(@Param('walletId') walletId: string, @Param('id') id: string) {
return this.tx.delete(walletId, id);
delete(
@Req() req: RequestWithUser,
@Param('walletId') walletId: string,
@Param('id') id: string,
) {
return this.tx.delete(req.user.userId, walletId, id);
}
}
}

View File

@@ -2,11 +2,12 @@ import { Module } from '@nestjs/common';
import { TransactionsService } from './transactions.service';
import { TransactionsController } from './transactions.controller';
import { PrismaModule } from '../prisma/prisma.module';
import { OtpModule } from '../otp/otp.module';
@Module({
imports: [PrismaModule],
imports: [PrismaModule, OtpModule],
providers: [TransactionsService],
controllers: [TransactionsController],
exports: [TransactionsService],
})
export class TransactionsModule {}
export class TransactionsModule {}

View File

@@ -1,6 +1,5 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { getTempUserId } from '../common/user.util';
import { Prisma } from '@prisma/client';
import type { TransactionUpdateDto } from './transaction.dto';
@@ -8,35 +7,37 @@ import type { TransactionUpdateDto } from './transaction.dto';
export class TransactionsService {
constructor(private prisma: PrismaService) {}
private userId(): string {
return getTempUserId();
}
list(walletId: string) {
list(userId: string, walletId: string) {
return this.prisma.transaction.findMany({
where: { userId: this.userId(), walletId },
where: { userId, walletId },
orderBy: { date: 'desc' },
take: 200,
});
}
listAll() {
listAll(userId: string) {
return this.prisma.transaction.findMany({
where: { userId: this.userId() },
where: { userId },
orderBy: { date: 'desc' },
take: 1000,
});
}
listWithFilters(
userId: string,
walletId: string,
filters: { from?: string; to?: string; category?: string; direction?: 'in' | 'out' }
filters: {
from?: string;
to?: string;
category?: string;
direction?: 'in' | 'out';
},
) {
const where: Prisma.TransactionWhereInput = {
userId: getTempUserId(),
userId,
walletId,
};
if (filters.direction) where.direction = filters.direction;
if (filters.category) where.category = filters.category;
if (filters.from || filters.to) {
@@ -44,32 +45,39 @@ export class TransactionsService {
if (filters.from) (where.date as any).gte = new Date(filters.from);
if (filters.to) (where.date as any).lte = new Date(filters.to);
}
return this.prisma.transaction.findMany({
where,
orderBy: { date: 'desc' },
});
}
async create(walletId: string, input: {
amount: string | number; direction: 'in' | 'out';
date?: string; category?: string; memo?: string;
}) {
const amountNum = typeof input.amount === 'string' ? Number(input.amount) : input.amount;
async create(
userId: string,
walletId: string,
input: {
amount: string | number;
direction: 'in' | 'out';
date?: string;
category?: string;
memo?: string;
},
) {
const amountNum =
typeof input.amount === 'string' ? Number(input.amount) : input.amount;
if (!Number.isFinite(amountNum)) throw new Error('amount must be a number');
const date = input.date ? new Date(input.date) : new Date();
const wallet = await this.prisma.wallet.findFirst({
where: { id: walletId, userId: this.userId(), deletedAt: null },
where: { id: walletId, userId, deletedAt: null },
select: { id: true },
});
if (!wallet) throw new Error('wallet not found');
return this.prisma.transaction.create({
data: {
userId: this.userId(),
userId,
walletId,
amount: amountNum,
direction: input.direction,
@@ -80,14 +88,18 @@ export class TransactionsService {
});
}
async update(walletId: string, id: string, dto: TransactionUpdateDto) {
async update(
userId: string,
walletId: string,
id: string,
dto: TransactionUpdateDto,
) {
// ensure the row exists and belongs to the current user + wallet
const existing = await this.prisma.transaction.findFirst({
where: { id, walletId, userId: this.userId() },
where: { id, walletId, userId },
});
if (!existing) throw new Error('transaction not found');
// normalize inputs
const data: any = {};
if (dto.amount !== undefined) data.amount = Number(dto.amount);
@@ -95,17 +107,17 @@ export class TransactionsService {
if (dto.category !== undefined) data.category = dto.category || null;
if (dto.memo !== undefined) data.memo = dto.memo || null;
if (dto.date !== undefined) data.date = new Date(dto.date);
return this.prisma.transaction.update({
where: { id: existing.id },
data,
});
}
async delete(walletId: string, id: string) {
async delete(userId: string, walletId: string, id: string) {
// ensure the row exists and belongs to the current user + wallet
const existing = await this.prisma.transaction.findFirst({
where: { id, walletId, userId: this.userId() },
where: { id, walletId, userId },
});
if (!existing) throw new Error('transaction not found');
@@ -113,4 +125,4 @@ export class TransactionsService {
where: { id: existing.id },
});
}
}
}

View File

@@ -1,7 +1,16 @@
import { Controller, Get } from '@nestjs/common';
import { Controller, Get, Put, Delete, Body, Req, UseGuards } from '@nestjs/common';
import { AuthGuard } from '../auth/auth.guard';
import { UsersService } from './users.service';
interface RequestWithUser extends Request {
user: {
userId: string;
email: string;
};
}
@Controller('users')
@UseGuards(AuthGuard)
export class UsersController {
constructor(private readonly users: UsersService) {}
@@ -9,4 +18,25 @@ export class UsersController {
me() {
return this.users.me();
}
}
@Put('profile')
async updateProfile(
@Req() req: RequestWithUser,
@Body() body: { name?: string; phone?: string },
) {
return this.users.updateProfile(req.user.userId, body);
}
@Get('auth-info')
async getAuthInfo(@Req() req: RequestWithUser) {
return this.users.getAuthInfo(req.user.userId);
}
@Delete('account')
async deleteAccount(
@Req() req: RequestWithUser,
@Body() body: { password: string },
) {
return this.users.deleteAccount(req.user.userId, body.password);
}
}

View File

@@ -9,4 +9,4 @@ import { PrismaModule } from '../prisma/prisma.module';
controllers: [UsersController],
exports: [UsersService],
})
export class UsersModule {}
export class UsersModule {}

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common';
import { Injectable, BadRequestException, UnauthorizedException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { getTempUserId } from '../common/user.util';
import * as bcrypt from 'bcrypt';
@Injectable()
export class UsersService {
@@ -10,4 +11,101 @@ export class UsersService {
const userId = getTempUserId();
return this.prisma.user.findUnique({ where: { id: userId } });
}
}
async updateProfile(userId: string, data: { name?: string; phone?: string }) {
try {
const user = await this.prisma.user.update({
where: { id: userId },
data: {
...(data.name !== undefined && { name: data.name }),
...(data.phone !== undefined && { phone: data.phone }),
},
select: {
id: true,
email: true,
name: true,
phone: true,
avatarUrl: true,
},
});
return {
success: true,
message: 'Profile updated successfully',
user,
};
} catch (error: any) {
if (error.code === 'P2002') {
throw new BadRequestException('Phone number already in use');
}
throw error;
}
}
async getAuthInfo(userId: string) {
// Get user with password hash and avatar
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: {
passwordHash: true,
avatarUrl: true,
},
});
// Check if user has Google OAuth (avatar from Google or starts with /avatars/)
const hasGoogleAuth =
user?.avatarUrl?.includes('googleusercontent.com') ||
user?.avatarUrl?.startsWith('/avatars/') ||
false;
return {
hasGoogleAuth,
hasPassword: user?.passwordHash !== null,
};
}
async deleteAccount(userId: string, password: string) {
// Get user with password hash
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: {
passwordHash: true,
},
});
if (!user) {
throw new BadRequestException('User not found');
}
if (!user.passwordHash) {
throw new BadRequestException(
'Cannot delete account without password. Please set a password first.',
);
}
// Verify password
const isValid = await bcrypt.compare(password, user.passwordHash);
if (!isValid) {
throw new UnauthorizedException('Incorrect password');
}
// Delete related data first (to avoid foreign key constraint errors)
// Delete AuthAccount records
await this.prisma.authAccount.deleteMany({
where: { userId: userId },
});
// Delete other related data if any
// Add more deleteMany calls here for other tables that reference User
// Finally, delete the user
await this.prisma.user.delete({
where: { id: userId },
});
return {
success: true,
message: 'Account deleted successfully',
};
}
}

Some files were not shown because too many files have changed in this diff Show More