Add spreadsheet-style Monthly Actuals page for entering monthly actuals against budget with auto-generated journal entries and reconciliation flag. Add file attachment support (PDF, images, spreadsheets) on journal entries for receipts and invoices. Enhance Budget vs Actual report with month filter dropdown. Add reconciled badge to Transactions page. Replace bcrypt with bcryptjs to fix Docker cross-platform native binding issues. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
124 lines
4.0 KiB
TypeScript
124 lines
4.0 KiB
TypeScript
import { Controller, Get, Post, Put, Body, Param, UseGuards, Req, ForbiddenException, BadRequestException } from '@nestjs/common';
|
|
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
|
import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
|
import { UsersService } from '../users/users.service';
|
|
import { OrganizationsService } from '../organizations/organizations.service';
|
|
import * as bcrypt from 'bcryptjs';
|
|
|
|
@ApiTags('admin')
|
|
@Controller('admin')
|
|
@ApiBearerAuth()
|
|
@UseGuards(JwtAuthGuard)
|
|
export class AdminController {
|
|
constructor(
|
|
private usersService: UsersService,
|
|
private orgService: OrganizationsService,
|
|
) {}
|
|
|
|
private async requireSuperadmin(req: any) {
|
|
const user = await this.usersService.findById(req.user.userId || req.user.sub);
|
|
if (!user?.isSuperadmin) {
|
|
throw new ForbiddenException('SuperUser Admin access required');
|
|
}
|
|
}
|
|
|
|
@Get('users')
|
|
async listUsers(@Req() req: any) {
|
|
await this.requireSuperadmin(req);
|
|
const users = await this.usersService.findAllUsers();
|
|
return users.map(u => ({
|
|
id: u.id, email: u.email, firstName: u.firstName, lastName: u.lastName,
|
|
isSuperadmin: u.isSuperadmin, lastLoginAt: u.lastLoginAt, createdAt: u.createdAt,
|
|
organizations: u.userOrganizations?.map(uo => ({
|
|
id: uo.organizationId, name: uo.organization?.name, role: uo.role,
|
|
})) || [],
|
|
}));
|
|
}
|
|
|
|
@Get('organizations')
|
|
async listOrganizations(@Req() req: any) {
|
|
await this.requireSuperadmin(req);
|
|
return this.usersService.findAllOrganizations();
|
|
}
|
|
|
|
@Post('users/:id/superadmin')
|
|
async toggleSuperadmin(@Req() req: any, @Param('id') id: string, @Body() body: { isSuperadmin: boolean }) {
|
|
await this.requireSuperadmin(req);
|
|
await this.usersService.setSuperadmin(id, body.isSuperadmin);
|
|
return { success: true };
|
|
}
|
|
|
|
@Post('tenants')
|
|
async createTenant(@Req() req: any, @Body() body: {
|
|
orgName: string;
|
|
email?: string;
|
|
phone?: string;
|
|
addressLine1?: string;
|
|
city?: string;
|
|
state?: string;
|
|
zipCode?: string;
|
|
contractNumber?: string;
|
|
planLevel?: string;
|
|
fiscalYearStartMonth?: number;
|
|
adminEmail: string;
|
|
adminPassword: string;
|
|
adminFirstName: string;
|
|
adminLastName: string;
|
|
}) {
|
|
await this.requireSuperadmin(req);
|
|
|
|
if (!body.orgName || !body.adminEmail || !body.adminPassword) {
|
|
throw new BadRequestException('Organization name, admin email and password are required');
|
|
}
|
|
|
|
// Check if admin email already exists
|
|
const existingUser = await this.usersService.findByEmail(body.adminEmail);
|
|
let userId: string;
|
|
|
|
if (existingUser) {
|
|
userId = existingUser.id;
|
|
} else {
|
|
// Create the first user for this tenant
|
|
const passwordHash = await bcrypt.hash(body.adminPassword, 12);
|
|
const newUser = await this.usersService.create({
|
|
email: body.adminEmail,
|
|
passwordHash,
|
|
firstName: body.adminFirstName,
|
|
lastName: body.adminLastName,
|
|
});
|
|
userId = newUser.id;
|
|
}
|
|
|
|
// Create the organization + tenant schema + membership
|
|
const org = await this.orgService.create({
|
|
name: body.orgName,
|
|
email: body.email,
|
|
phone: body.phone,
|
|
addressLine1: body.addressLine1,
|
|
city: body.city,
|
|
state: body.state,
|
|
zipCode: body.zipCode,
|
|
contractNumber: body.contractNumber,
|
|
planLevel: body.planLevel || 'standard',
|
|
fiscalYearStartMonth: body.fiscalYearStartMonth || 1,
|
|
}, userId);
|
|
|
|
return { success: true, organization: org };
|
|
}
|
|
|
|
@Put('organizations/:id/status')
|
|
async updateOrgStatus(
|
|
@Req() req: any,
|
|
@Param('id') id: string,
|
|
@Body() body: { status: string },
|
|
) {
|
|
await this.requireSuperadmin(req);
|
|
const validStatuses = ['active', 'suspended', 'trial', 'archived'];
|
|
if (!validStatuses.includes(body.status)) {
|
|
throw new BadRequestException(`Invalid status. Must be one of: ${validStatuses.join(', ')}`);
|
|
}
|
|
const org = await this.orgService.updateStatus(id, body.status);
|
|
return { success: true, organization: org };
|
|
}
|
|
}
|