Preferred language for the user

This commit is contained in:
2026-03-12 20:48:06 +02:00
parent fa8f9e4497
commit bdec75b6d4
13 changed files with 148 additions and 27 deletions

View File

@@ -212,8 +212,9 @@ public class ProductionAuthTests(ProductionApiTestFactory factory) : IClassFixtu
{ {
username = "editor", username = "editor",
password = "editorpass", password = "editorpass",
isAdmin = false, displayName = "Editor User",
displayName = "Editor User" preferredLanguage = "fi",
roles = Array.Empty<string>()
}); });
Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode); Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode);
@@ -227,14 +228,16 @@ public class ProductionAuthTests(ProductionApiTestFactory factory) : IClassFixtu
var updateResponse = await _client.PutAsJsonAsync("/users/editor", new var updateResponse = await _client.PutAsJsonAsync("/users/editor", new
{ {
password = "editorpass2", password = "editorpass2",
isAdmin = true, displayName = "Editor Admin",
displayName = "Editor Admin" preferredLanguage = "en",
roles = new[] { "admin" }
}); });
Assert.Equal(HttpStatusCode.OK, updateResponse.StatusCode); Assert.Equal(HttpStatusCode.OK, updateResponse.StatusCode);
var updatedUser = await updateResponse.Content.ReadFromJsonAsync<UserDto>(); var updatedUser = await updateResponse.Content.ReadFromJsonAsync<UserDto>();
Assert.NotNull(updatedUser); Assert.NotNull(updatedUser);
Assert.True(updatedUser.IsAdmin); Assert.Contains("admin", updatedUser.Roles);
Assert.Equal("en", updatedUser.PreferredLanguage);
Assert.Equal("Editor Admin", updatedUser.DisplayName); Assert.Equal("Editor Admin", updatedUser.DisplayName);
var deleteResponse = await _client.DeleteAsync("/users/editor"); var deleteResponse = await _client.DeleteAsync("/users/editor");
@@ -332,7 +335,9 @@ public class AuthTokenDto
public string DisplayName { get; set; } = string.Empty; public string DisplayName { get; set; } = string.Empty;
public bool IsAdmin { get; set; } public string PreferredLanguage { get; set; } = string.Empty;
public List<string> Roles { get; set; } = [];
public string TokenType { get; set; } = string.Empty; public string TokenType { get; set; } = string.Empty;
@@ -347,7 +352,9 @@ public class UserDto
public DateTime LastUpdated { get; set; } public DateTime LastUpdated { get; set; }
public bool IsAdmin { get; set; }
public string DisplayName { get; set; } = string.Empty; public string DisplayName { get; set; } = string.Empty;
public string PreferredLanguage { get; set; } = string.Empty;
public List<string> Roles { get; set; } = [];
} }

View File

@@ -53,6 +53,7 @@ public static class AuthEndpoints
new(ClaimTypes.Name, authenticatedUser.Username), new(ClaimTypes.Name, authenticatedUser.Username),
new("username", authenticatedUser.Username), new("username", authenticatedUser.Username),
new("display_name", authenticatedUser.DisplayName), new("display_name", authenticatedUser.DisplayName),
new("preferred_language", authenticatedUser.PreferredLanguage),
}; };
foreach (var role in authenticatedUser.Roles) foreach (var role in authenticatedUser.Roles)
@@ -74,6 +75,7 @@ public static class AuthEndpoints
AccessToken = tokenValue, AccessToken = tokenValue,
Username = authenticatedUser.Username, Username = authenticatedUser.Username,
DisplayName = authenticatedUser.DisplayName, DisplayName = authenticatedUser.DisplayName,
PreferredLanguage = authenticatedUser.PreferredLanguage,
Roles = authenticatedUser.Roles, Roles = authenticatedUser.Roles,
TokenType = "Bearer", TokenType = "Bearer",
ExpiresIn = 43200 ExpiresIn = 43200

View File

@@ -6,6 +6,23 @@ public static class AppRoles
public static readonly IReadOnlyList<string> All = [Lok, Admin]; public static readonly IReadOnlyList<string> All = [Lok, Admin];
} }
public static class AppLanguages
{
public const string Finnish = "fi";
public const string English = "en";
public const string Slovak = "sk";
public static readonly IReadOnlyList<string> All = [Finnish, English, Slovak];
public static string NormalizeOrDefault(string? language)
{
var normalized = language?.Trim().ToLowerInvariant();
return !string.IsNullOrWhiteSpace(normalized) && All.Contains(normalized)
? normalized
: Finnish;
}
}
public class AppUser public class AppUser
{ {
public long Id { get; set; } public long Id { get; set; }
@@ -20,6 +37,8 @@ public class AppUser
public string DisplayName { get; set; } = string.Empty; public string DisplayName { get; set; } = string.Empty;
public string PreferredLanguage { get; set; } = AppLanguages.Finnish;
public List<string> Roles { get; set; } = []; public List<string> Roles { get; set; } = [];
} }
@@ -33,6 +52,8 @@ public class AppUserView
public string DisplayName { get; set; } = string.Empty; public string DisplayName { get; set; } = string.Empty;
public string PreferredLanguage { get; set; } = AppLanguages.Finnish;
public List<string> Roles { get; set; } = []; public List<string> Roles { get; set; } = [];
} }
@@ -44,6 +65,8 @@ public class AppUserCreateRequest
public string DisplayName { get; set; } = string.Empty; public string DisplayName { get; set; } = string.Empty;
public string PreferredLanguage { get; set; } = AppLanguages.Finnish;
public List<string> Roles { get; set; } = []; public List<string> Roles { get; set; } = [];
} }
@@ -53,5 +76,7 @@ public class AppUserUpdateRequest
public string DisplayName { get; set; } = string.Empty; public string DisplayName { get; set; } = string.Empty;
public string PreferredLanguage { get; set; } = AppLanguages.Finnish;
public List<string> Roles { get; set; } = []; public List<string> Roles { get; set; } = [];
} }

View File

@@ -241,6 +241,22 @@ public class Program
SELECT id, 'admin' FROM Users WHERE isAdmin = 1;"; SELECT id, 'admin' FROM Users WHERE isAdmin = 1;";
command.ExecuteNonQuery(); 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();
} }
} }

View File

@@ -19,12 +19,13 @@ public class UserService
await using var upsertCommand = _connection.CreateCommand(); await using var upsertCommand = _connection.CreateCommand();
upsertCommand.CommandText = @" upsertCommand.CommandText = @"
INSERT INTO Users (username, password, added, lastUpdated, displayName) INSERT INTO Users (username, password, added, lastUpdated, displayName, preferredLanguage)
VALUES (@username, @password, @added, @lastUpdated, @displayName) VALUES (@username, @password, @added, @lastUpdated, @displayName, @preferredLanguage)
ON CONFLICT(username) DO UPDATE SET ON CONFLICT(username) DO UPDATE SET
password = excluded.password, password = excluded.password,
lastUpdated = excluded.lastUpdated, lastUpdated = excluded.lastUpdated,
displayName = excluded.displayName; displayName = excluded.displayName,
preferredLanguage = excluded.preferredLanguage;
SELECT id FROM Users WHERE username = @username;"; SELECT id FROM Users WHERE username = @username;";
upsertCommand.Parameters.AddWithValue("@username", normalizedUsername); upsertCommand.Parameters.AddWithValue("@username", normalizedUsername);
@@ -32,6 +33,7 @@ public class UserService
upsertCommand.Parameters.AddWithValue("@added", now.ToString("O")); upsertCommand.Parameters.AddWithValue("@added", now.ToString("O"));
upsertCommand.Parameters.AddWithValue("@lastUpdated", now.ToString("O")); upsertCommand.Parameters.AddWithValue("@lastUpdated", now.ToString("O"));
upsertCommand.Parameters.AddWithValue("@displayName", admin.DisplayName.Trim()); upsertCommand.Parameters.AddWithValue("@displayName", admin.DisplayName.Trim());
upsertCommand.Parameters.AddWithValue("@preferredLanguage", AppLanguages.Finnish);
var userId = Convert.ToInt64(await upsertCommand.ExecuteScalarAsync()); var userId = Convert.ToInt64(await upsertCommand.ExecuteScalarAsync());
@@ -49,7 +51,7 @@ public class UserService
await using var command = _connection.CreateCommand(); await using var command = _connection.CreateCommand();
command.CommandText = @" command.CommandText = @"
SELECT u.id, u.username, u.password, u.added, u.lastUpdated, u.displayName, SELECT u.id, u.username, u.password, u.added, u.lastUpdated, u.displayName, u.preferredLanguage,
GROUP_CONCAT(ur.roleName) AS roles GROUP_CONCAT(ur.roleName) AS roles
FROM Users u FROM Users u
LEFT JOIN UserRoles ur ON ur.userId = u.id LEFT JOIN UserRoles ur ON ur.userId = u.id
@@ -81,7 +83,7 @@ public class UserService
await using var command = _connection.CreateCommand(); await using var command = _connection.CreateCommand();
command.CommandText = @" command.CommandText = @"
SELECT u.id, u.username, u.added, u.lastUpdated, u.displayName, SELECT u.id, u.username, u.added, u.lastUpdated, u.displayName, u.preferredLanguage,
GROUP_CONCAT(ur.roleName) AS roles GROUP_CONCAT(ur.roleName) AS roles
FROM Users u FROM Users u
LEFT JOIN UserRoles ur ON ur.userId = u.id LEFT JOIN UserRoles ur ON ur.userId = u.id
@@ -99,6 +101,7 @@ public class UserService
Added = ParseDate(reader["added"]?.ToString()), Added = ParseDate(reader["added"]?.ToString()),
LastUpdated = ParseDate(reader["lastUpdated"]?.ToString()), LastUpdated = ParseDate(reader["lastUpdated"]?.ToString()),
DisplayName = reader["displayName"]?.ToString() ?? string.Empty, DisplayName = reader["displayName"]?.ToString() ?? string.Empty,
PreferredLanguage = AppLanguages.NormalizeOrDefault(reader["preferredLanguage"]?.ToString()),
Roles = ParseRoles(reader["roles"]?.ToString()) Roles = ParseRoles(reader["roles"]?.ToString())
}); });
} }
@@ -112,11 +115,12 @@ public class UserService
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
var normalizedUsername = request.Username.Trim().ToLowerInvariant(); var normalizedUsername = request.Username.Trim().ToLowerInvariant();
var preferredLanguage = AppLanguages.NormalizeOrDefault(request.PreferredLanguage);
await using var insertCommand = _connection.CreateCommand(); await using var insertCommand = _connection.CreateCommand();
insertCommand.CommandText = @" insertCommand.CommandText = @"
INSERT INTO Users (username, password, added, lastUpdated, displayName) INSERT INTO Users (username, password, added, lastUpdated, displayName, preferredLanguage)
VALUES (@username, @password, @added, @lastUpdated, @displayName); VALUES (@username, @password, @added, @lastUpdated, @displayName, @preferredLanguage);
SELECT last_insert_rowid();"; SELECT last_insert_rowid();";
insertCommand.Parameters.AddWithValue("@username", normalizedUsername); insertCommand.Parameters.AddWithValue("@username", normalizedUsername);
@@ -124,6 +128,7 @@ public class UserService
insertCommand.Parameters.AddWithValue("@added", now.ToString("O")); insertCommand.Parameters.AddWithValue("@added", now.ToString("O"));
insertCommand.Parameters.AddWithValue("@lastUpdated", now.ToString("O")); insertCommand.Parameters.AddWithValue("@lastUpdated", now.ToString("O"));
insertCommand.Parameters.AddWithValue("@displayName", request.DisplayName.Trim()); insertCommand.Parameters.AddWithValue("@displayName", request.DisplayName.Trim());
insertCommand.Parameters.AddWithValue("@preferredLanguage", preferredLanguage);
var insertedId = Convert.ToInt64(await insertCommand.ExecuteScalarAsync()); var insertedId = Convert.ToInt64(await insertCommand.ExecuteScalarAsync());
@@ -143,6 +148,7 @@ public class UserService
Added = now, Added = now,
LastUpdated = now, LastUpdated = now,
DisplayName = request.DisplayName.Trim(), DisplayName = request.DisplayName.Trim(),
PreferredLanguage = preferredLanguage,
Roles = validRoles Roles = validRoles
}; };
} }
@@ -153,6 +159,7 @@ public class UserService
var normalizedUsername = username.Trim().ToLowerInvariant(); var normalizedUsername = username.Trim().ToLowerInvariant();
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
var preferredLanguage = AppLanguages.NormalizeOrDefault(request.PreferredLanguage);
var currentUser = await GetUserByUsername(normalizedUsername); var currentUser = await GetUserByUsername(normalizedUsername);
if (currentUser is null) if (currentUser is null)
@@ -165,13 +172,15 @@ public class UserService
UPDATE Users UPDATE Users
SET password = @password, SET password = @password,
lastUpdated = @lastUpdated, lastUpdated = @lastUpdated,
displayName = @displayName displayName = @displayName,
preferredLanguage = @preferredLanguage
WHERE username = @username;"; WHERE username = @username;";
command.Parameters.AddWithValue("@username", normalizedUsername); command.Parameters.AddWithValue("@username", normalizedUsername);
command.Parameters.AddWithValue("@password", string.IsNullOrWhiteSpace(request.Password) ? currentUser.Password : request.Password); command.Parameters.AddWithValue("@password", string.IsNullOrWhiteSpace(request.Password) ? currentUser.Password : request.Password);
command.Parameters.AddWithValue("@lastUpdated", now.ToString("O")); command.Parameters.AddWithValue("@lastUpdated", now.ToString("O"));
command.Parameters.AddWithValue("@displayName", request.DisplayName.Trim()); command.Parameters.AddWithValue("@displayName", request.DisplayName.Trim());
command.Parameters.AddWithValue("@preferredLanguage", preferredLanguage);
var affectedRows = await command.ExecuteNonQueryAsync(); var affectedRows = await command.ExecuteNonQueryAsync();
if (affectedRows == 0) if (affectedRows == 0)
@@ -192,6 +201,7 @@ public class UserService
Added = currentUser.Added, Added = currentUser.Added,
LastUpdated = now, LastUpdated = now,
DisplayName = request.DisplayName.Trim(), DisplayName = request.DisplayName.Trim(),
PreferredLanguage = preferredLanguage,
Roles = validRoles Roles = validRoles
}; };
} }
@@ -225,7 +235,7 @@ public class UserService
await using var command = _connection.CreateCommand(); await using var command = _connection.CreateCommand();
command.CommandText = @" command.CommandText = @"
SELECT u.id, u.username, u.added, u.lastUpdated, u.displayName, SELECT u.id, u.username, u.added, u.lastUpdated, u.displayName, u.preferredLanguage,
GROUP_CONCAT(ur.roleName) AS roles GROUP_CONCAT(ur.roleName) AS roles
FROM Users u FROM Users u
LEFT JOIN UserRoles ur ON ur.userId = u.id LEFT JOIN UserRoles ur ON ur.userId = u.id
@@ -247,6 +257,7 @@ public class UserService
Added = ParseDate(reader["added"]?.ToString()), Added = ParseDate(reader["added"]?.ToString()),
LastUpdated = ParseDate(reader["lastUpdated"]?.ToString()), LastUpdated = ParseDate(reader["lastUpdated"]?.ToString()),
DisplayName = reader["displayName"]?.ToString() ?? string.Empty, DisplayName = reader["displayName"]?.ToString() ?? string.Empty,
PreferredLanguage = AppLanguages.NormalizeOrDefault(reader["preferredLanguage"]?.ToString()),
Roles = ParseRoles(reader["roles"]?.ToString()) Roles = ParseRoles(reader["roles"]?.ToString())
}; };
} }
@@ -272,7 +283,7 @@ public class UserService
{ {
await using var command = _connection.CreateCommand(); await using var command = _connection.CreateCommand();
command.CommandText = @" command.CommandText = @"
SELECT u.id, u.username, u.password, u.added, u.lastUpdated, u.displayName, SELECT u.id, u.username, u.password, u.added, u.lastUpdated, u.displayName, u.preferredLanguage,
GROUP_CONCAT(ur.roleName) AS roles GROUP_CONCAT(ur.roleName) AS roles
FROM Users u FROM Users u
LEFT JOIN UserRoles ur ON ur.userId = u.id LEFT JOIN UserRoles ur ON ur.userId = u.id
@@ -301,6 +312,7 @@ public class UserService
Added = ParseDate(reader["added"]?.ToString()), Added = ParseDate(reader["added"]?.ToString()),
LastUpdated = ParseDate(reader["lastUpdated"]?.ToString()), LastUpdated = ParseDate(reader["lastUpdated"]?.ToString()),
DisplayName = reader["displayName"]?.ToString() ?? string.Empty, DisplayName = reader["displayName"]?.ToString() ?? string.Empty,
PreferredLanguage = AppLanguages.NormalizeOrDefault(reader["preferredLanguage"]?.ToString()),
Roles = ParseRoles(reader["roles"]?.ToString()) Roles = ParseRoles(reader["roles"]?.ToString())
}; };
} }

View File

@@ -16,7 +16,8 @@ CREATE TABLE IF NOT EXISTS Users (
password TEXT NOT NULL, password TEXT NOT NULL,
added TEXT NOT NULL, added TEXT NOT NULL,
lastUpdated TEXT NOT NULL, lastUpdated TEXT NOT NULL,
displayName TEXT NOT NULL DEFAULT '' displayName TEXT NOT NULL DEFAULT '',
preferredLanguage TEXT NOT NULL DEFAULT 'fi'
); );
CREATE TABLE IF NOT EXISTS UserRoles ( CREATE TABLE IF NOT EXISTS UserRoles (

View File

@@ -4,6 +4,7 @@ type AuthTokenResponse = {
accessToken: string; accessToken: string;
username: string; username: string;
displayName: string; displayName: string;
preferredLanguage: "fi" | "en" | "sk";
roles: string[]; roles: string[];
tokenType: string; tokenType: string;
expiresIn: number; expiresIn: number;
@@ -64,6 +65,7 @@ export type User = {
added: string; added: string;
lastUpdated: string; lastUpdated: string;
displayName: string; displayName: string;
preferredLanguage: "fi" | "en" | "sk";
roles: string[]; roles: string[];
}; };
@@ -71,12 +73,14 @@ export type CreateUserInput = {
username: string; username: string;
password: string; password: string;
displayName: string; displayName: string;
preferredLanguage: "fi" | "en" | "sk";
roles: string[]; roles: string[];
}; };
export type UpdateUserInput = { export type UpdateUserInput = {
password?: string; password?: string;
displayName: string; displayName: string;
preferredLanguage: "fi" | "en" | "sk";
roles: string[]; roles: string[];
}; };

View File

@@ -19,12 +19,14 @@ function AppShell() {
const setSession = useSetRecoilState(sessionAtom); const setSession = useSetRecoilState(sessionAtom);
useEffect(() => { useEffect(() => {
initializeLanguage(setLanguage); const storedPreferredLanguage = localStorage.getItem("session-preferred-language");
initializeLanguage(setLanguage, storedPreferredLanguage === "en" || storedPreferredLanguage === "sk" ? storedPreferredLanguage : "fi");
}, [setLanguage]); }, [setLanguage]);
useEffect(() => { useEffect(() => {
const storedUsername = localStorage.getItem("session-username"); const storedUsername = localStorage.getItem("session-username");
const storedDisplayName = localStorage.getItem("session-display-name"); const storedDisplayName = localStorage.getItem("session-display-name");
const storedPreferredLanguage = localStorage.getItem("session-preferred-language");
const storedRoles = localStorage.getItem("session-roles"); const storedRoles = localStorage.getItem("session-roles");
const storedToken = localStorage.getItem("session-token"); const storedToken = localStorage.getItem("session-token");
if (!storedUsername || !storedDisplayName || !storedToken) { if (!storedUsername || !storedDisplayName || !storedToken) {
@@ -35,6 +37,7 @@ function AppShell() {
setSession({ setSession({
username: storedUsername, username: storedUsername,
displayName: storedDisplayName, displayName: storedDisplayName,
preferredLanguage: storedPreferredLanguage === "en" || storedPreferredLanguage === "sk" ? storedPreferredLanguage : "fi",
roles: (storedRoles ?? "").split(",").filter(Boolean), roles: (storedRoles ?? "").split(",").filter(Boolean),
token: storedToken, token: storedToken,
}); });

View File

@@ -15,7 +15,8 @@ export default function Nav() {
setSession(null); setSession(null);
localStorage.removeItem("session-username"); localStorage.removeItem("session-username");
localStorage.removeItem("session-display-name"); localStorage.removeItem("session-display-name");
localStorage.removeItem("session-is-admin"); localStorage.removeItem("session-preferred-language");
localStorage.removeItem("session-roles");
localStorage.removeItem("session-token"); localStorage.removeItem("session-token");
navigate("/login"); navigate("/login");
}; };

View File

@@ -71,6 +71,10 @@ const translations = {
"management.username": "Username", "management.username": "Username",
"management.password": "Password", "management.password": "Password",
"management.displayName": "Display name", "management.displayName": "Display name",
"management.preferredLanguage": "Preferred language",
"management.language.fi": "Finnish",
"management.language.en": "English",
"management.language.sk": "Slovak",
"management.added": "Added", "management.added": "Added",
"management.updated": "Last updated", "management.updated": "Last updated",
"management.loading": "Loading users...", "management.loading": "Loading users...",
@@ -156,6 +160,10 @@ const translations = {
"management.username": "Käyttäjätunnus", "management.username": "Käyttäjätunnus",
"management.password": "Salasana", "management.password": "Salasana",
"management.displayName": "Näyttönimi", "management.displayName": "Näyttönimi",
"management.preferredLanguage": "Ensisijainen kieli",
"management.language.fi": "Suomi",
"management.language.en": "Englanti",
"management.language.sk": "Slovakki",
"management.added": "Lisätty", "management.added": "Lisätty",
"management.updated": "Viimeksi päivitetty", "management.updated": "Viimeksi päivitetty",
"management.loading": "Ladataan käyttäjiä...", "management.loading": "Ladataan käyttäjiä...",
@@ -243,6 +251,10 @@ const translations = {
"management.username": "Používateľské meno", "management.username": "Používateľské meno",
"management.password": "Heslo", "management.password": "Heslo",
"management.displayName": "Zobrazované meno", "management.displayName": "Zobrazované meno",
"management.preferredLanguage": "Preferovaný jazyk",
"management.language.fi": "Fínčina",
"management.language.en": "Angličtina",
"management.language.sk": "Slovenčina",
"management.added": "Pridané", "management.added": "Pridané",
"management.updated": "Naposledy aktualizované", "management.updated": "Naposledy aktualizované",
"management.loading": "Načítavajú sa používatelia...", "management.loading": "Načítavajú sa používatelia...",
@@ -268,11 +280,19 @@ const translations = {
export type TranslationKey = keyof typeof translations.en; export type TranslationKey = keyof typeof translations.en;
export const normalizeLanguage = (value: unknown): Language => export const normalizeLanguage = (value: unknown): Language =>
value === "fi" || value === "sk" ? value : "en"; value === "en" || value === "sk" ? value : "fi";
export const initializeLanguage = (setLanguage: (lang: Language) => void) => { export const initializeLanguage = (
const stored = normalizeLanguage(localStorage.getItem(STORAGE_KEY)); setLanguage: (lang: Language) => void,
setLanguage(stored); fallbackLanguage?: Language,
) => {
const storedValue = localStorage.getItem(STORAGE_KEY);
if (storedValue !== null) {
setLanguage(normalizeLanguage(storedValue));
return;
}
setLanguage(normalizeLanguage(fallbackLanguage));
}; };
export const useLanguage = () => { export const useLanguage = () => {

View File

@@ -2,11 +2,12 @@ import { FormEvent, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useRecoilState } from "recoil"; import { useRecoilState } from "recoil";
import { requestAuthToken } from "~/api"; import { requestAuthToken } from "~/api";
import { useT } from "~/i18n"; import { normalizeLanguage, useLanguage, useT } from "~/i18n";
import { sessionAtom } from "~/state/appState"; import { sessionAtom } from "~/state/appState";
export default function Login() { export default function Login() {
const t = useT(); const t = useT();
const { setLanguage } = useLanguage();
const navigate = useNavigate(); const navigate = useNavigate();
const [session, setSession] = useRecoilState(sessionAtom); const [session, setSession] = useRecoilState(sessionAtom);
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
@@ -38,12 +39,17 @@ export default function Login() {
setSession({ setSession({
username: auth.username, username: auth.username,
displayName: auth.displayName, displayName: auth.displayName,
preferredLanguage: auth.preferredLanguage,
roles: auth.roles, roles: auth.roles,
token: auth.accessToken, token: auth.accessToken,
}); });
const preferredLanguage = normalizeLanguage(auth.preferredLanguage);
setLanguage(preferredLanguage);
localStorage.setItem("session-username", auth.username); localStorage.setItem("session-username", auth.username);
localStorage.setItem("session-display-name", auth.displayName); localStorage.setItem("session-display-name", auth.displayName);
localStorage.setItem("session-preferred-language", preferredLanguage);
localStorage.setItem("session-roles", auth.roles.join(",")); localStorage.setItem("session-roles", auth.roles.join(","));
localStorage.setItem("session-token", auth.accessToken); localStorage.setItem("session-token", auth.accessToken);
setError(""); setError("");

View File

@@ -5,6 +5,7 @@ import { useT } from "~/i18n";
const AVAILABLE_ROLES = ["lok", "admin"]; const AVAILABLE_ROLES = ["lok", "admin"];
type Mode = "create" | "edit"; type Mode = "create" | "edit";
type LanguageOption = "fi" | "en" | "sk";
export default function Management() { export default function Management() {
const t = useT(); const t = useT();
@@ -13,6 +14,7 @@ export default function Management() {
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [displayName, setDisplayName] = useState(""); const [displayName, setDisplayName] = useState("");
const [preferredLanguage, setPreferredLanguage] = useState<LanguageOption>("fi");
const [selectedRoles, setSelectedRoles] = useState<Set<string>>(new Set()); const [selectedRoles, setSelectedRoles] = useState<Set<string>>(new Set());
const [selectedUsername, setSelectedUsername] = useState(""); const [selectedUsername, setSelectedUsername] = useState("");
const [error, setError] = useState(""); const [error, setError] = useState("");
@@ -44,6 +46,7 @@ export default function Management() {
setUsername(""); setUsername("");
setPassword(""); setPassword("");
setDisplayName(""); setDisplayName("");
setPreferredLanguage("fi");
setSelectedRoles(new Set()); setSelectedRoles(new Set());
setSelectedUsername(""); setSelectedUsername("");
}; };
@@ -53,6 +56,7 @@ export default function Management() {
setUsername(user.username); setUsername(user.username);
setPassword(""); setPassword("");
setDisplayName(user.displayName); setDisplayName(user.displayName);
setPreferredLanguage(user.preferredLanguage);
setSelectedRoles(new Set(user.roles)); setSelectedRoles(new Set(user.roles));
setSelectedUsername(user.username); setSelectedUsername(user.username);
setError(""); setError("");
@@ -81,12 +85,14 @@ export default function Management() {
username: username.trim(), username: username.trim(),
password: password, password: password,
displayName: displayName.trim(), displayName: displayName.trim(),
preferredLanguage,
roles: [...selectedRoles], roles: [...selectedRoles],
}); });
} else { } else {
await updateUser(selectedUsername, { await updateUser(selectedUsername, {
password: password.trim() ? password : undefined, password: password.trim() ? password : undefined,
displayName: displayName.trim(), displayName: displayName.trim(),
preferredLanguage,
roles: [...selectedRoles], roles: [...selectedRoles],
}); });
} }
@@ -153,6 +159,20 @@ export default function Management() {
/> />
</label> </label>
<label htmlFor="management-language" className="block text-left">
{t("management.preferredLanguage")}
<select
id="management-language"
value={preferredLanguage}
onChange={(event) => setPreferredLanguage(event.target.value as LanguageOption)}
className="mt-1 block w-full rounded-md border border-[#C99763] bg-[#FFF7EE] px-4 py-2 focus:outline-none focus:ring-2 focus:ring-[#A56C38]"
>
<option value="fi">{t("management.language.fi")}</option>
<option value="en">{t("management.language.en")}</option>
<option value="sk">{t("management.language.sk")}</option>
</select>
</label>
<div className="block text-left"> <div className="block text-left">
<span className="block">{t("management.rolesAssign")}</span> <span className="block">{t("management.rolesAssign")}</span>
<div className="mt-1 flex flex-wrap gap-3"> <div className="mt-1 flex flex-wrap gap-3">
@@ -210,6 +230,9 @@ export default function Management() {
<div className="text-xs text-[#8E4F24]"> <div className="text-xs text-[#8E4F24]">
{t("management.updated")}: {new Date(user.lastUpdated).toLocaleString()} {t("management.updated")}: {new Date(user.lastUpdated).toLocaleString()}
</div> </div>
<div className="text-xs text-[#8E4F24]">
{t("management.preferredLanguage")}: {t(`management.language.${user.preferredLanguage}` as "management.language.fi" | "management.language.en" | "management.language.sk")}
</div>
{user.roles.length > 0 ? ( {user.roles.length > 0 ? (
<div className="mt-1 flex flex-wrap gap-1"> <div className="mt-1 flex flex-wrap gap-1">
{user.roles.map((role) => ( {user.roles.map((role) => (

View File

@@ -6,6 +6,7 @@ export type Language = "fi" | "en" | "sk";
export type Session = { export type Session = {
username: string; username: string;
displayName: string; displayName: string;
preferredLanguage: Language;
roles: string[]; roles: string[];
token: string; token: string;
}; };
@@ -19,7 +20,7 @@ export type Toast = {
export const languageAtom = atom<Language>({ export const languageAtom = atom<Language>({
key: "languageAtom", key: "languageAtom",
default: "en", default: "fi",
}); });
export const sessionAtom = atom<Session | null>({ export const sessionAtom = atom<Session | null>({