-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathProgram.cs
More file actions
223 lines (185 loc) · 7.75 KB
/
Copy pathProgram.cs
File metadata and controls
223 lines (185 loc) · 7.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using PATHFINDER_BACKEND.Data;
using PATHFINDER_BACKEND.Repositories;
using PATHFINDER_BACKEND.Services;
var builder = WebApplication.CreateBuilder(args);
// Add logging
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.AddDebug();
// Add MVC controllers + Swagger
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Register app services (ADO.NET + Repositories + Services)
// Db: connection provider
builder.Services.AddSingleton<Db>();
// Repositories: DB access
builder.Services.AddScoped<StudentRepository>();
builder.Services.AddScoped<CompanyRepository>();
builder.Services.AddScoped<AdminRepository>();
builder.Services.AddScoped<CompanyProfileRepository>();
builder.Services.AddScoped<CompanyJobRepository>();
builder.Services.AddScoped<ApplicationRepository>();
builder.Services.AddScoped<JobRepository>();
builder.Services.AddScoped<StudentProfileRepository>();
builder.Services.AddScoped<DashboardRepository>();
// Services: stateless helpers (hashing, token creation, revocation tracking)
builder.Services.AddSingleton<PasswordService>();
builder.Services.AddSingleton<JwtTokenService>();
builder.Services.AddScoped<BlobService>();
builder.Services.AddScoped<LocalFileStorageService>();
builder.Services.AddScoped<IEmailService, EmailService>();
builder.Services.AddScoped<CvTextExtractorService>();
builder.Services.AddScoped<PasswordResetRepository>();
builder.Services.AddScoped<PasswordResetService>();
//AI Services
builder.Services.AddMemoryCache();
builder.Services.AddScoped<GeminiAIService>();
builder.Services.AddScoped<AtsScoringService>();
builder.Services.AddScoped<JobMatchingService>();
builder.Services.AddScoped<CachingService>();
builder.Services.AddScoped<AiAnalyticsRepository>();
builder.Services.AddScoped<AiInsightsGeneratorService>();
// Add HTTP client for Gemini
builder.Services.AddHttpClient();
// Token revocation is stored in-memory (sufficient for single-instance demo).
builder.Services.AddSingleton<TokenRevocationService>();
builder.Services.AddApplicationInsightsTelemetry();
// JWT Authentication settings
var jwtKey = builder.Configuration["Jwt:Key"] ?? throw new Exception("Jwt:Key missing in appsettings.json");
var jwtIssuer = builder.Configuration["Jwt:Issuer"] ?? "PathFinder";
var jwtAudience = builder.Configuration["Jwt:Audience"] ?? "PathFinderUsers";
// Configure JWT validation middleware
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
// Validate issuer/audience/signature/lifetime for security
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtIssuer,
ValidAudience = jwtAudience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey)),
// Allow small server/client time mismatch
ClockSkew = TimeSpan.FromMinutes(1)
};
// Custom logic after token signature is validated:
// - check whether token has been revoked (logout)
options.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
// JTI claim uniquely identifies the token.
// Your JwtTokenService uses JwtRegisteredClaimNames.Jti ("jti")
var jti = context.Principal?.FindFirst("jti")?.Value;
if (string.IsNullOrWhiteSpace(jti))
{
context.Fail("Token missing jti.");
return Task.CompletedTask;
}
// If token is revoked, block access even if not expired
var revocationService = context.HttpContext.RequestServices.GetRequiredService<TokenRevocationService>();
if (revocationService.IsRevoked(jti))
{
context.Fail("Token has been revoked.");
}
return Task.CompletedTask;
}
};
});
builder.Services.AddAuthorization();
// Add CORS (AllowAll policy is fine for development/demo)
// For production, lock down origins and headers.
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll",
policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
var app = builder.Build();
// Create upload directories for local storage
var webRootPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot");
if (!Directory.Exists(webRootPath))
{
Directory.CreateDirectory(webRootPath);
}
var uploadsPath = Path.Combine(webRootPath, "uploads");
if (!Directory.Exists(uploadsPath))
{
Directory.CreateDirectory(uploadsPath);
}
var companyLogoPath = Path.Combine(uploadsPath, "company-logos");
if (!Directory.Exists(companyLogoPath))
{
Directory.CreateDirectory(companyLogoPath);
}
// Enable static files to serve uploaded images
app.UseStaticFiles();
// Swagger UI in Development and Production environments
if (app.Environment.IsDevelopment() || app.Environment.IsProduction())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
// CORS should be before auth middleware
app.UseCors("AllowAll");
// Authentication MUST run before Authorization
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
// Root endpoint
app.MapGet("/", () => "PathFinder API is running!");
// Health endpoint for monitoring/testing
app.MapGet("/health", () => Results.Ok(new { status = "Healthy", timestamp = DateTime.Now }));
// Seed default admin if enabled (idempotent) and create AI tables
// Credentials should be provided via environment variables to avoid hardcoding secrets.
using (var scope = app.Services.CreateScope())
{
var config = scope.ServiceProvider.GetRequiredService<IConfiguration>();
var adminRepo = scope.ServiceProvider.GetRequiredService<AdminRepository>();
var pwd = scope.ServiceProvider.GetRequiredService<PasswordService>();
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
var seedEnabled = config.GetValue("AdminSeed:Enabled", true);
if (seedEnabled)
{
var seedFullName = (config["AdminSeed:FullName"] ?? "System Admin").Trim();
var seedEmail = (Environment.GetEnvironmentVariable("ADMIN_SEED_EMAIL") ?? config["AdminSeed:Email"] ?? string.Empty)
.Trim()
.ToLowerInvariant();
var seedPassword = Environment.GetEnvironmentVariable("ADMIN_SEED_PASSWORD");
if (string.IsNullOrWhiteSpace(seedEmail) || string.IsNullOrWhiteSpace(seedPassword))
{
app.Logger.LogWarning(
"Admin seed skipped. Set ADMIN_SEED_EMAIL and ADMIN_SEED_PASSWORD environment variables when AdminSeed:Enabled is true.");
}
else
{
// Hash password before inserting (never store plain password)
await adminRepo.EnsureSeedAdminAsync(seedFullName, seedEmail, pwd.Hash(seedPassword));
app.Logger.LogInformation("Admin seeded successfully");
}
}
// Create AI analytics tables if they don't exist
try
{
var aiRepo = scope.ServiceProvider.GetRequiredService<AiAnalyticsRepository>();
await aiRepo.EnsureAiTablesExistAsync();
app.Logger.LogInformation("AI analytics tables verified/created successfully");
}
catch (Exception ex)
{
app.Logger.LogWarning(ex, "Failed to ensure AI analytics tables exist. AI features may not work properly.");
}
}
app.Run();