Preferred language for the user
This commit is contained in:
@@ -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; } = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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; } = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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())
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 = () => {
|
||||||
|
|||||||
@@ -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("");
|
||||||
|
|||||||
@@ -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) => (
|
||||||
|
|||||||
@@ -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>({
|
||||||
|
|||||||
Reference in New Issue
Block a user