Files
klapi/api/App/Program.cs
2026-05-27 18:30:05 +03:00

316 lines
14 KiB
C#

using Microsoft.Data.Sqlite;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
var configuredConnectionString = builder.Configuration.GetConnectionString("DefaultConnection")
?? throw new InvalidOperationException("Connection string 'DefaultConnection' was not found.");
var sqliteConnectionStringBuilder = new SqliteConnectionStringBuilder(configuredConnectionString);
var databasePath = Path.GetFullPath(Path.Combine(builder.Environment.ContentRootPath, sqliteConnectionStringBuilder.DataSource));
var databaseDirectory = Path.GetDirectoryName(databasePath)
?? throw new InvalidOperationException("Could not determine database directory.");
Directory.CreateDirectory(databaseDirectory);
sqliteConnectionStringBuilder.DataSource = databasePath;
var resolvedConnectionString = sqliteConnectionStringBuilder.ToString();
var authOptions = builder.Configuration.GetSection("Auth").Get<AuthOptions>()
?? throw new InvalidOperationException("Auth configuration was not found.");
if (builder.Environment.IsProduction())
{
authOptions.Admin.Password =
Environment.GetEnvironmentVariable("KLAPI_ADMIN_PASSWORD")
?? throw new InvalidOperationException("Admin password must be set in production environment using KLAPI_ADMIN_PASSWORD environment variable.");
}
if (string.IsNullOrWhiteSpace(authOptions.SigningKey) || authOptions.SigningKey.Length < 32)
{
throw new InvalidOperationException("Auth:SigningKey must be at least 32 characters long.");
}
if (string.IsNullOrWhiteSpace(authOptions.Admin.Username)
|| string.IsNullOrWhiteSpace(authOptions.Admin.Password)
|| string.IsNullOrWhiteSpace(authOptions.Admin.DisplayName))
{
throw new InvalidOperationException("Auth:Admin username, password and display name must be configured.");
}
builder.Services.Configure<AuthOptions>(builder.Configuration.GetSection("Auth"));
var emailOptions = builder.Configuration.GetSection("Email").Get<EmailOptions>()
?? throw new InvalidOperationException("Email configuration was not found.");
if (builder.Environment.IsProduction())
{
emailOptions.Username =
Environment.GetEnvironmentVariable("KLAPI_SMTP_USERNAME")
?? throw new InvalidOperationException("SMTP username must be set in production using KLAPI_SMTP_USERNAME environment variable.");
emailOptions.Password =
Environment.GetEnvironmentVariable("KLAPI_SMTP_PASSWORD")
?? throw new InvalidOperationException("SMTP password must be set in production using KLAPI_SMTP_PASSWORD environment variable.");
}
builder.Services.Configure<EmailOptions>(o =>
{
o.SmtpHost = emailOptions.SmtpHost;
o.SmtpPort = emailOptions.SmtpPort;
o.FromAddress = emailOptions.FromAddress;
o.ToAddress = emailOptions.ToAddress;
o.Username = emailOptions.Username;
o.Password = emailOptions.Password;
});
builder.Services.AddScoped(_ => new SqliteConnection(resolvedConnectionString));
builder.Services.AddScoped<LokService>();
builder.Services.AddScoped<UserService>();
builder.Services.AddCors(options =>
{
options.AddPolicy("PublicReadCors", policy =>
{
policy
.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod();
});
options.AddPolicy("FrontendWriteCors", policy =>
{
policy
.WithOrigins(authOptions.AllowedOrigins.ToArray())
.AllowAnyHeader()
.AllowAnyMethod();
});
});
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
ValidIssuer = authOptions.Issuer,
ValidAudience = authOptions.Audience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(authOptions.SigningKey)),
ClockSkew = TimeSpan.FromMinutes(1)
};
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("HasLokRole", policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireRole(AppRoles.Lok, AppRoles.Admin);
});
options.AddPolicy("HasAdminRole", policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireRole(AppRoles.Admin);
});
});
builder.Services.AddOpenApi();
var app = builder.Build();
using (var connection = new SqliteConnection(resolvedConnectionString))
{
connection.Open();
var initScriptPath = Path.GetFullPath(Path.Combine(builder.Environment.ContentRootPath, "../Database/init.sql"));
if (File.Exists(initScriptPath))
{
var initSql = File.ReadAllText(initScriptPath);
if (!string.IsNullOrWhiteSpace(initSql))
{
using (var command = connection.CreateCommand())
{
command.CommandText = initSql;
command.ExecuteNonQuery();
}
}
}
using (var command = connection.CreateCommand())
{
command.CommandText = "SELECT COUNT(*) FROM pragma_table_info('LokOpenHours') WHERE name = 'version';";
var hasVersionColumn = Convert.ToInt32(command.ExecuteScalar()) > 0;
if (!hasVersionColumn)
{
command.CommandText = "ALTER TABLE LokOpenHours ADD COLUMN version TEXT NOT NULL DEFAULT '';";
command.ExecuteNonQuery();
}
command.CommandText = "SELECT COUNT(*) FROM pragma_table_info('LokOpenHours') WHERE name = 'name';";
var hasNameColumn = Convert.ToInt32(command.ExecuteScalar()) > 0;
if (!hasNameColumn)
{
command.CommandText = "ALTER TABLE LokOpenHours ADD COLUMN name TEXT NOT NULL DEFAULT '';";
command.ExecuteNonQuery();
}
command.CommandText = "SELECT COUNT(*) FROM pragma_table_info('LokOpenHours') WHERE name = 'isActive';";
var hasIsActiveColumn = Convert.ToInt32(command.ExecuteScalar()) > 0;
if (!hasIsActiveColumn)
{
command.CommandText = "ALTER TABLE LokOpenHours ADD COLUMN isActive INTEGER NOT NULL DEFAULT 0;";
command.ExecuteNonQuery();
}
command.CommandText = "SELECT COUNT(*) FROM LokOpenHours WHERE isActive = 1;";
var activeCount = Convert.ToInt32(command.ExecuteScalar());
if (activeCount == 0)
{
command.CommandText = @"
UPDATE LokOpenHours
SET isActive = 1
WHERE id = (
SELECT id
FROM LokOpenHours
ORDER BY datetime(version) DESC, id DESC
LIMIT 1
);";
command.ExecuteNonQuery();
}
else if (activeCount > 1)
{
command.CommandText = @"
WITH selected_active AS (
SELECT id
FROM LokOpenHours
WHERE isActive = 1
ORDER BY datetime(version) DESC, id DESC
LIMIT 1
)
UPDATE LokOpenHours
SET isActive = CASE
WHEN id = (SELECT id FROM selected_active) THEN 1
ELSE 0
END
WHERE isActive = 1
OR id = (SELECT id FROM selected_active);";
command.ExecuteNonQuery();
}
command.CommandText = @"
CREATE UNIQUE INDEX IF NOT EXISTS IX_LokOpenHours_OneActive
ON LokOpenHours(isActive)
WHERE isActive = 1;";
command.ExecuteNonQuery();
command.CommandText = @"
CREATE UNIQUE INDEX IF NOT EXISTS IX_Users_Username
ON Users(username);";
command.ExecuteNonQuery();
// Migration: if UserRoles still has roleId column, rebuild it with roleName
command.CommandText = "SELECT COUNT(*) FROM pragma_table_info('UserRoles') WHERE name = 'roleId';";
var userRolesHasRoleId = Convert.ToInt32(command.ExecuteScalar()) > 0;
if (userRolesHasRoleId)
{
command.CommandText = @"
CREATE TABLE IF NOT EXISTS UserRoles_new (
userId INTEGER NOT NULL REFERENCES Users(id) ON DELETE CASCADE,
roleName TEXT NOT NULL,
PRIMARY KEY (userId, roleName)
);
INSERT OR IGNORE INTO UserRoles_new (userId, roleName)
SELECT ur.userId, r.name
FROM UserRoles ur
JOIN Roles r ON r.id = ur.roleId;
DROP TABLE UserRoles;
ALTER TABLE UserRoles_new RENAME TO UserRoles;";
command.ExecuteNonQuery();
}
// Migration: drop old Roles table if it exists
command.CommandText = "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='Roles';";
var rolesTableExists = Convert.ToInt32(command.ExecuteScalar()) > 0;
if (rolesTableExists)
{
command.CommandText = "DROP TABLE Roles;";
command.ExecuteNonQuery();
}
// Migration: if Users table still has isAdmin column, migrate isAdmin=1 users to admin role
command.CommandText = "SELECT COUNT(*) FROM pragma_table_info('Users') WHERE name = 'isAdmin';";
var usersHasIsAdmin = Convert.ToInt32(command.ExecuteScalar()) > 0;
if (usersHasIsAdmin)
{
command.CommandText = @"
INSERT OR IGNORE INTO UserRoles (userId, roleName)
SELECT id, 'admin' FROM Users WHERE isAdmin = 1;";
command.ExecuteNonQuery();
}
// Migration: add preferredLanguage to Users if missing and backfill with Finnish
command.CommandText = "SELECT COUNT(*) FROM pragma_table_info('Users') WHERE name = 'preferredLanguage';";
var usersHasPreferredLanguage = Convert.ToInt32(command.ExecuteScalar()) > 0;
if (!usersHasPreferredLanguage)
{
command.CommandText = "ALTER TABLE Users ADD COLUMN preferredLanguage TEXT NOT NULL DEFAULT 'fi';";
command.ExecuteNonQuery();
}
command.CommandText = @"
UPDATE Users
SET preferredLanguage = 'fi'
WHERE preferredLanguage IS NULL OR TRIM(preferredLanguage) = '';";
command.ExecuteNonQuery();
}
}
using (var scope = app.Services.CreateScope())
{
var userService = scope.ServiceProvider.GetRequiredService<UserService>();
userService.EnsureAdminUser(authOptions.Admin).GetAwaiter().GetResult();
}
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
if (!app.Environment.IsDevelopment())
{
app.UseHttpsRedirection();
}
SystemEndpoints.MapSystemEndpoints(app);
AuthEndpoints.MapAuthEndpoints(app);
LokEndpoints.MapLokEndpoints(app);
UserEndpoints.MapUserEndpoints(app);
FeedbackEndpoints.MapFeedbackEndpoints(app);
app.Run();
}
}