import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ApiClientService } from '../shared/http/api-client.service';
import { TimesheetConfigService } from '../config/config.service';
@Injectable()
export class AuthService {
constructor(
private readonly apiClient: ApiClientService,
private readonly configService: TimesheetConfigService,
) {}
/**
* Login with username and password
*/
async login(username: string, password: string): Promise<any> {
const response = await this.apiClient.login(username, password);
if (response.success && response.data) {
// Save token and user data
const { token, user } = response.data;
this.configService.setToken(token);
this.configService.setUser(user);
// Auto-fetch and cache available projects, modules, and activities
try {
await this.fetchAndCacheAvailableData(token);
} catch (error) {
// Log warning but don't fail login if data fetch fails
console.warn('Failed to fetch available data:', error.message);
}
// Check if user needs to configure their project/modules
const isConfigured = this.configService.isUserConfigured();
const needsConfig = this.configService.needsConfiguration();
return {
success: true,
message: isConfigured
? 'Login successful'
: 'Login successful. Please configure your project and modules.',
user,
needsConfiguration: !isConfigured,
configurationNeeds: needsConfig,
};
}
throw new UnauthorizedException('Login failed: Invalid credentials');
}
/**
* Fetch and cache available projects, modules, and activities from API
*/
async fetchAndCacheAvailableData(token: string): Promise<void> {
try {
// Fetch all data in parallel
const [projectsResponse, activitiesResponse] = await Promise.all([
this.apiClient.getProjects(token),
this.apiClient.getActivities(token),
]);
// Extract and store projects
if (projectsResponse.success && projectsResponse.data) {
const projects = Array.isArray(projectsResponse.data)
? projectsResponse.data
: [projectsResponse.data];
const availableProjects = projects.map((p: any) => ({
id: p.project_id || p.id,
name: p.project_name || p.name,
}));
this.configService.setAvailableProjects(availableProjects);
}
// Extract and store activities
if (activitiesResponse.success && activitiesResponse.data) {
const activities = Array.isArray(activitiesResponse.data)
? activitiesResponse.data
: [activitiesResponse.data];
const availableActivities = activities.map((a: any) => ({
id: a.activity_id || a.id,
name: a.activity_name || a.name,
}));
this.configService.setAvailableActivities(availableActivities);
}
// Fetch modules for all projects
const allModules: Array<{ id: number; name: string; project_id: number }> = [];
if (projectsResponse.success && projectsResponse.data) {
const projects = Array.isArray(projectsResponse.data)
? projectsResponse.data
: [projectsResponse.data];
for (const project of projects) {
const projectId = project.project_id || project.id;
try {
const modulesResponse = await this.apiClient.getModules(projectId, token);
if (modulesResponse.success && modulesResponse.data) {
const modules = Array.isArray(modulesResponse.data)
? modulesResponse.data
: [modulesResponse.data];
modules.forEach((m: any) => {
allModules.push({
id: m.module_id || m.id,
name: m.module_name || m.name,
project_id: m.project_id || projectId,
});
});
}
} catch (error) {
console.warn(`Failed to fetch modules for project ${projectId}:`, error.message);
}
}
}
// Store all modules
if (allModules.length > 0) {
this.configService.setAvailableModules(allModules);
}
} catch (error) {
throw new Error(`Failed to fetch available data: ${error.message}`);
}
}
/**
* Manually refresh available data from API
*/
async refreshAvailableData(): Promise<void> {
const token = await this.ensureAuthenticated();
await this.fetchAndCacheAvailableData(token);
}
/**
* Refresh modules for a specific project
*/
async refreshModulesForProject(projectId: number): Promise<void> {
const token = await this.ensureAuthenticated();
try {
const modulesResponse = await this.apiClient.getModules(projectId, token);
if (modulesResponse.success && modulesResponse.data) {
const modules = Array.isArray(modulesResponse.data)
? modulesResponse.data
: [modulesResponse.data];
// Get existing modules and filter out old ones for this project
const existingModules = this.configService.getAvailableModules();
const filteredModules = existingModules.filter((m) => m.project_id !== projectId);
// Add new modules for this project
const newModules = modules.map((m: any) => ({
id: m.module_id || m.id,
name: m.module_name || m.name,
project_id: m.project_id || projectId,
}));
// Update config with combined modules
this.configService.setAvailableModules([...filteredModules, ...newModules]);
}
} catch (error) {
throw new Error(`Failed to fetch modules for project ${projectId}: ${error.message}`);
}
}
/**
* Automatically configure user with their assigned project/modules
* Called during auto-login - fetches user-specific project and auto-assigns
*/
async autoConfigureUserAssignments(userId: number): Promise<{
success: boolean;
configured: boolean;
message: string;
project?: any;
modules?: any[];
}> {
try {
const token = this.getToken();
// Fetch user-specific project
const userProjectsResponse = await this.apiClient.getUserProjects(userId, token);
if (!userProjectsResponse.success || !userProjectsResponse.data) {
return {
success: false,
configured: false,
message: 'Failed to fetch user project',
};
}
const userProjects = Array.isArray(userProjectsResponse.data)
? userProjectsResponse.data
: [userProjectsResponse.data];
// Get the first (and only) project
const project = userProjects[0];
if (!project || !(project.project_id || project.id)) {
return {
success: false,
configured: false,
message: 'No project assigned to user',
};
}
const projectId = project.project_id || project.id;
const projectName = project.project_name || project.name;
// Fetch modules for this project
const modulesResponse = await this.apiClient.getModules(projectId, token);
if (!modulesResponse.success || !modulesResponse.data) {
return {
success: false,
configured: false,
message: `Found project "${projectName}" but failed to fetch modules`,
};
}
const modules = Array.isArray(modulesResponse.data)
? modulesResponse.data
: [modulesResponse.data];
if (modules.length === 0) {
return {
success: false,
configured: false,
message: `Project "${projectName}" has no modules assigned`,
};
}
// Auto-assign all modules
const moduleIds = modules.map((m: any) => m.module_id || m.id);
const result = await this.configureUserAssignments(projectId, moduleIds);
return {
success: true,
configured: true,
message: `Auto-configured: "${projectName}" with ${modules.length} module(s)`,
project: result.project,
modules: result.modules,
};
} catch (error) {
return {
success: false,
configured: false,
message: `Auto-configuration error: ${error.message}`,
};
}
}
/**
* Configure user's assigned project and modules
*/
async configureUserAssignments(
projectId: number,
moduleIds: number[],
): Promise<{
success: boolean;
message: string;
project?: any;
modules?: any[];
}> {
// Validate project exists in available projects
const availableProjects = this.configService.getAvailableProjects();
const selectedProject = availableProjects.find((p) => p.id === projectId);
if (!selectedProject) {
throw new Error(`Project with ID ${projectId} not found in available projects`);
}
// Validate modules exist and belong to selected project
const availableModules = this.configService.getAvailableModules();
const selectedModules = availableModules.filter(
(m) => moduleIds.includes(m.id) && m.project_id === projectId,
);
if (selectedModules.length !== moduleIds.length) {
throw new Error('Some module IDs are invalid or do not belong to selected project');
}
// Save configuration
this.configService.setUserAssignedProject(selectedProject);
this.configService.setUserAssignedModules(selectedModules);
return {
success: true,
message: `Configuration saved successfully`,
project: selectedProject,
modules: selectedModules,
};
}
/**
* Logout - clear stored credentials
*/
logout(): void {
this.configService.clearConfig();
}
/**
* Get current JWT token
*/
getToken(): string {
const token = this.configService.getToken();
if (!token) {
throw new UnauthorizedException('Not authenticated. Please login first.');
}
return token;
}
/**
* Get current user
*/
getCurrentUser(): any {
const user = this.configService.getUser();
if (!user) {
throw new UnauthorizedException('Not authenticated. Please login first.');
}
return user;
}
/**
* Refresh JWT token
*/
async refreshToken(): Promise<string> {
const currentToken = this.getToken();
try {
const response = await this.apiClient.refreshToken(currentToken);
if (response.success && response.data) {
const { token, user } = response.data;
this.configService.setToken(token);
this.configService.setUser(user);
return token;
}
throw new Error('Token refresh failed');
} catch (error) {
// If refresh fails, clear config and require re-login
this.logout();
throw new UnauthorizedException('Session expired. Please login again.');
}
}
/**
* Check if user is authenticated
*/
isAuthenticated(): boolean {
return this.configService.isConfigured();
}
/**
* Ensure user is authenticated, refresh token if needed
*/
async ensureAuthenticated(): Promise<string> {
if (!this.isAuthenticated()) {
throw new UnauthorizedException('Not configured. Please login first.');
}
try {
return this.getToken();
} catch (error) {
// Try to refresh token
return await this.refreshToken();
}
}
}