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,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":"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"}