mirror of
https://github.com/pocket-id/pocket-id.git
synced 2025-12-07 09:13:21 +03:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
664a1cf8ef | ||
|
|
e6f50191cf | ||
|
|
de9a3cce03 | ||
|
|
8c963818bb | ||
|
|
26b2de4f00 | ||
|
|
b8dcda8049 | ||
|
|
7888d70656 | ||
|
|
35766af055 | ||
|
|
c53de25d25 | ||
|
|
cdfe8161d4 | ||
|
|
e2f74e5687 | ||
|
|
132efd675c | ||
|
|
1167454c4f | ||
|
|
af5b2f7913 | ||
|
|
bc4af846e1 | ||
|
|
edf1097dd3 | ||
|
|
eb34535c5a | ||
|
|
3120ebf239 | ||
|
|
2fb41937ca | ||
|
|
d78a1c6974 | ||
|
|
c578baba95 | ||
|
|
bb23194e88 | ||
|
|
31ac56004a | ||
|
|
d59ec01b33 | ||
|
|
3ee26a2cfb | ||
|
|
39395c79c3 | ||
|
|
269b5a3c92 | ||
|
|
041c565dc1 |
20
.github/ISSUE_TEMPLATE/language-request.yml
vendored
Normal file
20
.github/ISSUE_TEMPLATE/language-request.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: "🌐 Language request"
|
||||
description: "You want to contribute to a language that isn't on Crowdin yet?"
|
||||
title: "🌐 Language Request: <language name in english>"
|
||||
labels: [language-request]
|
||||
body:
|
||||
- type: input
|
||||
id: language-name-native
|
||||
attributes:
|
||||
label: "🌐 Language Name (native)"
|
||||
placeholder: "Schweizerdeutsch"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: language-code
|
||||
attributes:
|
||||
label: "🌐 ISO 639-1 Language Code"
|
||||
description: "You can find your language code [here](https://www.andiamo.co.uk/resources/iso-language-codes/)."
|
||||
placeholder: "de-CH"
|
||||
validations:
|
||||
required: true
|
||||
5
.github/workflows/e2e-tests.yml
vendored
5
.github/workflows/e2e-tests.yml
vendored
@@ -15,6 +15,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.event.pull_request.head.ref != 'i18n_crowdin'
|
||||
timeout-minutes: 20
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -34,6 +35,7 @@ jobs:
|
||||
path: /tmp/docker-image.tar
|
||||
|
||||
test-sqlite:
|
||||
if: github.event.pull_request.head.ref != 'i18n_crowdin'
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
@@ -80,6 +82,7 @@ jobs:
|
||||
retention-days: 15
|
||||
|
||||
test-postgres:
|
||||
if: github.event.pull_request.head.ref != 'i18n_crowdin'
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
@@ -145,7 +148,7 @@ jobs:
|
||||
run: npx playwright test
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
if: always() && github.event.pull_request.head.ref != 'i18n_crowdin'
|
||||
with:
|
||||
name: playwright-report-postgres
|
||||
path: frontend/tests/.report
|
||||
|
||||
34
.github/workflows/update-aaguids.yml
vendored
Normal file
34
.github/workflows/update-aaguids.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Update AAGUIDs
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * 1" # Runs every Monday at midnight
|
||||
workflow_dispatch: # Allows manual triggering of the workflow
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update-aaguids:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Fetch JSON data
|
||||
run: |
|
||||
curl -o data.json https://raw.githubusercontent.com/pocket-id/passkey-aaguids/refs/heads/main/combined_aaguid.json
|
||||
|
||||
- name: Process JSON data
|
||||
run: |
|
||||
mkdir -p backend/resources
|
||||
jq -c 'map_values(.name)' data.json > backend/resources/aaguids.json
|
||||
|
||||
- name: Commit changes
|
||||
run: |
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git add backend/resources/aaguids.json
|
||||
git diff --staged --quiet || git commit -m "chore: update AAGUIDs"
|
||||
git push
|
||||
5
.vscode/extensions.json
vendored
Normal file
5
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"inlang.vs-code-extension"
|
||||
]
|
||||
}
|
||||
29
CHANGELOG.md
29
CHANGELOG.md
@@ -1,3 +1,32 @@
|
||||
## [](https://github.com/pocket-id/pocket-id/compare/v0.43.1...v) (2025-03-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add OIDC refresh_token support ([#325](https://github.com/pocket-id/pocket-id/issues/325)) ([b8dcda8](https://github.com/pocket-id/pocket-id/commit/b8dcda80497e554d163a370eff81fe000f8831f4))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* hash the refresh token in the DB (security) ([#379](https://github.com/pocket-id/pocket-id/issues/379)) ([8c96381](https://github.com/pocket-id/pocket-id/commit/8c963818bb90c84dac04018eec93790900d4b0ce))
|
||||
* skip ldap objects without a valid unique id ([#376](https://github.com/pocket-id/pocket-id/issues/376)) ([cdfe816](https://github.com/pocket-id/pocket-id/commit/cdfe8161d4429bdfe879887fe0b563a67c14f50b))
|
||||
* stop container if Caddy, the frontend or the backend fails ([e6f5019](https://github.com/pocket-id/pocket-id/commit/e6f50191cf05a5d0ac0e0000cf66423646f1920e))
|
||||
|
||||
## [](https://github.com/pocket-id/pocket-id/compare/v0.43.0...v) (2025-03-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* wrong base locale causes crash ([3120ebf](https://github.com/pocket-id/pocket-id/commit/3120ebf239b90f0bc0a0af33f30622e034782398))
|
||||
|
||||
## [](https://github.com/pocket-id/pocket-id/compare/v0.42.1...v) (2025-03-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add support for translations ([#349](https://github.com/pocket-id/pocket-id/issues/349)) ([269b5a3](https://github.com/pocket-id/pocket-id/commit/269b5a3c9249bb8081c74741141d3d5a69ea42a2))
|
||||
* **passkeys:** name new passkeys based on agguids ([#332](https://github.com/pocket-id/pocket-id/issues/332)) ([041c565](https://github.com/pocket-id/pocket-id/commit/041c565dc10f15edb3e8ab58e9a4df5e48a2a6d3))
|
||||
|
||||
## [](https://github.com/pocket-id/pocket-id/compare/v0.42.0...v) (2025-03-18)
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ require (
|
||||
github.com/go-ldap/ldap/v3 v3.4.10
|
||||
github.com/go-playground/validator/v10 v10.24.0
|
||||
github.com/go-webauthn/webauthn v0.11.2
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/golang-migrate/migrate/v4 v4.18.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
|
||||
@@ -78,8 +78,8 @@ github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
|
||||
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-migrate/migrate/v4 v4.18.2 h1:2VSCMz7x7mjyTXx3m2zPokOY82LTRgxK1yQYKo6wWQ8=
|
||||
github.com/golang-migrate/migrate/v4 v4.18.2/go.mod h1:2CM6tJvn2kqPXwnXO/d3rAQYiyoIm180VsO8PRX6Rpk=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
|
||||
@@ -255,3 +255,33 @@ type APIKeyExpirationDateError struct{}
|
||||
func (e *APIKeyExpirationDateError) Error() string {
|
||||
return "API Key expiration time must be in the future"
|
||||
}
|
||||
|
||||
type OidcInvalidRefreshTokenError struct{}
|
||||
|
||||
func (e *OidcInvalidRefreshTokenError) Error() string {
|
||||
return "refresh token is invalid or expired"
|
||||
}
|
||||
|
||||
func (e *OidcInvalidRefreshTokenError) HttpStatusCode() int {
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
|
||||
type OidcMissingRefreshTokenError struct{}
|
||||
|
||||
func (e *OidcMissingRefreshTokenError) Error() string {
|
||||
return "refresh token is required"
|
||||
}
|
||||
|
||||
func (e *OidcMissingRefreshTokenError) HttpStatusCode() int {
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
|
||||
type OidcMissingAuthorizationCodeError struct{}
|
||||
|
||||
func (e *OidcMissingAuthorizationCodeError) Error() string {
|
||||
return "authorization code is required"
|
||||
}
|
||||
|
||||
func (e *OidcMissingAuthorizationCodeError) HttpStatusCode() int {
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ func NewApiKeyController(group *gin.RouterGroup, authMiddleware *middleware.Auth
|
||||
// @Param sort_column query string false "Column to sort by" default("created_at")
|
||||
// @Param sort_direction query string false "Sort direction (asc or desc)" default("desc")
|
||||
// @Success 200 {object} dto.Paginated[dto.ApiKeyDto]
|
||||
// @Router /api-keys [get]
|
||||
// @Router /api/api-keys [get]
|
||||
func (c *ApiKeyController) listApiKeysHandler(ctx *gin.Context) {
|
||||
userID := ctx.GetString("userID")
|
||||
|
||||
@@ -77,7 +77,7 @@ func (c *ApiKeyController) listApiKeysHandler(ctx *gin.Context) {
|
||||
// @Tags API Keys
|
||||
// @Param api_key body dto.ApiKeyCreateDto true "API key information"
|
||||
// @Success 201 {object} dto.ApiKeyResponseDto "Created API key with token"
|
||||
// @Router /api-keys [post]
|
||||
// @Router /api/api-keys [post]
|
||||
func (c *ApiKeyController) createApiKeyHandler(ctx *gin.Context) {
|
||||
userID := ctx.GetString("userID")
|
||||
|
||||
@@ -111,7 +111,7 @@ func (c *ApiKeyController) createApiKeyHandler(ctx *gin.Context) {
|
||||
// @Tags API Keys
|
||||
// @Param id path string true "API Key ID"
|
||||
// @Success 204 "No Content"
|
||||
// @Router /api-keys/{id} [delete]
|
||||
// @Router /api/api-keys/{id} [delete]
|
||||
func (c *ApiKeyController) revokeApiKeyHandler(ctx *gin.Context) {
|
||||
userID := ctx.GetString("userID")
|
||||
apiKeyID := ctx.Param("id")
|
||||
|
||||
@@ -109,7 +109,7 @@ func (acc *AppConfigController) listAllAppConfigHandler(c *gin.Context) {
|
||||
// @Param body body dto.AppConfigUpdateDto true "Application Configuration"
|
||||
// @Success 200 {array} dto.AppConfigVariableDto
|
||||
// @Security BearerAuth
|
||||
// @Router /application-configuration [put]
|
||||
// @Router /api/application-configuration [put]
|
||||
func (acc *AppConfigController) updateAppConfigHandler(c *gin.Context) {
|
||||
var input dto.AppConfigUpdateDto
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
@@ -141,7 +141,7 @@ func (acc *AppConfigController) updateAppConfigHandler(c *gin.Context) {
|
||||
// @Produce image/jpeg
|
||||
// @Produce image/svg+xml
|
||||
// @Success 200 {file} binary "Logo image"
|
||||
// @Router /application-configuration/logo [get]
|
||||
// @Router /api/application-configuration/logo [get]
|
||||
func (acc *AppConfigController) getLogoHandler(c *gin.Context) {
|
||||
lightLogo := c.DefaultQuery("light", "true") == "true"
|
||||
|
||||
@@ -166,7 +166,7 @@ func (acc *AppConfigController) getLogoHandler(c *gin.Context) {
|
||||
// @Produce image/x-icon
|
||||
// @Success 200 {file} binary "Favicon image"
|
||||
// @Failure 404 {object} object "{"error": "File not found"}"
|
||||
// @Router /application-configuration/favicon [get]
|
||||
// @Router /api/application-configuration/favicon [get]
|
||||
func (acc *AppConfigController) getFaviconHandler(c *gin.Context) {
|
||||
acc.getImage(c, "favicon", "ico")
|
||||
}
|
||||
@@ -179,7 +179,7 @@ func (acc *AppConfigController) getFaviconHandler(c *gin.Context) {
|
||||
// @Produce image/jpeg
|
||||
// @Success 200 {file} binary "Background image"
|
||||
// @Failure 404 {object} object "{"error": "File not found"}"
|
||||
// @Router /application-configuration/background-image [get]
|
||||
// @Router /api/application-configuration/background-image [get]
|
||||
func (acc *AppConfigController) getBackgroundImageHandler(c *gin.Context) {
|
||||
imageType := acc.appConfigService.DbConfig.BackgroundImageType.Value
|
||||
acc.getImage(c, "background", imageType)
|
||||
@@ -194,7 +194,7 @@ func (acc *AppConfigController) getBackgroundImageHandler(c *gin.Context) {
|
||||
// @Param file formData file true "Logo image file"
|
||||
// @Success 204 "No Content"
|
||||
// @Security BearerAuth
|
||||
// @Router /application-configuration/logo [put]
|
||||
// @Router /api/application-configuration/logo [put]
|
||||
func (acc *AppConfigController) updateLogoHandler(c *gin.Context) {
|
||||
lightLogo := c.DefaultQuery("light", "true") == "true"
|
||||
|
||||
@@ -220,7 +220,7 @@ func (acc *AppConfigController) updateLogoHandler(c *gin.Context) {
|
||||
// @Param file formData file true "Favicon file (.ico)"
|
||||
// @Success 204 "No Content"
|
||||
// @Security BearerAuth
|
||||
// @Router /application-configuration/favicon [put]
|
||||
// @Router /api/application-configuration/favicon [put]
|
||||
func (acc *AppConfigController) updateFaviconHandler(c *gin.Context) {
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
@@ -244,7 +244,7 @@ func (acc *AppConfigController) updateFaviconHandler(c *gin.Context) {
|
||||
// @Param file formData file true "Background image file"
|
||||
// @Success 204 "No Content"
|
||||
// @Security BearerAuth
|
||||
// @Router /application-configuration/background-image [put]
|
||||
// @Router /api/application-configuration/background-image [put]
|
||||
func (acc *AppConfigController) updateBackgroundImageHandler(c *gin.Context) {
|
||||
imageType := acc.appConfigService.DbConfig.BackgroundImageType.Value
|
||||
acc.updateImage(c, "background", imageType)
|
||||
@@ -282,7 +282,7 @@ func (acc *AppConfigController) updateImage(c *gin.Context, imageName string, ol
|
||||
// @Tags Application Configuration
|
||||
// @Success 204 "No Content"
|
||||
// @Security BearerAuth
|
||||
// @Router /application-configuration/sync-ldap [post]
|
||||
// @Router /api/application-configuration/sync-ldap [post]
|
||||
func (acc *AppConfigController) syncLdapHandler(c *gin.Context) {
|
||||
err := acc.ldapService.SyncAll()
|
||||
if err != nil {
|
||||
@@ -299,7 +299,7 @@ func (acc *AppConfigController) syncLdapHandler(c *gin.Context) {
|
||||
// @Tags Application Configuration
|
||||
// @Success 204 "No Content"
|
||||
// @Security BearerAuth
|
||||
// @Router /application-configuration/test-email [post]
|
||||
// @Router /api/application-configuration/test-email [post]
|
||||
func (acc *AppConfigController) testEmailHandler(c *gin.Context) {
|
||||
userID := c.GetString("userID")
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ type AuditLogController struct {
|
||||
// @Param sort_column query string false "Column to sort by" default("created_at")
|
||||
// @Param sort_direction query string false "Sort direction (asc or desc)" default("desc")
|
||||
// @Success 200 {object} dto.Paginated[dto.AuditLogDto]
|
||||
// @Router /audit-logs [get]
|
||||
// @Router /api/audit-logs [get]
|
||||
func (alc *AuditLogController) listAuditLogsForUserHandler(c *gin.Context) {
|
||||
var sortedPaginationRequest utils.SortedPaginationRequest
|
||||
if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil {
|
||||
|
||||
@@ -39,7 +39,7 @@ type CustomClaimController struct {
|
||||
// @Failure 403 {object} object "Forbidden"
|
||||
// @Failure 500 {object} object "Internal server error"
|
||||
// @Security BearerAuth
|
||||
// @Router /custom-claims/suggestions [get]
|
||||
// @Router /api/custom-claims/suggestions [get]
|
||||
func (ccc *CustomClaimController) getSuggestionsHandler(c *gin.Context) {
|
||||
claims, err := ccc.customClaimService.GetSuggestions()
|
||||
if err != nil {
|
||||
@@ -59,7 +59,7 @@ func (ccc *CustomClaimController) getSuggestionsHandler(c *gin.Context) {
|
||||
// @Param userId path string true "User ID"
|
||||
// @Param claims body []dto.CustomClaimCreateDto true "List of custom claims to set for the user"
|
||||
// @Success 200 {array} dto.CustomClaimDto "Updated custom claims"
|
||||
// @Router /custom-claims/user/{userId} [put]
|
||||
// @Router /api/custom-claims/user/{userId} [put]
|
||||
func (ccc *CustomClaimController) UpdateCustomClaimsForUserHandler(c *gin.Context) {
|
||||
var input []dto.CustomClaimCreateDto
|
||||
|
||||
@@ -94,7 +94,7 @@ func (ccc *CustomClaimController) UpdateCustomClaimsForUserHandler(c *gin.Contex
|
||||
// @Param claims body []dto.CustomClaimCreateDto true "List of custom claims to set for the user group"
|
||||
// @Success 200 {array} dto.CustomClaimDto "Updated custom claims"
|
||||
// @Security BearerAuth
|
||||
// @Router /custom-claims/user-group/{userGroupId} [put]
|
||||
// @Router /api/custom-claims/user-group/{userGroupId} [put]
|
||||
func (ccc *CustomClaimController) UpdateCustomClaimsForUserGroupHandler(c *gin.Context) {
|
||||
var input []dto.CustomClaimCreateDto
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ type OidcController struct {
|
||||
// @Param request body dto.AuthorizeOidcClientRequestDto true "Authorization request parameters"
|
||||
// @Success 200 {object} dto.AuthorizeOidcClientResponseDto "Authorization code and callback URL"
|
||||
// @Security BearerAuth
|
||||
// @Router /oidc/authorize [post]
|
||||
// @Router /api/oidc/authorize [post]
|
||||
func (oc *OidcController) authorizeHandler(c *gin.Context) {
|
||||
var input dto.AuthorizeOidcClientRequestDto
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
@@ -92,7 +92,7 @@ func (oc *OidcController) authorizeHandler(c *gin.Context) {
|
||||
// @Param request body dto.AuthorizationRequiredDto true "Authorization check parameters"
|
||||
// @Success 200 {object} object "{ \"authorizationRequired\": true/false }"
|
||||
// @Security BearerAuth
|
||||
// @Router /oidc/authorization-required [post]
|
||||
// @Router /api/oidc/authorization-required [post]
|
||||
func (oc *OidcController) authorizationConfirmationRequiredHandler(c *gin.Context) {
|
||||
var input dto.AuthorizationRequiredDto
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
@@ -111,28 +111,39 @@ func (oc *OidcController) authorizationConfirmationRequiredHandler(c *gin.Contex
|
||||
|
||||
// createTokensHandler godoc
|
||||
// @Summary Create OIDC tokens
|
||||
// @Description Exchange authorization code for ID and access tokens
|
||||
// @Description Exchange authorization code or refresh token for access tokens
|
||||
// @Tags OIDC
|
||||
// @Accept application/x-www-form-urlencoded
|
||||
// @Produce json
|
||||
// @Param client_id formData string false "Client ID (if not using Basic Auth)"
|
||||
// @Param client_secret formData string false "Client secret (if not using Basic Auth)"
|
||||
// @Param code formData string true "Authorization code"
|
||||
// @Param grant_type formData string true "Grant type (must be 'authorization_code')"
|
||||
// @Param code_verifier formData string false "PKCE code verifier"
|
||||
// @Success 200 {object} object "{ \"id_token\": \"string\", \"access_token\": \"string\", \"token_type\": \"Bearer\" }"
|
||||
// @Router /oidc/token [post]
|
||||
// @Param code formData string false "Authorization code (required for 'authorization_code' grant)"
|
||||
// @Param grant_type formData string true "Grant type ('authorization_code' or 'refresh_token')"
|
||||
// @Param code_verifier formData string false "PKCE code verifier (for authorization_code with PKCE)"
|
||||
// @Param refresh_token formData string false "Refresh token (required for 'refresh_token' grant)"
|
||||
// @Success 200 {object} dto.OidcTokenResponseDto "Token response with access_token and optional id_token and refresh_token"
|
||||
// @Router /api/oidc/token [post]
|
||||
func (oc *OidcController) createTokensHandler(c *gin.Context) {
|
||||
// Disable cors for this endpoint
|
||||
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
|
||||
var input dto.OidcCreateTokensDto
|
||||
|
||||
if err := c.ShouldBind(&input); err != nil {
|
||||
c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate that code is provided for authorization_code grant type
|
||||
if input.GrantType == "authorization_code" && input.Code == "" {
|
||||
c.Error(&common.OidcMissingAuthorizationCodeError{})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate that refresh_token is provided for refresh_token grant type
|
||||
if input.GrantType == "refresh_token" && input.RefreshToken == "" {
|
||||
c.Error(&common.OidcMissingRefreshTokenError{})
|
||||
return
|
||||
}
|
||||
|
||||
clientID := input.ClientID
|
||||
clientSecret := input.ClientSecret
|
||||
|
||||
@@ -141,13 +152,37 @@ func (oc *OidcController) createTokensHandler(c *gin.Context) {
|
||||
clientID, clientSecret, _ = c.Request.BasicAuth()
|
||||
}
|
||||
|
||||
idToken, accessToken, err := oc.oidcService.CreateTokens(input.Code, input.GrantType, clientID, clientSecret, input.CodeVerifier)
|
||||
idToken, accessToken, refreshToken, expiresIn, err := oc.oidcService.CreateTokens(
|
||||
input.Code,
|
||||
input.GrantType,
|
||||
clientID,
|
||||
clientSecret,
|
||||
input.CodeVerifier,
|
||||
input.RefreshToken,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"id_token": idToken, "access_token": accessToken, "token_type": "Bearer"})
|
||||
response := dto.OidcTokenResponseDto{
|
||||
AccessToken: accessToken,
|
||||
TokenType: "Bearer",
|
||||
ExpiresIn: expiresIn,
|
||||
}
|
||||
|
||||
// Include ID token only for authorization_code grant
|
||||
if idToken != "" {
|
||||
response.IdToken = idToken
|
||||
}
|
||||
|
||||
// Include refresh token if generated
|
||||
if refreshToken != "" {
|
||||
response.RefreshToken = refreshToken
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// userInfoHandler godoc
|
||||
@@ -158,7 +193,7 @@ func (oc *OidcController) createTokensHandler(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Success 200 {object} object "User claims based on requested scopes"
|
||||
// @Security OAuth2AccessToken
|
||||
// @Router /oidc/userinfo [get]
|
||||
// @Router /api/oidc/userinfo [get]
|
||||
func (oc *OidcController) userInfoHandler(c *gin.Context) {
|
||||
authHeaderSplit := strings.Split(c.GetHeader("Authorization"), " ")
|
||||
if len(authHeaderSplit) != 2 {
|
||||
@@ -192,7 +227,7 @@ func (oc *OidcController) userInfoHandler(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Success 200 {object} object "User claims based on requested scopes"
|
||||
// @Security OAuth2AccessToken
|
||||
// @Router /oidc/userinfo [post]
|
||||
// @Router /api/oidc/userinfo [post]
|
||||
func (oc *OidcController) userInfoHandlerPost(c *gin.Context) {
|
||||
// Implementation is the same as GET
|
||||
}
|
||||
@@ -207,7 +242,7 @@ func (oc *OidcController) userInfoHandlerPost(c *gin.Context) {
|
||||
// @Param post_logout_redirect_uri query string false "URL to redirect to after logout"
|
||||
// @Param state query string false "State parameter to include in the redirect"
|
||||
// @Success 302 "Redirect to post-logout URL or application logout page"
|
||||
// @Router /oidc/end-session [get]
|
||||
// @Router /api/oidc/end-session [get]
|
||||
func (oc *OidcController) EndSessionHandler(c *gin.Context) {
|
||||
var input dto.OidcLogoutDto
|
||||
|
||||
@@ -256,7 +291,7 @@ func (oc *OidcController) EndSessionHandler(c *gin.Context) {
|
||||
// @Param post_logout_redirect_uri formData string false "URL to redirect to after logout"
|
||||
// @Param state formData string false "State parameter to include in the redirect"
|
||||
// @Success 302 "Redirect to post-logout URL or application logout page"
|
||||
// @Router /oidc/end-session [post]
|
||||
// @Router /api/oidc/end-session [post]
|
||||
func (oc *OidcController) EndSessionHandlerPost(c *gin.Context) {
|
||||
// Implementation is the same as GET
|
||||
}
|
||||
@@ -268,7 +303,7 @@ func (oc *OidcController) EndSessionHandlerPost(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Param id path string true "Client ID"
|
||||
// @Success 200 {object} dto.OidcClientMetaDataDto "Client metadata"
|
||||
// @Router /oidc/clients/{id}/meta [get]
|
||||
// @Router /api/oidc/clients/{id}/meta [get]
|
||||
func (oc *OidcController) getClientMetaDataHandler(c *gin.Context) {
|
||||
clientId := c.Param("id")
|
||||
client, err := oc.oidcService.GetClient(clientId)
|
||||
@@ -295,7 +330,7 @@ func (oc *OidcController) getClientMetaDataHandler(c *gin.Context) {
|
||||
// @Param id path string true "Client ID"
|
||||
// @Success 200 {object} dto.OidcClientWithAllowedUserGroupsDto "Client information"
|
||||
// @Security BearerAuth
|
||||
// @Router /oidc/clients/{id} [get]
|
||||
// @Router /api/oidc/clients/{id} [get]
|
||||
func (oc *OidcController) getClientHandler(c *gin.Context) {
|
||||
clientId := c.Param("id")
|
||||
client, err := oc.oidcService.GetClient(clientId)
|
||||
@@ -325,7 +360,7 @@ func (oc *OidcController) getClientHandler(c *gin.Context) {
|
||||
// @Param sort_direction query string false "Sort direction (asc or desc)" default("asc")
|
||||
// @Success 200 {object} dto.Paginated[dto.OidcClientDto]
|
||||
// @Security BearerAuth
|
||||
// @Router /oidc/clients [get]
|
||||
// @Router /api/oidc/clients [get]
|
||||
func (oc *OidcController) listClientsHandler(c *gin.Context) {
|
||||
searchTerm := c.Query("search")
|
||||
var sortedPaginationRequest utils.SortedPaginationRequest
|
||||
@@ -361,7 +396,7 @@ func (oc *OidcController) listClientsHandler(c *gin.Context) {
|
||||
// @Param client body dto.OidcClientCreateDto true "Client information"
|
||||
// @Success 201 {object} dto.OidcClientWithAllowedUserGroupsDto "Created client"
|
||||
// @Security BearerAuth
|
||||
// @Router /oidc/clients [post]
|
||||
// @Router /api/oidc/clients [post]
|
||||
func (oc *OidcController) createClientHandler(c *gin.Context) {
|
||||
var input dto.OidcClientCreateDto
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
@@ -391,7 +426,7 @@ func (oc *OidcController) createClientHandler(c *gin.Context) {
|
||||
// @Param id path string true "Client ID"
|
||||
// @Success 204 "No Content"
|
||||
// @Security BearerAuth
|
||||
// @Router /oidc/clients/{id} [delete]
|
||||
// @Router /api/oidc/clients/{id} [delete]
|
||||
func (oc *OidcController) deleteClientHandler(c *gin.Context) {
|
||||
err := oc.oidcService.DeleteClient(c.Param("id"))
|
||||
if err != nil {
|
||||
@@ -412,7 +447,7 @@ func (oc *OidcController) deleteClientHandler(c *gin.Context) {
|
||||
// @Param client body dto.OidcClientCreateDto true "Client information"
|
||||
// @Success 200 {object} dto.OidcClientWithAllowedUserGroupsDto "Updated client"
|
||||
// @Security BearerAuth
|
||||
// @Router /oidc/clients/{id} [put]
|
||||
// @Router /api/oidc/clients/{id} [put]
|
||||
func (oc *OidcController) updateClientHandler(c *gin.Context) {
|
||||
var input dto.OidcClientCreateDto
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
@@ -443,7 +478,7 @@ func (oc *OidcController) updateClientHandler(c *gin.Context) {
|
||||
// @Param id path string true "Client ID"
|
||||
// @Success 200 {object} object "{ \"secret\": \"string\" }"
|
||||
// @Security BearerAuth
|
||||
// @Router /oidc/clients/{id}/secret [post]
|
||||
// @Router /api/oidc/clients/{id}/secret [post]
|
||||
func (oc *OidcController) createClientSecretHandler(c *gin.Context) {
|
||||
secret, err := oc.oidcService.CreateClientSecret(c.Param("id"))
|
||||
if err != nil {
|
||||
@@ -463,7 +498,7 @@ func (oc *OidcController) createClientSecretHandler(c *gin.Context) {
|
||||
// @Produce image/svg+xml
|
||||
// @Param id path string true "Client ID"
|
||||
// @Success 200 {file} binary "Logo image"
|
||||
// @Router /oidc/clients/{id}/logo [get]
|
||||
// @Router /api/oidc/clients/{id}/logo [get]
|
||||
func (oc *OidcController) getClientLogoHandler(c *gin.Context) {
|
||||
imagePath, mimeType, err := oc.oidcService.GetClientLogo(c.Param("id"))
|
||||
if err != nil {
|
||||
@@ -484,7 +519,7 @@ func (oc *OidcController) getClientLogoHandler(c *gin.Context) {
|
||||
// @Param file formData file true "Logo image file (PNG, JPG, or SVG, max 2MB)"
|
||||
// @Success 204 "No Content"
|
||||
// @Security BearerAuth
|
||||
// @Router /oidc/clients/{id}/logo [post]
|
||||
// @Router /api/oidc/clients/{id}/logo [post]
|
||||
func (oc *OidcController) updateClientLogoHandler(c *gin.Context) {
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
@@ -508,7 +543,7 @@ func (oc *OidcController) updateClientLogoHandler(c *gin.Context) {
|
||||
// @Param id path string true "Client ID"
|
||||
// @Success 204 "No Content"
|
||||
// @Security BearerAuth
|
||||
// @Router /oidc/clients/{id}/logo [delete]
|
||||
// @Router /api/oidc/clients/{id}/logo [delete]
|
||||
func (oc *OidcController) deleteClientLogoHandler(c *gin.Context) {
|
||||
err := oc.oidcService.DeleteClientLogo(c.Param("id"))
|
||||
if err != nil {
|
||||
@@ -529,7 +564,7 @@ func (oc *OidcController) deleteClientLogoHandler(c *gin.Context) {
|
||||
// @Param groups body dto.OidcUpdateAllowedUserGroupsDto true "User group IDs"
|
||||
// @Success 200 {object} dto.OidcClientDto "Updated client"
|
||||
// @Security BearerAuth
|
||||
// @Router /oidc/clients/{id}/allowed-user-groups [put]
|
||||
// @Router /api/oidc/clients/{id}/allowed-user-groups [put]
|
||||
func (oc *OidcController) updateAllowedUserGroupsHandler(c *gin.Context) {
|
||||
var input dto.OidcUpdateAllowedUserGroupsDto
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
|
||||
@@ -63,7 +63,7 @@ type UserController struct {
|
||||
// @Tags Users,User Groups
|
||||
// @Param id path string true "User ID"
|
||||
// @Success 200 {array} dto.UserGroupDtoWithUsers
|
||||
// @Router /users/{id}/groups [get]
|
||||
// @Router /api/users/{id}/groups [get]
|
||||
func (uc *UserController) getUserGroupsHandler(c *gin.Context) {
|
||||
userID := c.Param("id")
|
||||
groups, err := uc.userService.GetUserGroups(userID)
|
||||
@@ -91,7 +91,7 @@ func (uc *UserController) getUserGroupsHandler(c *gin.Context) {
|
||||
// @Param sort_column query string false "Column to sort by" default("created_at")
|
||||
// @Param sort_direction query string false "Sort direction (asc or desc)" default("desc")
|
||||
// @Success 200 {object} dto.Paginated[dto.UserDto]
|
||||
// @Router /users [get]
|
||||
// @Router /api/users [get]
|
||||
func (uc *UserController) listUsersHandler(c *gin.Context) {
|
||||
searchTerm := c.Query("search")
|
||||
var sortedPaginationRequest utils.SortedPaginationRequest
|
||||
@@ -124,7 +124,7 @@ func (uc *UserController) listUsersHandler(c *gin.Context) {
|
||||
// @Tags Users
|
||||
// @Param id path string true "User ID"
|
||||
// @Success 200 {object} dto.UserDto
|
||||
// @Router /users/{id} [get]
|
||||
// @Router /api/users/{id} [get]
|
||||
func (uc *UserController) getUserHandler(c *gin.Context) {
|
||||
user, err := uc.userService.GetUser(c.Param("id"))
|
||||
if err != nil {
|
||||
@@ -146,7 +146,7 @@ func (uc *UserController) getUserHandler(c *gin.Context) {
|
||||
// @Description Retrieve information about the currently authenticated user
|
||||
// @Tags Users
|
||||
// @Success 200 {object} dto.UserDto
|
||||
// @Router /users/me [get]
|
||||
// @Router /api/users/me [get]
|
||||
func (uc *UserController) getCurrentUserHandler(c *gin.Context) {
|
||||
user, err := uc.userService.GetUser(c.GetString("userID"))
|
||||
if err != nil {
|
||||
@@ -169,7 +169,7 @@ func (uc *UserController) getCurrentUserHandler(c *gin.Context) {
|
||||
// @Tags Users
|
||||
// @Param id path string true "User ID"
|
||||
// @Success 204 "No Content"
|
||||
// @Router /users/{id} [delete]
|
||||
// @Router /api/users/{id} [delete]
|
||||
func (uc *UserController) deleteUserHandler(c *gin.Context) {
|
||||
if err := uc.userService.DeleteUser(c.Param("id")); err != nil {
|
||||
c.Error(err)
|
||||
@@ -185,7 +185,7 @@ func (uc *UserController) deleteUserHandler(c *gin.Context) {
|
||||
// @Tags Users
|
||||
// @Param user body dto.UserCreateDto true "User information"
|
||||
// @Success 201 {object} dto.UserDto
|
||||
// @Router /users [post]
|
||||
// @Router /api/users [post]
|
||||
func (uc *UserController) createUserHandler(c *gin.Context) {
|
||||
var input dto.UserCreateDto
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
@@ -215,7 +215,7 @@ func (uc *UserController) createUserHandler(c *gin.Context) {
|
||||
// @Param id path string true "User ID"
|
||||
// @Param user body dto.UserCreateDto true "User information"
|
||||
// @Success 200 {object} dto.UserDto
|
||||
// @Router /users/{id} [put]
|
||||
// @Router /api/users/{id} [put]
|
||||
func (uc *UserController) updateUserHandler(c *gin.Context) {
|
||||
uc.updateUser(c, false)
|
||||
}
|
||||
@@ -226,7 +226,7 @@ func (uc *UserController) updateUserHandler(c *gin.Context) {
|
||||
// @Tags Users
|
||||
// @Param user body dto.UserCreateDto true "User information"
|
||||
// @Success 200 {object} dto.UserDto
|
||||
// @Router /users/me [put]
|
||||
// @Router /api/users/me [put]
|
||||
func (uc *UserController) updateCurrentUserHandler(c *gin.Context) {
|
||||
if uc.appConfigService.DbConfig.AllowOwnAccountEdit.Value != "true" {
|
||||
c.Error(&common.AccountEditNotAllowedError{})
|
||||
@@ -242,7 +242,7 @@ func (uc *UserController) updateCurrentUserHandler(c *gin.Context) {
|
||||
// @Produce image/png
|
||||
// @Param id path string true "User ID"
|
||||
// @Success 200 {file} binary "PNG image"
|
||||
// @Router /users/{id}/profile-picture.png [get]
|
||||
// @Router /api/users/{id}/profile-picture.png [get]
|
||||
func (uc *UserController) getUserProfilePictureHandler(c *gin.Context) {
|
||||
userID := c.Param("id")
|
||||
|
||||
@@ -266,7 +266,7 @@ func (uc *UserController) getUserProfilePictureHandler(c *gin.Context) {
|
||||
// @Param id path string true "User ID"
|
||||
// @Param file formData file true "Profile picture image file (PNG, JPG, or JPEG)"
|
||||
// @Success 204 "No Content"
|
||||
// @Router /users/{id}/profile-picture [put]
|
||||
// @Router /api/users/{id}/profile-picture [put]
|
||||
func (uc *UserController) updateUserProfilePictureHandler(c *gin.Context) {
|
||||
userID := c.Param("id")
|
||||
fileHeader, err := c.FormFile("file")
|
||||
@@ -297,7 +297,7 @@ func (uc *UserController) updateUserProfilePictureHandler(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Param file formData file true "Profile picture image file (PNG, JPG, or JPEG)"
|
||||
// @Success 204 "No Content"
|
||||
// @Router /users/me/profile-picture [put]
|
||||
// @Router /api/users/me/profile-picture [put]
|
||||
func (uc *UserController) updateCurrentUserProfilePictureHandler(c *gin.Context) {
|
||||
userID := c.GetString("userID")
|
||||
fileHeader, err := c.FormFile("file")
|
||||
@@ -346,7 +346,7 @@ func (uc *UserController) createOneTimeAccessTokenHandler(c *gin.Context, own bo
|
||||
// @Param id path string true "User ID"
|
||||
// @Param body body dto.OneTimeAccessTokenCreateDto true "Token options"
|
||||
// @Success 201 {object} object "{ \"token\": \"string\" }"
|
||||
// @Router /users/{id}/one-time-access-token [post]
|
||||
// @Router /api/users/{id}/one-time-access-token [post]
|
||||
func (uc *UserController) createOwnOneTimeAccessTokenHandler(c *gin.Context) {
|
||||
uc.createOneTimeAccessTokenHandler(c, true)
|
||||
}
|
||||
@@ -377,7 +377,7 @@ func (uc *UserController) requestOneTimeAccessEmailHandler(c *gin.Context) {
|
||||
// @Tags Users
|
||||
// @Param token path string true "One-time access token"
|
||||
// @Success 200 {object} dto.UserDto
|
||||
// @Router /one-time-access-token/{token} [post]
|
||||
// @Router /api/one-time-access-token/{token} [post]
|
||||
func (uc *UserController) exchangeOneTimeAccessTokenHandler(c *gin.Context) {
|
||||
user, token, err := uc.userService.ExchangeOneTimeAccessToken(c.Param("token"), c.ClientIP(), c.Request.UserAgent())
|
||||
if err != nil {
|
||||
@@ -403,7 +403,7 @@ func (uc *UserController) exchangeOneTimeAccessTokenHandler(c *gin.Context) {
|
||||
// @Description Generate setup access token for initial admin user configuration
|
||||
// @Tags Users
|
||||
// @Success 200 {object} dto.UserDto
|
||||
// @Router /one-time-access-token/setup [post]
|
||||
// @Router /api/one-time-access-token/setup [post]
|
||||
func (uc *UserController) getSetupAccessTokenHandler(c *gin.Context) {
|
||||
user, token, err := uc.userService.SetupInitialAdmin()
|
||||
if err != nil {
|
||||
@@ -431,7 +431,7 @@ func (uc *UserController) getSetupAccessTokenHandler(c *gin.Context) {
|
||||
// @Param id path string true "User ID"
|
||||
// @Param groups body dto.UserUpdateUserGroupDto true "User group IDs"
|
||||
// @Success 200 {object} dto.UserDto
|
||||
// @Router /users/{id}/user-groups [put]
|
||||
// @Router /api/users/{id}/user-groups [put]
|
||||
func (uc *UserController) updateUserGroups(c *gin.Context) {
|
||||
var input dto.UserUpdateUserGroupDto
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
@@ -491,7 +491,7 @@ func (uc *UserController) updateUser(c *gin.Context, updateOwnUser bool) {
|
||||
// @Produce json
|
||||
// @Param id path string true "User ID"
|
||||
// @Success 204 "No Content"
|
||||
// @Router /users/{id}/profile-picture [delete]
|
||||
// @Router /api/users/{id}/profile-picture [delete]
|
||||
func (uc *UserController) resetUserProfilePictureHandler(c *gin.Context) {
|
||||
userID := c.Param("id")
|
||||
|
||||
@@ -509,7 +509,7 @@ func (uc *UserController) resetUserProfilePictureHandler(c *gin.Context) {
|
||||
// @Tags Users
|
||||
// @Produce json
|
||||
// @Success 204 "No Content"
|
||||
// @Router /users/me/profile-picture [delete]
|
||||
// @Router /api/users/me/profile-picture [delete]
|
||||
func (uc *UserController) resetCurrentUserProfilePictureHandler(c *gin.Context) {
|
||||
userID := c.GetString("userID")
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ type UserGroupController struct {
|
||||
// @Param sort_column query string false "Column to sort by" default("name")
|
||||
// @Param sort_direction query string false "Sort direction (asc or desc)" default("asc")
|
||||
// @Success 200 {object} dto.Paginated[dto.UserGroupDtoWithUserCount]
|
||||
// @Router /user-groups [get]
|
||||
// @Router /api/user-groups [get]
|
||||
func (ugc *UserGroupController) list(c *gin.Context) {
|
||||
searchTerm := c.Query("search")
|
||||
var sortedPaginationRequest utils.SortedPaginationRequest
|
||||
@@ -91,7 +91,7 @@ func (ugc *UserGroupController) list(c *gin.Context) {
|
||||
// @Param id path string true "User Group ID"
|
||||
// @Success 200 {object} dto.UserGroupDtoWithUsers
|
||||
// @Security BearerAuth
|
||||
// @Router /user-groups/{id} [get]
|
||||
// @Router /api/user-groups/{id} [get]
|
||||
func (ugc *UserGroupController) get(c *gin.Context) {
|
||||
group, err := ugc.UserGroupService.Get(c.Param("id"))
|
||||
if err != nil {
|
||||
@@ -117,7 +117,7 @@ func (ugc *UserGroupController) get(c *gin.Context) {
|
||||
// @Param userGroup body dto.UserGroupCreateDto true "User group information"
|
||||
// @Success 201 {object} dto.UserGroupDtoWithUsers "Created user group"
|
||||
// @Security BearerAuth
|
||||
// @Router /user-groups [post]
|
||||
// @Router /api/user-groups [post]
|
||||
func (ugc *UserGroupController) create(c *gin.Context) {
|
||||
var input dto.UserGroupCreateDto
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
@@ -150,7 +150,7 @@ func (ugc *UserGroupController) create(c *gin.Context) {
|
||||
// @Param userGroup body dto.UserGroupCreateDto true "User group information"
|
||||
// @Success 200 {object} dto.UserGroupDtoWithUsers "Updated user group"
|
||||
// @Security BearerAuth
|
||||
// @Router /user-groups/{id} [put]
|
||||
// @Router /api/user-groups/{id} [put]
|
||||
func (ugc *UserGroupController) update(c *gin.Context) {
|
||||
var input dto.UserGroupCreateDto
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
@@ -182,7 +182,7 @@ func (ugc *UserGroupController) update(c *gin.Context) {
|
||||
// @Param id path string true "User Group ID"
|
||||
// @Success 204 "No Content"
|
||||
// @Security BearerAuth
|
||||
// @Router /user-groups/{id} [delete]
|
||||
// @Router /api/user-groups/{id} [delete]
|
||||
func (ugc *UserGroupController) delete(c *gin.Context) {
|
||||
if err := ugc.UserGroupService.Delete(c.Param("id")); err != nil {
|
||||
c.Error(err)
|
||||
@@ -202,7 +202,7 @@ func (ugc *UserGroupController) delete(c *gin.Context) {
|
||||
// @Param users body dto.UserGroupUpdateUsersDto true "List of user IDs to assign to this group"
|
||||
// @Success 200 {object} dto.UserGroupDtoWithUsers
|
||||
// @Security BearerAuth
|
||||
// @Router /user-groups/{id}/users [put]
|
||||
// @Router /api/user-groups/{id}/users [put]
|
||||
func (ugc *UserGroupController) updateUsers(c *gin.Context) {
|
||||
var input dto.UserGroupUpdateUsersDto
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
|
||||
@@ -54,6 +54,7 @@ func (wkc *WellKnownController) openIDConfigurationHandler(c *gin.Context) {
|
||||
"userinfo_endpoint": appUrl + "/api/oidc/userinfo",
|
||||
"end_session_endpoint": appUrl + "/api/oidc/end-session",
|
||||
"jwks_uri": appUrl + "/.well-known/jwks.json",
|
||||
"grant_types_supported": []string{"authorization_code", "refresh_token"},
|
||||
"scopes_supported": []string{"openid", "profile", "email", "groups"},
|
||||
"claims_supported": []string{"sub", "given_name", "family_name", "name", "email", "email_verified", "preferred_username", "picture", "groups"},
|
||||
"response_types_supported": []string{"code", "id_token"},
|
||||
|
||||
@@ -48,10 +48,11 @@ type AuthorizationRequiredDto struct {
|
||||
|
||||
type OidcCreateTokensDto struct {
|
||||
GrantType string `form:"grant_type" binding:"required"`
|
||||
Code string `form:"code" binding:"required"`
|
||||
Code string `form:"code"`
|
||||
ClientID string `form:"client_id"`
|
||||
ClientSecret string `form:"client_secret"`
|
||||
CodeVerifier string `form:"code_verifier"`
|
||||
RefreshToken string `form:"refresh_token"`
|
||||
}
|
||||
|
||||
type OidcUpdateAllowedUserGroupsDto struct {
|
||||
@@ -64,3 +65,11 @@ type OidcLogoutDto struct {
|
||||
PostLogoutRedirectUri string `form:"post_logout_redirect_uri"`
|
||||
State string `form:"state"`
|
||||
}
|
||||
|
||||
type OidcTokenResponseDto struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
IdToken string `json:"id_token,omitempty"`
|
||||
RefreshToken string `json:"refresh_token,omitempty"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
}
|
||||
|
||||
@@ -9,18 +9,20 @@ type UserDto struct {
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
IsAdmin bool `json:"isAdmin"`
|
||||
Locale *string `json:"locale"`
|
||||
CustomClaims []CustomClaimDto `json:"customClaims"`
|
||||
UserGroups []UserGroupDto `json:"userGroups"`
|
||||
LdapID *string `json:"ldapId"`
|
||||
}
|
||||
|
||||
type UserCreateDto struct {
|
||||
Username string `json:"username" binding:"required,username,min=2,max=50"`
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
FirstName string `json:"firstName" binding:"required,min=1,max=50"`
|
||||
LastName string `json:"lastName" binding:"required,min=1,max=50"`
|
||||
IsAdmin bool `json:"isAdmin"`
|
||||
LdapID string `json:"-"`
|
||||
Username string `json:"username" binding:"required,username,min=2,max=50"`
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
FirstName string `json:"firstName" binding:"required,min=1,max=50"`
|
||||
LastName string `json:"lastName" binding:"required,min=1,max=50"`
|
||||
IsAdmin bool `json:"isAdmin"`
|
||||
Locale *string `json:"locale"`
|
||||
LdapID string `json:"-"`
|
||||
}
|
||||
|
||||
type OneTimeAccessTokenCreateDto struct {
|
||||
|
||||
@@ -22,6 +22,7 @@ func RegisterDbCleanupJobs(db *gorm.DB) {
|
||||
registerJob(scheduler, "ClearWebauthnSessions", "0 3 * * *", jobs.clearWebauthnSessions)
|
||||
registerJob(scheduler, "ClearOneTimeAccessTokens", "0 3 * * *", jobs.clearOneTimeAccessTokens)
|
||||
registerJob(scheduler, "ClearOidcAuthorizationCodes", "0 3 * * *", jobs.clearOidcAuthorizationCodes)
|
||||
registerJob(scheduler, "ClearOidcRefreshTokens", "0 3 * * *", jobs.clearOidcRefreshTokens)
|
||||
scheduler.Start()
|
||||
}
|
||||
|
||||
@@ -44,6 +45,11 @@ func (j *Jobs) clearOidcAuthorizationCodes() error {
|
||||
return j.db.Delete(&model.OidcAuthorizationCode{}, "expires_at < ?", datatype.DateTime(time.Now())).Error
|
||||
}
|
||||
|
||||
// ClearOidcAuthorizationCodes deletes OIDC authorization codes that have expired
|
||||
func (j *Jobs) clearOidcRefreshTokens() error {
|
||||
return j.db.Delete(&model.OidcRefreshToken{}, "expires_at < ?", datatype.DateTime(time.Now())).Error
|
||||
}
|
||||
|
||||
// ClearAuditLogs deletes audit logs older than 90 days
|
||||
func (j *Jobs) clearAuditLogs() error {
|
||||
return j.db.Delete(&model.AuditLog{}, "created_at < ?", datatype.DateTime(time.Now().AddDate(0, 0, -90))).Error
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type AppConfigVariable struct {
|
||||
Key string `gorm:"primaryKey;not null"`
|
||||
Type string
|
||||
@@ -9,6 +13,11 @@ type AppConfigVariable struct {
|
||||
DefaultValue string
|
||||
}
|
||||
|
||||
func (a *AppConfigVariable) IsTrue() bool {
|
||||
ok, _ := strconv.ParseBool(a.Value)
|
||||
return ok
|
||||
}
|
||||
|
||||
type AppConfig struct {
|
||||
// General
|
||||
AppName AppConfigVariable
|
||||
|
||||
@@ -51,6 +51,20 @@ type OidcClient struct {
|
||||
CreatedBy User
|
||||
}
|
||||
|
||||
type OidcRefreshToken struct {
|
||||
Base
|
||||
|
||||
Token string
|
||||
ExpiresAt datatype.DateTime
|
||||
Scope string
|
||||
|
||||
UserID string
|
||||
User User
|
||||
|
||||
ClientID string
|
||||
Client OidcClient
|
||||
}
|
||||
|
||||
func (c *OidcClient) AfterFind(_ *gorm.DB) (err error) {
|
||||
// Compute HasLogo field
|
||||
c.HasLogo = c.ImageType != nil && *c.ImageType != ""
|
||||
|
||||
@@ -14,6 +14,7 @@ type User struct {
|
||||
FirstName string `sortable:"true"`
|
||||
LastName string `sortable:"true"`
|
||||
IsAdmin bool `sortable:"true"`
|
||||
Locale *string
|
||||
LdapID *string
|
||||
|
||||
CustomClaims []CustomClaim
|
||||
|
||||
@@ -98,6 +98,13 @@ func (s *LdapService) SyncGroups() error {
|
||||
var membersUserId []string
|
||||
|
||||
ldapId := value.GetAttributeValue(uniqueIdentifierAttribute)
|
||||
|
||||
// Skip groups without a valid LDAP ID
|
||||
if ldapId == "" {
|
||||
log.Printf("Skipping LDAP group without a valid unique identifier (attribute: %s)", uniqueIdentifierAttribute)
|
||||
continue
|
||||
}
|
||||
|
||||
ldapGroupIDs[ldapId] = true
|
||||
|
||||
// Try to find the group in the database
|
||||
@@ -216,6 +223,13 @@ func (s *LdapService) SyncUsers() error {
|
||||
|
||||
for _, value := range result.Entries {
|
||||
ldapId := value.GetAttributeValue(uniqueIdentifierAttribute)
|
||||
|
||||
// Skip users without a valid LDAP ID
|
||||
if ldapId == "" {
|
||||
log.Printf("Skipping LDAP user without a valid unique identifier (attribute: %s)", uniqueIdentifierAttribute)
|
||||
continue
|
||||
}
|
||||
|
||||
ldapUserIDs[ldapId] = true
|
||||
|
||||
// Get the user from the database
|
||||
|
||||
@@ -145,60 +145,133 @@ func (s *OidcService) IsUserGroupAllowedToAuthorize(user model.User, client mode
|
||||
return isAllowedToAuthorize
|
||||
}
|
||||
|
||||
func (s *OidcService) CreateTokens(code, grantType, clientID, clientSecret, codeVerifier string) (string, string, error) {
|
||||
if grantType != "authorization_code" {
|
||||
return "", "", &common.OidcGrantTypeNotSupportedError{}
|
||||
func (s *OidcService) CreateTokens(code, grantType, clientID, clientSecret, codeVerifier, refreshToken string) (idToken string, accessToken string, newRefreshToken string, exp int, err error) {
|
||||
switch grantType {
|
||||
case "authorization_code":
|
||||
return s.createTokenFromAuthorizationCode(code, clientID, clientSecret, codeVerifier)
|
||||
case "refresh_token":
|
||||
accessToken, newRefreshToken, exp, err = s.createTokenFromRefreshToken(refreshToken, clientID, clientSecret)
|
||||
return "", accessToken, newRefreshToken, exp, err
|
||||
default:
|
||||
return "", "", "", 0, &common.OidcGrantTypeNotSupportedError{}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *OidcService) createTokenFromAuthorizationCode(code, clientID, clientSecret, codeVerifier string) (idToken string, accessToken string, refreshToken string, exp int, err error) {
|
||||
var client model.OidcClient
|
||||
if err := s.db.First(&client, "id = ?", clientID).Error; err != nil {
|
||||
return "", "", err
|
||||
return "", "", "", 0, err
|
||||
}
|
||||
|
||||
// Verify the client secret if the client is not public
|
||||
if !client.IsPublic {
|
||||
if clientID == "" || clientSecret == "" {
|
||||
return "", "", &common.OidcMissingClientCredentialsError{}
|
||||
return "", "", "", 0, &common.OidcMissingClientCredentialsError{}
|
||||
}
|
||||
|
||||
err := bcrypt.CompareHashAndPassword([]byte(client.Secret), []byte(clientSecret))
|
||||
if err != nil {
|
||||
return "", "", &common.OidcClientSecretInvalidError{}
|
||||
return "", "", "", 0, &common.OidcClientSecretInvalidError{}
|
||||
}
|
||||
}
|
||||
|
||||
var authorizationCodeMetaData model.OidcAuthorizationCode
|
||||
err := s.db.Preload("User").First(&authorizationCodeMetaData, "code = ?", code).Error
|
||||
err = s.db.Preload("User").First(&authorizationCodeMetaData, "code = ?", code).Error
|
||||
if err != nil {
|
||||
return "", "", &common.OidcInvalidAuthorizationCodeError{}
|
||||
return "", "", "", 0, &common.OidcInvalidAuthorizationCodeError{}
|
||||
}
|
||||
|
||||
// If the client is public or PKCE is enabled, the code verifier must match the code challenge
|
||||
if client.IsPublic || client.PkceEnabled {
|
||||
if !s.validateCodeVerifier(codeVerifier, *authorizationCodeMetaData.CodeChallenge, *authorizationCodeMetaData.CodeChallengeMethodSha256) {
|
||||
return "", "", &common.OidcInvalidCodeVerifierError{}
|
||||
return "", "", "", 0, &common.OidcInvalidCodeVerifierError{}
|
||||
}
|
||||
}
|
||||
|
||||
if authorizationCodeMetaData.ClientID != clientID && authorizationCodeMetaData.ExpiresAt.ToTime().Before(time.Now()) {
|
||||
return "", "", &common.OidcInvalidAuthorizationCodeError{}
|
||||
return "", "", "", 0, &common.OidcInvalidAuthorizationCodeError{}
|
||||
}
|
||||
|
||||
userClaims, err := s.GetUserClaimsForClient(authorizationCodeMetaData.UserID, clientID)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
return "", "", "", 0, err
|
||||
}
|
||||
|
||||
idToken, err := s.jwtService.GenerateIDToken(userClaims, clientID, authorizationCodeMetaData.Nonce)
|
||||
idToken, err = s.jwtService.GenerateIDToken(userClaims, clientID, authorizationCodeMetaData.Nonce)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
return "", "", "", 0, err
|
||||
}
|
||||
|
||||
accessToken, err := s.jwtService.GenerateOauthAccessToken(authorizationCodeMetaData.User, clientID)
|
||||
// Generate a refresh token
|
||||
refreshToken, err = s.createRefreshToken(clientID, authorizationCodeMetaData.UserID, authorizationCodeMetaData.Scope)
|
||||
if err != nil {
|
||||
return "", "", "", 0, err
|
||||
}
|
||||
|
||||
accessToken, err = s.jwtService.GenerateOauthAccessToken(authorizationCodeMetaData.User, clientID)
|
||||
|
||||
s.db.Delete(&authorizationCodeMetaData)
|
||||
|
||||
return idToken, accessToken, nil
|
||||
return idToken, accessToken, refreshToken, 3600, nil
|
||||
}
|
||||
|
||||
func (s *OidcService) createTokenFromRefreshToken(refreshToken, clientID, clientSecret string) (accessToken string, newRefreshToken string, exp int, err error) {
|
||||
if refreshToken == "" {
|
||||
return "", "", 0, &common.OidcMissingRefreshTokenError{}
|
||||
}
|
||||
|
||||
// Get the client to check if it's public
|
||||
var client model.OidcClient
|
||||
if err := s.db.First(&client, "id = ?", clientID).Error; err != nil {
|
||||
return "", "", 0, err
|
||||
}
|
||||
|
||||
// Verify the client secret if the client is not public
|
||||
if !client.IsPublic {
|
||||
if clientID == "" || clientSecret == "" {
|
||||
return "", "", 0, &common.OidcMissingClientCredentialsError{}
|
||||
}
|
||||
|
||||
err := bcrypt.CompareHashAndPassword([]byte(client.Secret), []byte(clientSecret))
|
||||
if err != nil {
|
||||
return "", "", 0, &common.OidcClientSecretInvalidError{}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify refresh token
|
||||
var storedRefreshToken model.OidcRefreshToken
|
||||
err = s.db.Preload("User").
|
||||
Where("token = ? AND expires_at > ?", utils.CreateSha256Hash(refreshToken), datatype.DateTime(time.Now())).
|
||||
First(&storedRefreshToken).
|
||||
Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return "", "", 0, &common.OidcInvalidRefreshTokenError{}
|
||||
}
|
||||
return "", "", 0, err
|
||||
}
|
||||
|
||||
// Verify that the refresh token belongs to the provided client
|
||||
if storedRefreshToken.ClientID != clientID {
|
||||
return "", "", 0, &common.OidcInvalidRefreshTokenError{}
|
||||
}
|
||||
|
||||
// Generate a new access token
|
||||
accessToken, err = s.jwtService.GenerateOauthAccessToken(storedRefreshToken.User, clientID)
|
||||
if err != nil {
|
||||
return "", "", 0, err
|
||||
}
|
||||
|
||||
// Generate a new refresh token and invalidate the old one
|
||||
newRefreshToken, err = s.createRefreshToken(clientID, storedRefreshToken.UserID, storedRefreshToken.Scope)
|
||||
if err != nil {
|
||||
return "", "", 0, err
|
||||
}
|
||||
|
||||
// Delete the used refresh token
|
||||
s.db.Delete(&storedRefreshToken)
|
||||
|
||||
return accessToken, newRefreshToken, 3600, nil
|
||||
}
|
||||
|
||||
func (s *OidcService) GetClient(clientID string) (model.OidcClient, error) {
|
||||
@@ -567,3 +640,28 @@ func (s *OidcService) getCallbackURL(urls []string, inputCallbackURL string) (ca
|
||||
|
||||
return "", &common.OidcInvalidCallbackURLError{}
|
||||
}
|
||||
|
||||
func (s *OidcService) createRefreshToken(clientID string, userID string, scope string) (string, error) {
|
||||
refreshToken, err := utils.GenerateRandomAlphanumericString(40)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Compute the hash of the refresh token to store in the DB
|
||||
// Refresh tokens are pretty long already, so a "simple" SHA-256 hash is enough
|
||||
refreshTokenHash := utils.CreateSha256Hash(refreshToken)
|
||||
|
||||
m := model.OidcRefreshToken{
|
||||
ExpiresAt: datatype.DateTime(time.Now().Add(30 * 24 * time.Hour)), // 30 days
|
||||
Token: refreshTokenHash,
|
||||
ClientID: clientID,
|
||||
UserID: userID,
|
||||
Scope: scope,
|
||||
}
|
||||
|
||||
if err := s.db.Create(&m).Error; err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return refreshToken, nil
|
||||
}
|
||||
|
||||
@@ -152,6 +152,17 @@ func (s *TestService) SeedDatabase() error {
|
||||
return err
|
||||
}
|
||||
|
||||
refreshToken := model.OidcRefreshToken{
|
||||
Token: utils.CreateSha256Hash("ou87UDg249r1StBLYkMEqy9TXDbV5HmGuDpMcZDo"),
|
||||
ExpiresAt: datatype.DateTime(time.Now().Add(24 * time.Hour)),
|
||||
Scope: "openid profile email",
|
||||
UserID: users[0].ID,
|
||||
ClientID: oidcClients[0].ID,
|
||||
}
|
||||
if err := tx.Create(&refreshToken).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
accessToken := model.OneTimeAccessToken{
|
||||
Token: "one-time-token",
|
||||
ExpiresAt: datatype.DateTime(time.Now().Add(1 * time.Hour)),
|
||||
|
||||
@@ -59,7 +59,7 @@ func (s *UserService) GetProfilePicture(userID string) (io.Reader, int64, error)
|
||||
return nil, 0, &common.InvalidUUIDError{}
|
||||
}
|
||||
|
||||
profilePicturePath := fmt.Sprintf("%s/profile-pictures/%s.png", common.EnvConfig.UploadPath, userID)
|
||||
profilePicturePath := common.EnvConfig.UploadPath + "/profile-pictures/" + userID + ".png"
|
||||
file, err := os.Open(profilePicturePath)
|
||||
if err == nil {
|
||||
// Get the file size
|
||||
@@ -94,7 +94,8 @@ func (s *UserService) GetUserGroups(userID string) ([]model.UserGroup, error) {
|
||||
|
||||
func (s *UserService) UpdateProfilePicture(userID string, file io.Reader) error {
|
||||
// Validate the user ID to prevent directory traversal
|
||||
if err := uuid.Validate(userID); err != nil {
|
||||
err := uuid.Validate(userID)
|
||||
if err != nil {
|
||||
return &common.InvalidUUIDError{}
|
||||
}
|
||||
|
||||
@@ -105,20 +106,14 @@ func (s *UserService) UpdateProfilePicture(userID string, file io.Reader) error
|
||||
}
|
||||
|
||||
// Ensure the directory exists
|
||||
profilePictureDir := fmt.Sprintf("%s/profile-pictures", common.EnvConfig.UploadPath)
|
||||
if err := os.MkdirAll(profilePictureDir, os.ModePerm); err != nil {
|
||||
profilePictureDir := common.EnvConfig.UploadPath + "/profile-pictures"
|
||||
err = os.MkdirAll(profilePictureDir, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create the profile picture file
|
||||
createdProfilePicture, err := os.Create(fmt.Sprintf("%s/%s.png", profilePictureDir, userID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer createdProfilePicture.Close()
|
||||
|
||||
// Copy the image to the file
|
||||
_, err = io.Copy(createdProfilePicture, profilePicture)
|
||||
err = utils.SaveFileStream(profilePicture, profilePictureDir+"/"+userID+".png")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -133,12 +128,12 @@ func (s *UserService) DeleteUser(userID string) error {
|
||||
}
|
||||
|
||||
// Disallow deleting the user if it is an LDAP user and LDAP is enabled
|
||||
if user.LdapID != nil && s.appConfigService.DbConfig.LdapEnabled.Value == "true" {
|
||||
if user.LdapID != nil && s.appConfigService.DbConfig.LdapEnabled.IsTrue() {
|
||||
return &common.LdapUserUpdateError{}
|
||||
}
|
||||
|
||||
// Delete the profile picture
|
||||
profilePicturePath := fmt.Sprintf("%s/profile-pictures/%s.png", common.EnvConfig.UploadPath, userID)
|
||||
profilePicturePath := common.EnvConfig.UploadPath + "/profile-pictures/" + userID + ".png"
|
||||
if err := os.Remove(profilePicturePath); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
@@ -153,6 +148,7 @@ func (s *UserService) CreateUser(input dto.UserCreateDto) (model.User, error) {
|
||||
Email: input.Email,
|
||||
Username: input.Username,
|
||||
IsAdmin: input.IsAdmin,
|
||||
Locale: input.Locale,
|
||||
}
|
||||
if input.LdapID != "" {
|
||||
user.LdapID = &input.LdapID
|
||||
@@ -174,7 +170,7 @@ func (s *UserService) UpdateUser(userID string, updatedUser dto.UserCreateDto, u
|
||||
}
|
||||
|
||||
// Disallow updating the user if it is an LDAP group and LDAP is enabled
|
||||
if !allowLdapUpdate && user.LdapID != nil && s.appConfigService.DbConfig.LdapEnabled.Value == "true" {
|
||||
if !allowLdapUpdate && user.LdapID != nil && s.appConfigService.DbConfig.LdapEnabled.IsTrue() {
|
||||
return model.User{}, &common.LdapUserUpdateError{}
|
||||
}
|
||||
|
||||
@@ -182,6 +178,7 @@ func (s *UserService) UpdateUser(userID string, updatedUser dto.UserCreateDto, u
|
||||
user.LastName = updatedUser.LastName
|
||||
user.Email = updatedUser.Email
|
||||
user.Username = updatedUser.Username
|
||||
user.Locale = updatedUser.Locale
|
||||
if !updateOwnUser {
|
||||
user.IsAdmin = updatedUser.IsAdmin
|
||||
}
|
||||
@@ -197,7 +194,7 @@ func (s *UserService) UpdateUser(userID string, updatedUser dto.UserCreateDto, u
|
||||
}
|
||||
|
||||
func (s *UserService) RequestOneTimeAccessEmail(emailAddress, redirectPath string) error {
|
||||
isDisabled := s.appConfigService.DbConfig.EmailOneTimeAccessEnabled.Value != "true"
|
||||
isDisabled := !s.appConfigService.DbConfig.EmailOneTimeAccessEnabled.IsTrue()
|
||||
if isDisabled {
|
||||
return &common.OneTimeAccessDisabledError{}
|
||||
}
|
||||
@@ -374,7 +371,7 @@ func (s *UserService) ResetProfilePicture(userID string) error {
|
||||
}
|
||||
|
||||
// Build path to profile picture
|
||||
profilePicturePath := fmt.Sprintf("%s/profile-pictures/%s.png", common.EnvConfig.UploadPath, userID)
|
||||
profilePicturePath := common.EnvConfig.UploadPath + "/profile-pictures/" + userID + ".png"
|
||||
|
||||
// Check if file exists and delete it
|
||||
if _, err := os.Stat(profilePicturePath); err == nil {
|
||||
|
||||
@@ -95,8 +95,11 @@ func (s *WebAuthnService) VerifyRegistration(sessionID, userID string, r *http.R
|
||||
return model.WebauthnCredential{}, err
|
||||
}
|
||||
|
||||
// Determine passkey name using AAGUID and User-Agent
|
||||
passkeyName := s.determinePasskeyName(credential.Authenticator.AAGUID)
|
||||
|
||||
credentialToStore := model.WebauthnCredential{
|
||||
Name: "New Passkey",
|
||||
Name: passkeyName,
|
||||
CredentialID: credential.ID,
|
||||
AttestationType: credential.AttestationType,
|
||||
PublicKey: credential.PublicKey,
|
||||
@@ -112,6 +115,16 @@ func (s *WebAuthnService) VerifyRegistration(sessionID, userID string, r *http.R
|
||||
return credentialToStore, nil
|
||||
}
|
||||
|
||||
func (s *WebAuthnService) determinePasskeyName(aaguid []byte) string {
|
||||
// First try to identify by AAGUID using a combination of builtin + MDS
|
||||
authenticatorName := utils.GetAuthenticatorName(aaguid)
|
||||
if authenticatorName != "" {
|
||||
return authenticatorName
|
||||
}
|
||||
|
||||
return "New Passkey" // Default fallback
|
||||
}
|
||||
|
||||
func (s *WebAuthnService) BeginLogin() (*model.PublicKeyCredentialRequestOptions, error) {
|
||||
options, session, err := s.webAuthn.BeginDiscoverableLogin()
|
||||
if err != nil {
|
||||
|
||||
64
backend/internal/utils/aaguid_util.go
Normal file
64
backend/internal/utils/aaguid_util.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/resources"
|
||||
)
|
||||
|
||||
var (
|
||||
aaguidMap map[string]string
|
||||
aaguidMapOnce sync.Once
|
||||
)
|
||||
|
||||
// FormatAAGUID converts an AAGUID byte slice to UUID string format
|
||||
func FormatAAGUID(aaguid []byte) string {
|
||||
if len(aaguid) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// If exactly 16 bytes, format as UUID
|
||||
if len(aaguid) == 16 {
|
||||
return fmt.Sprintf("%x-%x-%x-%x-%x",
|
||||
aaguid[0:4], aaguid[4:6], aaguid[6:8], aaguid[8:10], aaguid[10:16])
|
||||
}
|
||||
|
||||
// Otherwise just return as hex
|
||||
return hex.EncodeToString(aaguid)
|
||||
}
|
||||
|
||||
// GetAuthenticatorName returns the name of the authenticator for the given AAGUID
|
||||
func GetAuthenticatorName(aaguid []byte) string {
|
||||
aaguidStr := FormatAAGUID(aaguid)
|
||||
if aaguidStr == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Then check JSON-sourced map
|
||||
aaguidMapOnce.Do(loadAAGUIDsFromFile)
|
||||
|
||||
if name, ok := aaguidMap[aaguidStr]; ok {
|
||||
return name + " Passkey"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// loadAAGUIDsFromFile loads AAGUID data from the embedded file system
|
||||
func loadAAGUIDsFromFile() {
|
||||
// Read from embedded file system
|
||||
data, err := resources.FS.ReadFile("aaguids.json")
|
||||
if err != nil {
|
||||
log.Printf("Error reading embedded AAGUID file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &aaguidMap); err != nil {
|
||||
log.Printf("Error unmarshalling AAGUID data: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
126
backend/internal/utils/aaguid_util_test.go
Normal file
126
backend/internal/utils/aaguid_util_test.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFormatAAGUID(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
aaguid []byte
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty byte slice",
|
||||
aaguid: []byte{},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "16 byte slice - standard UUID",
|
||||
aaguid: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10},
|
||||
want: "01020304-0506-0708-090a-0b0c0d0e0f10",
|
||||
},
|
||||
{
|
||||
name: "non-16 byte slice",
|
||||
aaguid: []byte{0x01, 0x02, 0x03, 0x04, 0x05},
|
||||
want: "0102030405",
|
||||
},
|
||||
{
|
||||
name: "specific UUID example",
|
||||
aaguid: mustDecodeHex("adce000235bcc60a648b0b25f1f05503"),
|
||||
want: "adce0002-35bc-c60a-648b-0b25f1f05503",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := FormatAAGUID(tt.aaguid)
|
||||
if got != tt.want {
|
||||
t.Errorf("FormatAAGUID() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAuthenticatorName(t *testing.T) {
|
||||
// Reset the aaguidMap for testing
|
||||
originalMap := aaguidMap
|
||||
originalOnce := aaguidMapOnce
|
||||
defer func() {
|
||||
aaguidMap = originalMap
|
||||
aaguidMapOnce = originalOnce
|
||||
}()
|
||||
|
||||
// Inject a test AAGUID map
|
||||
aaguidMap = map[string]string{
|
||||
"adce0002-35bc-c60a-648b-0b25f1f05503": "Test Authenticator",
|
||||
"00000000-0000-0000-0000-000000000000": "Zero Authenticator",
|
||||
}
|
||||
aaguidMapOnce = sync.Once{}
|
||||
aaguidMapOnce.Do(func() {}) // Mark as done to avoid loading from file
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
aaguid []byte
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty byte slice",
|
||||
aaguid: []byte{},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "known AAGUID",
|
||||
aaguid: mustDecodeHex("adce000235bcc60a648b0b25f1f05503"),
|
||||
want: "Test Authenticator Passkey",
|
||||
},
|
||||
{
|
||||
name: "zero UUID",
|
||||
aaguid: mustDecodeHex("00000000000000000000000000000000"),
|
||||
want: "Zero Authenticator Passkey",
|
||||
},
|
||||
{
|
||||
name: "unknown AAGUID",
|
||||
aaguid: mustDecodeHex("ffffffffffffffffffffffffffffffff"),
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := GetAuthenticatorName(tt.aaguid)
|
||||
if got != tt.want {
|
||||
t.Errorf("GetAuthenticatorName() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadAAGUIDsFromFile(t *testing.T) {
|
||||
// Reset the map and once flag for clean testing
|
||||
aaguidMap = nil
|
||||
aaguidMapOnce = sync.Once{}
|
||||
|
||||
// Trigger loading of AAGUIDs by calling GetAuthenticatorName
|
||||
GetAuthenticatorName([]byte{0x01, 0x02, 0x03, 0x04})
|
||||
|
||||
if len(aaguidMap) == 0 {
|
||||
t.Error("loadAAGUIDsFromFile() failed to populate aaguidMap")
|
||||
}
|
||||
|
||||
// Check for a few known entries that should be in the embedded file
|
||||
// This test will be more brittle as it depends on the content of aaguids.json,
|
||||
// but it helps verify that the loading actually worked
|
||||
t.Log("AAGUID map loaded with", len(aaguidMap), "entries")
|
||||
}
|
||||
|
||||
// Helper function to convert hex string to bytes
|
||||
func mustDecodeHex(s string) []byte {
|
||||
bytes, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
panic("invalid hex in test: " + err.Error())
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/crc64"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/resources"
|
||||
)
|
||||
@@ -69,14 +74,70 @@ func SaveFile(file *multipart.FileHeader, dst string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
return SaveFileStream(src, dst)
|
||||
}
|
||||
|
||||
_, err = io.Copy(out, src)
|
||||
return err
|
||||
// SaveFileStream saves a stream to a file.
|
||||
func SaveFileStream(r io.Reader, dstFileName string) error {
|
||||
// Our strategy is to save to a separate file and then rename it to override the original file
|
||||
// First, get a temp file name that doesn't exist already
|
||||
var tmpFileName string
|
||||
var i int64
|
||||
for {
|
||||
seed := strconv.FormatInt(time.Now().UnixNano()+i, 10)
|
||||
suffix := crc64.Checksum([]byte(dstFileName+seed), crc64.MakeTable(crc64.ISO))
|
||||
tmpFileName = dstFileName + "." + strconv.FormatUint(suffix, 10)
|
||||
exists, err := FileExists(tmpFileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check if file '%s' exists: %w", tmpFileName, err)
|
||||
}
|
||||
if !exists {
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
// Write to the temporary file
|
||||
tmpFile, err := os.Create(tmpFileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open file '%s' for writing: %w", tmpFileName, err)
|
||||
}
|
||||
|
||||
n, err := io.Copy(tmpFile, r)
|
||||
if err != nil {
|
||||
// Delete the temporary file; we ignore errors here
|
||||
_ = tmpFile.Close()
|
||||
_ = os.Remove(tmpFileName)
|
||||
|
||||
return fmt.Errorf("failed to write to file '%s': %w", tmpFileName, err)
|
||||
}
|
||||
|
||||
err = tmpFile.Close()
|
||||
if err != nil {
|
||||
// Delete the temporary file; we ignore errors here
|
||||
_ = os.Remove(tmpFileName)
|
||||
|
||||
return fmt.Errorf("failed to close stream to file '%s': %w", tmpFileName, err)
|
||||
}
|
||||
|
||||
if n == 0 {
|
||||
// Delete the temporary file; we ignore errors here
|
||||
_ = os.Remove(tmpFileName)
|
||||
|
||||
return errors.New("no data written")
|
||||
}
|
||||
|
||||
// Rename to the final file, which overrides existing files
|
||||
// This is an atomic operation
|
||||
err = os.Rename(tmpFileName, dstFileName)
|
||||
if err != nil {
|
||||
// Delete the temporary file; we ignore errors here
|
||||
_ = os.Remove(tmpFileName)
|
||||
|
||||
return fmt.Errorf("failed to rename file '%s': %w", dstFileName, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FileExists returns true if a file exists on disk and is a regular file
|
||||
|
||||
@@ -3,22 +3,24 @@ package profilepicture
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/disintegration/imageorient"
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/pocket-id/pocket-id/backend/resources"
|
||||
"golang.org/x/image/font"
|
||||
"golang.org/x/image/font/opentype"
|
||||
"golang.org/x/image/math/fixed"
|
||||
"image"
|
||||
"image/color"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/disintegration/imageorient"
|
||||
"github.com/disintegration/imaging"
|
||||
"golang.org/x/image/font"
|
||||
"golang.org/x/image/font/opentype"
|
||||
"golang.org/x/image/math/fixed"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/resources"
|
||||
)
|
||||
|
||||
const profilePictureSize = 300
|
||||
|
||||
// CreateProfilePicture resizes the profile picture to a square
|
||||
func CreateProfilePicture(file io.Reader) (*bytes.Buffer, error) {
|
||||
func CreateProfilePicture(file io.Reader) (io.Reader, error) {
|
||||
img, _, err := imageorient.Decode(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode image: %w", err)
|
||||
@@ -26,13 +28,17 @@ func CreateProfilePicture(file io.Reader) (*bytes.Buffer, error) {
|
||||
|
||||
img = imaging.Fill(img, profilePictureSize, profilePictureSize, imaging.Center, imaging.Lanczos)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = imaging.Encode(&buf, img, imaging.PNG)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encode image: %v", err)
|
||||
}
|
||||
pr, pw := io.Pipe()
|
||||
go func() {
|
||||
err = imaging.Encode(pw, img, imaging.PNG)
|
||||
if err != nil {
|
||||
_ = pw.CloseWithError(fmt.Errorf("failed to encode image: %v", err))
|
||||
return
|
||||
}
|
||||
pw.Close()
|
||||
}()
|
||||
|
||||
return &buf, nil
|
||||
return pr, nil
|
||||
}
|
||||
|
||||
// CreateDefaultProfilePicture creates a profile picture with the initials
|
||||
|
||||
1
backend/resources/aaguids.json
Normal file
1
backend/resources/aaguids.json
Normal file
File diff suppressed because one or more lines are too long
@@ -4,5 +4,5 @@ import "embed"
|
||||
|
||||
// Embedded file systems for the project
|
||||
|
||||
//go:embed email-templates images migrations fonts
|
||||
//go:embed email-templates images migrations fonts aaguids.json
|
||||
var FS embed.FS
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE users DROP COLUMN locale;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE users ADD COLUMN locale TEXT;
|
||||
@@ -0,0 +1,2 @@
|
||||
DROP INDEX IF EXISTS idx_oidc_refresh_tokens_token;
|
||||
DROP TABLE IF EXISTS oidc_refresh_tokens;
|
||||
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE oidc_refresh_tokens (
|
||||
id UUID NOT NULL PRIMARY KEY,
|
||||
created_at TIMESTAMPTZ,
|
||||
token VARCHAR(255) NOT NULL UNIQUE,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
scope TEXT NOT NULL,
|
||||
user_id UUID NOT NULL REFERENCES users ON DELETE CASCADE,
|
||||
client_id UUID NOT NULL REFERENCES oidc_clients ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_oidc_refresh_tokens_token ON oidc_refresh_tokens(token);
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE users DROP COLUMN locale;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE users ADD COLUMN locale TEXT;
|
||||
@@ -0,0 +1,2 @@
|
||||
DROP INDEX IF EXISTS idx_oidc_refresh_tokens_token;
|
||||
DROP TABLE IF EXISTS oidc_refresh_tokens;
|
||||
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE oidc_refresh_tokens (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
created_at DATETIME,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
expires_at DATETIME NOT NULL,
|
||||
scope TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
client_id TEXT NOT NULL REFERENCES oidc_clients(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_oidc_refresh_tokens_token ON oidc_refresh_tokens(token);
|
||||
4
crowdin.yml
Normal file
4
crowdin.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
files:
|
||||
- source: /frontend/messages/en-US.json
|
||||
translation: /%original_path%/%locale%.json
|
||||
pull_request_title: 'chore(translations): update translations via Crowdin'
|
||||
316
frontend/messages/cs-CZ.json
Normal file
316
frontend/messages/cs-CZ.json
Normal file
@@ -0,0 +1,316 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"my_account": "Můj Účet",
|
||||
"logout": "Odhlásit se",
|
||||
"confirm": "Potvrdit",
|
||||
"key": "Klíč",
|
||||
"value": "Hodnota",
|
||||
"remove_custom_claim": "Odstranit vlastní nárok",
|
||||
"add_custom_claim": "Přidat vlastní nárok",
|
||||
"add_another": "Přidat další",
|
||||
"select_a_date": "Vyberte datum",
|
||||
"select_file": "Vyberte soubor",
|
||||
"profile_picture": "Profilový obrázek",
|
||||
"profile_picture_is_managed_by_ldap_server": "Profilový obrázek je spravován LDAP serverem a nelze jej zde změnit.",
|
||||
"click_profile_picture_to_upload_custom": "Klikněte na profilový obrázek pro nahrání vlastního ze souborů.",
|
||||
"image_should_be_in_format": "Obrázek by měl být ve formátu PNG nebo JPEG.",
|
||||
"items_per_page": "Položek na stránku",
|
||||
"no_items_found": "Nenalezeny žádné položky",
|
||||
"search": "Hledat...",
|
||||
"expand_card": "Rozbalit kartu",
|
||||
"copied": "Zkopírováno",
|
||||
"click_to_copy": "Kliknutím zkopírujete",
|
||||
"something_went_wrong": "Něco se pokazilo",
|
||||
"go_back_to_home": "Přejít zpět domů",
|
||||
"dont_have_access_to_your_passkey": "Nemáte přístup k Vašemu přístupovému klíči?",
|
||||
"login_background": "Pozadí přihlašovací stránky",
|
||||
"logo": "Logo",
|
||||
"login_code": "Přihlašovací kód",
|
||||
"create_a_login_code_to_sign_in_without_a_passkey_once": "Vytvořte přihlašovací kód, která může uživatel jedenktrát použít pro přihlášení bez přístupového klíče.",
|
||||
"one_hour": "1 hodina",
|
||||
"twelve_hours": "12 hodin",
|
||||
"one_day": "1 den",
|
||||
"one_week": "1 týden",
|
||||
"one_month": "1 měsíc",
|
||||
"expiration": "Expirace",
|
||||
"generate_code": "Vygenerovat kód",
|
||||
"name": "Jméno",
|
||||
"browser_unsupported": "Prohlížeče nepodporován",
|
||||
"this_browser_does_not_support_passkeys": "Tento prohlížeč nepodporuje přístupové klíče. Použijte prosím alternativní metodu přihlášení. přihlášení",
|
||||
"an_unknown_error_occurred": "Došlo k neznámé chybě",
|
||||
"authentication_process_was_aborted": "Proces přihlašování byl přerušen",
|
||||
"error_occurred_with_authenticator": "Došlo k chybě s autentifikátorem",
|
||||
"authenticator_does_not_support_discoverable_credentials": "Autentifikátor nepodporuje zobrazitelné přihlašovací údaje",
|
||||
"authenticator_does_not_support_resident_keys": "Autentikátor nepodporuje rezidentní klíče.",
|
||||
"passkey_was_previously_registered": "Tento přístupový klíč byl již dříve zaregistrován",
|
||||
"authenticator_does_not_support_any_of_the_requested_algorithms": "Autentikátor nepodporuje žádný z požadovaných algoritmů",
|
||||
"authenticator_timed_out": "Vypršel časový limit autentifikátoru",
|
||||
"critical_error_occurred_contact_administrator": "Došlo k kritické chybě. Obraťte se na správce.",
|
||||
"sign_in_to": "Přihlásit se k {name}",
|
||||
"client_not_found": "Klient nebyl nalezen",
|
||||
"client_wants_to_access_the_following_information": "<b>{client}</b> chce získat přístup k následujícím informacím:",
|
||||
"do_you_want_to_sign_in_to_client_with_your_app_name_account": "Chcete se přihlásit do <b>{client}</b> s vaším <b>{appName}</b> účtem?",
|
||||
"email": "E-mail",
|
||||
"view_your_email_address": "Zobrazit vaši e-mailovou adresu",
|
||||
"profile": "Profil",
|
||||
"view_your_profile_information": "Zobrazit informace o Vašem profilu",
|
||||
"groups": "Skupiny",
|
||||
"view_the_groups_you_are_a_member_of": "Zobrazit skupiny, které jste členem",
|
||||
"cancel": "Zrušit",
|
||||
"sign_in": "Přihlásit se",
|
||||
"try_again": "Zkusit znovu",
|
||||
"client_logo": "Logo klienta",
|
||||
"sign_out": "Odhlásit se",
|
||||
"do_you_want_to_sign_out_of_pocketid_with_the_account": "Chcete se s účtem <b>{username}</b> odhlásit z Pocket ID?",
|
||||
"sign_in_to_appname": "Přihlásit se k {appName}",
|
||||
"please_try_to_sign_in_again": "Zkuste se prosím znovu přihlásit.",
|
||||
"authenticate_yourself_with_your_passkey_to_access_the_admin_panel": "Autentizujte se pomocí Vašeho přístupového klíče pro přístup k administrátorskému panelu.",
|
||||
"authenticate": "Autentizovat",
|
||||
"appname_setup": "{appName} konfigurace",
|
||||
"please_try_again": "Prosím, zkuste znovu.",
|
||||
"you_are_about_to_sign_in_to_the_initial_admin_account": "Chystáte se přihlásit k počátečnímu účtu správce. Kdokoli s tímto odkazem může přistupovat k účtu, dokud nebude přidán přístupový účet. Prosím nastavte přístupový klíč co nejdříve, abyste zabránili neoprávněnému přístupu.",
|
||||
"continue": "Pokračovat",
|
||||
"alternative_sign_in": "Alternativní přihlášení",
|
||||
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "Pokud nemáte přístup k Vašemu přístupovému klíči, můžete se přihlášit pomocí jedné z následujících metod.",
|
||||
"use_your_passkey_instead": "Namísto toho použít svůj přístupový klíč?",
|
||||
"email_login": "Přihlášení e-mailem",
|
||||
"enter_a_login_code_to_sign_in": "Pro přihlášení zadejte přihlašovací kód.",
|
||||
"request_a_login_code_via_email": "Požádat o přihlášení pomocí e-mailu.",
|
||||
"go_back": "Jít zpět",
|
||||
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "Na zadaný e-mail byl zaslán e-mail, pokud existuje v systému.",
|
||||
"enter_code": "Zadejte kód",
|
||||
"enter_your_email_address_to_receive_an_email_with_a_login_code": "Zadejte svou e-mailovou adresu pro obdržení e-mailu s přihlašovacím kódem.",
|
||||
"your_email": "Váš e-mail",
|
||||
"submit": "Potvrdit",
|
||||
"enter_the_code_you_received_to_sign_in": "Zadejte kód, který jste obdrželi k přihlášení.",
|
||||
"code": "Kód",
|
||||
"invalid_redirect_url": "Neplatná URL přesměrování",
|
||||
"audit_log": "Protokol auditu",
|
||||
"users": "Uživatelé",
|
||||
"user_groups": "Uživatelské skupiny",
|
||||
"oidc_clients": "OIDC klienti",
|
||||
"api_keys": "API klíče",
|
||||
"application_configuration": "Konfigurace aplikace",
|
||||
"settings": "Nastavení",
|
||||
"update_pocket_id": "Aktualizovat Pocket ID",
|
||||
"powered_by": "Poháněno pomocí",
|
||||
"see_your_account_activities_from_the_last_3_months": "Podívejte se na aktivity Vašeho účtu za poslední 3 měsíce.",
|
||||
"time": "Čas",
|
||||
"event": "Událost",
|
||||
"approximate_location": "Přibližná poloha",
|
||||
"ip_address": "IP adresa",
|
||||
"device": "Zařízení",
|
||||
"client": "Klient",
|
||||
"unknown": "Neznámé",
|
||||
"account_details_updated_successfully": "Účet byl úspěšně aktualizován",
|
||||
"profile_picture_updated_successfully": "Profilový obrázek byl úspěšně aktualizován. Aktualizace může trvat několik minut.",
|
||||
"account_settings": "Nastavení účtu",
|
||||
"passkey_missing": "Chybí přístupový klíč",
|
||||
"please_provide_a_passkey_to_prevent_losing_access_to_your_account": "Přidejte prosím přístupový klíč, abyste zabránili ztrátě přístupu k Vašemu účtu.",
|
||||
"single_passkey_configured": "Nastaven jediný přístupový klíč",
|
||||
"it_is_recommended_to_add_more_than_one_passkey": "Doporučujeme přidat více než jeden přístupový klíč, aby nedošlo ke ztrátě přístupu k Vašemu účtu.",
|
||||
"account_details": "Podrobnosti účtu",
|
||||
"passkeys": "Přístupové klíče",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Spravujte své přístupový klíč, které můžete použít pro ověření.",
|
||||
"add_passkey": "Přidat přístupový klíč",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Vytvořte jednorázový přihlašovací kód pro přihlášení z jiného zařízení bez přístupového klíče.",
|
||||
"create": "Vytvořit",
|
||||
"first_name": "Jméno",
|
||||
"last_name": "Příjmení",
|
||||
"username": "Uživatelské jméno",
|
||||
"save": "Uložit",
|
||||
"username_can_only_contain": "Uživatelské jméno může obsahovat pouze malá písmena, číslice, podtržítka, tečky, pomlčky a symbol '@'",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Přihlaste se pomocí následujícího kódu. Platnost kódu vyprší za 15 minut.",
|
||||
"or_visit": "nebo navštívit",
|
||||
"added_on": "Přidáno",
|
||||
"rename": "Přejmenovat",
|
||||
"delete": "Smazat",
|
||||
"are_you_sure_you_want_to_delete_this_passkey": "Jste si jisti, že chcete odstranit tento přístupový klíč?",
|
||||
"passkey_deleted_successfully": "Přístupový klíč byl úspěšně smazán",
|
||||
"delete_passkey_name": "Odstranit {passkeyName}",
|
||||
"passkey_name_updated_successfully": "Název přístupového klíče byl úspěšně aktualizován",
|
||||
"name_passkey": "Jméno přístupového klíče",
|
||||
"name_your_passkey_to_easily_identify_it_later": "Pojmenujte Váš přístupový klíč, abyste ho snadno identifikovali později.",
|
||||
"create_api_key": "Vytvořit API klíč",
|
||||
"add_a_new_api_key_for_programmatic_access": "Přidejte nový API klíč pro programový přístup.",
|
||||
"add_api_key": "Přidat API klíč",
|
||||
"manage_api_keys": "Spravovat API klíče",
|
||||
"api_key_created": "API klíč vytvořen",
|
||||
"for_security_reasons_this_key_will_only_be_shown_once": "Z bezpečnostních důvodů bude tento klíč zobrazen pouze jednou. Uložte jej bezpečně.",
|
||||
"description": "Popis",
|
||||
"api_key": "API klíč",
|
||||
"close": "Zavřít",
|
||||
"name_to_identify_this_api_key": "Název pro identifikaci tohoto API klíče.",
|
||||
"expires_at": "Vyprší",
|
||||
"when_this_api_key_will_expire": "Až vyprší platnost tohoto API klíče.",
|
||||
"optional_description_to_help_identify_this_keys_purpose": "Volitelný popis, který pomůže identifikovat účel tohoto klíče.",
|
||||
"name_must_be_at_least_3_characters": "Název musí obsahovat alespoň 3 znaky",
|
||||
"name_cannot_exceed_50_characters": "Název nesmí překročit 50 znaků",
|
||||
"expiration_date_must_be_in_the_future": "Datum vypršení musí být v budoucnu",
|
||||
"revoke_api_key": "Zrušit API klíč",
|
||||
"never": "Nikdy",
|
||||
"revoke": "Odvolat",
|
||||
"api_key_revoked_successfully": "API klíč byl úspěšně odebrán",
|
||||
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Jste si jisti, že chcete zrušit klíč API \"{apiKeyName}\"? To naruší všechny integrace pomocí tohoto klíče.",
|
||||
"last_used": "Naposledy použito",
|
||||
"actions": "Akce",
|
||||
"images_updated_successfully": "Obrázky úspěšně aktualizovány",
|
||||
"general": "Obecné",
|
||||
"enable_email_notifications_to_alert_users_when_a_login_is_detected_from_a_new_device_or_location": "Povolte e-mailová oznámení pro upozornění uživatelů, pokud je zjištěno přihlášení z nového zařízení nebo umístění.",
|
||||
"ldap": "LDAP",
|
||||
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Nastavte LDAP pro synchronizaci uživatelů a skupin z LDAP serveru.",
|
||||
"images": "Obrázky",
|
||||
"update": "Aktualizace",
|
||||
"email_configuration_updated_successfully": "Konfigurace e-mailu byla úspěšně aktualizována",
|
||||
"save_changes_question": "Chcete uložit změny?",
|
||||
"you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "Musíte uložit změny před odesláním testovacího e-mailu. Chcete je nyní uložit?",
|
||||
"save_and_send": "Uložit a odeslat",
|
||||
"test_email_sent_successfully": "Testovací e-mail byl úspěšně odeslán na vaši e-mailovou adresu.",
|
||||
"failed_to_send_test_email": "Nepodařilo se odeslat testovací e-mail. Pro více informací zkontrolujte protokoly serveru.",
|
||||
"smtp_configuration": "Nastavení SMTP",
|
||||
"smtp_host": "SMTP Host",
|
||||
"smtp_port": "SMTP Port",
|
||||
"smtp_user": "SMTP Uživatel",
|
||||
"smtp_password": "SMTP Heslo",
|
||||
"smtp_from": "SMTP Od",
|
||||
"smtp_tls_option": "SMTP TLS volba",
|
||||
"email_tls_option": "Email TLS volba",
|
||||
"skip_certificate_verification": "Přeskočit ověření certifikátu",
|
||||
"this_can_be_useful_for_selfsigned_certificates": "To může být užitečné pro certifikáty s vlastními podpisy.",
|
||||
"enabled_emails": "Povolené e-maily",
|
||||
"email_login_notification": "E-mailovová oznámení o přihlášení",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Poslat uživateli e-mail, když se přihlásí z nového zařízení.",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Umožňuje uživatelům přihlásit se pomocí přihlašovacího kódu, který je odeslán na jejich e-mail. To výrazně snižuje bezpečnost, protože každý, kdo má přístup k e-mailu uživatele, může získat vstup.",
|
||||
"send_test_email": "Odeslat testovací e-mail",
|
||||
"application_configuration_updated_successfully": "Nastavení aplikace bylo úspěšně aktualizováno",
|
||||
"application_name": "Název aplikace",
|
||||
"session_duration": "Délka trvání relace",
|
||||
"the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again": "Doba trvání relace v minutách, než se uživatel musí znovu přihlásit.",
|
||||
"enable_self_account_editing": "Povolit úpravy vlastního účtu",
|
||||
"whether_the_users_should_be_able_to_edit_their_own_account_details": "Zda by uživatelé měli mít možnost upravit vlastní údaje o účtu.",
|
||||
"emails_verified": "E-mail ověřen",
|
||||
"whether_the_users_email_should_be_marked_as_verified_for_the_oidc_clients": "Zda má být e-mail uživatele označen jako ověřený pro OIDC klienty.",
|
||||
"ldap_configuration_updated_successfully": "Nastavení LDAP bylo úspěšně aktualizováno",
|
||||
"ldap_disabled_successfully": "LDAP úspěšně zakázán",
|
||||
"ldap_sync_finished": "LDAP synchronizace dokončena",
|
||||
"client_configuration": "Nastavení klienta",
|
||||
"ldap_url": "LDAP URL",
|
||||
"ldap_bind_dn": "LDAP Bind DN",
|
||||
"ldap_bind_password": "LDAP Bind Password",
|
||||
"ldap_base_dn": "LDAP Base DN",
|
||||
"user_search_filter": "Filtr vyhledávání uživatelů",
|
||||
"the_search_filter_to_use_to_search_or_sync_users": "Hledaný filtr pro vyhledávání/synchronizaci uživatelů.",
|
||||
"groups_search_filter": "Filtr hledání skupin",
|
||||
"the_search_filter_to_use_to_search_or_sync_groups": "Hledaný filtr pro vyhledávání/synchronizaci skupin.",
|
||||
"attribute_mapping": "Mapování atributů",
|
||||
"user_unique_identifier_attribute": "Atribut unikátního identifikátoru skupiny",
|
||||
"the_value_of_this_attribute_should_never_change": "Hodnota tohoto atributu by se nikdy neměla měnit.",
|
||||
"username_attribute": "Atribut uživatelského jména",
|
||||
"user_mail_attribute": "Atribut e-mailové adresy uživatele",
|
||||
"user_first_name_attribute": "Atribut jména uživatele",
|
||||
"user_last_name_attribute": "Atribut příjmení uživatele",
|
||||
"user_profile_picture_attribute": "Atribut uživatelského profilu obrázku",
|
||||
"the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image": "Hodnota tohoto atributu může být buď URL, binární nebo base64 zakódovaný obrázek.",
|
||||
"group_members_attribute": "Atribut členů skupiny",
|
||||
"the_attribute_to_use_for_querying_members_of_a_group": "Atribut použitý pro dotazování členů skupiny.",
|
||||
"group_unique_identifier_attribute": "Atribut unikátního identifikátoru skupiny",
|
||||
"group_name_attribute": "Atribut názvu skupiny",
|
||||
"admin_group_name": "Název skupiny administrátorů",
|
||||
"members_of_this_group_will_have_admin_privileges_in_pocketid": "Členové této skupiny budou mít práva administrátora v Pocket ID.",
|
||||
"disable": "Zakázat",
|
||||
"sync_now": "Synchronizovat",
|
||||
"enable": "Povolit",
|
||||
"user_created_successfully": "Uživatel byl úspěšně vytvořen",
|
||||
"create_user": "Vytvořit uživatele",
|
||||
"add_a_new_user_to_appname": "Přidat nového uživatele do {appName}",
|
||||
"add_user": "Přidat uživatele",
|
||||
"manage_users": "Správa uživatelů",
|
||||
"admin_privileges": "Administrátorská práva",
|
||||
"admins_have_full_access_to_the_admin_panel": "Administrátoři mají plný přístup do administrátorského panelu.",
|
||||
"delete_firstname_lastname": "Odstranit {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_delete_this_user": "Opravdu chcete odstranit tohoto uživatele?",
|
||||
"user_deleted_successfully": "Uživatel úspěšně odstraněn",
|
||||
"role": "Role",
|
||||
"source": "Zdroj",
|
||||
"admin": "Administrátor",
|
||||
"user": "Uživatel",
|
||||
"local": "Místní",
|
||||
"toggle_menu": "Rozbalovací nabídka ",
|
||||
"edit": "Upravit",
|
||||
"user_groups_updated_successfully": "Uživatelské skupiny úspěšně aktualizovány",
|
||||
"user_updated_successfully": "Uživatel úspěšně aktualizován",
|
||||
"custom_claims_updated_successfully": "Vlastní nároky byly úspěšně aktualizovány",
|
||||
"back": "Zpět",
|
||||
"user_details_firstname_lastname": "Podrobnosti o uživateli {firstName} {lastName}",
|
||||
"manage_which_groups_this_user_belongs_to": "Spravovat, ke kterým skupinám patří tento uživatel.",
|
||||
"custom_claims": "Vlastní nároky",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user": "Vlastní nároky jsou dvojice klíčů a hodnot, které lze použít k ukládání dalších informací o uživateli. Tyto nároky budou zahrnuty do identifikačního tokenu, pokud je požadován rozsah 'profil'.",
|
||||
"user_group_created_successfully": "Uživatelská skupina úspěšně vytvořena",
|
||||
"create_user_group": "Vytvořit uživatelskou skupinu",
|
||||
"create_a_new_group_that_can_be_assigned_to_users": "Vytvořte novou skupinu, která může být přiřazena uživatelům.",
|
||||
"add_group": "Přidat skupinu",
|
||||
"manage_user_groups": "Správa uživatelských skupin",
|
||||
"friendly_name": "Přátelské jméno",
|
||||
"name_that_will_be_displayed_in_the_ui": "Název, který se zobrazí v uživatelském rozhraní",
|
||||
"name_that_will_be_in_the_groups_claim": "Název, který se bude nacházet v nároku „skupiny“",
|
||||
"delete_name": "Odstranit {name}",
|
||||
"are_you_sure_you_want_to_delete_this_user_group": "Opravdu chcete odebrat tuto uživatelskou skupinu?",
|
||||
"user_group_deleted_successfully": "Uživatelská skupina úspěšně vytvořena",
|
||||
"user_count": "Počet uživatelů",
|
||||
"user_group_updated_successfully": "Uživatelská skupina úspěšně aktualizována",
|
||||
"users_updated_successfully": "Uživatelé byli úspěšně aktualizováni",
|
||||
"user_group_details_name": "Podrobnosti uživatelské skupiny {name}",
|
||||
"assign_users_to_this_group": "Přiřadit uživatele k této skupině.",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized": "Uživatelská tvrzení jsou dvojice klíčů a hodnot, které lze použít k ukládání dalších informací o uživateli. Tyto nároky budou zahrnuty do identifikačního tokenu, pokud je požadován rozsah 'profil'. Vlastní nároky definované uživatelem budou upřednostněny, pokud vzniknou konflikty.",
|
||||
"oidc_client_created_successfully": "OIDC klient byl úspěšně vytvořen",
|
||||
"create_oidc_client": "Vytvořit OIDC klienta",
|
||||
"add_a_new_oidc_client_to_appname": "Přidat nového OIDC klienta do {appName}.",
|
||||
"add_oidc_client": "Přidat OIDC klienta",
|
||||
"manage_oidc_clients": "Spravovat OIDC klienty",
|
||||
"one_time_link": "Jednorázový odkaz",
|
||||
"use_this_link_to_sign_in_once": "Pomocí tohoto odkazu se přihlásíte jednou. Toto je to nutné pro uživatele, kteří ještě nepřidali přístupový klíč nebo jej ztratili.",
|
||||
"add": "Přidat",
|
||||
"callback_urls": "URL zpětného volání",
|
||||
"logout_callback_urls": "URL zpětného volání při odhlášení",
|
||||
"public_client": "Veřejný klient",
|
||||
"public_clients_do_not_have_a_client_secret_and_use_pkce_instead": "Veřejní klienti nemají client secret a místo toho používají PKCE. Povolte to, pokud je váš klient SPA nebo mobilní aplikace.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Exchange je bezpečnostní funkce, která zabraňuje útokům CSRF a narušení autorizačních kódů.",
|
||||
"name_logo": "Logo {name}",
|
||||
"change_logo": "Změnit logo",
|
||||
"upload_logo": "Nahrát logo",
|
||||
"remove_logo": "Odstranit logo",
|
||||
"are_you_sure_you_want_to_delete_this_oidc_client": "Jste si jisti, že chcete odstranit tohoto OIDC klienta?",
|
||||
"oidc_client_deleted_successfully": "OIDC klient byl úspěšně smazán",
|
||||
"authorization_url": "Autorizační URL",
|
||||
"oidc_discovery_url": "OIDC Discovery URL",
|
||||
"token_url": "Token URL",
|
||||
"userinfo_url": "Userinfo URL",
|
||||
"logout_url": "Logout URL",
|
||||
"certificate_url": "Certificate URL",
|
||||
"enabled": "Povoleno",
|
||||
"disabled": "Zakázáno",
|
||||
"oidc_client_updated_successfully": "OIDC klient úspěšně aktualizován",
|
||||
"create_new_client_secret": "Vytvořit nový client secret",
|
||||
"are_you_sure_you_want_to_create_a_new_client_secret": "Jste si jisti, že chcete vytvořit nový client secret? Dosavadní bude zneplatněn.",
|
||||
"generate": "Generovat",
|
||||
"new_client_secret_created_successfully": "Nový client secret byl úspěšně vytvořen",
|
||||
"allowed_user_groups_updated_successfully": "Povolené skupiny uživatelů byly úspěšně aktualizovány",
|
||||
"oidc_client_name": "OIDC Klient {name}",
|
||||
"client_id": "ID klienta",
|
||||
"client_secret": "Client secret",
|
||||
"show_more_details": "Zobrazit další podrobnosti",
|
||||
"allowed_user_groups": "Povolené skupiny uživatelů",
|
||||
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Přidejte do tohoto klienta uživatelské skupiny, abyste omezili přístup pouze pro uživatele v těchto skupinách. Pokud nejsou vybrány žádné skupiny uživatelů, všichni uživatelé budou mít přístup k tomuto klientovi.",
|
||||
"favicon": "Favicon",
|
||||
"light_mode_logo": "Logo světlého režimu",
|
||||
"dark_mode_logo": "Logo tmavého režimu",
|
||||
"background_image": "Obrázek na pozadí",
|
||||
"language": "Jazyk",
|
||||
"reset_profile_picture_question": "Resetovat profilový obrázek?",
|
||||
"this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default": "Tímto odstraníte nahraný obrázek a obnovíte výchozí. Chcete pokračovat?",
|
||||
"reset": "Obnovit",
|
||||
"reset_to_default": "Obnovit výchozí",
|
||||
"profile_picture_has_been_reset": "Profilový obrázek byl obnoven. Aktualizace může trvat několik minut.",
|
||||
"select_the_language_you_want_to_use": "Vyberte jazyk, který chcete použít. Některé jazyky nemusí být plně přeloženy."
|
||||
}
|
||||
316
frontend/messages/de-DE.json
Normal file
316
frontend/messages/de-DE.json
Normal file
@@ -0,0 +1,316 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"my_account": "Mein Konto",
|
||||
"logout": "Abmelden",
|
||||
"confirm": "Bestätigen",
|
||||
"key": "Schlüssel",
|
||||
"value": "Wert",
|
||||
"remove_custom_claim": "Benutzerdefinierten Claim entfernen",
|
||||
"add_custom_claim": "Benutzerdefinierten Claim hinzufügen",
|
||||
"add_another": "Weitere hinzufügen",
|
||||
"select_a_date": "Datum auswählen",
|
||||
"select_file": "Datei auswählen",
|
||||
"profile_picture": "Profilbild",
|
||||
"profile_picture_is_managed_by_ldap_server": "Das Profilbild wird vom LDAP-Server verwaltet und kann hier nicht geändert werden.",
|
||||
"click_profile_picture_to_upload_custom": "Klicke auf das Profilbild, um ein benutzerdefiniertes Bild aus deinen Dateien hochzuladen.",
|
||||
"image_should_be_in_format": "Das Bild sollte im PNG- oder JPEG-Format vorliegen.",
|
||||
"items_per_page": "Einträge pro Seite",
|
||||
"no_items_found": "Keine Einträge gefunden",
|
||||
"search": "Suchen...",
|
||||
"expand_card": "Karte erweitern",
|
||||
"copied": "Kopiert",
|
||||
"click_to_copy": "Zum Kopieren klicken",
|
||||
"something_went_wrong": "Etwas ist schiefgelaufen",
|
||||
"go_back_to_home": "Zurück zur Startseite",
|
||||
"dont_have_access_to_your_passkey": "Du hast keinen Zugriff auf deinen Passkey?",
|
||||
"login_background": "Login Hintergrund",
|
||||
"logo": "Logo",
|
||||
"login_code": "Anmeldecode",
|
||||
"create_a_login_code_to_sign_in_without_a_passkey_once": "Erstelle einen Anmeldecode, mit dem sich der Benutzer einmalig ohne Passkey anmelden kann.",
|
||||
"one_hour": "1 Stunde",
|
||||
"twelve_hours": "12 Stunden",
|
||||
"one_day": "1 Tag",
|
||||
"one_week": "1 Woche",
|
||||
"one_month": "1 Monat",
|
||||
"expiration": "Ablaufdatum",
|
||||
"generate_code": "Code generieren",
|
||||
"name": "Name",
|
||||
"browser_unsupported": "Browser nicht unterstützt",
|
||||
"this_browser_does_not_support_passkeys": "Dieser Browser unterstützt keine Passkeys. Bitte verwende eine alternative Anmeldemethode.",
|
||||
"an_unknown_error_occurred": "Ein unbekannter Fehler ist aufgetreten",
|
||||
"authentication_process_was_aborted": "Der Authentifizierungsprozess wurde abgebrochen",
|
||||
"error_occurred_with_authenticator": "Beim Authentifikator ist ein Fehler aufgetreten",
|
||||
"authenticator_does_not_support_discoverable_credentials": "Der Authentifikator unterstützt keine erkennbaren Anmeldeinformationen",
|
||||
"authenticator_does_not_support_resident_keys": "Der Authentifikator unterstützt keine residenten Schlüssel",
|
||||
"passkey_was_previously_registered": "Dieser Passkey wurde bereits registriert",
|
||||
"authenticator_does_not_support_any_of_the_requested_algorithms": "Der Authentifikator unterstützt keinen der angeforderten Algorithmen",
|
||||
"authenticator_timed_out": "Timeout für den Authentifikator",
|
||||
"critical_error_occurred_contact_administrator": "Ein kritischer Fehler ist aufgetreten. Bitte kontaktiere deinen Administrator.",
|
||||
"sign_in_to": "Bei {name} anmelden",
|
||||
"client_not_found": "Client nicht gefunden",
|
||||
"client_wants_to_access_the_following_information": "<b>{client}</b> möchte auf die folgenden Informationen zugreifen:",
|
||||
"do_you_want_to_sign_in_to_client_with_your_app_name_account": "Möchtest du dich bei <b>{client}</b> mit deinem <b>{appName}</b> Konto anmelden?",
|
||||
"email": "E-Mail",
|
||||
"view_your_email_address": "Deine E-Mail-Adresse anzeigen",
|
||||
"profile": "Profil",
|
||||
"view_your_profile_information": "Profilinformationen anzeigen",
|
||||
"groups": "Gruppen",
|
||||
"view_the_groups_you_are_a_member_of": "Zeige die Gruppen, in denen du Mitglied bist",
|
||||
"cancel": "Abbrechen",
|
||||
"sign_in": "Anmelden",
|
||||
"try_again": "Erneut versuchen",
|
||||
"client_logo": "Client-Logo",
|
||||
"sign_out": "Abmelden",
|
||||
"do_you_want_to_sign_out_of_pocketid_with_the_account": "Möchtest du dich mit deinem Konto <b>{username}</b> von Pocket ID abmelden?",
|
||||
"sign_in_to_appname": "Bei {appName} anmelden",
|
||||
"please_try_to_sign_in_again": "Bitte versuche dich erneut anzumelden.",
|
||||
"authenticate_yourself_with_your_passkey_to_access_the_admin_panel": "Authentifiziere dich mit deinem Passkey, um auf das Admin Panel zugreifen zu können.",
|
||||
"authenticate": "Authentifizieren",
|
||||
"appname_setup": "{appName} Einrichtung",
|
||||
"please_try_again": "Bitte versuche es noch einmal.",
|
||||
"you_are_about_to_sign_in_to_the_initial_admin_account": "Du bist dabei, dich beim initialen Administratorkonto anzumelden. Jeder, der diesen Link hat, kann auf das Konto zugreifen, bis ein Passkey hinzugefügt wird. Bitte richte so schnell wie möglich einen Passkey ein, um unbefugten Zugriff zu verhindern.",
|
||||
"continue": "Fortsetzen",
|
||||
"alternative_sign_in": "Alternative Anmeldemethoden",
|
||||
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "Wenn du keinen Zugang zu deinen Passkey hast, kannst du dich mit einer der folgenden Methoden anmelden.",
|
||||
"use_your_passkey_instead": "Deinen Passkey stattdessen verwenden?",
|
||||
"email_login": "E-Mail Anmeldung",
|
||||
"enter_a_login_code_to_sign_in": "Gebe einen Anmeldecode zum Anmelden ein.",
|
||||
"request_a_login_code_via_email": "Login-Code per E-Mail anfordern.",
|
||||
"go_back": "Zurück",
|
||||
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "Eine E-Mail wurde an die angegebene E-Mail gesendet, sofern sie im System vorhanden ist.",
|
||||
"enter_code": "Code eingeben",
|
||||
"enter_your_email_address_to_receive_an_email_with_a_login_code": "Gib deine E-Mail-Adresse ein, um eine E-Mail mit einem Login-Code zu erhalten.",
|
||||
"your_email": "Deine E-Mail",
|
||||
"submit": "Bestätigen",
|
||||
"enter_the_code_you_received_to_sign_in": "Gebe den Code ein, den du erhalten hast, um dich anzumelden.",
|
||||
"code": "Code",
|
||||
"invalid_redirect_url": "Ungültige Weiterleitungs-URL",
|
||||
"audit_log": "Aktivitäts-Log",
|
||||
"users": "Benutzer",
|
||||
"user_groups": "Benutzergruppen",
|
||||
"oidc_clients": "OIDC Clients",
|
||||
"api_keys": "API Keys",
|
||||
"application_configuration": "Anwendungskonfiguration",
|
||||
"settings": "Einstellungen",
|
||||
"update_pocket_id": "Pocket ID aktualisieren",
|
||||
"powered_by": "Powered by",
|
||||
"see_your_account_activities_from_the_last_3_months": "Sehe dir deine Kontoaktivitäten der letzten drei Monate an.",
|
||||
"time": "Zeit",
|
||||
"event": "Ereignis",
|
||||
"approximate_location": "Ungefährer Standort",
|
||||
"ip_address": "IP-Adresse",
|
||||
"device": "Gerät",
|
||||
"client": "Client",
|
||||
"unknown": "unbekannt",
|
||||
"account_details_updated_successfully": "Kontodetails erfolgreich aktualisiert",
|
||||
"profile_picture_updated_successfully": "Profilbild erfolgreich aktualisiert. Die Aktualisierung kann einige Minuten dauern.",
|
||||
"account_settings": "Konto Einstellungen",
|
||||
"passkey_missing": "Passkey fehlt",
|
||||
"please_provide_a_passkey_to_prevent_losing_access_to_your_account": "Bitte füge einen Hauptschlüssel hinzu, um zu verhindern, dass du den Zugriff auf dein Konto verlierst.",
|
||||
"single_passkey_configured": "Nur ein Passkey hinterlegt",
|
||||
"it_is_recommended_to_add_more_than_one_passkey": "Es wird empfohlen, mehr als einen Passkey zu hinterlegen, um den Zugriff auf das Konto nicht zu verlieren.",
|
||||
"account_details": "Kontodetails",
|
||||
"passkeys": "Passkeys",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Verwalte deine Passkeys, mit denen du dich authentifizieren kannst.",
|
||||
"add_passkey": "Passkey hinzufügen",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Erstelle einen einmaligen Anmeldecode, um dich ohne Passkey von einem anderen Gerät aus anzumelden.",
|
||||
"create": "Erzeugen",
|
||||
"first_name": "Vorname",
|
||||
"last_name": "Nachname",
|
||||
"username": "Benutzername",
|
||||
"save": "Speichern",
|
||||
"username_can_only_contain": "Der Benutzername darf nur Kleinbuchstaben, Ziffern, Unterstriche, Punkte, Bindestriche und das Symbol „@“ enthalten",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Melde dich mit dem folgenden Code an. Der Code läuft in 15 Minuten ab.",
|
||||
"or_visit": "oder besuchen",
|
||||
"added_on": "Hinzugefügt am",
|
||||
"rename": "Umbenennen",
|
||||
"delete": "Löschen",
|
||||
"are_you_sure_you_want_to_delete_this_passkey": "Möchtest du diesen Hauptschlüssel wirklich löschen?",
|
||||
"passkey_deleted_successfully": "Passkey erfolgreich gelöscht",
|
||||
"delete_passkey_name": "Lösche {passkeyName}",
|
||||
"passkey_name_updated_successfully": "Passkey Name erfolgreich aktualisiert",
|
||||
"name_passkey": "Passkey Name",
|
||||
"name_your_passkey_to_easily_identify_it_later": "Benenne deinen Passkey, um ihn später leicht identifizieren zu können.",
|
||||
"create_api_key": "API Key erstellen",
|
||||
"add_a_new_api_key_for_programmatic_access": "Füge einen neuen API-Schlüssel für programmatischen Zugriff hinzu.",
|
||||
"add_api_key": "API Key hinzufügen",
|
||||
"manage_api_keys": "API Keys verwalten",
|
||||
"api_key_created": "API Key erstellt",
|
||||
"for_security_reasons_this_key_will_only_be_shown_once": "Aus Sicherheitsgründen wird dieser Schlüssel nur einmal angezeigt. Bitte speichere ihn sicher.",
|
||||
"description": "Beschreibung",
|
||||
"api_key": "API Key",
|
||||
"close": "Schließen",
|
||||
"name_to_identify_this_api_key": "Name zum identifizieren des API Keys.",
|
||||
"expires_at": "Ablaufdatum",
|
||||
"when_this_api_key_will_expire": "Wann der API Key ablaufen wird.",
|
||||
"optional_description_to_help_identify_this_keys_purpose": "Optionale Beschreibung, um den Zweck dieses Schlüssels zu identifizieren.",
|
||||
"name_must_be_at_least_3_characters": "Der Name muss mindestens 3 Zeichen lang sein",
|
||||
"name_cannot_exceed_50_characters": "Der Name darf nicht länger als 50 Zeichen sein",
|
||||
"expiration_date_must_be_in_the_future": "Ablaufdatum muss in der Zukunft liegen",
|
||||
"revoke_api_key": "API Key widerrufen",
|
||||
"never": "Nie",
|
||||
"revoke": "Widerrufen",
|
||||
"api_key_revoked_successfully": "API Key erfolgreich widerrufen",
|
||||
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Bist du sicher, dass du den API Schlüssel \"{apiKeyName}\" widerrufen willst? Das wird jegliche Integrationen, die diesen Schlüssel verwenden, brechen.",
|
||||
"last_used": "Letzte Verwendung",
|
||||
"actions": "Aktionen",
|
||||
"images_updated_successfully": "Bild erfolgreich aktualisiert",
|
||||
"general": "Allgemein",
|
||||
"enable_email_notifications_to_alert_users_when_a_login_is_detected_from_a_new_device_or_location": "Aktiviere E-Mail Benachrichtigungen, um Benutzer zu informieren, wenn ein Login von einem neuen Gerät oder Standort erkannt wird.",
|
||||
"ldap": "LDAP",
|
||||
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Konfiguriere LDAP-Einstellungen, um Benutzer und Gruppen von einem LDAP-Server zu synchronisieren.",
|
||||
"images": "Bilder",
|
||||
"update": "Aktualisieren",
|
||||
"email_configuration_updated_successfully": "E-Mail-Konfiguration erfolgreich aktualisiert",
|
||||
"save_changes_question": "Änderungen speichern?",
|
||||
"you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "Du musst die Änderungen speichern, bevor du eine Test-E-Mail senden kannst. Möchtest du jetzt speichern?",
|
||||
"save_and_send": "Speichern und senden",
|
||||
"test_email_sent_successfully": "Test-E-Mail wurde erfolgreich an deine E-Mail-Adresse gesendet.",
|
||||
"failed_to_send_test_email": "Test-E-Mail konnte nicht gesendet werden. Weitere Informationen findest du in den Serverprotokollen.",
|
||||
"smtp_configuration": "SMTP Konfiguration",
|
||||
"smtp_host": "SMTP Host",
|
||||
"smtp_port": "SMTP Port",
|
||||
"smtp_user": "SMTP Benutzer",
|
||||
"smtp_password": "SMTP Passwort",
|
||||
"smtp_from": "SMTP Absender",
|
||||
"smtp_tls_option": "SMTP TLS Option",
|
||||
"email_tls_option": "E-Mail-TLS-Option",
|
||||
"skip_certificate_verification": "Zertifikatsüberprüfung überspringen",
|
||||
"this_can_be_useful_for_selfsigned_certificates": "Das kann nützlich für selbstsignierte Zertifikate sein.",
|
||||
"enabled_emails": "E-Mails aktivieren",
|
||||
"email_login_notification": "E-Mail Benachrichtigung bei Login",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Sende dem Benutzer eine E-Mail, wenn er sich von einem neuen Gerät aus anmeldet.",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Ermöglicht Benutzer, sich mit einem Login-Code anzumelden, der an ihre E-Mail gesendet wurde. Dies reduziert die Sicherheit erheblich, da jeder, der Zugriff auf die E-Mail des Benutzers hat, Zugang bekommen kann.",
|
||||
"send_test_email": "Test-E-Mail senden",
|
||||
"application_configuration_updated_successfully": "Anwendungskonfiguration erfolgreich aktualisiert",
|
||||
"application_name": "Anwendungsname",
|
||||
"session_duration": "Sitzungsdauer",
|
||||
"the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again": "Die Dauer einer Sitzung in Minuten, bevor sich der Benutzer erneut anmelden muss.",
|
||||
"enable_self_account_editing": "Selbstverwaltung des Kontos aktivieren",
|
||||
"whether_the_users_should_be_able_to_edit_their_own_account_details": "Gibt an, ob die Benutzer in der Lage sein sollen, ihre eigenen Kontodetails zu ändern.",
|
||||
"emails_verified": "E-Mail-Adressen verifiziert",
|
||||
"whether_the_users_email_should_be_marked_as_verified_for_the_oidc_clients": "Gibt an, ob die E-Mail des Benutzers für die OIDC-Clients als verifiziert markiert werden soll.",
|
||||
"ldap_configuration_updated_successfully": "LDAP-Konfiguration erfolgreich aktualisiert",
|
||||
"ldap_disabled_successfully": "LDAP erfolgreich deaktiviert",
|
||||
"ldap_sync_finished": "LDAP-Synchronisation beendet",
|
||||
"client_configuration": "Client Konfiguration",
|
||||
"ldap_url": "LDAP URL",
|
||||
"ldap_bind_dn": "LDAP Bind DN",
|
||||
"ldap_bind_password": "LDAP Bind Passwort",
|
||||
"ldap_base_dn": "LDAP Base DN",
|
||||
"user_search_filter": "Benutzer Suchfilter",
|
||||
"the_search_filter_to_use_to_search_or_sync_users": "Der Suchfilter, der verwendet wird, um Benutzer zu suchen/synchronisieren.",
|
||||
"groups_search_filter": "Gruppensuchfilter",
|
||||
"the_search_filter_to_use_to_search_or_sync_groups": "Der Suchfilter, der verwendet wird, um Gruppen zu suchen/synchronisieren.",
|
||||
"attribute_mapping": "Attribut Zuordnung",
|
||||
"user_unique_identifier_attribute": "Eindeutiges Benutzerkennungs-Attribut",
|
||||
"the_value_of_this_attribute_should_never_change": "Der Wert dieses Attributs sollte sich nie ändern.",
|
||||
"username_attribute": "Benutzername Attribut",
|
||||
"user_mail_attribute": "Benutzer E-Mail Attribut",
|
||||
"user_first_name_attribute": "Benutzer Vornamen Attribut",
|
||||
"user_last_name_attribute": "Benutzer Nachname Attribut",
|
||||
"user_profile_picture_attribute": "Benutzer Profilbild Attribut",
|
||||
"the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image": "Der Wert dieses Attributs kann entweder eine URL, eine Binärdatei oder ein base64-kodiertes Bild sein.",
|
||||
"group_members_attribute": "Gruppenmitglieder Attribut",
|
||||
"the_attribute_to_use_for_querying_members_of_a_group": "Das zu verwendende Attribut zur Abfrage von Mitgliedern einer Gruppe.",
|
||||
"group_unique_identifier_attribute": "Eindeutiges Gruppenkennungs-Attribut",
|
||||
"group_name_attribute": "Gruppennamen Attribut",
|
||||
"admin_group_name": "Name der Admingruppe",
|
||||
"members_of_this_group_will_have_admin_privileges_in_pocketid": "Mitglieder dieser Gruppe werden Admin-Privilegien in Pocket ID haben.",
|
||||
"disable": "Deaktivieren",
|
||||
"sync_now": "Jetzt synchronisieren",
|
||||
"enable": "Aktivieren",
|
||||
"user_created_successfully": "Benutzer erfolgreich erstellt",
|
||||
"create_user": "Benutzer erstellen",
|
||||
"add_a_new_user_to_appname": "Neuen Benutzer zu {appName} hinzufügen",
|
||||
"add_user": "Benutzer hinzufügen",
|
||||
"manage_users": "Benutzer verwalten",
|
||||
"admin_privileges": "Administratorrechte",
|
||||
"admins_have_full_access_to_the_admin_panel": "Admins haben vollen Zugriff auf das Admin Panel.",
|
||||
"delete_firstname_lastname": "{firstName} {lastName} löschen",
|
||||
"are_you_sure_you_want_to_delete_this_user": "Bist du sicher, dass du diesen Benutzer löschen willst?",
|
||||
"user_deleted_successfully": "Benutzer erfolgreich gelöscht",
|
||||
"role": "Rolle",
|
||||
"source": "Quelle",
|
||||
"admin": "Admin",
|
||||
"user": "Benutzer",
|
||||
"local": "Lokal",
|
||||
"toggle_menu": "Menü umschalten",
|
||||
"edit": "Bearbeiten",
|
||||
"user_groups_updated_successfully": "Benutzergruppen erfolgreich aktualisiert",
|
||||
"user_updated_successfully": "Benutzer erfolgreich aktualisiert",
|
||||
"custom_claims_updated_successfully": "Benutzerdefinierte Claims erfolgreich aktualisiert",
|
||||
"back": "Zurück",
|
||||
"user_details_firstname_lastname": "Benutzerdetails {firstName} {lastName}",
|
||||
"manage_which_groups_this_user_belongs_to": "Verwalte, zu welchen Gruppen dieser Benutzer gehört.",
|
||||
"custom_claims": "Benutzerdefinierte Claims",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user": "Benutzerdefinierte Claims sind Schlüssel-Wert-Paare, die verwendet werden können, um zusätzliche Informationen über einen Benutzer zu speichern. Diese Claims werden im ID-Token aufgenommen, wenn der Scope \"profile\" angefordert wird.",
|
||||
"user_group_created_successfully": "Benutzergruppe erfolgreich erstellt",
|
||||
"create_user_group": "Benutzergruppe erstellen",
|
||||
"create_a_new_group_that_can_be_assigned_to_users": "Eine neue Benutzergruppe erstellen, der Benutzer zugewiesen werden können.",
|
||||
"add_group": "Gruppe hinzufügen",
|
||||
"manage_user_groups": "Benutzergruppen verwalten",
|
||||
"friendly_name": "Anzeigename",
|
||||
"name_that_will_be_displayed_in_the_ui": "Name, der in der Benutzeroberfläche angezeigt wird",
|
||||
"name_that_will_be_in_the_groups_claim": "Name, der im \"groups\" claim vorhanden sein wird",
|
||||
"delete_name": "{name} löschen",
|
||||
"are_you_sure_you_want_to_delete_this_user_group": "Bist du sicher, dass du diese Benutzer Gruppe löschen willst?",
|
||||
"user_group_deleted_successfully": "Benutzergruppe erfolgreich gelöscht",
|
||||
"user_count": "Benutzeranzahl",
|
||||
"user_group_updated_successfully": "Benutzergruppe erfolgreich aktualisiert",
|
||||
"users_updated_successfully": "Benutzer erfolgreich aktualisiert",
|
||||
"user_group_details_name": "Benutzergruppendetails {name}",
|
||||
"assign_users_to_this_group": "Benutzer dieser Gruppe zuweisen.",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized": "Benutzerdefinierte Claims sind Schlüssel-Wert-Paare, die verwendet werden können, um zusätzliche Informationen über einen Benutzer zu speichern. Diese Claims werden im ID-Token aufgenommen, wenn der Scope \"profile\" angefordert wird. Benutzerdefinierte Claims werden priorisiert, wenn Konflikte auftreten.",
|
||||
"oidc_client_created_successfully": "OIDC Client erfolgreich erstellt",
|
||||
"create_oidc_client": "OIDC Client erstellen",
|
||||
"add_a_new_oidc_client_to_appname": "Einen neuen OIDC Client zu {appName} hinzufügen.",
|
||||
"add_oidc_client": "OIDC Client hinzufügen",
|
||||
"manage_oidc_clients": "OIDC Clients verwalten",
|
||||
"one_time_link": "Einmallink",
|
||||
"use_this_link_to_sign_in_once": "Benutze diesen Link, um dich einmal anzumelden. Dieser wird für Benutzer benötigt, die noch keinem Passkey hinzugefügt haben oder diesen verloren haben.",
|
||||
"add": "Hinzufügen",
|
||||
"callback_urls": "Callback URLs",
|
||||
"logout_callback_urls": "Abmelde Callback URLs",
|
||||
"public_client": "Öffentlicher Client",
|
||||
"public_clients_do_not_have_a_client_secret_and_use_pkce_instead": "Öffentliche Clients haben kein Client-Geheimnis und verwenden stattdessen PKCE. Aktiviere dies, wenn dein Client eine SPA oder mobile App ist.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Der Public Key Code Exchange (öffentlicher Schlüsselaustausch) ist eine Sicherheitsfunktion, um CSRF Angriffe und Angriffe zum Abfangen von Autorisierungscodes zu verhindern.",
|
||||
"name_logo": "{name} Logo",
|
||||
"change_logo": "Logo ändern",
|
||||
"upload_logo": "Logo hochladen",
|
||||
"remove_logo": "Logo entfernen",
|
||||
"are_you_sure_you_want_to_delete_this_oidc_client": "Bist du sicher, dass du diesen OIDC Client löschen willst?",
|
||||
"oidc_client_deleted_successfully": "OIDC Client erfolgreich gelöscht",
|
||||
"authorization_url": "Autorisierungs-URL",
|
||||
"oidc_discovery_url": "OIDC Discovery URL",
|
||||
"token_url": "Token URL",
|
||||
"userinfo_url": "Benutzerinfo URL",
|
||||
"logout_url": "Abmelde URL",
|
||||
"certificate_url": "Zertifikats-URL",
|
||||
"enabled": "Aktiviert",
|
||||
"disabled": "Deaktiviert",
|
||||
"oidc_client_updated_successfully": "OIDC Client erfolgreich aktualisiert",
|
||||
"create_new_client_secret": "Neues Client-Geheimnis erstellen",
|
||||
"are_you_sure_you_want_to_create_a_new_client_secret": "Bist du sicher, dass du ein neues Client-Geheimnis erstellen möchtest? Das alte Client-Geheimnis wird dadurch ungültig.",
|
||||
"generate": "Generieren",
|
||||
"new_client_secret_created_successfully": "Neues Client-Geheimnis erfolgreich erstellt",
|
||||
"allowed_user_groups_updated_successfully": "Erlaubte Benutzergruppen erfolgreich aktualisiert",
|
||||
"oidc_client_name": "OIDC Client {name}",
|
||||
"client_id": "Client ID",
|
||||
"client_secret": "Client-Geheimnis",
|
||||
"show_more_details": "Mehr Details anzeigen",
|
||||
"allowed_user_groups": "Erlaubte Benutzergruppen",
|
||||
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Füge diesem Client Benutzergruppen hinzu, um den Zugriff auf Benutzer in diesen Gruppen zu beschränken. Wenn keine Benutzergruppen ausgewählt sind, werden alle Benutzer Zugriff auf diesen Client haben.",
|
||||
"favicon": "Favicon",
|
||||
"light_mode_logo": "Hell-Modus Logo",
|
||||
"dark_mode_logo": "Dunkel-Modus Logo",
|
||||
"background_image": "Hintergrundbild",
|
||||
"language": "Sprache",
|
||||
"reset_profile_picture_question": "Profilbild zurücksetzen?",
|
||||
"this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default": "Das hochgeladene Bild wird entfernt und das Profilbild auf die Standardeinstellung zurückgesetzt. Möchten Sie fortfahren?",
|
||||
"reset": "Zurücksetzen",
|
||||
"reset_to_default": "Auf Standard zurücksetzen",
|
||||
"profile_picture_has_been_reset": "Das Profilbild wurde zurückgesetzt. Es kann einige Minuten dauern, bis es aktualisiert wird.",
|
||||
"select_the_language_you_want_to_use": "Wähle die Sprache aus, die du verwenden möchtest. Einige Sprachen sind möglicherweise nicht vollständig übersetzt."
|
||||
}
|
||||
316
frontend/messages/en-US.json
Normal file
316
frontend/messages/en-US.json
Normal file
@@ -0,0 +1,316 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"my_account": "My Account",
|
||||
"logout": "Logout",
|
||||
"confirm": "Confirm",
|
||||
"key": "Key",
|
||||
"value": "Value",
|
||||
"remove_custom_claim": "Remove custom claim",
|
||||
"add_custom_claim": "Add custom claim",
|
||||
"add_another": "Add another",
|
||||
"select_a_date": "Select a date",
|
||||
"select_file": "Select File",
|
||||
"profile_picture": "Profile Picture",
|
||||
"profile_picture_is_managed_by_ldap_server": "The profile picture is managed by the LDAP server and cannot be changed here.",
|
||||
"click_profile_picture_to_upload_custom": "Click on the profile picture to upload a custom one from your files.",
|
||||
"image_should_be_in_format": "The image should be in PNG or JPEG format.",
|
||||
"items_per_page": "Items per page",
|
||||
"no_items_found": "No items found",
|
||||
"search": "Search...",
|
||||
"expand_card": "Expand card",
|
||||
"copied": "Copied",
|
||||
"click_to_copy": "Click to copy",
|
||||
"something_went_wrong": "Something went wrong",
|
||||
"go_back_to_home": "Go back to home",
|
||||
"dont_have_access_to_your_passkey": "Don't have access to your passkey?",
|
||||
"login_background": "Login background",
|
||||
"logo": "Logo",
|
||||
"login_code": "Login Code",
|
||||
"create_a_login_code_to_sign_in_without_a_passkey_once": "Create a login code that the user can use to sign in without a passkey once.",
|
||||
"one_hour": "1 hour",
|
||||
"twelve_hours": "12 hours",
|
||||
"one_day": "1 day",
|
||||
"one_week": "1 week",
|
||||
"one_month": "1 month",
|
||||
"expiration": "Expiration",
|
||||
"generate_code": "Generate Code",
|
||||
"name": "Name",
|
||||
"browser_unsupported": "Browser unsupported",
|
||||
"this_browser_does_not_support_passkeys": "This browser doesn't support passkeys. Please or use a alternative sign in method.",
|
||||
"an_unknown_error_occurred": "An unknown error occurred",
|
||||
"authentication_process_was_aborted": "The authentication process was aborted",
|
||||
"error_occurred_with_authenticator": "An error occurred with the authenticator",
|
||||
"authenticator_does_not_support_discoverable_credentials": "The authenticator does not support discoverable credentials",
|
||||
"authenticator_does_not_support_resident_keys": "The authenticator does not support resident keys",
|
||||
"passkey_was_previously_registered": "This passkey was previously registered",
|
||||
"authenticator_does_not_support_any_of_the_requested_algorithms": "The authenticator does not support any of the requested algorithms",
|
||||
"authenticator_timed_out": "The authenticator timed out",
|
||||
"critical_error_occurred_contact_administrator": "A critical error occurred. Please contact your administrator.",
|
||||
"sign_in_to": "Sign in to {name}",
|
||||
"client_not_found": "Client not found",
|
||||
"client_wants_to_access_the_following_information": "<b>{client}</b> wants to access the following information:",
|
||||
"do_you_want_to_sign_in_to_client_with_your_app_name_account": "Do you want to sign in to <b>{client}</b> with your <b>{appName}</b> account?",
|
||||
"email": "Email",
|
||||
"view_your_email_address": "View your email address",
|
||||
"profile": "Profile",
|
||||
"view_your_profile_information": "View your profile information",
|
||||
"groups": "Groups",
|
||||
"view_the_groups_you_are_a_member_of": "View the groups you are a member of",
|
||||
"cancel": "Cancel",
|
||||
"sign_in": "Sign in",
|
||||
"try_again": "Try again",
|
||||
"client_logo": "Client Logo",
|
||||
"sign_out": "Sign out",
|
||||
"do_you_want_to_sign_out_of_pocketid_with_the_account": "Do you want to sign out of Pocket ID with the account <b>{username}</b>?",
|
||||
"sign_in_to_appname": "Sign in to {appName}",
|
||||
"please_try_to_sign_in_again": "Please try to sign in again.",
|
||||
"authenticate_yourself_with_your_passkey_to_access_the_admin_panel": "Authenticate yourself with your passkey to access the admin panel.",
|
||||
"authenticate": "Authenticate",
|
||||
"appname_setup": "{appName} Setup",
|
||||
"please_try_again": "Please try again.",
|
||||
"you_are_about_to_sign_in_to_the_initial_admin_account": "You're about to sign in to the initial admin account. Anyone with this link can access the account until a passkey is added. Please set up a passkey as soon as possible to prevent unauthorized access.",
|
||||
"continue": "Continue",
|
||||
"alternative_sign_in": "Alternative Sign In",
|
||||
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "If you dont't have access to your passkey, you can sign in using one of the following methods.",
|
||||
"use_your_passkey_instead": "Use your passkey instead?",
|
||||
"email_login": "Email Login",
|
||||
"enter_a_login_code_to_sign_in": "Enter a login code to sign in.",
|
||||
"request_a_login_code_via_email": "Request a login code via email.",
|
||||
"go_back": "Go back",
|
||||
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "An email has been sent to the provided email, if it exists in the system.",
|
||||
"enter_code": "Enter code",
|
||||
"enter_your_email_address_to_receive_an_email_with_a_login_code": "Enter your email address to receive an email with a login code.",
|
||||
"your_email": "Your email",
|
||||
"submit": "Submit",
|
||||
"enter_the_code_you_received_to_sign_in": "Enter the code you received to sign in.",
|
||||
"code": "Code",
|
||||
"invalid_redirect_url": "Invalid redirect URL",
|
||||
"audit_log": "Audit Log",
|
||||
"users": "Users",
|
||||
"user_groups": "User Groups",
|
||||
"oidc_clients": "OIDC Clients",
|
||||
"api_keys": "API Keys",
|
||||
"application_configuration": "Application Configuration",
|
||||
"settings": "Settings",
|
||||
"update_pocket_id": "Update Pocket ID",
|
||||
"powered_by": "Powered by",
|
||||
"see_your_account_activities_from_the_last_3_months": "See your account activities from the last 3 months.",
|
||||
"time": "Time",
|
||||
"event": "Event",
|
||||
"approximate_location": "Approximate Location",
|
||||
"ip_address": "IP Address",
|
||||
"device": "Device",
|
||||
"client": "Client",
|
||||
"unknown": "Unknown",
|
||||
"account_details_updated_successfully": "Account details updated successfully",
|
||||
"profile_picture_updated_successfully": "Profile picture updated successfully. It may take a few minutes to update.",
|
||||
"account_settings": "Account Settings",
|
||||
"passkey_missing": "Passkey missing",
|
||||
"please_provide_a_passkey_to_prevent_losing_access_to_your_account": "Please add a passkey to prevent losing access to your account.",
|
||||
"single_passkey_configured": "Single Passkey Configured",
|
||||
"it_is_recommended_to_add_more_than_one_passkey": "It is recommended to add more than one passkey to avoid losing access to your account.",
|
||||
"account_details": "Account Details",
|
||||
"passkeys": "Passkeys",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Manage your passkeys that you can use to authenticate yourself.",
|
||||
"add_passkey": "Add Passkey",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Create a one-time login code to sign in from a different device without a passkey.",
|
||||
"create": "Create",
|
||||
"first_name": "First name",
|
||||
"last_name": "Last name",
|
||||
"username": "Username",
|
||||
"save": "Save",
|
||||
"username_can_only_contain": "Username can only contain lowercase letters, numbers, underscores, dots, hyphens, and '@' symbols",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Sign in using the following code. The code will expire in 15 minutes.",
|
||||
"or_visit": "or visit",
|
||||
"added_on": "Added on",
|
||||
"rename": "Rename",
|
||||
"delete": "Delete",
|
||||
"are_you_sure_you_want_to_delete_this_passkey": "Are you sure you want to delete this passkey?",
|
||||
"passkey_deleted_successfully": "Passkey deleted successfully",
|
||||
"delete_passkey_name": "Delete {passkeyName}",
|
||||
"passkey_name_updated_successfully": "Passkey name updated successfully",
|
||||
"name_passkey": "Name Passkey",
|
||||
"name_your_passkey_to_easily_identify_it_later": "Name your passkey to easily identify it later.",
|
||||
"create_api_key": "Create API Key",
|
||||
"add_a_new_api_key_for_programmatic_access": "Add a new API key for programmatic access.",
|
||||
"add_api_key": "Add API Key",
|
||||
"manage_api_keys": "Manage API Keys",
|
||||
"api_key_created": "API Key Created",
|
||||
"for_security_reasons_this_key_will_only_be_shown_once": "For security reasons, this key will only be shown once. Please store it securely.",
|
||||
"description": "Description",
|
||||
"api_key": "API Key",
|
||||
"close": "Close",
|
||||
"name_to_identify_this_api_key": "Name to identify this API key.",
|
||||
"expires_at": "Expires At",
|
||||
"when_this_api_key_will_expire": "When this API key will expire.",
|
||||
"optional_description_to_help_identify_this_keys_purpose": "Optional description to help identify this key's purpose.",
|
||||
"name_must_be_at_least_3_characters": "Name must be at least 3 characters",
|
||||
"name_cannot_exceed_50_characters": "Name cannot exceed 50 characters",
|
||||
"expiration_date_must_be_in_the_future": "Expiration date must be in the future",
|
||||
"revoke_api_key": "Revoke API Key",
|
||||
"never": "Never",
|
||||
"revoke": "Revoke",
|
||||
"api_key_revoked_successfully": "API key revoked successfully",
|
||||
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Are you sure you want to revoke the API key \"{apiKeyName}\"? This will break any integrations using this key.",
|
||||
"last_used": "Last Used",
|
||||
"actions": "Actions",
|
||||
"images_updated_successfully": "Images updated successfully",
|
||||
"general": "General",
|
||||
"enable_email_notifications_to_alert_users_when_a_login_is_detected_from_a_new_device_or_location": "Enable email notifications to alert users when a login is detected from a new device or location.",
|
||||
"ldap": "LDAP",
|
||||
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Configure LDAP settings to sync users and groups from an LDAP server.",
|
||||
"images": "Images",
|
||||
"update": "Update",
|
||||
"email_configuration_updated_successfully": "Email configuration updated successfully",
|
||||
"save_changes_question": "Save changes?",
|
||||
"you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "You have to save the changes before sending a test email. Do you want to save now?",
|
||||
"save_and_send": "Save and send",
|
||||
"test_email_sent_successfully": "Test email sent successfully to your email address.",
|
||||
"failed_to_send_test_email": "Failed to send test email. Check the server logs for more information.",
|
||||
"smtp_configuration": "SMTP Configuration",
|
||||
"smtp_host": "SMTP Host",
|
||||
"smtp_port": "SMTP Port",
|
||||
"smtp_user": "SMTP User",
|
||||
"smtp_password": "SMTP Password",
|
||||
"smtp_from": "SMTP From",
|
||||
"smtp_tls_option": "SMTP TLS Option",
|
||||
"email_tls_option": "Email TLS Option",
|
||||
"skip_certificate_verification": "Skip Certificate Verification",
|
||||
"this_can_be_useful_for_selfsigned_certificates": "This can be useful for self-signed certificates.",
|
||||
"enabled_emails": "Enabled Emails",
|
||||
"email_login_notification": "Email Login Notification",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Send an email to the user when they log in from a new device.",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to sign in with a login code sent to their email. This reduces the security significantly as anyone with access to the user's email can gain entry.",
|
||||
"send_test_email": "Send test email",
|
||||
"application_configuration_updated_successfully": "Application configuration updated successfully",
|
||||
"application_name": "Application Name",
|
||||
"session_duration": "Session Duration",
|
||||
"the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again": "The duration of a session in minutes before the user has to sign in again.",
|
||||
"enable_self_account_editing": "Enable Self-Account Editing",
|
||||
"whether_the_users_should_be_able_to_edit_their_own_account_details": "Whether the users should be able to edit their own account details.",
|
||||
"emails_verified": "Emails Verified",
|
||||
"whether_the_users_email_should_be_marked_as_verified_for_the_oidc_clients": "Whether the user's email should be marked as verified for the OIDC clients.",
|
||||
"ldap_configuration_updated_successfully": "LDAP configuration updated successfully",
|
||||
"ldap_disabled_successfully": "LDAP disabled successfully",
|
||||
"ldap_sync_finished": "LDAP sync finished",
|
||||
"client_configuration": "Client Configuration",
|
||||
"ldap_url": "LDAP URL",
|
||||
"ldap_bind_dn": "LDAP Bind DN",
|
||||
"ldap_bind_password": "LDAP Bind Password",
|
||||
"ldap_base_dn": "LDAP Base DN",
|
||||
"user_search_filter": "User Search Filter",
|
||||
"the_search_filter_to_use_to_search_or_sync_users": "The Search filter to use to search/sync users.",
|
||||
"groups_search_filter": "Groups Search Filter",
|
||||
"the_search_filter_to_use_to_search_or_sync_groups": "The Search filter to use to search/sync groups.",
|
||||
"attribute_mapping": "Attribute Mapping",
|
||||
"user_unique_identifier_attribute": "User Unique Identifier Attribute",
|
||||
"the_value_of_this_attribute_should_never_change": "The value of this attribute should never change.",
|
||||
"username_attribute": "Username Attribute",
|
||||
"user_mail_attribute": "User Mail Attribute",
|
||||
"user_first_name_attribute": "User First Name Attribute",
|
||||
"user_last_name_attribute": "User Last Name Attribute",
|
||||
"user_profile_picture_attribute": "User Profile Picture Attribute",
|
||||
"the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image": "The value of this attribute can either be a URL, a binary or a base64 encoded image.",
|
||||
"group_members_attribute": "Group Members Attribute",
|
||||
"the_attribute_to_use_for_querying_members_of_a_group": "The attribute to use for querying members of a group.",
|
||||
"group_unique_identifier_attribute": "Group Unique Identifier Attribute",
|
||||
"group_name_attribute": "Group Name Attribute",
|
||||
"admin_group_name": "Admin Group Name",
|
||||
"members_of_this_group_will_have_admin_privileges_in_pocketid": "Members of this group will have Admin Privileges in Pocket ID.",
|
||||
"disable": "Disable",
|
||||
"sync_now": "Sync now",
|
||||
"enable": "Enable",
|
||||
"user_created_successfully": "User created successfully",
|
||||
"create_user": "Create User",
|
||||
"add_a_new_user_to_appname": "Add a new user to {appName}",
|
||||
"add_user": "Add User",
|
||||
"manage_users": "Manage Users",
|
||||
"admin_privileges": "Admin Privileges",
|
||||
"admins_have_full_access_to_the_admin_panel": "Admins have full access to the admin panel.",
|
||||
"delete_firstname_lastname": "Delete {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_delete_this_user": "Are you sure you want to delete this user?",
|
||||
"user_deleted_successfully": "User deleted successfully",
|
||||
"role": "Role",
|
||||
"source": "Source",
|
||||
"admin": "Admin",
|
||||
"user": "User",
|
||||
"local": "Local",
|
||||
"toggle_menu": "Toggle menu",
|
||||
"edit": "Edit",
|
||||
"user_groups_updated_successfully": "User groups updated successfully",
|
||||
"user_updated_successfully": "User updated successfully",
|
||||
"custom_claims_updated_successfully": "Custom claims updated successfully",
|
||||
"back": "Back",
|
||||
"user_details_firstname_lastname": "User Details {firstName} {lastName}",
|
||||
"manage_which_groups_this_user_belongs_to": "Manage which groups this user belongs to.",
|
||||
"custom_claims": "Custom Claims",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user": "Custom claims are key-value pairs that can be used to store additional information about a user. These claims will be included in the ID token if the scope 'profile' is requested.",
|
||||
"user_group_created_successfully": "User group created successfully",
|
||||
"create_user_group": "Create User Group",
|
||||
"create_a_new_group_that_can_be_assigned_to_users": "Create a new group that can be assigned to users.",
|
||||
"add_group": "Add Group",
|
||||
"manage_user_groups": "Manage User Groups",
|
||||
"friendly_name": "Friendly Name",
|
||||
"name_that_will_be_displayed_in_the_ui": "Name that will be displayed in the UI",
|
||||
"name_that_will_be_in_the_groups_claim": "Name that will be in the \"groups\" claim",
|
||||
"delete_name": "Delete {name}",
|
||||
"are_you_sure_you_want_to_delete_this_user_group": "Are you sure you want to delete this user group?",
|
||||
"user_group_deleted_successfully": "User group deleted successfully",
|
||||
"user_count": "User Count",
|
||||
"user_group_updated_successfully": "User group updated successfully",
|
||||
"users_updated_successfully": "Users updated successfully",
|
||||
"user_group_details_name": "User Group Details {name}",
|
||||
"assign_users_to_this_group": "Assign users to this group.",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized": "Custom claims are key-value pairs that can be used to store additional information about a user. These claims will be included in the ID token if the scope 'profile' is requested. Custom claims defined on the user will be prioritized if there are conflicts.",
|
||||
"oidc_client_created_successfully": "OIDC client created successfully",
|
||||
"create_oidc_client": "Create OIDC Client",
|
||||
"add_a_new_oidc_client_to_appname": "Add a new OIDC client to {appName}.",
|
||||
"add_oidc_client": "Add OIDC Client",
|
||||
"manage_oidc_clients": "Manage OIDC Clients",
|
||||
"one_time_link": "One Time Link",
|
||||
"use_this_link_to_sign_in_once": "Use this link to sign in once. This is needed for users who haven't added a passkey yet or\n\t\t\t\thave lost it.",
|
||||
"add": "Add",
|
||||
"callback_urls": "Callback URLs",
|
||||
"logout_callback_urls": "Logout Callback URLs",
|
||||
"public_client": "Public Client",
|
||||
"public_clients_do_not_have_a_client_secret_and_use_pkce_instead": "Public clients do not have a client secret and use PKCE instead. Enable this if your client is a SPA or mobile app.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange is a security feature to prevent CSRF and authorization code interception attacks.",
|
||||
"name_logo": "{name} logo",
|
||||
"change_logo": "Change Logo",
|
||||
"upload_logo": "Upload Logo",
|
||||
"remove_logo": "Remove Logo",
|
||||
"are_you_sure_you_want_to_delete_this_oidc_client": "Are you sure you want to delete this OIDC client?",
|
||||
"oidc_client_deleted_successfully": "OIDC client deleted successfully",
|
||||
"authorization_url": "Authorization URL",
|
||||
"oidc_discovery_url": "OIDC Discovery URL",
|
||||
"token_url": "Token URL",
|
||||
"userinfo_url": "Userinfo URL",
|
||||
"logout_url": "Logout URL",
|
||||
"certificate_url": "Certificate URL",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"oidc_client_updated_successfully": "OIDC client updated successfully",
|
||||
"create_new_client_secret": "Create new client secret",
|
||||
"are_you_sure_you_want_to_create_a_new_client_secret": "Are you sure you want to create a new client secret? The old one will be invalidated.",
|
||||
"generate": "Generate",
|
||||
"new_client_secret_created_successfully": "New client secret created successfully",
|
||||
"allowed_user_groups_updated_successfully": "Allowed user groups updated successfully",
|
||||
"oidc_client_name": "OIDC Client {name}",
|
||||
"client_id": "Client ID",
|
||||
"client_secret": "Client secret",
|
||||
"show_more_details": "Show more details",
|
||||
"allowed_user_groups": "Allowed User Groups",
|
||||
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Add user groups to this client to restrict access to users in these groups. If no user groups are selected, all users will have access to this client.",
|
||||
"favicon": "Favicon",
|
||||
"light_mode_logo": "Light Mode Logo",
|
||||
"dark_mode_logo": "Dark Mode Logo",
|
||||
"background_image": "Background Image",
|
||||
"language": "Language",
|
||||
"reset_profile_picture_question": "Reset profile picture?",
|
||||
"this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default": "This will remove the uploaded image, and reset the profile picture to default. Do you want to continue?",
|
||||
"reset": "Reset",
|
||||
"reset_to_default": "Reset to default",
|
||||
"profile_picture_has_been_reset": "Profile picture has been reset. It may take a few minutes to update.",
|
||||
"select_the_language_you_want_to_use": "Select the language you want to use. Some languages may not be fully translated."
|
||||
}
|
||||
316
frontend/messages/es-ES.json
Normal file
316
frontend/messages/es-ES.json
Normal file
@@ -0,0 +1,316 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"my_account": "My Account",
|
||||
"logout": "Logout",
|
||||
"confirm": "Confirm",
|
||||
"key": "Key",
|
||||
"value": "Value",
|
||||
"remove_custom_claim": "Remove custom claim",
|
||||
"add_custom_claim": "Add custom claim",
|
||||
"add_another": "Add another",
|
||||
"select_a_date": "Select a date",
|
||||
"select_file": "Select File",
|
||||
"profile_picture": "Profile Picture",
|
||||
"profile_picture_is_managed_by_ldap_server": "The profile picture is managed by the LDAP server and cannot be changed here.",
|
||||
"click_profile_picture_to_upload_custom": "Click on the profile picture to upload a custom one from your files.",
|
||||
"image_should_be_in_format": "The image should be in PNG or JPEG format.",
|
||||
"items_per_page": "Items per page",
|
||||
"no_items_found": "No items found",
|
||||
"search": "Search...",
|
||||
"expand_card": "Expand card",
|
||||
"copied": "Copied",
|
||||
"click_to_copy": "Click to copy",
|
||||
"something_went_wrong": "Something went wrong",
|
||||
"go_back_to_home": "Go back to home",
|
||||
"dont_have_access_to_your_passkey": "Don't have access to your passkey?",
|
||||
"login_background": "Login background",
|
||||
"logo": "Logo",
|
||||
"login_code": "Login Code",
|
||||
"create_a_login_code_to_sign_in_without_a_passkey_once": "Create a login code that the user can use to sign in without a passkey once.",
|
||||
"one_hour": "1 hour",
|
||||
"twelve_hours": "12 hours",
|
||||
"one_day": "1 day",
|
||||
"one_week": "1 week",
|
||||
"one_month": "1 month",
|
||||
"expiration": "Expiration",
|
||||
"generate_code": "Generate Code",
|
||||
"name": "Name",
|
||||
"browser_unsupported": "Browser unsupported",
|
||||
"this_browser_does_not_support_passkeys": "This browser doesn't support passkeys. Please or use a alternative sign in method.",
|
||||
"an_unknown_error_occurred": "An unknown error occurred",
|
||||
"authentication_process_was_aborted": "The authentication process was aborted",
|
||||
"error_occurred_with_authenticator": "An error occurred with the authenticator",
|
||||
"authenticator_does_not_support_discoverable_credentials": "The authenticator does not support discoverable credentials",
|
||||
"authenticator_does_not_support_resident_keys": "The authenticator does not support resident keys",
|
||||
"passkey_was_previously_registered": "This passkey was previously registered",
|
||||
"authenticator_does_not_support_any_of_the_requested_algorithms": "The authenticator does not support any of the requested algorithms",
|
||||
"authenticator_timed_out": "The authenticator timed out",
|
||||
"critical_error_occurred_contact_administrator": "A critical error occurred. Please contact your administrator.",
|
||||
"sign_in_to": "Sign in to {name}",
|
||||
"client_not_found": "Client not found",
|
||||
"client_wants_to_access_the_following_information": "<b>{client}</b> wants to access the following information:",
|
||||
"do_you_want_to_sign_in_to_client_with_your_app_name_account": "Do you want to sign in to <b>{client}</b> with your <b>{appName}</b> account?",
|
||||
"email": "Email",
|
||||
"view_your_email_address": "View your email address",
|
||||
"profile": "Profile",
|
||||
"view_your_profile_information": "View your profile information",
|
||||
"groups": "Groups",
|
||||
"view_the_groups_you_are_a_member_of": "View the groups you are a member of",
|
||||
"cancel": "Cancel",
|
||||
"sign_in": "Sign in",
|
||||
"try_again": "Try again",
|
||||
"client_logo": "Client Logo",
|
||||
"sign_out": "Sign out",
|
||||
"do_you_want_to_sign_out_of_pocketid_with_the_account": "Do you want to sign out of Pocket ID with the account <b>{username}</b>?",
|
||||
"sign_in_to_appname": "Sign in to {appName}",
|
||||
"please_try_to_sign_in_again": "Please try to sign in again.",
|
||||
"authenticate_yourself_with_your_passkey_to_access_the_admin_panel": "Authenticate yourself with your passkey to access the admin panel.",
|
||||
"authenticate": "Authenticate",
|
||||
"appname_setup": "{appName} Setup",
|
||||
"please_try_again": "Please try again.",
|
||||
"you_are_about_to_sign_in_to_the_initial_admin_account": "You're about to sign in to the initial admin account. Anyone with this link can access the account until a passkey is added. Please set up a passkey as soon as possible to prevent unauthorized access.",
|
||||
"continue": "Continue",
|
||||
"alternative_sign_in": "Alternative Sign In",
|
||||
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "If you dont't have access to your passkey, you can sign in using one of the following methods.",
|
||||
"use_your_passkey_instead": "Use your passkey instead?",
|
||||
"email_login": "Email Login",
|
||||
"enter_a_login_code_to_sign_in": "Enter a login code to sign in.",
|
||||
"request_a_login_code_via_email": "Request a login code via email.",
|
||||
"go_back": "Go back",
|
||||
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "An email has been sent to the provided email, if it exists in the system.",
|
||||
"enter_code": "Enter code",
|
||||
"enter_your_email_address_to_receive_an_email_with_a_login_code": "Enter your email address to receive an email with a login code.",
|
||||
"your_email": "Your email",
|
||||
"submit": "Submit",
|
||||
"enter_the_code_you_received_to_sign_in": "Enter the code you received to sign in.",
|
||||
"code": "Code",
|
||||
"invalid_redirect_url": "Invalid redirect URL",
|
||||
"audit_log": "Audit Log",
|
||||
"users": "Users",
|
||||
"user_groups": "User Groups",
|
||||
"oidc_clients": "OIDC Clients",
|
||||
"api_keys": "API Keys",
|
||||
"application_configuration": "Application Configuration",
|
||||
"settings": "Settings",
|
||||
"update_pocket_id": "Update Pocket ID",
|
||||
"powered_by": "Powered by",
|
||||
"see_your_account_activities_from_the_last_3_months": "See your account activities from the last 3 months.",
|
||||
"time": "Time",
|
||||
"event": "Event",
|
||||
"approximate_location": "Approximate Location",
|
||||
"ip_address": "IP Address",
|
||||
"device": "Device",
|
||||
"client": "Client",
|
||||
"unknown": "Unknown",
|
||||
"account_details_updated_successfully": "Account details updated successfully",
|
||||
"profile_picture_updated_successfully": "Profile picture updated successfully. It may take a few minutes to update.",
|
||||
"account_settings": "Account Settings",
|
||||
"passkey_missing": "Passkey missing",
|
||||
"please_provide_a_passkey_to_prevent_losing_access_to_your_account": "Please add a passkey to prevent losing access to your account.",
|
||||
"single_passkey_configured": "Single Passkey Configured",
|
||||
"it_is_recommended_to_add_more_than_one_passkey": "It is recommended to add more than one passkey to avoid losing access to your account.",
|
||||
"account_details": "Account Details",
|
||||
"passkeys": "Passkeys",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Manage your passkeys that you can use to authenticate yourself.",
|
||||
"add_passkey": "Add Passkey",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Create a one-time login code to sign in from a different device without a passkey.",
|
||||
"create": "Create",
|
||||
"first_name": "First name",
|
||||
"last_name": "Last name",
|
||||
"username": "Username",
|
||||
"save": "Save",
|
||||
"username_can_only_contain": "Username can only contain lowercase letters, numbers, underscores, dots, hyphens, and '@' symbols",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Sign in using the following code. The code will expire in 15 minutes.",
|
||||
"or_visit": "or visit",
|
||||
"added_on": "Added on",
|
||||
"rename": "Rename",
|
||||
"delete": "Delete",
|
||||
"are_you_sure_you_want_to_delete_this_passkey": "Are you sure you want to delete this passkey?",
|
||||
"passkey_deleted_successfully": "Passkey deleted successfully",
|
||||
"delete_passkey_name": "Delete {passkeyName}",
|
||||
"passkey_name_updated_successfully": "Passkey name updated successfully",
|
||||
"name_passkey": "Name Passkey",
|
||||
"name_your_passkey_to_easily_identify_it_later": "Name your passkey to easily identify it later.",
|
||||
"create_api_key": "Create API Key",
|
||||
"add_a_new_api_key_for_programmatic_access": "Add a new API key for programmatic access.",
|
||||
"add_api_key": "Add API Key",
|
||||
"manage_api_keys": "Manage API Keys",
|
||||
"api_key_created": "API Key Created",
|
||||
"for_security_reasons_this_key_will_only_be_shown_once": "For security reasons, this key will only be shown once. Please store it securely.",
|
||||
"description": "Description",
|
||||
"api_key": "API Key",
|
||||
"close": "Close",
|
||||
"name_to_identify_this_api_key": "Name to identify this API key.",
|
||||
"expires_at": "Expires At",
|
||||
"when_this_api_key_will_expire": "When this API key will expire.",
|
||||
"optional_description_to_help_identify_this_keys_purpose": "Optional description to help identify this key's purpose.",
|
||||
"name_must_be_at_least_3_characters": "Name must be at least 3 characters",
|
||||
"name_cannot_exceed_50_characters": "Name cannot exceed 50 characters",
|
||||
"expiration_date_must_be_in_the_future": "Expiration date must be in the future",
|
||||
"revoke_api_key": "Revoke API Key",
|
||||
"never": "Never",
|
||||
"revoke": "Revoke",
|
||||
"api_key_revoked_successfully": "API key revoked successfully",
|
||||
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Are you sure you want to revoke the API key \"{apiKeyName}\"? This will break any integrations using this key.",
|
||||
"last_used": "Last Used",
|
||||
"actions": "Actions",
|
||||
"images_updated_successfully": "Images updated successfully",
|
||||
"general": "General",
|
||||
"enable_email_notifications_to_alert_users_when_a_login_is_detected_from_a_new_device_or_location": "Enable email notifications to alert users when a login is detected from a new device or location.",
|
||||
"ldap": "LDAP",
|
||||
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Configure LDAP settings to sync users and groups from an LDAP server.",
|
||||
"images": "Images",
|
||||
"update": "Update",
|
||||
"email_configuration_updated_successfully": "Email configuration updated successfully",
|
||||
"save_changes_question": "Save changes?",
|
||||
"you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "You have to save the changes before sending a test email. Do you want to save now?",
|
||||
"save_and_send": "Save and send",
|
||||
"test_email_sent_successfully": "Test email sent successfully to your email address.",
|
||||
"failed_to_send_test_email": "Failed to send test email. Check the server logs for more information.",
|
||||
"smtp_configuration": "SMTP Configuration",
|
||||
"smtp_host": "SMTP Host",
|
||||
"smtp_port": "SMTP Port",
|
||||
"smtp_user": "SMTP User",
|
||||
"smtp_password": "SMTP Password",
|
||||
"smtp_from": "SMTP From",
|
||||
"smtp_tls_option": "SMTP TLS Option",
|
||||
"email_tls_option": "Email TLS Option",
|
||||
"skip_certificate_verification": "Skip Certificate Verification",
|
||||
"this_can_be_useful_for_selfsigned_certificates": "This can be useful for self-signed certificates.",
|
||||
"enabled_emails": "Enabled Emails",
|
||||
"email_login_notification": "Email Login Notification",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Send an email to the user when they log in from a new device.",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to sign in with a login code sent to their email. This reduces the security significantly as anyone with access to the user's email can gain entry.",
|
||||
"send_test_email": "Send test email",
|
||||
"application_configuration_updated_successfully": "Application configuration updated successfully",
|
||||
"application_name": "Application Name",
|
||||
"session_duration": "Session Duration",
|
||||
"the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again": "The duration of a session in minutes before the user has to sign in again.",
|
||||
"enable_self_account_editing": "Enable Self-Account Editing",
|
||||
"whether_the_users_should_be_able_to_edit_their_own_account_details": "Whether the users should be able to edit their own account details.",
|
||||
"emails_verified": "Emails Verified",
|
||||
"whether_the_users_email_should_be_marked_as_verified_for_the_oidc_clients": "Whether the user's email should be marked as verified for the OIDC clients.",
|
||||
"ldap_configuration_updated_successfully": "LDAP configuration updated successfully",
|
||||
"ldap_disabled_successfully": "LDAP disabled successfully",
|
||||
"ldap_sync_finished": "LDAP sync finished",
|
||||
"client_configuration": "Client Configuration",
|
||||
"ldap_url": "LDAP URL",
|
||||
"ldap_bind_dn": "LDAP Bind DN",
|
||||
"ldap_bind_password": "LDAP Bind Password",
|
||||
"ldap_base_dn": "LDAP Base DN",
|
||||
"user_search_filter": "User Search Filter",
|
||||
"the_search_filter_to_use_to_search_or_sync_users": "The Search filter to use to search/sync users.",
|
||||
"groups_search_filter": "Groups Search Filter",
|
||||
"the_search_filter_to_use_to_search_or_sync_groups": "The Search filter to use to search/sync groups.",
|
||||
"attribute_mapping": "Attribute Mapping",
|
||||
"user_unique_identifier_attribute": "User Unique Identifier Attribute",
|
||||
"the_value_of_this_attribute_should_never_change": "The value of this attribute should never change.",
|
||||
"username_attribute": "Username Attribute",
|
||||
"user_mail_attribute": "User Mail Attribute",
|
||||
"user_first_name_attribute": "User First Name Attribute",
|
||||
"user_last_name_attribute": "User Last Name Attribute",
|
||||
"user_profile_picture_attribute": "User Profile Picture Attribute",
|
||||
"the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image": "The value of this attribute can either be a URL, a binary or a base64 encoded image.",
|
||||
"group_members_attribute": "Group Members Attribute",
|
||||
"the_attribute_to_use_for_querying_members_of_a_group": "The attribute to use for querying members of a group.",
|
||||
"group_unique_identifier_attribute": "Group Unique Identifier Attribute",
|
||||
"group_name_attribute": "Group Name Attribute",
|
||||
"admin_group_name": "Admin Group Name",
|
||||
"members_of_this_group_will_have_admin_privileges_in_pocketid": "Members of this group will have Admin Privileges in Pocket ID.",
|
||||
"disable": "Disable",
|
||||
"sync_now": "Sync now",
|
||||
"enable": "Enable",
|
||||
"user_created_successfully": "User created successfully",
|
||||
"create_user": "Create User",
|
||||
"add_a_new_user_to_appname": "Add a new user to {appName}",
|
||||
"add_user": "Add User",
|
||||
"manage_users": "Manage Users",
|
||||
"admin_privileges": "Admin Privileges",
|
||||
"admins_have_full_access_to_the_admin_panel": "Admins have full access to the admin panel.",
|
||||
"delete_firstname_lastname": "Delete {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_delete_this_user": "Are you sure you want to delete this user?",
|
||||
"user_deleted_successfully": "User deleted successfully",
|
||||
"role": "Role",
|
||||
"source": "Source",
|
||||
"admin": "Admin",
|
||||
"user": "User",
|
||||
"local": "Local",
|
||||
"toggle_menu": "Toggle menu",
|
||||
"edit": "Edit",
|
||||
"user_groups_updated_successfully": "User groups updated successfully",
|
||||
"user_updated_successfully": "User updated successfully",
|
||||
"custom_claims_updated_successfully": "Custom claims updated successfully",
|
||||
"back": "Back",
|
||||
"user_details_firstname_lastname": "User Details {firstName} {lastName}",
|
||||
"manage_which_groups_this_user_belongs_to": "Manage which groups this user belongs to.",
|
||||
"custom_claims": "Custom Claims",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user": "Custom claims are key-value pairs that can be used to store additional information about a user. These claims will be included in the ID token if the scope 'profile' is requested.",
|
||||
"user_group_created_successfully": "User group created successfully",
|
||||
"create_user_group": "Create User Group",
|
||||
"create_a_new_group_that_can_be_assigned_to_users": "Create a new group that can be assigned to users.",
|
||||
"add_group": "Add Group",
|
||||
"manage_user_groups": "Manage User Groups",
|
||||
"friendly_name": "Friendly Name",
|
||||
"name_that_will_be_displayed_in_the_ui": "Name that will be displayed in the UI",
|
||||
"name_that_will_be_in_the_groups_claim": "Name that will be in the \"groups\" claim",
|
||||
"delete_name": "Delete {name}",
|
||||
"are_you_sure_you_want_to_delete_this_user_group": "Are you sure you want to delete this user group?",
|
||||
"user_group_deleted_successfully": "User group deleted successfully",
|
||||
"user_count": "User Count",
|
||||
"user_group_updated_successfully": "User group updated successfully",
|
||||
"users_updated_successfully": "Users updated successfully",
|
||||
"user_group_details_name": "User Group Details {name}",
|
||||
"assign_users_to_this_group": "Assign users to this group.",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized": "Custom claims are key-value pairs that can be used to store additional information about a user. These claims will be included in the ID token if the scope 'profile' is requested. Custom claims defined on the user will be prioritized if there are conflicts.",
|
||||
"oidc_client_created_successfully": "OIDC client created successfully",
|
||||
"create_oidc_client": "Create OIDC Client",
|
||||
"add_a_new_oidc_client_to_appname": "Add a new OIDC client to {appName}.",
|
||||
"add_oidc_client": "Add OIDC Client",
|
||||
"manage_oidc_clients": "Manage OIDC Clients",
|
||||
"one_time_link": "One Time Link",
|
||||
"use_this_link_to_sign_in_once": "Use this link to sign in once. This is needed for users who haven't added a passkey yet or\n\t\t\t\thave lost it.",
|
||||
"add": "Add",
|
||||
"callback_urls": "Callback URLs",
|
||||
"logout_callback_urls": "Logout Callback URLs",
|
||||
"public_client": "Public Client",
|
||||
"public_clients_do_not_have_a_client_secret_and_use_pkce_instead": "Public clients do not have a client secret and use PKCE instead. Enable this if your client is a SPA or mobile app.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange is a security feature to prevent CSRF and authorization code interception attacks.",
|
||||
"name_logo": "{name} logo",
|
||||
"change_logo": "Change Logo",
|
||||
"upload_logo": "Upload Logo",
|
||||
"remove_logo": "Remove Logo",
|
||||
"are_you_sure_you_want_to_delete_this_oidc_client": "Are you sure you want to delete this OIDC client?",
|
||||
"oidc_client_deleted_successfully": "OIDC client deleted successfully",
|
||||
"authorization_url": "Authorization URL",
|
||||
"oidc_discovery_url": "OIDC Discovery URL",
|
||||
"token_url": "Token URL",
|
||||
"userinfo_url": "Userinfo URL",
|
||||
"logout_url": "Logout URL",
|
||||
"certificate_url": "Certificate URL",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"oidc_client_updated_successfully": "OIDC client updated successfully",
|
||||
"create_new_client_secret": "Create new client secret",
|
||||
"are_you_sure_you_want_to_create_a_new_client_secret": "Are you sure you want to create a new client secret? The old one will be invalidated.",
|
||||
"generate": "Generate",
|
||||
"new_client_secret_created_successfully": "New client secret created successfully",
|
||||
"allowed_user_groups_updated_successfully": "Allowed user groups updated successfully",
|
||||
"oidc_client_name": "OIDC Client {name}",
|
||||
"client_id": "Client ID",
|
||||
"client_secret": "Client secret",
|
||||
"show_more_details": "Show more details",
|
||||
"allowed_user_groups": "Allowed User Groups",
|
||||
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Add user groups to this client to restrict access to users in these groups. If no user groups are selected, all users will have access to this client.",
|
||||
"favicon": "Favicon",
|
||||
"light_mode_logo": "Light Mode Logo",
|
||||
"dark_mode_logo": "Dark Mode Logo",
|
||||
"background_image": "Background Image",
|
||||
"language": "Language",
|
||||
"reset_profile_picture_question": "Reset profile picture?",
|
||||
"this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default": "This will remove the uploaded image, and reset the profile picture to default. Do you want to continue?",
|
||||
"reset": "Reset",
|
||||
"reset_to_default": "Reset to default",
|
||||
"profile_picture_has_been_reset": "Profile picture has been reset. It may take a few minutes to update.",
|
||||
"select_the_language_you_want_to_use": "Select the language you want to use. Some languages may not be fully translated."
|
||||
}
|
||||
316
frontend/messages/fr-FR.json
Normal file
316
frontend/messages/fr-FR.json
Normal file
@@ -0,0 +1,316 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"my_account": "Mon compte",
|
||||
"logout": "Déconnexion",
|
||||
"confirm": "Confirmer",
|
||||
"key": "Clé",
|
||||
"value": "Valeur",
|
||||
"remove_custom_claim": "Remove custom claim",
|
||||
"add_custom_claim": "Add custom claim",
|
||||
"add_another": "Ajouter un autre",
|
||||
"select_a_date": "Sélectionner une date",
|
||||
"select_file": "Sélectionner un fichier",
|
||||
"profile_picture": "Photo de profil",
|
||||
"profile_picture_is_managed_by_ldap_server": "La photo de profil est gérée par le serveur LDAP et ne peut pas être modifiée ici.",
|
||||
"click_profile_picture_to_upload_custom": "Cliquez sur la photo de profil pour télécharger une photo depuis votre ordinateur.",
|
||||
"image_should_be_in_format": "L'image doit être au format PNG ou JPEG.",
|
||||
"items_per_page": "Éléments par page",
|
||||
"no_items_found": "Aucune donnée trouvée",
|
||||
"search": "Rechercher...",
|
||||
"expand_card": "Expand card",
|
||||
"copied": "Copié",
|
||||
"click_to_copy": "Cliquer pour copier",
|
||||
"something_went_wrong": "Quelque chose n'a pas fonctionné",
|
||||
"go_back_to_home": "Retourner à l'accueil",
|
||||
"dont_have_access_to_your_passkey": "Vous n'avez pas accès à votre clé d'accès ?",
|
||||
"login_background": "Arrière-plan de connexion",
|
||||
"logo": "Logo",
|
||||
"login_code": "Code de connexion",
|
||||
"create_a_login_code_to_sign_in_without_a_passkey_once": "Créer un code de connexion à usage unique pour que l'utilisateur puisse se connecter sans clé d'accès.",
|
||||
"one_hour": "1 heure",
|
||||
"twelve_hours": "12 heures",
|
||||
"one_day": "1 jour",
|
||||
"one_week": "1 semaine",
|
||||
"one_month": "1 mois",
|
||||
"expiration": "Expiration",
|
||||
"generate_code": "Générer un code",
|
||||
"name": "Nom",
|
||||
"browser_unsupported": "Navigateur non pris en charge",
|
||||
"this_browser_does_not_support_passkeys": "Ce navigateur ne supporte pas les clés d'accès. Veuillez ou utilisez une autre méthode de connexion.",
|
||||
"an_unknown_error_occurred": "Une erreur inconnue est survenue",
|
||||
"authentication_process_was_aborted": "Le processus d'authentification a été interrompu",
|
||||
"error_occurred_with_authenticator": "Une erreur est survenue pendant l'authentification",
|
||||
"authenticator_does_not_support_discoverable_credentials": "L'authentificateur ne prend pas en charge les identifiants découvrables",
|
||||
"authenticator_does_not_support_resident_keys": "L'authentificateur ne prend pas en charge les clés résidentes",
|
||||
"passkey_was_previously_registered": "Cette clé d'accès à déjà été enregistré",
|
||||
"authenticator_does_not_support_any_of_the_requested_algorithms": "L'authentificateur ne supporte aucun des algorithmes requis",
|
||||
"authenticator_timed_out": "L'authentification a expiré",
|
||||
"critical_error_occurred_contact_administrator": "Une erreur critique s'est produite. Veuillez contacter votre administrateur.",
|
||||
"sign_in_to": "Connexion à {name}",
|
||||
"client_not_found": "Client introuvable",
|
||||
"client_wants_to_access_the_following_information": "<b>{client}</b> souhaite accéder aux informations suivantes :",
|
||||
"do_you_want_to_sign_in_to_client_with_your_app_name_account": "Voulez-vous vous connecter à <b>{client}</b> avec votre compte <b>{appName}</b>?",
|
||||
"email": "E-mail",
|
||||
"view_your_email_address": "Afficher votre e-mail",
|
||||
"profile": "Profil",
|
||||
"view_your_profile_information": "Voir les informations de votre profil",
|
||||
"groups": "Groupes",
|
||||
"view_the_groups_you_are_a_member_of": "Afficher les groupes dont vous êtes membre",
|
||||
"cancel": "Annuler",
|
||||
"sign_in": "Se connecter",
|
||||
"try_again": "Réessayer",
|
||||
"client_logo": "Logo du client",
|
||||
"sign_out": "Déconnexion",
|
||||
"do_you_want_to_sign_out_of_pocketid_with_the_account": "Voulez-vous vous déconnecter de Pocket ID avec le compte <b>{username}</b>?",
|
||||
"sign_in_to_appname": "Se connecter à {appName}",
|
||||
"please_try_to_sign_in_again": "Veuillez essayer de vous connecter à nouveau.",
|
||||
"authenticate_yourself_with_your_passkey_to_access_the_admin_panel": "Authentifiez-vous avec votre clé d'accès pour accéder au panneau d'administration.",
|
||||
"authenticate": "S'authentifier",
|
||||
"appname_setup": "Configuration {appName}",
|
||||
"please_try_again": "Veuillez réessayer.",
|
||||
"you_are_about_to_sign_in_to_the_initial_admin_account": "Vous êtes sur le point de vous connecter au compte administrateur initial. N'importe qui avec ce lien peut accéder au compte jusqu'à ce qu'une clé d'accès soit ajouté. Veuillez configurer une clé d'accès dès que possible pour éviter tout accès non autorisé.",
|
||||
"continue": "Continuer",
|
||||
"alternative_sign_in": "Connexion alternative",
|
||||
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "Si vous n'avez pas accès à votre clé d'accès, vous pouvez vous connecter en utilisant l'une des méthodes suivantes.",
|
||||
"use_your_passkey_instead": "Utiliser votre clé d'accès à la place ?",
|
||||
"email_login": "Connexion par e-mail",
|
||||
"enter_a_login_code_to_sign_in": "Entrez un code de connexion pour vous connecter.",
|
||||
"request_a_login_code_via_email": "Demander un code de connexion par e-mail.",
|
||||
"go_back": "Retour",
|
||||
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "Un e-mail a été envoyé à l'e-mail mentionné, si elle existe dans le système.",
|
||||
"enter_code": "Entrez le code",
|
||||
"enter_your_email_address_to_receive_an_email_with_a_login_code": "Entrez votre adresse e-mail pour recevoir un email avec un code de connexion.",
|
||||
"your_email": "Votre email",
|
||||
"submit": "Envoyer",
|
||||
"enter_the_code_you_received_to_sign_in": "Entrez le code que vous avez reçu pour vous connecter.",
|
||||
"code": "Code",
|
||||
"invalid_redirect_url": "URL de redirection invalide",
|
||||
"audit_log": "Journal d'audit",
|
||||
"users": "Utilisateurs",
|
||||
"user_groups": "Groupes d’utilisateurs",
|
||||
"oidc_clients": "Clients OIDC",
|
||||
"api_keys": "Clés API",
|
||||
"application_configuration": "Configuration de l’application",
|
||||
"settings": "Paramètres",
|
||||
"update_pocket_id": "Mise à jour de Pocket ID",
|
||||
"powered_by": "Propulsé par",
|
||||
"see_your_account_activities_from_the_last_3_months": "Consultez les activités de votre compte au cours des 3 derniers mois.",
|
||||
"time": "Date et heure",
|
||||
"event": "Événement",
|
||||
"approximate_location": "Lieu approximatif",
|
||||
"ip_address": "Adresse IP",
|
||||
"device": "Périphérique",
|
||||
"client": "Application",
|
||||
"unknown": "Indisponible",
|
||||
"account_details_updated_successfully": "Les informations du compte ont été mises à jour avec succès",
|
||||
"profile_picture_updated_successfully": "La photo de profil a été mise à jour avec succès. La mise à jour peut prendre quelques minutes.",
|
||||
"account_settings": "Paramètres du compte",
|
||||
"passkey_missing": "Clé d'accès manquante",
|
||||
"please_provide_a_passkey_to_prevent_losing_access_to_your_account": "Veuillez ajouter une clé d'accès pour éviter de perdre l'accès à votre compte.",
|
||||
"single_passkey_configured": "Une seul clé d'accès configuré",
|
||||
"it_is_recommended_to_add_more_than_one_passkey": "Il est recommandé d'ajouter plus d'une clé d'accès pour éviter de perdre l'accès à votre compte.",
|
||||
"account_details": "Paramètres du compte",
|
||||
"passkeys": "Clés d'accès",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Gérez vos clés d'accès que vous pouvez utiliser pour vous authentifier.",
|
||||
"add_passkey": "Ajouter une clé d'accès",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Créez un code de connexion unique pour vous connecter depuis un autre appareil sans mot de passe.",
|
||||
"create": "Créer",
|
||||
"first_name": "Prénom",
|
||||
"last_name": "Nom",
|
||||
"username": "Nom d'utilisateur",
|
||||
"save": "Enregistrer",
|
||||
"username_can_only_contain": "Le nom d'utilisateur ne peut contenir que des lettres minuscules, des chiffres, des tirets, des tirets bas et le symbole '@'",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Connectez-vous avec le code suivant. Le code expirera dans 15 minutes.",
|
||||
"or_visit": "ou visiter",
|
||||
"added_on": "Ajoutée le",
|
||||
"rename": "Renommer",
|
||||
"delete": "Supprimer",
|
||||
"are_you_sure_you_want_to_delete_this_passkey": "Êtes-vous sûr de vouloir supprimer cette clé d'accès ?",
|
||||
"passkey_deleted_successfully": "Clé d'accès supprimé avec succès",
|
||||
"delete_passkey_name": "Supprimer {passkeyName}",
|
||||
"passkey_name_updated_successfully": "Le nom de la clé d'accès a bien été mis à jour",
|
||||
"name_passkey": "Nom de la clé d'accès",
|
||||
"name_your_passkey_to_easily_identify_it_later": "Nommez votre clé d'accès pour l'identifier plus tard.",
|
||||
"create_api_key": "Créer une clé API",
|
||||
"add_a_new_api_key_for_programmatic_access": "Ajouter une nouvelle clé API pour l'accès par des programmes tiers.",
|
||||
"add_api_key": "Crée une clé API",
|
||||
"manage_api_keys": "Gérer les clés API",
|
||||
"api_key_created": "Clé API créée",
|
||||
"for_security_reasons_this_key_will_only_be_shown_once": "Pour des raisons de sécurité, cette clé ne sera affichée qu'une seule fois. Veuillez la conserver en toute sécurité.",
|
||||
"description": "Description",
|
||||
"api_key": "Clé API",
|
||||
"close": "Fermer",
|
||||
"name_to_identify_this_api_key": "Nom pour identifier cette clé API.",
|
||||
"expires_at": "Date d'expiration",
|
||||
"when_this_api_key_will_expire": "Date d'expiration de la clé API.",
|
||||
"optional_description_to_help_identify_this_keys_purpose": "Description facultative pour aider à identifier le but de cette clé.",
|
||||
"name_must_be_at_least_3_characters": "Le nom doit contenir au moins 3 caractères",
|
||||
"name_cannot_exceed_50_characters": "Le nom ne doit pas dépasser un maximum de 50 caractères",
|
||||
"expiration_date_must_be_in_the_future": "La date d'expiration doit être dans le futur",
|
||||
"revoke_api_key": "Révoquer la clé API",
|
||||
"never": "Jamais",
|
||||
"revoke": "Révoquer",
|
||||
"api_key_revoked_successfully": "Clé API révoquée avec succès",
|
||||
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Êtes-vous sûr de vouloir révoquer la clé API \"{apiKeyName}\" ? Cela va casser toutes les intégrations utilisant cette clé.",
|
||||
"last_used": "Dernière utilisation",
|
||||
"actions": "Actions",
|
||||
"images_updated_successfully": "Image mise à jour avec succès",
|
||||
"general": "Général",
|
||||
"enable_email_notifications_to_alert_users_when_a_login_is_detected_from_a_new_device_or_location": "Activer les notifications par e-mail pour alerter les utilisateurs lorsqu'une connexion est détecté à partir d'un nouvel appareil ou d'un nouvel emplacement.",
|
||||
"ldap": "LDAP",
|
||||
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Configurer les paramètres LDAP pour synchroniser les utilisateurs et les groupes à partir d'un serveur LDAP.",
|
||||
"images": "Images",
|
||||
"update": "Mise à jour",
|
||||
"email_configuration_updated_successfully": "La configuration du serveur mail à été mise à jour avec succès",
|
||||
"save_changes_question": "Enregistrer des changements ?",
|
||||
"you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "Vous devez enregistrer les modifications avant d'envoyer un e-mail de test. Voulez-vous enregistrer maintenant ?",
|
||||
"save_and_send": "Enregistrer et envoyer",
|
||||
"test_email_sent_successfully": "L'e-mail de test a été envoyé avec succès à votre adresse e-mail.",
|
||||
"failed_to_send_test_email": "Échec de l'envoi du courriel de test. Vérifiez les logs du serveur pour plus d'informations.",
|
||||
"smtp_configuration": "Configuration du serveur SMTP",
|
||||
"smtp_host": "Hôte SMTP",
|
||||
"smtp_port": "Port SMTP",
|
||||
"smtp_user": "Utilisateur SMTP",
|
||||
"smtp_password": "Mot de passe SMTP",
|
||||
"smtp_from": "Nom d'expédition SMTP",
|
||||
"smtp_tls_option": "Option TLS SMTP",
|
||||
"email_tls_option": "Option TLS",
|
||||
"skip_certificate_verification": "Passer la vérification de certificat",
|
||||
"this_can_be_useful_for_selfsigned_certificates": "Cela peut être utile pour les certificats autosignés.",
|
||||
"enabled_emails": "Emails activés",
|
||||
"email_login_notification": "Notification de connexion par e-mail",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Envoyer un email à l'utilisateur lorsqu'il se connecte à partir d'un nouvel appareil.",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Permet aux utilisateurs de se connecter avec un code de connexion envoyé à leur adresse e-mail. Cela réduit considérablement la sécurité car toute personne ayant accès à l'e-mail de l'utilisateur peuvent se connecter.",
|
||||
"send_test_email": "",
|
||||
"application_configuration_updated_successfully": "Mise à jour de l'application avec succès",
|
||||
"application_name": "Nom de l'application",
|
||||
"session_duration": "Durée de la session",
|
||||
"the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again": "La durée d'une session en minutes avant que l'utilisateur ne doive se reconnecter.",
|
||||
"enable_self_account_editing": "Activer l'édition de compte par l'utilisateur",
|
||||
"whether_the_users_should_be_able_to_edit_their_own_account_details": "Cela permet aux utilisateurs de modifier les détails de leur compte.",
|
||||
"emails_verified": "Email vérifié",
|
||||
"whether_the_users_email_should_be_marked_as_verified_for_the_oidc_clients": "Indique si l'adresse e-mail de l'utilisateur doit être marquée comme vérifiée pour les clients OIDC.",
|
||||
"ldap_configuration_updated_successfully": "Configuration LDAP mise à jour avec succès",
|
||||
"ldap_disabled_successfully": "LDAP désactivé avec succès",
|
||||
"ldap_sync_finished": "Synchronisation LDAP terminée",
|
||||
"client_configuration": "Configuration du client",
|
||||
"ldap_url": "URL du serveur LDAP",
|
||||
"ldap_bind_dn": "LDAP Bind DN",
|
||||
"ldap_bind_password": "LDAP Bind Password",
|
||||
"ldap_base_dn": "LDAP Base DN",
|
||||
"user_search_filter": "Filtre de recherche utilisateur",
|
||||
"the_search_filter_to_use_to_search_or_sync_users": "Le filtre de recherche à utiliser pour rechercher/synchroniser les utilisateurs.",
|
||||
"groups_search_filter": "Filtre de recherche de groupes",
|
||||
"the_search_filter_to_use_to_search_or_sync_groups": "Le filtre de recherche à utiliser pour rechercher/synchroniser les groupes.",
|
||||
"attribute_mapping": "Mappage d’attributs",
|
||||
"user_unique_identifier_attribute": "Attribut d'identifiant unique de l'utilisateur",
|
||||
"the_value_of_this_attribute_should_never_change": "La valeur de cet attribut ne doit jamais changer.",
|
||||
"username_attribute": "Attribut du nom d'utilisateur",
|
||||
"user_mail_attribute": "Attribut de l'e-mail de l'utilisateur",
|
||||
"user_first_name_attribute": "Attribut de prénom d'utilisateur",
|
||||
"user_last_name_attribute": "Attribut du nom d'utilisateur",
|
||||
"user_profile_picture_attribute": "Attribut de la photo de profil utilisateur",
|
||||
"the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image": "La valeur de cet attribut peut être une URL, un binaire ou une image encodée en base64.",
|
||||
"group_members_attribute": "Attribut des membres du groupe",
|
||||
"the_attribute_to_use_for_querying_members_of_a_group": "L'attribut à utiliser pour interroger les membres d'un groupe.",
|
||||
"group_unique_identifier_attribute": "Group Unique Identifier Attribute",
|
||||
"group_name_attribute": "Attribut de nom de groupe",
|
||||
"admin_group_name": "Nom du groupe administrateur",
|
||||
"members_of_this_group_will_have_admin_privileges_in_pocketid": "Les membres de ce groupe auront des privilèges d'administrateur dans Pocket ID.",
|
||||
"disable": "Désactiver",
|
||||
"sync_now": "Synchroniser maintenant",
|
||||
"enable": "Activer",
|
||||
"user_created_successfully": "Utilisateur créé avec succès",
|
||||
"create_user": "Créer un utilisateur",
|
||||
"add_a_new_user_to_appname": "Ajouter un nouvel utilisateur à {appName}",
|
||||
"add_user": "Ajouter un utilisateur",
|
||||
"manage_users": "Gérer les utilisateurs",
|
||||
"admin_privileges": "Privilèges administrateurs",
|
||||
"admins_have_full_access_to_the_admin_panel": "Les administrateurs ont un accès complet à l'administration.",
|
||||
"delete_firstname_lastname": "Supprimer {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_delete_this_user": "Êtes-vous sûr de vouloir supprimer cet utilisateur ?",
|
||||
"user_deleted_successfully": "Utilisateur supprimé avec succès",
|
||||
"role": "Rôle",
|
||||
"source": "Source",
|
||||
"admin": "Administrateur",
|
||||
"user": "Utilisateur",
|
||||
"local": "Local",
|
||||
"toggle_menu": "Afficher/Masquer le menu",
|
||||
"edit": "Modifier",
|
||||
"user_groups_updated_successfully": "Groupes d'utilisateurs mis à jour avec succès",
|
||||
"user_updated_successfully": "Utilisateur mis à jour avec succès",
|
||||
"custom_claims_updated_successfully": "Custom claims updated successfully",
|
||||
"back": "Retour",
|
||||
"user_details_firstname_lastname": "Détails de l'utilisateur {firstName} {lastName}",
|
||||
"manage_which_groups_this_user_belongs_to": "Gérer les groupes auxquels cet utilisateur appartient.",
|
||||
"custom_claims": "Custom Claims",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user": "Les revendications personnalisées sont des paires clé-valeur qui permettent de stocker des informations supplémentaires sur un utilisateur. Elles seront incluses dans le jeton d'identité (ID token) si la portée 'profile' est demandée.",
|
||||
"user_group_created_successfully": "Groupe d'utilisateurs créé avec succès",
|
||||
"create_user_group": "Créer un groupe d'utilisateurs",
|
||||
"create_a_new_group_that_can_be_assigned_to_users": "Créer un nouveau groupe pouvant être attribué aux utilisateurs.",
|
||||
"add_group": "Ajouter un groupe",
|
||||
"manage_user_groups": "Gérer les groupes d'utilisateurs",
|
||||
"friendly_name": "Nom d'affichage",
|
||||
"name_that_will_be_displayed_in_the_ui": "Nom qui sera affiché dans l'interface utilisateur",
|
||||
"name_that_will_be_in_the_groups_claim": "Name that will be in the \"groups\" claim",
|
||||
"delete_name": "Supprimer {name}",
|
||||
"are_you_sure_you_want_to_delete_this_user_group": "Êtes-vous sûr de vouloir supprimer ce groupe d'utilisateurs?",
|
||||
"user_group_deleted_successfully": "Groupe d'utilisateurs supprimé avec succès",
|
||||
"user_count": "Nombre d'utilisateurs",
|
||||
"user_group_updated_successfully": "Groupes d'utilisateurs mis à jour avec succès",
|
||||
"users_updated_successfully": "Utilisateurs mis à jour avec succès",
|
||||
"user_group_details_name": "Détails du groupe d'utilisateurs {name}",
|
||||
"assign_users_to_this_group": "Assigner des utilisateurs à ce groupe.",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized": "Les revendications personnalisées sont des paires clé-valeur qui permettent de stocker des informations supplémentaires sur un utilisateur. Elles seront incluses dans le jeton d'identité (ID token) si la portée 'profile' est demandée. En cas de conflit, les revendications personnalisées définies directement sur l'utilisateur seront prioritaires.",
|
||||
"oidc_client_created_successfully": "Client OIDC créé avec succès",
|
||||
"create_oidc_client": "Créer un client OIDC",
|
||||
"add_a_new_oidc_client_to_appname": "Ajouter un nouveau client OIDC à {appName}.",
|
||||
"add_oidc_client": "Ajouter un client OIDC",
|
||||
"manage_oidc_clients": "Gérer les clients OIDC",
|
||||
"one_time_link": "Lien de connexion unique",
|
||||
"use_this_link_to_sign_in_once": "Utilisez ce lien pour vous connecter. Ceci est nécessaire pour les utilisateurs qui n'ont pas encore ajouté de clé d'accès ou l'ont perdu.",
|
||||
"add": "Ajouter",
|
||||
"callback_urls": "URL de callback",
|
||||
"logout_callback_urls": "URL de callback de déconnexion",
|
||||
"public_client": "Client public",
|
||||
"public_clients_do_not_have_a_client_secret_and_use_pkce_instead": "Les clients publics n'ont pas de secret client et utilisent PKCE à la place. Activez cette option si votre client est une application SPA ou une application mobile.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Le Public Key Code Exchange est une fonctionnalité de sécurité conçue pour prévenir les attaques CSRF et l’interception de code d’autorisation.",
|
||||
"name_logo": "Logo {name}",
|
||||
"change_logo": "Changer le logo",
|
||||
"upload_logo": "Télécharger un logo",
|
||||
"remove_logo": "Supprimer le logo",
|
||||
"are_you_sure_you_want_to_delete_this_oidc_client": "Êtes-vous sûr de vouloir supprimer ce client OIDC ?",
|
||||
"oidc_client_deleted_successfully": "Client OIDC supprimé avec succès",
|
||||
"authorization_url": "URL d’autorisation",
|
||||
"oidc_discovery_url": "URL de découverte OIDC",
|
||||
"token_url": "URL Token",
|
||||
"userinfo_url": "URL userinfo",
|
||||
"logout_url": "URL de déconnection",
|
||||
"certificate_url": "URL du certificat",
|
||||
"enabled": "Activé",
|
||||
"disabled": "Désactivé",
|
||||
"oidc_client_updated_successfully": "Client OIDC mis à jour avec succès",
|
||||
"create_new_client_secret": "Créer un nouveau secret client",
|
||||
"are_you_sure_you_want_to_create_a_new_client_secret": "Êtes-vous sûr de vouloir créer un nouveau secret client ? L'ancien secret sera invalidé.",
|
||||
"generate": "Générer",
|
||||
"new_client_secret_created_successfully": "Nouveau secret client créé avec succès",
|
||||
"allowed_user_groups_updated_successfully": "Groupes d'utilisateurs autorisés mis à jour avec succès",
|
||||
"oidc_client_name": "Client OIDC {name}",
|
||||
"client_id": "ID du client",
|
||||
"client_secret": "Client secret",
|
||||
"show_more_details": "Afficher plus de détails",
|
||||
"allowed_user_groups": "Groupes d'utilisateurs autorisés",
|
||||
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Ajouter des groupes d'utilisateurs à ce client permet de restreindre l'accès aux utilisateurs de ces groupes. Si aucun groupe d'utilisateurs n'est sélectionné, tous les utilisateurs auront accès à ce client.",
|
||||
"favicon": "Icône du site",
|
||||
"light_mode_logo": "Logo du Mode Clair",
|
||||
"dark_mode_logo": "Logo du Mode Sombre",
|
||||
"background_image": "Image d'arrière-plan",
|
||||
"language": "Langue",
|
||||
"reset_profile_picture_question": "Réinitialiser la photo de profil ?",
|
||||
"this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default": "Cela réinitialisera l'image de profil par défaut. Voulez-vous continuer ?",
|
||||
"reset": "Réinitialiser",
|
||||
"reset_to_default": "Valeurs par défaut",
|
||||
"profile_picture_has_been_reset": "La photo de profil a été réinitialisée. La mise à jour peut prendre quelques minutes.",
|
||||
"select_the_language_you_want_to_use": "Sélectionnez la langue que vous souhaitez utiliser. Certaines langues peuvent ne pas être entièrement traduites."
|
||||
}
|
||||
316
frontend/messages/nl-NL.json
Normal file
316
frontend/messages/nl-NL.json
Normal file
@@ -0,0 +1,316 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"my_account": "Mijn account",
|
||||
"logout": "Uitloggen",
|
||||
"confirm": "Bevestigen",
|
||||
"key": "Sleutel",
|
||||
"value": "Waarde",
|
||||
"remove_custom_claim": "Aangepaste claim verwijderen",
|
||||
"add_custom_claim": "Aangepaste claim toevoegen",
|
||||
"add_another": "Voeg er nog een toe",
|
||||
"select_a_date": "Selecteer een datum",
|
||||
"select_file": "Selecteer bestand",
|
||||
"profile_picture": "Profielfoto",
|
||||
"profile_picture_is_managed_by_ldap_server": "De profielfoto wordt beheerd door de LDAP-server en kan hier niet worden gewijzigd.",
|
||||
"click_profile_picture_to_upload_custom": "Klik op de profielfoto om een aangepaste foto uit uw bestanden te uploaden.",
|
||||
"image_should_be_in_format": "De afbeelding moet in PNG- of JPEG-formaat zijn.",
|
||||
"items_per_page": "Aantal per pagina",
|
||||
"no_items_found": "Geen items gevonden",
|
||||
"search": "Zoek...",
|
||||
"expand_card": "Kaart uitbreiden",
|
||||
"copied": "Gekopieerd",
|
||||
"click_to_copy": "Klik om te kopiëren",
|
||||
"something_went_wrong": "Er is iets misgegaan",
|
||||
"go_back_to_home": "Ga terug naar huis",
|
||||
"dont_have_access_to_your_passkey": "Hebt u geen toegang tot uw toegangscode?",
|
||||
"login_background": "Inlogachtergrond",
|
||||
"logo": "Logo",
|
||||
"login_code": "Inlogcode",
|
||||
"create_a_login_code_to_sign_in_without_a_passkey_once": "Maak een inlogcode aan waarmee de gebruiker zich eenmalig kan aanmelden zonder passkey.",
|
||||
"one_hour": "1 uur",
|
||||
"twelve_hours": "12 uur",
|
||||
"one_day": "1 dag",
|
||||
"one_week": "1 week",
|
||||
"one_month": "1 maand",
|
||||
"expiration": "Vervaldatum",
|
||||
"generate_code": "Genereer code",
|
||||
"name": "Naam",
|
||||
"browser_unsupported": "Browser niet ondersteund",
|
||||
"this_browser_does_not_support_passkeys": "Deze browser ondersteunt geen passkeys. Gebruik een alternatieve aanmeldmethode.",
|
||||
"an_unknown_error_occurred": "Er is een onbekende fout opgetreden",
|
||||
"authentication_process_was_aborted": "Het authenticatieproces is afgebroken",
|
||||
"error_occurred_with_authenticator": "Er is een fout opgetreden met de authenticator",
|
||||
"authenticator_does_not_support_discoverable_credentials": "De authenticator ondersteunt geen vindbare referenties",
|
||||
"authenticator_does_not_support_resident_keys": "De authenticator ondersteunt geen residente sleutels",
|
||||
"passkey_was_previously_registered": "Deze toegangscode is eerder geregistreerd",
|
||||
"authenticator_does_not_support_any_of_the_requested_algorithms": "De authenticator ondersteunt geen van de gevraagde algoritmen",
|
||||
"authenticator_timed_out": "De authenticator is verlopen",
|
||||
"critical_error_occurred_contact_administrator": "Er is een kritieke fout opgetreden. Neem contact op met uw beheerder.",
|
||||
"sign_in_to": "Meld u aan bij {name}",
|
||||
"client_not_found": "Client niet gevonden",
|
||||
"client_wants_to_access_the_following_information": "<b>{client}</b> wil toegang tot de volgende informatie:",
|
||||
"do_you_want_to_sign_in_to_client_with_your_app_name_account": "Wilt u zich aanmelden bij <b>{client}</b> met uw <b>{appName}</b> account?",
|
||||
"email": "E-mail",
|
||||
"view_your_email_address": "Bekijk uw e-mailadres",
|
||||
"profile": "Profiel",
|
||||
"view_your_profile_information": "Bekijk uw profielgegevens",
|
||||
"groups": "Groepen",
|
||||
"view_the_groups_you_are_a_member_of": "Bekijk de groepen waarvan u lid bent",
|
||||
"cancel": "Annuleren",
|
||||
"sign_in": "Aanmelden",
|
||||
"try_again": "Probeer het opnieuw",
|
||||
"client_logo": "Client logo",
|
||||
"sign_out": "Afmelden",
|
||||
"do_you_want_to_sign_out_of_pocketid_with_the_account": "Wilt u zich afmelden bij Pocket ID met het account <b>{username}</b> ?",
|
||||
"sign_in_to_appname": "Meld u aan bij {appName}",
|
||||
"please_try_to_sign_in_again": "Probeer opnieuw in te loggen.",
|
||||
"authenticate_yourself_with_your_passkey_to_access_the_admin_panel": "Verifieer uzelf met uw toegangscode om toegang te krijgen tot het beheerderspaneel.",
|
||||
"authenticate": "Authenticeren",
|
||||
"appname_setup": "{appName} Instellen",
|
||||
"please_try_again": "Probeer het opnieuw.",
|
||||
"you_are_about_to_sign_in_to_the_initial_admin_account": "U staat op het punt om in te loggen op het oorspronkelijke beheerdersaccount. Iedereen met deze link heeft toegang tot het account totdat er een passkey is toegevoegd. Stel zo snel mogelijk een passkey in om ongeautoriseerde toegang te voorkomen.",
|
||||
"continue": "Doorgaan",
|
||||
"alternative_sign_in": "Alternatieve aanmelding",
|
||||
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "Als u geen toegang hebt tot uw toegangscode, kunt u zich op een van de volgende manieren aanmelden.",
|
||||
"use_your_passkey_instead": "Wilt u in plaats daarvan uw toegangscode gebruiken?",
|
||||
"email_login": "E-mail inloggen",
|
||||
"enter_a_login_code_to_sign_in": "Voer een inlogcode in om in te loggen.",
|
||||
"request_a_login_code_via_email": "Vraag een inlogcode aan via e-mail.",
|
||||
"go_back": "Ga terug",
|
||||
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "Er is een e-mail verzonden naar het opgegeven e-mailadres, indien dit in het systeem voorkomt.",
|
||||
"enter_code": "Voer code in",
|
||||
"enter_your_email_address_to_receive_an_email_with_a_login_code": "Voer uw e-mailadres in om een e-mail met een inlogcode te ontvangen.",
|
||||
"your_email": "Uw e-mail",
|
||||
"submit": "Indienen",
|
||||
"enter_the_code_you_received_to_sign_in": "Voer de code in die u hebt ontvangen om in te loggen.",
|
||||
"code": "Code",
|
||||
"invalid_redirect_url": "Ongeldige omleidings-URL",
|
||||
"audit_log": "Audit logboek",
|
||||
"users": "Gebruikers",
|
||||
"user_groups": "Gebruikersgroepen",
|
||||
"oidc_clients": "OIDC-clients",
|
||||
"api_keys": "API-sleutels",
|
||||
"application_configuration": "Toepassingsconfiguratie",
|
||||
"settings": "Instellingen",
|
||||
"update_pocket_id": "Pocket-ID bijwerken",
|
||||
"powered_by": "Aangedreven door",
|
||||
"see_your_account_activities_from_the_last_3_months": "Bekijk uw accountactiviteiten van de afgelopen 3 maanden.",
|
||||
"time": "Tijd",
|
||||
"event": "Evenement",
|
||||
"approximate_location": "Geschatte locatie",
|
||||
"ip_address": "IP-adres",
|
||||
"device": "Apparaat",
|
||||
"client": "Cliënt",
|
||||
"unknown": "Onbekend",
|
||||
"account_details_updated_successfully": "Accountgegevens succesvol bijgewerkt",
|
||||
"profile_picture_updated_successfully": "Profielfoto succesvol bijgewerkt. Het kan enkele minuten duren voordat de wijzigingen zichtbaar zijn.",
|
||||
"account_settings": "Accountinstellingen",
|
||||
"passkey_missing": "Passkey ontbreekt",
|
||||
"please_provide_a_passkey_to_prevent_losing_access_to_your_account": "Voeg een passkey toe om te voorkomen dat u de toegang tot uw account verliest.",
|
||||
"single_passkey_configured": "Eén enkele toegangscode geconfigureerd",
|
||||
"it_is_recommended_to_add_more_than_one_passkey": "Het is raadzaam om meer dan één toegangscode toe te voegen om te voorkomen dat u de toegang tot uw account verliest.",
|
||||
"account_details": "Accountgegevens",
|
||||
"passkeys": "Toegangscodes",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Beheer de toegangscodes waarmee u uzelf kunt verifiëren.",
|
||||
"add_passkey": "Passkey toevoegen",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Maak een eenmalige inlogcode aan om in te loggen vanaf een ander apparaat zonder passkey.",
|
||||
"create": "Creëren",
|
||||
"first_name": "Voornaam",
|
||||
"last_name": "Achternaam",
|
||||
"username": "Gebruikersnaam",
|
||||
"save": "Opslaan",
|
||||
"username_can_only_contain": "Gebruikersnaam mag alleen kleine letters, cijfers, onderstrepingstekens, punten, koppeltekens en '@'-symbolen bevatten",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Meld u aan met de volgende code. De code verloopt over 15 minuten.",
|
||||
"or_visit": "of bezoek",
|
||||
"added_on": "Toegevoegd op",
|
||||
"rename": "Hernoemen",
|
||||
"delete": "Verwijderen",
|
||||
"are_you_sure_you_want_to_delete_this_passkey": "Weet u zeker dat u deze toegangscode wilt verwijderen?",
|
||||
"passkey_deleted_successfully": "Passkey succesvol verwijderd",
|
||||
"delete_passkey_name": "Verwijder {passkeyName}",
|
||||
"passkey_name_updated_successfully": "Passkey naam succesvol bijgewerkt",
|
||||
"name_passkey": "Naam Passkey",
|
||||
"name_your_passkey_to_easily_identify_it_later": "Geef uw toegangscode een naam, zodat u deze later gemakkelijk kunt terugvinden.",
|
||||
"create_api_key": "API-sleutel aanmaken",
|
||||
"add_a_new_api_key_for_programmatic_access": "Voeg een nieuwe API-sleutel toe voor programmatische toegang.",
|
||||
"add_api_key": "API-sleutel toevoegen",
|
||||
"manage_api_keys": "API-sleutels beheren",
|
||||
"api_key_created": "API-sleutel gemaakt",
|
||||
"for_security_reasons_this_key_will_only_be_shown_once": "Om veiligheidsredenen wordt deze sleutel slechts één keer getoond. Bewaar hem veilig.",
|
||||
"description": "Beschrijving",
|
||||
"api_key": "API-sleutel",
|
||||
"close": "Dichtbij",
|
||||
"name_to_identify_this_api_key": "Naam om deze API-sleutel te identificeren.",
|
||||
"expires_at": "Verloopt op",
|
||||
"when_this_api_key_will_expire": "Wanneer deze API-sleutel verloopt.",
|
||||
"optional_description_to_help_identify_this_keys_purpose": "Optionele beschrijving om het doel van deze sleutel te helpen identificeren.",
|
||||
"name_must_be_at_least_3_characters": "Naam moet minimaal 3 tekens lang zijn",
|
||||
"name_cannot_exceed_50_characters": "Naam mag niet langer zijn dan 50 tekens",
|
||||
"expiration_date_must_be_in_the_future": "Vervaldatum moet in de toekomst liggen",
|
||||
"revoke_api_key": "API-sleutel intrekken",
|
||||
"never": "Nooit",
|
||||
"revoke": "Herroepen",
|
||||
"api_key_revoked_successfully": "API-sleutel succesvol ingetrokken",
|
||||
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Weet u zeker dat u de API-sleutel \" {apiKeyName} \" wilt intrekken? Hiermee worden alle integraties die deze sleutel gebruiken, verbroken.",
|
||||
"last_used": "Laatst gebruikt",
|
||||
"actions": "Acties",
|
||||
"images_updated_successfully": "Afbeeldingen succesvol bijgewerkt",
|
||||
"general": "Algemeen",
|
||||
"enable_email_notifications_to_alert_users_when_a_login_is_detected_from_a_new_device_or_location": "Schakel e-mailmeldingen in om gebruikers te waarschuwen wanneer er wordt ingelogd vanaf een nieuw apparaat of een nieuwe locatie.",
|
||||
"ldap": "LDAP",
|
||||
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Configureer LDAP-instellingen om gebruikers en groepen vanaf een LDAP-server te synchroniseren.",
|
||||
"images": "Afbeeldingen",
|
||||
"update": "Update",
|
||||
"email_configuration_updated_successfully": "E-mailconfiguratie succesvol bijgewerkt",
|
||||
"save_changes_question": "Wijzigingen opslaan?",
|
||||
"you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "U moet de wijzigingen opslaan voordat u een test-e-mail verzendt. Wilt u nu opslaan?",
|
||||
"save_and_send": "Opslaan en verzenden",
|
||||
"test_email_sent_successfully": "Test-e-mail succesvol verzonden naar uw e-mailadres.",
|
||||
"failed_to_send_test_email": "Het is niet gelukt om een test-e-mail te versturen. Controleer de serverlogs voor meer informatie.",
|
||||
"smtp_configuration": "SMTP-configuratie",
|
||||
"smtp_host": "SMTP-host",
|
||||
"smtp_port": "SMTP-poort",
|
||||
"smtp_user": "SMTP-gebruiker",
|
||||
"smtp_password": "SMTP-wachtwoord",
|
||||
"smtp_from": "SMTP van",
|
||||
"smtp_tls_option": "SMTP TLS-optie",
|
||||
"email_tls_option": "E-mail TLS-optie",
|
||||
"skip_certificate_verification": "Certificaatverificatie overslaan",
|
||||
"this_can_be_useful_for_selfsigned_certificates": "Dit kan handig zijn voor zelfondertekende certificaten.",
|
||||
"enabled_emails": "Ingeschakelde e-mails",
|
||||
"email_login_notification": "E-mail-inlogmelding",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Stuur een e-mail naar de gebruiker wanneer deze zich aanmeldt vanaf een nieuw apparaat.",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Hiermee kunnen gebruikers inloggen met een inlogcode die naar hun e-mail is gestuurd. Dit vermindert de beveiliging aanzienlijk, omdat iedereen met toegang tot de e-mail van de gebruiker toegang kan krijgen.",
|
||||
"send_test_email": "Test-e-mail verzenden",
|
||||
"application_configuration_updated_successfully": "Toepassingsconfiguratie succesvol bijgewerkt",
|
||||
"application_name": "Toepassingsnaam",
|
||||
"session_duration": "Sessieduur",
|
||||
"the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again": "De duur van een sessie in minuten voordat de gebruiker zich opnieuw moet aanmelden.",
|
||||
"enable_self_account_editing": "Zelf-accountbewerking inschakelen",
|
||||
"whether_the_users_should_be_able_to_edit_their_own_account_details": "Of gebruikers hun eigen accountgegevens moeten kunnen bewerken.",
|
||||
"emails_verified": "E-mails geverifieerd",
|
||||
"whether_the_users_email_should_be_marked_as_verified_for_the_oidc_clients": "Of het e-mailadres van de gebruiker als geverifieerd moet worden gemarkeerd voor de OIDC-clients.",
|
||||
"ldap_configuration_updated_successfully": "LDAP-configuratie succesvol bijgewerkt",
|
||||
"ldap_disabled_successfully": "LDAP succesvol uitgeschakeld",
|
||||
"ldap_sync_finished": "LDAP-synchronisatie voltooid",
|
||||
"client_configuration": "Clientconfiguratie",
|
||||
"ldap_url": "LDAP-URL",
|
||||
"ldap_bind_dn": "LDAP Bind-DN",
|
||||
"ldap_bind_password": "LDAP Bind-wachtwoord",
|
||||
"ldap_base_dn": "LDAP-basis-DN",
|
||||
"user_search_filter": "Gebruikerszoekfilter",
|
||||
"the_search_filter_to_use_to_search_or_sync_users": "Het zoekfilter waarmee u gebruikers kunt zoeken/synchroniseren.",
|
||||
"groups_search_filter": "Groepen Zoekfilter",
|
||||
"the_search_filter_to_use_to_search_or_sync_groups": "Het zoekfilter waarmee u groepen kunt zoeken/synchroniseren.",
|
||||
"attribute_mapping": "Attribuuttoewijzing",
|
||||
"user_unique_identifier_attribute": "Gebruiker uniek identificatiekenmerk",
|
||||
"the_value_of_this_attribute_should_never_change": "De waarde van dit kenmerk mag nooit veranderen.",
|
||||
"username_attribute": "Gebruikersnaam Attribuut",
|
||||
"user_mail_attribute": "Gebruikersmailkenmerk",
|
||||
"user_first_name_attribute": "Gebruikersvoornaam Attribuut",
|
||||
"user_last_name_attribute": "Gebruikersnaam Achternaam Attribuut",
|
||||
"user_profile_picture_attribute": "Gebruikersprofielfoto-attribuut",
|
||||
"the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image": "De waarde van dit kenmerk kan een URL, een binair bestand of een base64-gecodeerde afbeelding zijn.",
|
||||
"group_members_attribute": "Groepsleden Attribuut",
|
||||
"the_attribute_to_use_for_querying_members_of_a_group": "Het kenmerk dat gebruikt moet worden om leden van een groep te bevragen.",
|
||||
"group_unique_identifier_attribute": "Groeps uniek identificatiekenmerk",
|
||||
"group_name_attribute": "Groepsnaam Attribuut",
|
||||
"admin_group_name": "Naam van beheerdersgroep",
|
||||
"members_of_this_group_will_have_admin_privileges_in_pocketid": "Leden van deze groep hebben beheerdersrechten in Pocket ID.",
|
||||
"disable": "Uitzetten",
|
||||
"sync_now": "Nu synchroniseren",
|
||||
"enable": "Inschakelen",
|
||||
"user_created_successfully": "Gebruiker succesvol aangemaakt",
|
||||
"create_user": "Gebruiker aanmaken",
|
||||
"add_a_new_user_to_appname": "Voeg een nieuwe gebruiker toe aan {appName}",
|
||||
"add_user": "Gebruiker toevoegen",
|
||||
"manage_users": "Gebruikers beheren",
|
||||
"admin_privileges": "Beheerdersrechten",
|
||||
"admins_have_full_access_to_the_admin_panel": "Beheerders hebben volledige toegang tot het beheerderspaneel.",
|
||||
"delete_firstname_lastname": "Verwijderen {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_delete_this_user": "Weet u zeker dat u deze gebruiker wilt verwijderen?",
|
||||
"user_deleted_successfully": "Gebruiker succesvol verwijderd",
|
||||
"role": "Rol",
|
||||
"source": "Bron",
|
||||
"admin": "Beheerder",
|
||||
"user": "Gebruiker",
|
||||
"local": "Lokaal",
|
||||
"toggle_menu": "Menu wisselen",
|
||||
"edit": "Bewerking",
|
||||
"user_groups_updated_successfully": "Gebruikersgroepen succesvol bijgewerkt",
|
||||
"user_updated_successfully": "Gebruiker succesvol bijgewerkt",
|
||||
"custom_claims_updated_successfully": "Aangepaste claims succesvol bijgewerkt",
|
||||
"back": "Terug",
|
||||
"user_details_firstname_lastname": "Gebruikersgegevens {firstName} {lastName}",
|
||||
"manage_which_groups_this_user_belongs_to": "Beheer tot welke groepen deze gebruiker behoort.",
|
||||
"custom_claims": "Aangepaste claims",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user": "Aangepaste claims zijn sleutel-waardeparen die kunnen worden gebruikt om aanvullende informatie over een gebruiker op te slaan. Deze claims worden opgenomen in het ID-token als de scope 'profile' wordt aangevraagd.",
|
||||
"user_group_created_successfully": "Gebruikersgroep succesvol aangemaakt",
|
||||
"create_user_group": "Gebruikersgroep aanmaken",
|
||||
"create_a_new_group_that_can_be_assigned_to_users": "Maak een nieuwe groep aan die aan gebruikers kan worden toegewezen.",
|
||||
"add_group": "Groep toevoegen",
|
||||
"manage_user_groups": "Gebruikersgroepen beheren",
|
||||
"friendly_name": "Vriendelijke naam",
|
||||
"name_that_will_be_displayed_in_the_ui": "Naam die in de gebruikersinterface wordt weergegeven",
|
||||
"name_that_will_be_in_the_groups_claim": "Naam die in de claim 'groepen' zal staan",
|
||||
"delete_name": "Verwijder {name}",
|
||||
"are_you_sure_you_want_to_delete_this_user_group": "Weet u zeker dat u deze gebruikersgroep wilt verwijderen?",
|
||||
"user_group_deleted_successfully": "Gebruikersgroep succesvol verwijderd",
|
||||
"user_count": "Gebruikersaantal",
|
||||
"user_group_updated_successfully": "Gebruikersgroep succesvol bijgewerkt",
|
||||
"users_updated_successfully": "Gebruikers succesvol bijgewerkt",
|
||||
"user_group_details_name": "Gebruikersgroepdetails {name}",
|
||||
"assign_users_to_this_group": "Gebruikers aan deze groep toewijzen.",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized": "Aangepaste claims zijn sleutel-waardeparen die kunnen worden gebruikt om aanvullende informatie over een gebruiker op te slaan. Deze claims worden opgenomen in het ID-token als de scope 'profile' wordt opgevraagd. Aangepaste claims die zijn gedefinieerd voor de gebruiker, krijgen prioriteit als er conflicten zijn.",
|
||||
"oidc_client_created_successfully": "OIDC-client succesvol aangemaakt",
|
||||
"create_oidc_client": "Maak een OIDC-client",
|
||||
"add_a_new_oidc_client_to_appname": "Voeg een nieuwe OIDC-client toe aan {appName} .",
|
||||
"add_oidc_client": "OIDC-client toevoegen",
|
||||
"manage_oidc_clients": "OIDC-clients beheren",
|
||||
"one_time_link": "Eenmalige link",
|
||||
"use_this_link_to_sign_in_once": "Gebruik deze link om u eenmalig aan te melden. Dit is nodig voor gebruikers die nog geen passkey hebben toegevoegd of\nben het kwijt.",
|
||||
"add": "Toevoegen",
|
||||
"callback_urls": "Callback-URL's",
|
||||
"logout_callback_urls": "Callback-URL's voor afmelden",
|
||||
"public_client": "Publieke client",
|
||||
"public_clients_do_not_have_a_client_secret_and_use_pkce_instead": "Publieke clients hebben geen client secret en gebruiken in plaats daarvan PKCE. Schakel dit in als uw client een SPA of mobiele app is.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange is een beveiligingsfunctie om CSRF- en autorisatiecode-onderscheppingsaanvallen te voorkomen.",
|
||||
"name_logo": "{name} logo",
|
||||
"change_logo": "Logo wijzigen",
|
||||
"upload_logo": "Logo uploaden",
|
||||
"remove_logo": "Logo verwijderen",
|
||||
"are_you_sure_you_want_to_delete_this_oidc_client": "Weet u zeker dat u deze OIDC-client wilt verwijderen?",
|
||||
"oidc_client_deleted_successfully": "OIDC-client succesvol verwijderd",
|
||||
"authorization_url": "Autorisatie-URL",
|
||||
"oidc_discovery_url": "OIDC-ontdekkings-URL",
|
||||
"token_url": "Token-URL",
|
||||
"userinfo_url": "Gebruikersinfo-URL",
|
||||
"logout_url": "Uitlog-URL",
|
||||
"certificate_url": "Certificaat-URL",
|
||||
"enabled": "Ingeschakeld",
|
||||
"disabled": "Uitgeschakeld",
|
||||
"oidc_client_updated_successfully": "OIDC-client succesvol bijgewerkt",
|
||||
"create_new_client_secret": "Nieuw clientgeheim aanmaken",
|
||||
"are_you_sure_you_want_to_create_a_new_client_secret": "Weet u zeker dat u een nieuw client secret wilt aanmaken? De oude wordt ongeldig.",
|
||||
"generate": "Genereren",
|
||||
"new_client_secret_created_successfully": "Nieuw clientgeheim succesvol aangemaakt",
|
||||
"allowed_user_groups_updated_successfully": "Toegestane gebruikersgroepen succesvol bijgewerkt",
|
||||
"oidc_client_name": "OIDC-client {name}",
|
||||
"client_id": "Client id",
|
||||
"client_secret": "Client geheim",
|
||||
"show_more_details": "Meer details weergeven",
|
||||
"allowed_user_groups": "Toegestane gebruikersgroepen",
|
||||
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Voeg gebruikersgroepen toe aan deze client om de toegang tot gebruikers in deze groepen te beperken. Als er geen gebruikersgroepen zijn geselecteerd, hebben alle gebruikers toegang tot deze client.",
|
||||
"favicon": "Favicon",
|
||||
"light_mode_logo": "Lichte modus logo",
|
||||
"dark_mode_logo": "Donkere modus logo",
|
||||
"background_image": "Achtergrondfoto",
|
||||
"language": "Taal",
|
||||
"reset_profile_picture_question": "Profielfoto opnieuw instellen?",
|
||||
"this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default": "Dit verwijdert de geüploade afbeelding en de zet de profielfoto terug naar de standaard-profielfoto. Wilt u doorgaan?",
|
||||
"reset": "Reset",
|
||||
"reset_to_default": "Standaardinstellingen herstellen",
|
||||
"profile_picture_has_been_reset": "Profielfoto is gereset. Het kan enkele minuten duren voordat de wijzigingen zichtbaar zijn.",
|
||||
"select_the_language_you_want_to_use": "Selecteer de taal die u wilt gebruiken. Sommige talen zijn mogelijk niet volledig vertaald."
|
||||
}
|
||||
316
frontend/messages/pt-PT.json
Normal file
316
frontend/messages/pt-PT.json
Normal file
@@ -0,0 +1,316 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"my_account": "My Account",
|
||||
"logout": "Logout",
|
||||
"confirm": "Confirm",
|
||||
"key": "Key",
|
||||
"value": "Value",
|
||||
"remove_custom_claim": "Remove custom claim",
|
||||
"add_custom_claim": "Add custom claim",
|
||||
"add_another": "Add another",
|
||||
"select_a_date": "Select a date",
|
||||
"select_file": "Select File",
|
||||
"profile_picture": "Profile Picture",
|
||||
"profile_picture_is_managed_by_ldap_server": "The profile picture is managed by the LDAP server and cannot be changed here.",
|
||||
"click_profile_picture_to_upload_custom": "Click on the profile picture to upload a custom one from your files.",
|
||||
"image_should_be_in_format": "The image should be in PNG or JPEG format.",
|
||||
"items_per_page": "Items per page",
|
||||
"no_items_found": "No items found",
|
||||
"search": "Search...",
|
||||
"expand_card": "Expand card",
|
||||
"copied": "Copied",
|
||||
"click_to_copy": "Click to copy",
|
||||
"something_went_wrong": "Something went wrong",
|
||||
"go_back_to_home": "Go back to home",
|
||||
"dont_have_access_to_your_passkey": "Don't have access to your passkey?",
|
||||
"login_background": "Login background",
|
||||
"logo": "Logo",
|
||||
"login_code": "Login Code",
|
||||
"create_a_login_code_to_sign_in_without_a_passkey_once": "Create a login code that the user can use to sign in without a passkey once.",
|
||||
"one_hour": "1 hour",
|
||||
"twelve_hours": "12 hours",
|
||||
"one_day": "1 day",
|
||||
"one_week": "1 week",
|
||||
"one_month": "1 month",
|
||||
"expiration": "Expiration",
|
||||
"generate_code": "Generate Code",
|
||||
"name": "Name",
|
||||
"browser_unsupported": "Browser unsupported",
|
||||
"this_browser_does_not_support_passkeys": "This browser doesn't support passkeys. Please or use a alternative sign in method.",
|
||||
"an_unknown_error_occurred": "An unknown error occurred",
|
||||
"authentication_process_was_aborted": "The authentication process was aborted",
|
||||
"error_occurred_with_authenticator": "An error occurred with the authenticator",
|
||||
"authenticator_does_not_support_discoverable_credentials": "The authenticator does not support discoverable credentials",
|
||||
"authenticator_does_not_support_resident_keys": "The authenticator does not support resident keys",
|
||||
"passkey_was_previously_registered": "This passkey was previously registered",
|
||||
"authenticator_does_not_support_any_of_the_requested_algorithms": "The authenticator does not support any of the requested algorithms",
|
||||
"authenticator_timed_out": "The authenticator timed out",
|
||||
"critical_error_occurred_contact_administrator": "A critical error occurred. Please contact your administrator.",
|
||||
"sign_in_to": "Sign in to {name}",
|
||||
"client_not_found": "Client not found",
|
||||
"client_wants_to_access_the_following_information": "<b>{client}</b> wants to access the following information:",
|
||||
"do_you_want_to_sign_in_to_client_with_your_app_name_account": "Do you want to sign in to <b>{client}</b> with your <b>{appName}</b> account?",
|
||||
"email": "Email",
|
||||
"view_your_email_address": "View your email address",
|
||||
"profile": "Profile",
|
||||
"view_your_profile_information": "View your profile information",
|
||||
"groups": "Groups",
|
||||
"view_the_groups_you_are_a_member_of": "View the groups you are a member of",
|
||||
"cancel": "Cancel",
|
||||
"sign_in": "Sign in",
|
||||
"try_again": "Try again",
|
||||
"client_logo": "Client Logo",
|
||||
"sign_out": "Sign out",
|
||||
"do_you_want_to_sign_out_of_pocketid_with_the_account": "Do you want to sign out of Pocket ID with the account <b>{username}</b>?",
|
||||
"sign_in_to_appname": "Sign in to {appName}",
|
||||
"please_try_to_sign_in_again": "Please try to sign in again.",
|
||||
"authenticate_yourself_with_your_passkey_to_access_the_admin_panel": "Authenticate yourself with your passkey to access the admin panel.",
|
||||
"authenticate": "Authenticate",
|
||||
"appname_setup": "{appName} Setup",
|
||||
"please_try_again": "Please try again.",
|
||||
"you_are_about_to_sign_in_to_the_initial_admin_account": "You're about to sign in to the initial admin account. Anyone with this link can access the account until a passkey is added. Please set up a passkey as soon as possible to prevent unauthorized access.",
|
||||
"continue": "Continue",
|
||||
"alternative_sign_in": "Alternative Sign In",
|
||||
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "If you dont't have access to your passkey, you can sign in using one of the following methods.",
|
||||
"use_your_passkey_instead": "Use your passkey instead?",
|
||||
"email_login": "Email Login",
|
||||
"enter_a_login_code_to_sign_in": "Enter a login code to sign in.",
|
||||
"request_a_login_code_via_email": "Request a login code via email.",
|
||||
"go_back": "Go back",
|
||||
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "An email has been sent to the provided email, if it exists in the system.",
|
||||
"enter_code": "Enter code",
|
||||
"enter_your_email_address_to_receive_an_email_with_a_login_code": "Enter your email address to receive an email with a login code.",
|
||||
"your_email": "Your email",
|
||||
"submit": "Submit",
|
||||
"enter_the_code_you_received_to_sign_in": "Enter the code you received to sign in.",
|
||||
"code": "Code",
|
||||
"invalid_redirect_url": "Invalid redirect URL",
|
||||
"audit_log": "Audit Log",
|
||||
"users": "Users",
|
||||
"user_groups": "User Groups",
|
||||
"oidc_clients": "OIDC Clients",
|
||||
"api_keys": "API Keys",
|
||||
"application_configuration": "Application Configuration",
|
||||
"settings": "Settings",
|
||||
"update_pocket_id": "Update Pocket ID",
|
||||
"powered_by": "Powered by",
|
||||
"see_your_account_activities_from_the_last_3_months": "See your account activities from the last 3 months.",
|
||||
"time": "Time",
|
||||
"event": "Event",
|
||||
"approximate_location": "Approximate Location",
|
||||
"ip_address": "IP Address",
|
||||
"device": "Device",
|
||||
"client": "Client",
|
||||
"unknown": "Unknown",
|
||||
"account_details_updated_successfully": "Account details updated successfully",
|
||||
"profile_picture_updated_successfully": "Profile picture updated successfully. It may take a few minutes to update.",
|
||||
"account_settings": "Account Settings",
|
||||
"passkey_missing": "Passkey missing",
|
||||
"please_provide_a_passkey_to_prevent_losing_access_to_your_account": "Please add a passkey to prevent losing access to your account.",
|
||||
"single_passkey_configured": "Single Passkey Configured",
|
||||
"it_is_recommended_to_add_more_than_one_passkey": "It is recommended to add more than one passkey to avoid losing access to your account.",
|
||||
"account_details": "Account Details",
|
||||
"passkeys": "Passkeys",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Manage your passkeys that you can use to authenticate yourself.",
|
||||
"add_passkey": "Add Passkey",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Create a one-time login code to sign in from a different device without a passkey.",
|
||||
"create": "Create",
|
||||
"first_name": "First name",
|
||||
"last_name": "Last name",
|
||||
"username": "Username",
|
||||
"save": "Save",
|
||||
"username_can_only_contain": "Username can only contain lowercase letters, numbers, underscores, dots, hyphens, and '@' symbols",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Sign in using the following code. The code will expire in 15 minutes.",
|
||||
"or_visit": "or visit",
|
||||
"added_on": "Added on",
|
||||
"rename": "Rename",
|
||||
"delete": "Delete",
|
||||
"are_you_sure_you_want_to_delete_this_passkey": "Are you sure you want to delete this passkey?",
|
||||
"passkey_deleted_successfully": "Passkey deleted successfully",
|
||||
"delete_passkey_name": "Delete {passkeyName}",
|
||||
"passkey_name_updated_successfully": "Passkey name updated successfully",
|
||||
"name_passkey": "Name Passkey",
|
||||
"name_your_passkey_to_easily_identify_it_later": "Name your passkey to easily identify it later.",
|
||||
"create_api_key": "Create API Key",
|
||||
"add_a_new_api_key_for_programmatic_access": "Add a new API key for programmatic access.",
|
||||
"add_api_key": "Add API Key",
|
||||
"manage_api_keys": "Manage API Keys",
|
||||
"api_key_created": "API Key Created",
|
||||
"for_security_reasons_this_key_will_only_be_shown_once": "For security reasons, this key will only be shown once. Please store it securely.",
|
||||
"description": "Description",
|
||||
"api_key": "API Key",
|
||||
"close": "Close",
|
||||
"name_to_identify_this_api_key": "Name to identify this API key.",
|
||||
"expires_at": "Expires At",
|
||||
"when_this_api_key_will_expire": "When this API key will expire.",
|
||||
"optional_description_to_help_identify_this_keys_purpose": "Optional description to help identify this key's purpose.",
|
||||
"name_must_be_at_least_3_characters": "Name must be at least 3 characters",
|
||||
"name_cannot_exceed_50_characters": "Name cannot exceed 50 characters",
|
||||
"expiration_date_must_be_in_the_future": "Expiration date must be in the future",
|
||||
"revoke_api_key": "Revoke API Key",
|
||||
"never": "Never",
|
||||
"revoke": "Revoke",
|
||||
"api_key_revoked_successfully": "API key revoked successfully",
|
||||
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Are you sure you want to revoke the API key \"{apiKeyName}\"? This will break any integrations using this key.",
|
||||
"last_used": "Last Used",
|
||||
"actions": "Actions",
|
||||
"images_updated_successfully": "Images updated successfully",
|
||||
"general": "General",
|
||||
"enable_email_notifications_to_alert_users_when_a_login_is_detected_from_a_new_device_or_location": "Enable email notifications to alert users when a login is detected from a new device or location.",
|
||||
"ldap": "LDAP",
|
||||
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Configure LDAP settings to sync users and groups from an LDAP server.",
|
||||
"images": "Images",
|
||||
"update": "Update",
|
||||
"email_configuration_updated_successfully": "Email configuration updated successfully",
|
||||
"save_changes_question": "Save changes?",
|
||||
"you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "You have to save the changes before sending a test email. Do you want to save now?",
|
||||
"save_and_send": "Save and send",
|
||||
"test_email_sent_successfully": "Test email sent successfully to your email address.",
|
||||
"failed_to_send_test_email": "Failed to send test email. Check the server logs for more information.",
|
||||
"smtp_configuration": "SMTP Configuration",
|
||||
"smtp_host": "SMTP Host",
|
||||
"smtp_port": "SMTP Port",
|
||||
"smtp_user": "SMTP User",
|
||||
"smtp_password": "SMTP Password",
|
||||
"smtp_from": "SMTP From",
|
||||
"smtp_tls_option": "SMTP TLS Option",
|
||||
"email_tls_option": "Email TLS Option",
|
||||
"skip_certificate_verification": "Skip Certificate Verification",
|
||||
"this_can_be_useful_for_selfsigned_certificates": "This can be useful for self-signed certificates.",
|
||||
"enabled_emails": "Enabled Emails",
|
||||
"email_login_notification": "Email Login Notification",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Send an email to the user when they log in from a new device.",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to sign in with a login code sent to their email. This reduces the security significantly as anyone with access to the user's email can gain entry.",
|
||||
"send_test_email": "Send test email",
|
||||
"application_configuration_updated_successfully": "Application configuration updated successfully",
|
||||
"application_name": "Application Name",
|
||||
"session_duration": "Session Duration",
|
||||
"the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again": "The duration of a session in minutes before the user has to sign in again.",
|
||||
"enable_self_account_editing": "Enable Self-Account Editing",
|
||||
"whether_the_users_should_be_able_to_edit_their_own_account_details": "Whether the users should be able to edit their own account details.",
|
||||
"emails_verified": "Emails Verified",
|
||||
"whether_the_users_email_should_be_marked_as_verified_for_the_oidc_clients": "Whether the user's email should be marked as verified for the OIDC clients.",
|
||||
"ldap_configuration_updated_successfully": "LDAP configuration updated successfully",
|
||||
"ldap_disabled_successfully": "LDAP disabled successfully",
|
||||
"ldap_sync_finished": "LDAP sync finished",
|
||||
"client_configuration": "Client Configuration",
|
||||
"ldap_url": "LDAP URL",
|
||||
"ldap_bind_dn": "LDAP Bind DN",
|
||||
"ldap_bind_password": "LDAP Bind Password",
|
||||
"ldap_base_dn": "LDAP Base DN",
|
||||
"user_search_filter": "User Search Filter",
|
||||
"the_search_filter_to_use_to_search_or_sync_users": "The Search filter to use to search/sync users.",
|
||||
"groups_search_filter": "Groups Search Filter",
|
||||
"the_search_filter_to_use_to_search_or_sync_groups": "The Search filter to use to search/sync groups.",
|
||||
"attribute_mapping": "Attribute Mapping",
|
||||
"user_unique_identifier_attribute": "User Unique Identifier Attribute",
|
||||
"the_value_of_this_attribute_should_never_change": "The value of this attribute should never change.",
|
||||
"username_attribute": "Username Attribute",
|
||||
"user_mail_attribute": "User Mail Attribute",
|
||||
"user_first_name_attribute": "User First Name Attribute",
|
||||
"user_last_name_attribute": "User Last Name Attribute",
|
||||
"user_profile_picture_attribute": "User Profile Picture Attribute",
|
||||
"the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image": "The value of this attribute can either be a URL, a binary or a base64 encoded image.",
|
||||
"group_members_attribute": "Group Members Attribute",
|
||||
"the_attribute_to_use_for_querying_members_of_a_group": "The attribute to use for querying members of a group.",
|
||||
"group_unique_identifier_attribute": "Group Unique Identifier Attribute",
|
||||
"group_name_attribute": "Group Name Attribute",
|
||||
"admin_group_name": "Admin Group Name",
|
||||
"members_of_this_group_will_have_admin_privileges_in_pocketid": "Members of this group will have Admin Privileges in Pocket ID.",
|
||||
"disable": "Disable",
|
||||
"sync_now": "Sync now",
|
||||
"enable": "Enable",
|
||||
"user_created_successfully": "User created successfully",
|
||||
"create_user": "Create User",
|
||||
"add_a_new_user_to_appname": "Add a new user to {appName}",
|
||||
"add_user": "Add User",
|
||||
"manage_users": "Manage Users",
|
||||
"admin_privileges": "Admin Privileges",
|
||||
"admins_have_full_access_to_the_admin_panel": "Admins have full access to the admin panel.",
|
||||
"delete_firstname_lastname": "Delete {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_delete_this_user": "Are you sure you want to delete this user?",
|
||||
"user_deleted_successfully": "User deleted successfully",
|
||||
"role": "Role",
|
||||
"source": "Source",
|
||||
"admin": "Admin",
|
||||
"user": "User",
|
||||
"local": "Local",
|
||||
"toggle_menu": "Toggle menu",
|
||||
"edit": "Edit",
|
||||
"user_groups_updated_successfully": "User groups updated successfully",
|
||||
"user_updated_successfully": "User updated successfully",
|
||||
"custom_claims_updated_successfully": "Custom claims updated successfully",
|
||||
"back": "Back",
|
||||
"user_details_firstname_lastname": "User Details {firstName} {lastName}",
|
||||
"manage_which_groups_this_user_belongs_to": "Manage which groups this user belongs to.",
|
||||
"custom_claims": "Custom Claims",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user": "Custom claims are key-value pairs that can be used to store additional information about a user. These claims will be included in the ID token if the scope 'profile' is requested.",
|
||||
"user_group_created_successfully": "User group created successfully",
|
||||
"create_user_group": "Create User Group",
|
||||
"create_a_new_group_that_can_be_assigned_to_users": "Create a new group that can be assigned to users.",
|
||||
"add_group": "Add Group",
|
||||
"manage_user_groups": "Manage User Groups",
|
||||
"friendly_name": "Friendly Name",
|
||||
"name_that_will_be_displayed_in_the_ui": "Name that will be displayed in the UI",
|
||||
"name_that_will_be_in_the_groups_claim": "Name that will be in the \"groups\" claim",
|
||||
"delete_name": "Delete {name}",
|
||||
"are_you_sure_you_want_to_delete_this_user_group": "Are you sure you want to delete this user group?",
|
||||
"user_group_deleted_successfully": "User group deleted successfully",
|
||||
"user_count": "User Count",
|
||||
"user_group_updated_successfully": "User group updated successfully",
|
||||
"users_updated_successfully": "Users updated successfully",
|
||||
"user_group_details_name": "User Group Details {name}",
|
||||
"assign_users_to_this_group": "Assign users to this group.",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized": "Custom claims are key-value pairs that can be used to store additional information about a user. These claims will be included in the ID token if the scope 'profile' is requested. Custom claims defined on the user will be prioritized if there are conflicts.",
|
||||
"oidc_client_created_successfully": "OIDC client created successfully",
|
||||
"create_oidc_client": "Create OIDC Client",
|
||||
"add_a_new_oidc_client_to_appname": "Add a new OIDC client to {appName}.",
|
||||
"add_oidc_client": "Add OIDC Client",
|
||||
"manage_oidc_clients": "Manage OIDC Clients",
|
||||
"one_time_link": "One Time Link",
|
||||
"use_this_link_to_sign_in_once": "Use this link to sign in once. This is needed for users who haven't added a passkey yet or\n\t\t\t\thave lost it.",
|
||||
"add": "Add",
|
||||
"callback_urls": "Callback URLs",
|
||||
"logout_callback_urls": "Logout Callback URLs",
|
||||
"public_client": "Public Client",
|
||||
"public_clients_do_not_have_a_client_secret_and_use_pkce_instead": "Public clients do not have a client secret and use PKCE instead. Enable this if your client is a SPA or mobile app.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange is a security feature to prevent CSRF and authorization code interception attacks.",
|
||||
"name_logo": "{name} logo",
|
||||
"change_logo": "Change Logo",
|
||||
"upload_logo": "Upload Logo",
|
||||
"remove_logo": "Remove Logo",
|
||||
"are_you_sure_you_want_to_delete_this_oidc_client": "Are you sure you want to delete this OIDC client?",
|
||||
"oidc_client_deleted_successfully": "OIDC client deleted successfully",
|
||||
"authorization_url": "Authorization URL",
|
||||
"oidc_discovery_url": "OIDC Discovery URL",
|
||||
"token_url": "Token URL",
|
||||
"userinfo_url": "Userinfo URL",
|
||||
"logout_url": "Logout URL",
|
||||
"certificate_url": "Certificate URL",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"oidc_client_updated_successfully": "OIDC client updated successfully",
|
||||
"create_new_client_secret": "Create new client secret",
|
||||
"are_you_sure_you_want_to_create_a_new_client_secret": "Are you sure you want to create a new client secret? The old one will be invalidated.",
|
||||
"generate": "Generate",
|
||||
"new_client_secret_created_successfully": "New client secret created successfully",
|
||||
"allowed_user_groups_updated_successfully": "Allowed user groups updated successfully",
|
||||
"oidc_client_name": "OIDC Client {name}",
|
||||
"client_id": "Client ID",
|
||||
"client_secret": "Client secret",
|
||||
"show_more_details": "Show more details",
|
||||
"allowed_user_groups": "Allowed User Groups",
|
||||
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Add user groups to this client to restrict access to users in these groups. If no user groups are selected, all users will have access to this client.",
|
||||
"favicon": "Favicon",
|
||||
"light_mode_logo": "Light Mode Logo",
|
||||
"dark_mode_logo": "Dark Mode Logo",
|
||||
"background_image": "Background Image",
|
||||
"language": "Language",
|
||||
"reset_profile_picture_question": "Reset profile picture?",
|
||||
"this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default": "This will remove the uploaded image, and reset the profile picture to default. Do you want to continue?",
|
||||
"reset": "Reset",
|
||||
"reset_to_default": "Reset to default",
|
||||
"profile_picture_has_been_reset": "Profile picture has been reset. It may take a few minutes to update.",
|
||||
"select_the_language_you_want_to_use": "Select the language you want to use. Some languages may not be fully translated."
|
||||
}
|
||||
316
frontend/messages/ru-RU.json
Normal file
316
frontend/messages/ru-RU.json
Normal file
@@ -0,0 +1,316 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"my_account": "Моя учетная запись",
|
||||
"logout": "Выйти",
|
||||
"confirm": "Подтвердить",
|
||||
"key": "Ключ",
|
||||
"value": "Значение",
|
||||
"remove_custom_claim": "Удалить пользовательский claim",
|
||||
"add_custom_claim": "Добавить пользовательский claim",
|
||||
"add_another": "Добавить ещё",
|
||||
"select_a_date": "Выбрать дату",
|
||||
"select_file": "Выбрать файл",
|
||||
"profile_picture": "Изображение профиля",
|
||||
"profile_picture_is_managed_by_ldap_server": "Изображение профиля управляется LDAP сервером и не может быть изменено здесь.",
|
||||
"click_profile_picture_to_upload_custom": "Нажмите на изображение профиля, чтобы загрузить его из ваших файлов.",
|
||||
"image_should_be_in_format": "Изображение должно быть в формате PNG или JPEG.",
|
||||
"items_per_page": "Элементов на странице",
|
||||
"no_items_found": "Элементов не найдено",
|
||||
"search": "Поиск...",
|
||||
"expand_card": "Развернуть карточку",
|
||||
"copied": "Скопировано",
|
||||
"click_to_copy": "Нажмите, чтобы скопировать",
|
||||
"something_went_wrong": "Что-то пошло не так",
|
||||
"go_back_to_home": "Вернуться на главную",
|
||||
"dont_have_access_to_your_passkey": "Нет доступа к вашему passkey?",
|
||||
"login_background": "Фон страницы входа",
|
||||
"logo": "Логотип",
|
||||
"login_code": "Код входа",
|
||||
"create_a_login_code_to_sign_in_without_a_passkey_once": "Создайте код входа, с которым пользователь сможет войти без passkey один раз.",
|
||||
"one_hour": "1 час",
|
||||
"twelve_hours": "12 часов",
|
||||
"one_day": "1 день",
|
||||
"one_week": "1 неделя",
|
||||
"one_month": "1 месяц",
|
||||
"expiration": "Срок действия",
|
||||
"generate_code": "Сгенерировать код",
|
||||
"name": "Имя",
|
||||
"browser_unsupported": "Браузер не поддерживается",
|
||||
"this_browser_does_not_support_passkeys": "Этот браузер не поддерживает passkeys. Пожалуйста, воспользуйтесь альтернативным способом входа.",
|
||||
"an_unknown_error_occurred": "Произошла неизвестная ошибка",
|
||||
"authentication_process_was_aborted": "Процесс аутентификации был прерван",
|
||||
"error_occurred_with_authenticator": "С аутентификатором произошла ошибка",
|
||||
"authenticator_does_not_support_discoverable_credentials": "Аутентификатор не поддерживает discoverable credentials",
|
||||
"authenticator_does_not_support_resident_keys": "Аутентификатор не поддерживает resident keys",
|
||||
"passkey_was_previously_registered": "Этот passkey был ранее зарегистрирован",
|
||||
"authenticator_does_not_support_any_of_the_requested_algorithms": "Аутентификатор не поддерживает ни один из запрошенных алгоритмов",
|
||||
"authenticator_timed_out": "Время ожидания аутентификатора истекло",
|
||||
"critical_error_occurred_contact_administrator": "Произошла критическая ошибка. Обратитесь к администратору.",
|
||||
"sign_in_to": "Вход в {name}",
|
||||
"client_not_found": "Клиент не найден",
|
||||
"client_wants_to_access_the_following_information": "<b>{client}</b> запрашивает доступ к следующей информации:",
|
||||
"do_you_want_to_sign_in_to_client_with_your_app_name_account": "Вы хотите войти в <b>{client}</b> с помощью вашей учетной записи <b>{appName}</b>?",
|
||||
"email": "Электронная почта",
|
||||
"view_your_email_address": "Просмотр адреса электронной почты",
|
||||
"profile": "Профиль",
|
||||
"view_your_profile_information": "Просмотр информации о вашем профиле",
|
||||
"groups": "Группы",
|
||||
"view_the_groups_you_are_a_member_of": "Просмотр групп, в которых вы состоите",
|
||||
"cancel": "Отменить",
|
||||
"sign_in": "Войти",
|
||||
"try_again": "Попробовать снова",
|
||||
"client_logo": "Логотип клиента",
|
||||
"sign_out": "Выйти",
|
||||
"do_you_want_to_sign_out_of_pocketid_with_the_account": "Вы хотите выйти из Pocket ID с учетной записью <b>{username}</b>?",
|
||||
"sign_in_to_appname": "Вход в {appName}",
|
||||
"please_try_to_sign_in_again": "Пожалуйста, попробуйте войти снова.",
|
||||
"authenticate_yourself_with_your_passkey_to_access_the_admin_panel": "Авторизуйтесь с использованием passkey для доступа к панели администратора.",
|
||||
"authenticate": "Авторизоваться",
|
||||
"appname_setup": "Настройка {appName}",
|
||||
"please_try_again": "Пожалуйста, повторите попытку.",
|
||||
"you_are_about_to_sign_in_to_the_initial_admin_account": "Вы собираетесь впервые войти в учетную запись администратора. Любой пользователь с этой ссылкой может получить доступ к учетной записи до тех пор, пока не будет добавлен passkey. Пожалуйста, настройте passkey как можно скорее для предотвращения несанкционированного доступа.",
|
||||
"continue": "Продолжить",
|
||||
"alternative_sign_in": "Альтернативный вход",
|
||||
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "Если у вас нет доступа к passkey, вы можете войти одним из следующих способов.",
|
||||
"use_your_passkey_instead": "Воспользоваться passkey вместо этого?",
|
||||
"email_login": "Вход через электронную почту",
|
||||
"enter_a_login_code_to_sign_in": "Введите предварительно созданный код входа.",
|
||||
"request_a_login_code_via_email": "Запросить код входа на электронную почту.",
|
||||
"go_back": "Назад",
|
||||
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "Письмо было отправлено на указанный адрес электронной почты, если он существует в системе.",
|
||||
"enter_code": "Введите код",
|
||||
"enter_your_email_address_to_receive_an_email_with_a_login_code": "Введите ваш адрес электронной почты, чтобы получить письмо с кодом входа.",
|
||||
"your_email": "Ваш адрес электронной почты",
|
||||
"submit": "Отправить",
|
||||
"enter_the_code_you_received_to_sign_in": "Введите полученный код входа.",
|
||||
"code": "Код",
|
||||
"invalid_redirect_url": "Неправильный URL-адрес перенаправления",
|
||||
"audit_log": "Журнал аудита",
|
||||
"users": "Пользователи",
|
||||
"user_groups": "Группы пользователей",
|
||||
"oidc_clients": "Клиенты OIDC",
|
||||
"api_keys": "API ключи",
|
||||
"application_configuration": "Конфигурация приложения",
|
||||
"settings": "Настройки",
|
||||
"update_pocket_id": "Обновите Pocket ID",
|
||||
"powered_by": "Powered by",
|
||||
"see_your_account_activities_from_the_last_3_months": "Смотрите активность вашей учетной записи за последние 3 месяца.",
|
||||
"time": "Время",
|
||||
"event": "Событие",
|
||||
"approximate_location": "Приблизительное местоположение",
|
||||
"ip_address": "IP адрес",
|
||||
"device": "Устройство",
|
||||
"client": "Клиент",
|
||||
"unknown": "Неизвестно",
|
||||
"account_details_updated_successfully": "Данные учетной записи успешно обновлены",
|
||||
"profile_picture_updated_successfully": "Изображение профиля успешно обновлено. Обновление может занять несколько минут.",
|
||||
"account_settings": "Настройки учетной записи",
|
||||
"passkey_missing": "Passkey отсутствует",
|
||||
"please_provide_a_passkey_to_prevent_losing_access_to_your_account": "Пожалуйста, добавьте passkey, чтобы избежать утери доступа к вашей учетной записи.",
|
||||
"single_passkey_configured": "Настроен один passkey",
|
||||
"it_is_recommended_to_add_more_than_one_passkey": "Рекомендуется добавить более одного passkey во избежание потери доступа к вашей учетной записи.",
|
||||
"account_details": "Детали учетной записи",
|
||||
"passkeys": "Passkeys",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Управляйте passkeys, которые вы можете использовать для аутентификации себя.",
|
||||
"add_passkey": "Добавить Passkey",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Создайте одноразовый код входа, чтобы войти с другого устройства без passkey.",
|
||||
"create": "Создать",
|
||||
"first_name": "Имя",
|
||||
"last_name": "Фамилия",
|
||||
"username": "Имя пользователя",
|
||||
"save": "Сохранить",
|
||||
"username_can_only_contain": "Имя пользователя может содержать только строчные буквы, цифры, знак подчеркивания, точки, дефиса и символ '@'",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Войдите, используя следующий код. Код истечет через 15 минут.",
|
||||
"or_visit": "или посетите",
|
||||
"added_on": "Добавлен",
|
||||
"rename": "Переименовать",
|
||||
"delete": "Удалить",
|
||||
"are_you_sure_you_want_to_delete_this_passkey": "Вы уверены, что хотите удалить этот passkey?",
|
||||
"passkey_deleted_successfully": "Passkey успешно удален",
|
||||
"delete_passkey_name": "Удалить {passkeyName}",
|
||||
"passkey_name_updated_successfully": "Имя passkey успешно обновлено",
|
||||
"name_passkey": "Имя Passkey",
|
||||
"name_your_passkey_to_easily_identify_it_later": "Назовите ваш passkey, чтобы легко идентифицировать его позже.",
|
||||
"create_api_key": "Создать API ключ",
|
||||
"add_a_new_api_key_for_programmatic_access": "Добавить новый API ключ для программного доступа.",
|
||||
"add_api_key": "Добавить API ключ",
|
||||
"manage_api_keys": "Управление API ключами",
|
||||
"api_key_created": "API ключ создан",
|
||||
"for_security_reasons_this_key_will_only_be_shown_once": "По соображениям безопасности, этот ключ будет показан только один раз. Пожалуйста, храните его в безопасном месте.",
|
||||
"description": "Описание",
|
||||
"api_key": "API ключ",
|
||||
"close": "Закрыть",
|
||||
"name_to_identify_this_api_key": "Имя для идентификации API ключа.",
|
||||
"expires_at": "Действителен до",
|
||||
"when_this_api_key_will_expire": "Когда срок действия этого API ключа истечет.",
|
||||
"optional_description_to_help_identify_this_keys_purpose": "Опциональное описание, чтобы помочь определить цель этого ключа.",
|
||||
"name_must_be_at_least_3_characters": "Имя должно содержать не менее 3 символов",
|
||||
"name_cannot_exceed_50_characters": "Длина имени не может превышать 50 символов",
|
||||
"expiration_date_must_be_in_the_future": "Дата истечения должна быть определена в будущем",
|
||||
"revoke_api_key": "Отозвать API ключ",
|
||||
"never": "Никогда",
|
||||
"revoke": "Отозвать",
|
||||
"api_key_revoked_successfully": "API ключ успешно отозван",
|
||||
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Вы уверены, что хотите отозвать API ключ \"{apiKeyName}\"? Это разрушит интеграцию, использующую этот ключ.",
|
||||
"last_used": "Последнее использование",
|
||||
"actions": "Действия",
|
||||
"images_updated_successfully": "Изображения успешно обновлены",
|
||||
"general": "Основное",
|
||||
"enable_email_notifications_to_alert_users_when_a_login_is_detected_from_a_new_device_or_location": "Включить уведомления пользователей по электронной почте при обнаружении логина с нового устройства или локации.",
|
||||
"ldap": "LDAP",
|
||||
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Настроить конфигурацию LDAP для синхронизации пользователей и групп с сервером LDAP.",
|
||||
"images": "Изображения",
|
||||
"update": "Изменить",
|
||||
"email_configuration_updated_successfully": "Конфигурация электронной почты успешно обновлена",
|
||||
"save_changes_question": "Сохранить изменения?",
|
||||
"you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "Вы должны сохранить изменения перед отправкой тестового письма. Сохранить сейчас?",
|
||||
"save_and_send": "Сохранить и отправить",
|
||||
"test_email_sent_successfully": "Тестовое письмо успешно отправлено на ваш адрес электронной почты.",
|
||||
"failed_to_send_test_email": "Не удалось отправить тестовое письмо. Проверьте журналы сервера для получения дополнительной информации.",
|
||||
"smtp_configuration": "Конфигурация SMTP",
|
||||
"smtp_host": "SMTP хост",
|
||||
"smtp_port": "SMTP порт",
|
||||
"smtp_user": "SMTP пользователь",
|
||||
"smtp_password": "SMTP пароль",
|
||||
"smtp_from": "SMTP отравитель",
|
||||
"smtp_tls_option": "SMTP тип TLS",
|
||||
"email_tls_option": "Тип TLS",
|
||||
"skip_certificate_verification": "Пропустить верификацию сертификата",
|
||||
"this_can_be_useful_for_selfsigned_certificates": "Это может быть полезно для самоподписанных сертификатов.",
|
||||
"enabled_emails": "Отправляемые письма",
|
||||
"email_login_notification": "Уведомление о логине по электронной почте",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Отправлять пользователю письмо при входе с нового устройства.",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Позволяет пользователям войти с помощью кода входа, отправленного на их электронную почту. Это значительно снижает безопасность так как любой человек, имеющий доступ к электронной почте пользователя, сможет получить доступ.",
|
||||
"send_test_email": "Отправить тестовое письмо",
|
||||
"application_configuration_updated_successfully": "Конфигурация приложения успешно обновлена",
|
||||
"application_name": "Название приложения",
|
||||
"session_duration": "Длительность сессии",
|
||||
"the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again": "Продолжительность сессии в минутах, прежде чем пользователь должен войти снова.",
|
||||
"enable_self_account_editing": "Включить редактирование собственной учетной записи",
|
||||
"whether_the_users_should_be_able_to_edit_their_own_account_details": "Должны ли пользователи иметь возможность редактировать данные своей учетной записи.",
|
||||
"emails_verified": "Адреса электронной почты подтверждены",
|
||||
"whether_the_users_email_should_be_marked_as_verified_for_the_oidc_clients": "Должен ли адрес электронной почты пользователя быть отмечен как проверенный для OIDC клиентов.",
|
||||
"ldap_configuration_updated_successfully": "Конфигурация LDAP успешно обновлена",
|
||||
"ldap_disabled_successfully": "LDAP успешно отключен",
|
||||
"ldap_sync_finished": "Синхронизация с LDAP завершена",
|
||||
"client_configuration": "Конфигурация клиента",
|
||||
"ldap_url": "URL-адрес LDAP",
|
||||
"ldap_bind_dn": "LDAP DN для аутентификации",
|
||||
"ldap_bind_password": "LDAP пароль для аутентификации",
|
||||
"ldap_base_dn": "Базовый DN LDAP",
|
||||
"user_search_filter": "Фильтр для поиска пользователей",
|
||||
"the_search_filter_to_use_to_search_or_sync_users": "Поисковый фильтр, чтобы найти/синхронизировать пользователей.",
|
||||
"groups_search_filter": "Фильтр для поиска групп",
|
||||
"the_search_filter_to_use_to_search_or_sync_groups": "Поисковый фильтр, чтобы найти/синхронизировать группы.",
|
||||
"attribute_mapping": "Маппинг атрибутов",
|
||||
"user_unique_identifier_attribute": "Атрибут уникального идентификатора пользователя",
|
||||
"the_value_of_this_attribute_should_never_change": "Значение этого атрибута никогда не должно изменяться.",
|
||||
"username_attribute": "Атрибут имени пользователя",
|
||||
"user_mail_attribute": "Атрибут электронной почты пользователя",
|
||||
"user_first_name_attribute": "Атрибут имени пользователя",
|
||||
"user_last_name_attribute": "Атрибут фамилии пользователя",
|
||||
"user_profile_picture_attribute": "Атрибут изображения профиля пользователя",
|
||||
"the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image": "Значением этого атрибута может быть либо URL, либо бинарное изображение в кодировке Base64.",
|
||||
"group_members_attribute": "Атрибут членов группы",
|
||||
"the_attribute_to_use_for_querying_members_of_a_group": "Атрибут, используемый для запроса членов группы.",
|
||||
"group_unique_identifier_attribute": "Атрибут уникального идентификатора группы",
|
||||
"group_name_attribute": "Атрибут имени группы",
|
||||
"admin_group_name": "Имя группы администраторов",
|
||||
"members_of_this_group_will_have_admin_privileges_in_pocketid": "Члены этой группы будут иметь права администратора в Pocket ID.",
|
||||
"disable": "Отключить",
|
||||
"sync_now": "Синхронизировать сейчас",
|
||||
"enable": "Включить",
|
||||
"user_created_successfully": "Пользователь успешно создан",
|
||||
"create_user": "Создать пользователя",
|
||||
"add_a_new_user_to_appname": "Добавить нового пользователя в {appName}",
|
||||
"add_user": "Добавить пользователя",
|
||||
"manage_users": "Управление пользователями",
|
||||
"admin_privileges": "Привилегии администратора",
|
||||
"admins_have_full_access_to_the_admin_panel": "Администраторы имеют полный доступ к панели администратора.",
|
||||
"delete_firstname_lastname": "Удалить {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_delete_this_user": "Вы действительно хотите удалить этого пользователя?",
|
||||
"user_deleted_successfully": "Пользователь успешно удален",
|
||||
"role": "Роль",
|
||||
"source": "Источник",
|
||||
"admin": "Администратор",
|
||||
"user": "Пользователь",
|
||||
"local": "Локальная",
|
||||
"toggle_menu": "Открыть меню",
|
||||
"edit": "Редактировать",
|
||||
"user_groups_updated_successfully": "Группы пользователей успешно обновлены",
|
||||
"user_updated_successfully": "Пользователь успешно обновлен",
|
||||
"custom_claims_updated_successfully": "Пользовательские claims успешно обновлены",
|
||||
"back": "Назад",
|
||||
"user_details_firstname_lastname": "Данные пользователя {firstName} {lastName}",
|
||||
"manage_which_groups_this_user_belongs_to": "Управление группами, к которым принадлежит этот пользователь.",
|
||||
"custom_claims": "Пользовательские claims",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user": "Пользовательские claims — это пары ключ-значение, которые могут использоваться для хранения дополнительной информации о пользователе. Эти пары будут включены в ID Token при запросе scope 'profile'.",
|
||||
"user_group_created_successfully": "Группа пользователей успешно создана",
|
||||
"create_user_group": "Создать группу пользователей",
|
||||
"create_a_new_group_that_can_be_assigned_to_users": "Создайте новую группу, которая может быть назначена пользователям.",
|
||||
"add_group": "Добавить группу",
|
||||
"manage_user_groups": "Управление группами пользователей",
|
||||
"friendly_name": "Удобное имя",
|
||||
"name_that_will_be_displayed_in_the_ui": "Название, которое будет отображаться в интерфейсе",
|
||||
"name_that_will_be_in_the_groups_claim": "Название, которое будет в 'groups' claim",
|
||||
"delete_name": "Удалить {name}",
|
||||
"are_you_sure_you_want_to_delete_this_user_group": "Вы уверены, что хотите удалить эту группу пользователей?",
|
||||
"user_group_deleted_successfully": "Группа пользователей успешно удалена",
|
||||
"user_count": "Число пользователей",
|
||||
"user_group_updated_successfully": "Группа пользователей успешно обновлена",
|
||||
"users_updated_successfully": "Пользователи успешно обновлены",
|
||||
"user_group_details_name": "Группа пользователей {name}",
|
||||
"assign_users_to_this_group": "Назначить пользователей этой группе.",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized": "Пользовательские claims — это пары ключ-значение, которые могут использоваться для хранения дополнительной информации о пользователе. Эти пары будут включены в ID Token при запросе scope 'profile'. Пользовательские claims, определенные для пользователя, в случае конфликта будут приоритизированы.",
|
||||
"oidc_client_created_successfully": "OIDC клиент успешно создан",
|
||||
"create_oidc_client": "Создать OIDC клиент",
|
||||
"add_a_new_oidc_client_to_appname": "Добавить новый OIDC клиент в {appName}.",
|
||||
"add_oidc_client": "Добавить OIDC клиент",
|
||||
"manage_oidc_clients": "Управление OIDC клиентами",
|
||||
"one_time_link": "Одноразовая ссылка",
|
||||
"use_this_link_to_sign_in_once": "Используйте эту ссылку, чтобы войти единожды. Это необходимо для пользователей, которые ещё не добавили passkey или потеряли его.",
|
||||
"add": "Добавить",
|
||||
"callback_urls": "Callback URLs",
|
||||
"logout_callback_urls": "Logout Callback URLs",
|
||||
"public_client": "Публичный клиент",
|
||||
"public_clients_do_not_have_a_client_secret_and_use_pkce_instead": "Публичные клиенты не имеют клиентского секрета и вместо этого используют PKCE. Включите, если ваш клиент является SPA или мобильным приложением.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange — это функция безопасности для предотвращения атак CSRF и перехвата кода авторизации.",
|
||||
"name_logo": "Логотип {name}",
|
||||
"change_logo": "Изменить логотип",
|
||||
"upload_logo": "Загрузить логотип",
|
||||
"remove_logo": "Удалить логотип",
|
||||
"are_you_sure_you_want_to_delete_this_oidc_client": "Вы уверены, что хотите удалить этот OIDC клиент?",
|
||||
"oidc_client_deleted_successfully": "Клиент OIDC успешно удален",
|
||||
"authorization_url": "Authorization URL",
|
||||
"oidc_discovery_url": "OIDC Discovery URL",
|
||||
"token_url": "Token URL",
|
||||
"userinfo_url": "Userinfo URL",
|
||||
"logout_url": "Logout URL",
|
||||
"certificate_url": "Certificate URL",
|
||||
"enabled": "Включен",
|
||||
"disabled": "Выключен",
|
||||
"oidc_client_updated_successfully": "OIDC клиент успешно обновлен",
|
||||
"create_new_client_secret": "Создать новый клиентский секрет",
|
||||
"are_you_sure_you_want_to_create_a_new_client_secret": "Вы уверены, что хотите создать новый клиентский секрет? Старый будет аннулирован.",
|
||||
"generate": "Сгенерировать",
|
||||
"new_client_secret_created_successfully": "Новый клиентский секрет успешно сгенерирован",
|
||||
"allowed_user_groups_updated_successfully": "Разрешенные группы пользователей успешно обновлены",
|
||||
"oidc_client_name": "OIDC клиент {name}",
|
||||
"client_id": "ID клиента",
|
||||
"client_secret": "Клиентский секрет",
|
||||
"show_more_details": "Показать больше деталей",
|
||||
"allowed_user_groups": "Разрешенные группы пользователей",
|
||||
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Добавить группы пользователей к этому клиенту для ограничения доступа пользователей в этих группах. Если группы пользователей не выбраны, все пользователи будут иметь доступ к этому клиенту.",
|
||||
"favicon": "Иконка",
|
||||
"light_mode_logo": "Логотип для светлой темы",
|
||||
"dark_mode_logo": "Логотип для темной темы",
|
||||
"background_image": "Фоновое изображение",
|
||||
"language": "Язык",
|
||||
"reset_profile_picture_question": "Сбросить изображение профиля?",
|
||||
"this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default": "Это удалит загруженное изображение и сбросит изображение профиля на изображение по умолчанию. Вы хотите продолжить?",
|
||||
"reset": "Сбросить",
|
||||
"reset_to_default": "Сбросить по умолчанию",
|
||||
"profile_picture_has_been_reset": "Изображение профиля было сброшено. Обновление может занять несколько минут.",
|
||||
"select_the_language_you_want_to_use": "Выберите язык, который вы хотите использовать. Некоторые языки могут быть переведены не полностью."
|
||||
}
|
||||
296
frontend/package-lock.json
generated
296
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pocket-id-frontend",
|
||||
"version": "0.39.0",
|
||||
"version": "0.43.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pocket-id-frontend",
|
||||
"version": "0.39.0",
|
||||
"version": "0.43.1",
|
||||
"dependencies": {
|
||||
"@simplewebauthn/browser": "^13.1.0",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
@@ -25,6 +25,7 @@
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@inlang/paraglide-js": "^2.0.0",
|
||||
"@internationalized/date": "^3.7.0",
|
||||
"@playwright/test": "^1.50.0",
|
||||
"@sveltejs/adapter-auto": "^4.0.0",
|
||||
@@ -46,7 +47,7 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.21.0",
|
||||
"vite": "^6.2.1"
|
||||
"vite": "^6.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
@@ -747,6 +748,78 @@
|
||||
"url": "https://github.com/sponsors/nzakas"
|
||||
}
|
||||
},
|
||||
"node_modules/@inlang/paraglide-js": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@inlang/paraglide-js/-/paraglide-js-2.0.0.tgz",
|
||||
"integrity": "sha512-ufe/k4tfBIQrJf6X1L+KGtvHYRhvDPX53m7vVe+IOYs0DkyR7RkBgwPBQb3kbXKpr5atCD+D2BDh/I7EpK5Clg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@inlang/recommend-sherlock": "0.2.1",
|
||||
"@inlang/sdk": "2.4.2",
|
||||
"commander": "11.1.0",
|
||||
"consola": "3.4.0",
|
||||
"json5": "2.2.3",
|
||||
"unplugin": "^2.1.2",
|
||||
"urlpattern-polyfill": "^10.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"paraglide-js": "bin/run.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@inlang/paraglide-js/node_modules/@inlang/sdk": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@inlang/sdk/-/sdk-2.4.2.tgz",
|
||||
"integrity": "sha512-EqL32PcFHOlXWEg2o0nftSBZ376tSxuAhV8uTZoaq521AKSRMEvjTpVsJ9eS6ZJDCRiIXx7avtsdVNwkUntf8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lix-js/sdk": "0.4.2",
|
||||
"@sinclair/typebox": "^0.31.17",
|
||||
"kysely": "^0.27.4",
|
||||
"sqlite-wasm-kysely": "0.3.0",
|
||||
"uuid": "^10.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@inlang/paraglide-js/node_modules/@lix-js/sdk": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@lix-js/sdk/-/sdk-0.4.2.tgz",
|
||||
"integrity": "sha512-wrQQMAZzOxQEAssxUnajn7Djua98MlIzs+V6GdX51VN6b7iA3qvZJY4L9xEEMh0nRFvpAO3wOt7uBth9580pog==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@lix-js/server-api-schema": "0.1.1",
|
||||
"dedent": "1.5.1",
|
||||
"human-id": "^4.1.1",
|
||||
"js-sha256": "^0.11.0",
|
||||
"kysely": "^0.27.4",
|
||||
"sqlite-wasm-kysely": "0.3.0",
|
||||
"uuid": "^10.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=21"
|
||||
}
|
||||
},
|
||||
"node_modules/@inlang/paraglide-js/node_modules/@sinclair/typebox": {
|
||||
"version": "0.31.28",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.31.28.tgz",
|
||||
"integrity": "sha512-/s55Jujywdw/Jpan+vsy6JZs1z2ZTGxTmbZTPiuSL2wz9mfzA2gN1zzaqmvfi4pq+uOt7Du85fkiwv5ymW84aQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@inlang/recommend-sherlock": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@inlang/recommend-sherlock/-/recommend-sherlock-0.2.1.tgz",
|
||||
"integrity": "sha512-ckv8HvHy/iTqaVAEKrr+gnl+p3XFNwe5D2+6w6wJk2ORV2XkcRkKOJ/XsTUJbPSiyi4PI+p+T3bqbmNx/rDUlg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"comment-json": "^4.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@internationalized/date": {
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.7.0.tgz",
|
||||
@@ -798,6 +871,13 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@lix-js/server-api-schema": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@lix-js/server-api-schema/-/server-api-schema-0.1.1.tgz",
|
||||
"integrity": "sha512-W1Z7KKOxAQ4Dag9V2wrDevHPh5rPk+icBUsxNfNCNB2tlPrKpba99562vcTCPoT03KXpihEbWutZNujCRtMA+g==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -1214,6 +1294,16 @@
|
||||
"integrity": "sha512-TJ7Al17j3+by5y2QkTLcF/oBVMbgXBhILVgi9PuwpxQVZZvGh5BFRzWbJPmZVNKpbRLjuMzFuRwR+tdFPqCkvA==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@sqlite.org/sqlite-wasm": {
|
||||
"version": "3.48.0-build4",
|
||||
"resolved": "https://registry.npmjs.org/@sqlite.org/sqlite-wasm/-/sqlite-wasm-3.48.0-build4.tgz",
|
||||
"integrity": "sha512-hI6twvUkzOmyGZhQMza1gpfqErZxXRw6JEsiVjUbo7tFanVD+8Oil0Ih3l2nGzHdxPI41zFmfUQG7GHqhciKZQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"sqlite-wasm": "bin/index.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/adapter-auto": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-4.0.0.tgz",
|
||||
@@ -1909,6 +1999,13 @@
|
||||
"@ark/util": "0.38.0"
|
||||
}
|
||||
},
|
||||
"node_modules/array-timsort": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz",
|
||||
"integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
@@ -2099,6 +2196,33 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
|
||||
"integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/comment-json": {
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.5.tgz",
|
||||
"integrity": "sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"array-timsort": "^1.0.3",
|
||||
"core-util-is": "^1.0.3",
|
||||
"esprima": "^4.0.1",
|
||||
"has-own-prop": "^2.0.0",
|
||||
"repeat-string": "^1.6.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/commondir": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
|
||||
@@ -2111,6 +2235,16 @@
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/consola": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/consola/-/consola-3.4.0.tgz",
|
||||
"integrity": "sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||
@@ -2119,6 +2253,13 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -2173,6 +2314,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/dedent": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz",
|
||||
"integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"babel-plugin-macros": "^3.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"babel-plugin-macros": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/deep-is": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||
@@ -2490,6 +2646,20 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/esprima": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
||||
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"bin": {
|
||||
"esparse": "bin/esparse.js",
|
||||
"esvalidate": "bin/esvalidate.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/esquery": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
|
||||
@@ -2814,6 +2984,16 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/has-own-prop": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-own-prop/-/has-own-prop-2.0.0.tgz",
|
||||
"integrity": "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
@@ -2826,6 +3006,16 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/human-id": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/human-id/-/human-id-4.1.1.tgz",
|
||||
"integrity": "sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"human-id": "dist/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
@@ -2964,6 +3154,13 @@
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/js-sha256": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.11.0.tgz",
|
||||
"integrity": "sha512-6xNlKayMZvds9h1Y1VWc0fQHQ82BxTXizWPEtEeGvmOUYpBRy4gbWroHLpzowe6xiQhHpelCQiE7HEdznyBL9Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
@@ -3007,6 +3204,19 @@
|
||||
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"json5": "lib/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@@ -3030,6 +3240,16 @@
|
||||
"integrity": "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/kysely": {
|
||||
"version": "0.27.6",
|
||||
"resolved": "https://registry.npmjs.org/kysely/-/kysely-0.27.6.tgz",
|
||||
"integrity": "sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/levn": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||
@@ -3906,6 +4126,16 @@
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/repeat-string": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
|
||||
"integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.10",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
|
||||
@@ -4094,6 +4324,18 @@
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sqlite-wasm-kysely": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/sqlite-wasm-kysely/-/sqlite-wasm-kysely-0.3.0.tgz",
|
||||
"integrity": "sha512-TzjBNv7KwRw6E3pdKdlRyZiTmUIE0UttT/Sl56MVwVARl/u5gp978KepazCJZewFUnlWHz9i3NQd4kOtP/Afdg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@sqlite.org/sqlite-wasm": "^3.48.0-build2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"kysely": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-json-comments": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||
@@ -4548,6 +4790,20 @@
|
||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/unplugin": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.2.0.tgz",
|
||||
"integrity": "sha512-m1ekpSwuOT5hxkJeZGRxO7gXbXT3gF26NjQ7GdVHoLoF8/nopLcd/QfPigpCy7i51oFHiRJg/CyHhj4vs2+KGw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.14.0",
|
||||
"webpack-virtual-modules": "^0.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uri-js": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
@@ -4557,12 +4813,33 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/urlpattern-polyfill": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz",
|
||||
"integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/valibot": {
|
||||
"version": "1.0.0-beta.11",
|
||||
"resolved": "https://registry.npmjs.org/valibot/-/valibot-1.0.0-beta.11.tgz",
|
||||
@@ -4587,9 +4864,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.1.tgz",
|
||||
"integrity": "sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q==",
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.3.tgz",
|
||||
"integrity": "sha512-IzwM54g4y9JA/xAeBPNaDXiBF8Jsgl3VBQ2YQ/wOY6fyW3xMdSoltIV3Bo59DErdqdE6RxUfv8W69DvUorE4Eg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
@@ -4687,6 +4964,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-virtual-modules": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
|
||||
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pocket-id-frontend",
|
||||
"version": "0.42.1",
|
||||
"version": "0.44.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -30,6 +30,7 @@
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@inlang/paraglide-js": "^2.0.0",
|
||||
"@internationalized/date": "^3.7.0",
|
||||
"@playwright/test": "^1.50.0",
|
||||
"@sveltejs/adapter-auto": "^4.0.0",
|
||||
@@ -51,6 +52,6 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.21.0",
|
||||
"vite": "^6.2.1"
|
||||
"vite": "^6.2.3"
|
||||
}
|
||||
}
|
||||
|
||||
1
frontend/project.inlang/.gitignore
vendored
Normal file
1
frontend/project.inlang/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
cache
|
||||
1
frontend/project.inlang/project_id
Normal file
1
frontend/project.inlang/project_id
Normal file
@@ -0,0 +1 @@
|
||||
O2jvFph6P4Jpehf2BT
|
||||
12
frontend/project.inlang/settings.json
Normal file
12
frontend/project.inlang/settings.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/project-settings",
|
||||
"baseLocale": "en-US",
|
||||
"locales": ["en-US", "nl-NL", "ru-RU", "de-DE", "fr-FR", "cs-CZ"],
|
||||
"modules": [
|
||||
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js",
|
||||
"https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js"
|
||||
],
|
||||
"plugin.inlang.messageFormat": {
|
||||
"pathPattern": "./messages/{locale}.json"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="%lang%">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="/api/application-configuration/favicon" />
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { env } from '$env/dynamic/private';
|
||||
import { ACCESS_TOKEN_COOKIE_NAME } from '$lib/constants';
|
||||
import { paraglideMiddleware } from '$lib/paraglide/server';
|
||||
import type { Handle, HandleServerError } from '@sveltejs/kit';
|
||||
import { sequence } from '@sveltejs/kit/hooks';
|
||||
import { AxiosError } from 'axios';
|
||||
import { decodeJwt } from 'jose';
|
||||
|
||||
@@ -9,7 +11,16 @@ import { decodeJwt } from 'jose';
|
||||
// this is still secure as process will just be undefined in the browser
|
||||
process.env.INTERNAL_BACKEND_URL = env.INTERNAL_BACKEND_URL ?? 'http://localhost:8080';
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
// Handle to use the paraglide middleware
|
||||
const paraglideHandle: Handle = ({ event, resolve }) => {
|
||||
return paraglideMiddleware(event.request, ({ locale }) => {
|
||||
return resolve(event, {
|
||||
transformPageChunk: ({ html }) => html.replace('%lang%', locale)
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const authenticationHandle: Handle = async ({ event, resolve }) => {
|
||||
const { isSignedIn, isAdmin } = verifyJwt(event.cookies.get(ACCESS_TOKEN_COOKIE_NAME));
|
||||
|
||||
const isUnauthenticatedOnlyPath = event.url.pathname.startsWith('/login') || event.url.pathname.startsWith('/lc')
|
||||
@@ -43,6 +54,8 @@ export const handle: Handle = async ({ event, resolve }) => {
|
||||
return response;
|
||||
};
|
||||
|
||||
export const handle: Handle = sequence(paraglideHandle, authenticationHandle);
|
||||
|
||||
export const handleError: HandleServerError = async ({ error, message, status }) => {
|
||||
if (error instanceof AxiosError) {
|
||||
message = error.response?.data.error || message;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import { ChevronDown } from 'lucide-svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
import Button from './ui/button/button.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let {
|
||||
items,
|
||||
@@ -93,7 +94,7 @@
|
||||
'relative z-50 mb-4 max-w-sm',
|
||||
items.data.length == 0 && searchValue == '' && 'hidden'
|
||||
)}
|
||||
placeholder={'Search...'}
|
||||
placeholder={m.search()}
|
||||
type="text"
|
||||
oninput={(e) => onSearch((e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
@@ -102,7 +103,7 @@
|
||||
{#if items.data.length === 0 && searchValue === ''}
|
||||
<div class="my-5 flex flex-col items-center">
|
||||
<Empty class="text-muted-foreground h-20" />
|
||||
<p class="text-muted-foreground mt-3 text-sm">No items found</p>
|
||||
<p class="text-muted-foreground mt-3 text-sm">{m.no_items_found()}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<Table.Root class="min-w-full table-auto overflow-x-auto">
|
||||
@@ -166,7 +167,7 @@
|
||||
|
||||
<div class="mt-5 flex flex-col-reverse items-center justify-between gap-3 sm:flex-row">
|
||||
<div class="flex items-center space-x-2">
|
||||
<p class="text-sm font-medium">Items per page</p>
|
||||
<p class="text-sm font-medium">{m.items_per_page()}</p>
|
||||
<Select.Root
|
||||
selected={{
|
||||
label: items.pagination.itemsPerPage.toString(),
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { slide } from 'svelte/transition';
|
||||
import { Button } from './ui/button';
|
||||
import * as Card from './ui/card';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let {
|
||||
id,
|
||||
@@ -55,7 +56,7 @@
|
||||
<Card.Description>{description}</Card.Description>
|
||||
{/if}
|
||||
</div>
|
||||
<Button class="ml-10 h-8 p-3" variant="ghost" aria-label="Expand card">
|
||||
<Button class="ml-10 h-8 p-3" variant="ghost" aria-label={m.expand_card()}>
|
||||
<LucideChevronDown
|
||||
class={cn(
|
||||
'h-5 w-5 transition-transform duration-200',
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import ConfirmDialog from './confirm-dialog.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
export const confirmDialogStore = writable({
|
||||
open: false,
|
||||
title: '',
|
||||
message: '',
|
||||
confirm: {
|
||||
label: 'Confirm',
|
||||
label: m.confirm(),
|
||||
destructive: false,
|
||||
action: () => {}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { LucideCheck } from 'lucide-svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
@@ -31,9 +32,9 @@
|
||||
<Tooltip.Trigger class="text-start" tabindex={-1} onclick={onClick}>{@render children()}</Tooltip.Trigger>
|
||||
<Tooltip.Content onclick={copyToClipboard}>
|
||||
{#if copied}
|
||||
<span class="flex items-center"><LucideCheck class="mr-1 h-4 w-4" /> Copied</span>
|
||||
<span class="flex items-center"><LucideCheck class="mr-1 h-4 w-4" /> {m.copied()}</span>
|
||||
{:else}
|
||||
<span>Click to copy</span>
|
||||
<span>{m.click_to_copy()}</span>
|
||||
{/if}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { LucideXCircle } from 'lucide-svelte';
|
||||
|
||||
let { message, showButton = true }: { message: string; showButton?: boolean } = $props();
|
||||
@@ -7,9 +8,9 @@
|
||||
|
||||
<div class="mt-[20%] flex flex-col items-center">
|
||||
<LucideXCircle class="h-12 w-12 text-muted-foreground" />
|
||||
<h1 class="mt-3 text-2xl font-semibold">Something went wrong</h1>
|
||||
<h1 class="mt-3 text-2xl font-semibold">{m.something_went_wrong()}</h1>
|
||||
<p class="text-muted-foreground">{message}</p>
|
||||
{#if showButton}
|
||||
<Button size="sm" class="mt-5" href="/">Go back to home</Button>
|
||||
<Button size="sm" class="mt-5" href="/">{m.go_back_to_home()}</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { onMount, type Snippet } from 'svelte';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import AutoCompleteInput from './auto-complete-input.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let {
|
||||
customClaims = $bindable(),
|
||||
@@ -41,15 +42,15 @@
|
||||
{#each customClaims as _, i}
|
||||
<div class="flex gap-x-2">
|
||||
<AutoCompleteInput
|
||||
placeholder="Key"
|
||||
placeholder={m.key()}
|
||||
suggestions={filteredSuggestions}
|
||||
bind:value={customClaims[i].key}
|
||||
/>
|
||||
<Input placeholder="Value" bind:value={customClaims[i].value} />
|
||||
<Input placeholder={m.value()} bind:value={customClaims[i].value} />
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
aria-label="Remove custom claim"
|
||||
aria-label={m.remove_custom_claim()}
|
||||
on:click={() => (customClaims = customClaims.filter((_, index) => index !== i))}
|
||||
>
|
||||
<LucideMinus class="h-4 w-4" />
|
||||
@@ -69,7 +70,7 @@
|
||||
on:click={() => (customClaims = [...customClaims, { key: '', value: '' }])}
|
||||
>
|
||||
<LucidePlus class="mr-1 h-4 w-4" />
|
||||
{customClaims.length === 0 ? 'Add custom claim' : 'Add another'}
|
||||
{customClaims.length === 0 ? m.add_custom_claim() : m.add_another()}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Calendar } from '$lib/components/ui/calendar';
|
||||
import * as Popover from '$lib/components/ui/popover';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { getLocale } from '$lib/paraglide/runtime';
|
||||
import { cn } from '$lib/utils/style';
|
||||
import {
|
||||
CalendarDate,
|
||||
@@ -30,7 +32,7 @@
|
||||
open = false;
|
||||
}
|
||||
|
||||
const df = new DateFormatter('en-US', {
|
||||
const df = new DateFormatter(getLocale(), {
|
||||
dateStyle: 'long'
|
||||
});
|
||||
</script>
|
||||
@@ -44,7 +46,7 @@
|
||||
builders={[builder]}
|
||||
>
|
||||
<CalendarIcon class="mr-2 h-4 w-4" />
|
||||
{date ? df.format(date.toDate(getLocalTimeZone())) : 'Select a date'}
|
||||
{date ? df.format(date.toDate(getLocalTimeZone())) : m.select_a_date()}
|
||||
</Button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content class="w-auto p-0" align="start">
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import type { HTMLInputAttributes } from 'svelte/elements';
|
||||
import type { VariantProps } from 'tailwind-variants';
|
||||
import type { buttonVariants } from '$lib/components/ui/button';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let {
|
||||
id,
|
||||
@@ -21,7 +22,7 @@
|
||||
{#if restProps.children}
|
||||
{@render restProps.children()}
|
||||
{:else}
|
||||
Select File
|
||||
{m.select_file()}
|
||||
{/if}
|
||||
</button>
|
||||
<input {id} {...restProps} type="file" class="hidden" />
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import Button from '$lib/components/ui/button/button.svelte';
|
||||
import { LucideLoader, LucideRefreshCw, LucideUpload } from 'lucide-svelte';
|
||||
import { openConfirmDialog } from '../confirm-dialog';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let {
|
||||
userId,
|
||||
@@ -40,11 +41,10 @@
|
||||
|
||||
function onReset() {
|
||||
openConfirmDialog({
|
||||
title: 'Reset profile picture?',
|
||||
message:
|
||||
'This will remove the uploaded image, and reset the profile picture to default. Do you want to continue?',
|
||||
title: m.reset_profile_picture_question(),
|
||||
message: m.this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default(),
|
||||
confirm: {
|
||||
label: 'Reset',
|
||||
label: m.reset(),
|
||||
action: async () => {
|
||||
isLoading = true;
|
||||
await resetCallback().catch();
|
||||
@@ -58,16 +58,16 @@
|
||||
<div class="flex gap-5">
|
||||
<div class="flex w-full flex-col justify-between gap-5 sm:flex-row">
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold">Profile Picture</h3>
|
||||
<h3 class="text-xl font-semibold">{m.profile_picture()}</h3>
|
||||
{#if isLdapUser}
|
||||
<p class="text-muted-foreground mt-1 text-sm">
|
||||
The profile picture is managed by the LDAP server and cannot be changed here.
|
||||
{m.profile_picture_is_managed_by_ldap_server()}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-muted-foreground mt-1 text-sm">
|
||||
Click on the profile picture to upload a custom one from your files.
|
||||
{m.click_profile_picture_to_upload_custom()}
|
||||
</p>
|
||||
<p class="text-muted-foreground mt-1 text-sm">The image should be in PNG or JPEG format.</p>
|
||||
<p class="text-muted-foreground mt-1 text-sm">{m.image_should_be_in_format()}</p>
|
||||
{/if}
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -77,7 +77,7 @@
|
||||
disabled={isLoading || isLdapUser}
|
||||
>
|
||||
<LucideRefreshCw class="mr-2 h-4 w-4" />
|
||||
Reset to default
|
||||
{m.reset_to_default()}
|
||||
</Button>
|
||||
</div>
|
||||
{#if isLdapUser}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import * as Avatar from '$lib/components/ui/avatar';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import WebAuthnService from '$lib/services/webauthn-service';
|
||||
import userStore from '$lib/stores/user-store';
|
||||
import { LucideLogOut, LucideUser } from 'lucide-svelte';
|
||||
@@ -32,10 +33,10 @@
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item href="/settings/account"
|
||||
><LucideUser class="mr-2 h-4 w-4" /> My Account</DropdownMenu.Item
|
||||
><LucideUser class="mr-2 h-4 w-4" /> {m.my_account()}</DropdownMenu.Item
|
||||
>
|
||||
<DropdownMenu.Item on:click={logout}
|
||||
><LucideLogOut class="mr-2 h-4 w-4" /> Logout</DropdownMenu.Item
|
||||
><LucideLogOut class="mr-2 h-4 w-4" /> {m.logout()}</DropdownMenu.Item
|
||||
>
|
||||
</DropdownMenu.Group>
|
||||
</DropdownMenu.Content>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
import userStore from '$lib/stores/user-store';
|
||||
import Logo from '../logo.svelte';
|
||||
@@ -8,7 +8,7 @@
|
||||
const authUrls = [/^\/authorize$/, /^\/login(?:\/.*)?$/, /^\/logout$/];
|
||||
|
||||
let isAuthPage = $derived(
|
||||
!$page.error && authUrls.some((pattern) => pattern.test($page.url.pathname))
|
||||
!page.error && authUrls.some((pattern) => pattern.test(page.url.pathname))
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -26,8 +26,10 @@
|
||||
</h1>
|
||||
{/if}
|
||||
</div>
|
||||
{#if $userStore?.id}
|
||||
<HeaderAvatar />
|
||||
{/if}
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
{#if $userStore?.id}
|
||||
<HeaderAvatar />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { page } from '$app/state';
|
||||
import type { Snippet } from 'svelte';
|
||||
import * as Card from './ui/card';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let {
|
||||
children,
|
||||
@@ -29,7 +30,7 @@
|
||||
)}`}
|
||||
class="text-muted-foreground text-xs"
|
||||
>
|
||||
Don't have access to your passkey?
|
||||
{m.dont_have_access_to_your_passkey()}
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -38,7 +39,7 @@
|
||||
<img
|
||||
src="/api/application-configuration/background-image"
|
||||
class="h-screen w-[calc(100vw-650px)] rounded-l-[60px] object-cover"
|
||||
alt="Login background"
|
||||
alt={m.login_background()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -60,7 +61,7 @@
|
||||
)}`}
|
||||
class="text-muted-foreground mt-7 flex justify-center text-xs"
|
||||
>
|
||||
Don't have access to your passkey?
|
||||
{m.dont_have_access_to_your_passkey()}
|
||||
</a>
|
||||
{/if}
|
||||
</Card.CardContent>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { mode } from 'mode-watcher';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
@@ -7,4 +8,4 @@
|
||||
const isDarkMode = $derived($mode === 'dark');
|
||||
</script>
|
||||
|
||||
<img {...props} src="/api/application-configuration/logo?light={!isDarkMode}" alt="Logo" />
|
||||
<img {...props} src="/api/application-configuration/logo?light={!isDarkMode}" alt={m.logo()} />
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import Input from '$lib/components/ui/input/input.svelte';
|
||||
import Label from '$lib/components/ui/label/label.svelte';
|
||||
import * as Select from '$lib/components/ui/select/index.js';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import UserService from '$lib/services/user-service';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
|
||||
@@ -17,14 +18,14 @@
|
||||
const userService = new UserService();
|
||||
|
||||
let oneTimeLink: string | null = $state(null);
|
||||
let selectedExpiration: keyof typeof availableExpirations = $state('1 hour');
|
||||
let selectedExpiration: keyof typeof availableExpirations = $state(m.one_hour());
|
||||
|
||||
let availableExpirations = {
|
||||
'1 hour': 60 * 60,
|
||||
'12 hours': 60 * 60 * 12,
|
||||
'1 day': 60 * 60 * 24,
|
||||
'1 week': 60 * 60 * 24 * 7,
|
||||
'1 month': 60 * 60 * 24 * 30
|
||||
[m.one_hour()]: 60 * 60,
|
||||
[m.twelve_hours()]: 60 * 60 * 12,
|
||||
[m.one_day()]: 60 * 60 * 24,
|
||||
[m.one_week()]: 60 * 60 * 24 * 7,
|
||||
[m.one_month()]: 60 * 60 * 24 * 30
|
||||
};
|
||||
|
||||
async function createOneTimeAccessToken() {
|
||||
@@ -48,14 +49,14 @@
|
||||
<Dialog.Root open={!!userId} {onOpenChange}>
|
||||
<Dialog.Content class="max-w-md">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Login Code</Dialog.Title>
|
||||
<Dialog.Title>{m.login_code()}</Dialog.Title>
|
||||
<Dialog.Description
|
||||
>Create a login code that the user can use to sign in without a passkey once.</Dialog.Description
|
||||
>{m.create_a_login_code_to_sign_in_without_a_passkey_once()}</Dialog.Description
|
||||
>
|
||||
</Dialog.Header>
|
||||
{#if oneTimeLink === null}
|
||||
<div>
|
||||
<Label for="expiration">Expiration</Label>
|
||||
<Label for="expiration">{m.expiration()}</Label>
|
||||
<Select.Root
|
||||
selected={{
|
||||
label: Object.keys(availableExpirations)[0],
|
||||
@@ -75,10 +76,10 @@
|
||||
</Select.Root>
|
||||
</div>
|
||||
<Button onclick={() => createOneTimeAccessToken()} disabled={!selectedExpiration}>
|
||||
Generate Code
|
||||
{m.generate_code()}
|
||||
</Button>
|
||||
{:else}
|
||||
<Label for="login-code" class="sr-only">Login Code</Label>
|
||||
<Label for="login-code" class="sr-only">{m.login_code()}</Label>
|
||||
<Input id="login-code" value={oneTimeLink} readonly />
|
||||
{/if}
|
||||
</Dialog.Content>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import AdvancedTable from '$lib/components/advanced-table.svelte';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import UserGroupService from '$lib/services/user-group-service';
|
||||
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
import type { UserGroup } from '$lib/types/user-group.type';
|
||||
@@ -34,7 +35,7 @@
|
||||
items={groups}
|
||||
{requestOptions}
|
||||
onRefresh={async (o) => (groups = await userGroupService.list(o))}
|
||||
columns={[{ label: 'Name', sortColumn: 'friendlyName' }]}
|
||||
columns={[{ label: m.name(), sortColumn: 'friendlyName' }]}
|
||||
bind:selectedIds={selectedGroupIds}
|
||||
{selectionDisabled}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import Logo from './logo.svelte';
|
||||
</script>
|
||||
|
||||
@@ -6,8 +7,8 @@
|
||||
<div class="bg-muted mx-auto rounded-2xl p-3">
|
||||
<Logo class="h-10 w-10" />
|
||||
</div>
|
||||
<p class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">Browser unsupported</p>
|
||||
<p class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">{m.browser_unsupported()}</p>
|
||||
<p class="text-muted-foreground mt-3">
|
||||
This browser doesn't support passkeys. Please or use a alternative sign in method.
|
||||
{m.this_browser_does_not_support_passkeys()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { setLocale } from '$lib/paraglide/runtime';
|
||||
import type { User } from '$lib/types/user.type';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
const userStore = writable<User | null>(null);
|
||||
|
||||
const setUser = (user: User) => {
|
||||
if (user.locale) {
|
||||
setLocale(user.locale, { reload: false });
|
||||
}
|
||||
userStore.set(user);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Locale } from '$lib/paraglide/runtime';
|
||||
import type { CustomClaim } from './custom-claim.type';
|
||||
import type { UserGroup } from './user-group.type';
|
||||
|
||||
@@ -10,6 +11,7 @@ export type User = {
|
||||
isAdmin: boolean;
|
||||
userGroups: UserGroup[];
|
||||
customClaims: CustomClaim[];
|
||||
locale?: Locale;
|
||||
ldapId?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { WebAuthnError } from '@simplewebauthn/browser';
|
||||
import { AxiosError } from 'axios';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
export function getAxiosErrorMessage(
|
||||
e: unknown,
|
||||
defaultMessage: string = 'An unknown error occurred'
|
||||
defaultMessage: string = m.an_unknown_error_occurred()
|
||||
) {
|
||||
let message = defaultMessage;
|
||||
if (e instanceof AxiosError) {
|
||||
@@ -13,29 +14,29 @@ export function getAxiosErrorMessage(
|
||||
return message;
|
||||
}
|
||||
|
||||
export function axiosErrorToast(e: unknown, defaultMessage: string = 'An unknown error occurred') {
|
||||
export function axiosErrorToast(e: unknown, defaultMessage: string = m.an_unknown_error_occurred()) {
|
||||
const message = getAxiosErrorMessage(e, defaultMessage);
|
||||
toast.error(message);
|
||||
}
|
||||
|
||||
export function getWebauthnErrorMessage(e: unknown) {
|
||||
const errors = {
|
||||
ERROR_CEREMONY_ABORTED: 'The authentication process was aborted',
|
||||
ERROR_AUTHENTICATOR_GENERAL_ERROR: 'An error occurred with the authenticator',
|
||||
ERROR_CEREMONY_ABORTED: m.authentication_process_was_aborted(),
|
||||
ERROR_AUTHENTICATOR_GENERAL_ERROR: m.error_occurred_with_authenticator(),
|
||||
ERROR_AUTHENTICATOR_MISSING_DISCOVERABLE_CREDENTIAL_SUPPORT:
|
||||
'The authenticator does not support discoverable credentials',
|
||||
m.authenticator_does_not_support_discoverable_credentials(),
|
||||
ERROR_AUTHENTICATOR_MISSING_RESIDENT_KEY_SUPPORT:
|
||||
'The authenticator does not support resident keys',
|
||||
ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED: 'This passkey was previously registered',
|
||||
m.authenticator_does_not_support_resident_keys(),
|
||||
ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED: m.passkey_was_previously_registered(),
|
||||
ERROR_AUTHENTICATOR_NO_SUPPORTED_PUBKEYCREDPARAMS_ALG:
|
||||
'The authenticator does not support any of the requested algorithms'
|
||||
m.authenticator_does_not_support_any_of_the_requested_algorithms()
|
||||
};
|
||||
|
||||
let message = 'An unknown error occurred';
|
||||
let message = m.an_unknown_error_occurred();
|
||||
if (e instanceof WebAuthnError && e.code in errors) {
|
||||
message = errors[e.code as keyof typeof errors];
|
||||
} else if (e instanceof WebAuthnError && e?.message.includes('timed out')) {
|
||||
message = 'The authenticator timed out';
|
||||
message = m.authenticator_timed_out();
|
||||
} else if (e instanceof AxiosError && e.response?.data.error) {
|
||||
message = e.response?.data.error;
|
||||
} else {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import Error from '$lib/components/error.svelte';
|
||||
import Header from '$lib/components/header/header.svelte';
|
||||
import { Toaster } from '$lib/components/ui/sonner';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
import userStore from '$lib/stores/user-store';
|
||||
import { ModeWatcher } from 'mode-watcher';
|
||||
@@ -30,10 +31,7 @@
|
||||
</script>
|
||||
|
||||
{#if !appConfig}
|
||||
<Error
|
||||
message="A critical error occurred. Please contact your administrator."
|
||||
showButton={false}
|
||||
/>
|
||||
<Error message={m.critical_error_occurred_contact_administrator()} showButton={false} />
|
||||
{:else}
|
||||
<Header />
|
||||
{@render children()}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import type { PageData } from './$types';
|
||||
import ClientProviderImages from './components/client-provider-images.svelte';
|
||||
import ScopeItem from './components/scope-item.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
const webauthnService = new WebAuthnService();
|
||||
const oidService = new OidcService();
|
||||
@@ -77,15 +78,15 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Sign in to {client.name}</title>
|
||||
<title>{m.sign_in_to({name: client.name})}</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if client == null}
|
||||
<p>Client not found</p>
|
||||
<p>{m.client_not_found()}</p>
|
||||
{:else}
|
||||
<SignInWrapper showAlternativeSignInMethodButton>
|
||||
<ClientProviderImages {client} {success} error={!!errorMessage} />
|
||||
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">Sign in to {client.name}</h1>
|
||||
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">{m.sign_in_to({name: client.name})}</h1>
|
||||
{#if errorMessage}
|
||||
<p class="text-muted-foreground mb-10 mt-2">
|
||||
{errorMessage}.
|
||||
@@ -93,34 +94,36 @@
|
||||
{/if}
|
||||
{#if !authorizationRequired && !errorMessage}
|
||||
<p class="text-muted-foreground mb-10 mt-2">
|
||||
Do you want to sign in to <b>{client.name}</b> with your
|
||||
<b>{$appConfigStore.appName}</b> account?
|
||||
{@html m.do_you_want_to_sign_in_to_client_with_your_app_name_account({
|
||||
client: client.name,
|
||||
appName: $appConfigStore.appName
|
||||
})}
|
||||
</p>
|
||||
{:else if authorizationRequired}
|
||||
<div transition:slide={{ duration: 300 }}>
|
||||
<Card.Root class="mb-10 mt-6">
|
||||
<Card.Header class="pb-5">
|
||||
<p class="text-muted-foreground text-start">
|
||||
<b>{client.name}</b> wants to access the following information:
|
||||
{@html m.client_wants_to_access_the_following_information({ client: client.name })}
|
||||
</p>
|
||||
</Card.Header>
|
||||
<Card.Content data-testid="scopes">
|
||||
<div class="flex flex-col gap-3">
|
||||
{#if scope!.includes('email')}
|
||||
<ScopeItem icon={LucideMail} name="Email" description="View your email address" />
|
||||
<ScopeItem icon={LucideMail} name={m.email()} description={m.view_your_email_address()} />
|
||||
{/if}
|
||||
{#if scope!.includes('profile')}
|
||||
<ScopeItem
|
||||
icon={LucideUser}
|
||||
name="Profile"
|
||||
description="View your profile information"
|
||||
name={m.profile()}
|
||||
description={m.view_your_profile_information()}
|
||||
/>
|
||||
{/if}
|
||||
{#if scope!.includes('groups')}
|
||||
<ScopeItem
|
||||
icon={LucideUsers}
|
||||
name="Groups"
|
||||
description="View the groups you are a member of"
|
||||
name={m.groups()}
|
||||
description={m.view_the_groups_you_are_a_member_of()}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -129,11 +132,11 @@
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex w-full justify-stretch gap-2">
|
||||
<Button onclick={() => history.back()} class="w-full" variant="secondary">Cancel</Button>
|
||||
<Button onclick={() => history.back()} class="w-full" variant="secondary">{m.cancel()}</Button>
|
||||
{#if !errorMessage}
|
||||
<Button class="w-full" {isLoading} on:click={authorize}>Sign in</Button>
|
||||
<Button class="w-full" {isLoading} on:click={authorize}>{m.sign_in()}</Button>
|
||||
{:else}
|
||||
<Button class="w-full" on:click={() => (errorMessage = null)}>Try again</Button>
|
||||
<Button class="w-full" on:click={() => (errorMessage = null)}>{m.try_again()}</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</SignInWrapper>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import CheckmarkAnimated from '$lib/icons/checkmark-animated.svelte';
|
||||
import ConnectArrow from '$lib/icons/connect-arrow.svelte';
|
||||
import CrossAnimated from '$lib/icons/cross-animated.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import type { OidcClientMetaData } from '$lib/types/oidc.type';
|
||||
|
||||
const {
|
||||
@@ -61,7 +62,7 @@
|
||||
class="h-10 w-10"
|
||||
src="/api/oidc/clients/{client.id}/logo"
|
||||
draggable={false}
|
||||
alt="Client Logo"
|
||||
alt={m.client_logo()}
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex h-10 w-10 items-center justify-center text-3xl font-bold">
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import { startAuthentication } from '@simplewebauthn/browser';
|
||||
import { fade } from 'svelte/transition';
|
||||
import LoginLogoErrorSuccessIndicator from './components/login-logo-error-success-indicator.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
const webauthnService = new WebAuthnService();
|
||||
|
||||
let isLoading = $state(false);
|
||||
@@ -32,7 +33,7 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Sign In</title>
|
||||
<title>{m.sign_in()}</title>
|
||||
</svelte:head>
|
||||
|
||||
<SignInWrapper showAlternativeSignInMethodButton>
|
||||
@@ -40,18 +41,18 @@
|
||||
<LoginLogoErrorSuccessIndicator error={!!error} />
|
||||
</div>
|
||||
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">
|
||||
Sign in to {$appConfigStore.appName}
|
||||
{m.sign_in_to_appname({ appName: $appConfigStore.appName})}
|
||||
</h1>
|
||||
{#if error}
|
||||
<p class="text-muted-foreground mt-2" in:fade>
|
||||
{error}. Please try to sign in again.
|
||||
{error}. {m.please_try_to_sign_in_again()}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-muted-foreground mt-2" in:fade>
|
||||
Authenticate yourself with your passkey to access the admin panel.
|
||||
{m.authenticate_yourself_with_your_passkey_to_access_the_admin_panel()}
|
||||
</p>
|
||||
{/if}
|
||||
<Button class="mt-10" {isLoading} on:click={authenticate}
|
||||
>{error ? 'Try again' : 'Authenticate'}</Button
|
||||
>{error ? m.try_again() : m.authenticate()}</Button
|
||||
>
|
||||
</SignInWrapper>
|
||||
|
||||
@@ -4,14 +4,15 @@
|
||||
import Logo from '$lib/components/logo.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
import { LucideChevronRight, LucideMail, LucideRectangleEllipsis } from 'lucide-svelte';
|
||||
|
||||
const methods = [
|
||||
{
|
||||
icon: LucideRectangleEllipsis,
|
||||
title: 'Login Code',
|
||||
description: 'Enter a login code to sign in.',
|
||||
title: m.login_code(),
|
||||
description: m.enter_a_login_code_to_sign_in(),
|
||||
href: '/login/alternative/code'
|
||||
}
|
||||
];
|
||||
@@ -19,15 +20,15 @@
|
||||
if ($appConfigStore.emailOneTimeAccessEnabled) {
|
||||
methods.push({
|
||||
icon: LucideMail,
|
||||
title: 'Email Login',
|
||||
description: 'Request a login code via email.',
|
||||
title: m.email_login(),
|
||||
description: m.request_a_login_code_via_email(),
|
||||
href: '/login/alternative/email'
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Sign In</title>
|
||||
<title>{m.sign_in()}</title>
|
||||
</svelte:head>
|
||||
|
||||
<SignInWrapper>
|
||||
@@ -35,9 +36,9 @@
|
||||
<div class="bg-muted mx-auto rounded-2xl p-3">
|
||||
<Logo class="h-10 w-10" />
|
||||
</div>
|
||||
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">Alternative Sign In</h1>
|
||||
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">{m.alternative_sign_in()}</h1>
|
||||
<p class="text-muted-foreground mt-3">
|
||||
If you dont't have access to your passkey, you can sign in using one of the following methods.
|
||||
{m.if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods()}
|
||||
</p>
|
||||
<div class="mt-5 flex flex-col gap-3">
|
||||
{#each methods as method}
|
||||
@@ -59,7 +60,7 @@
|
||||
</div>
|
||||
|
||||
<a class="text-muted-foreground mt-5 text-xs" href={'/login' + page.url.search}
|
||||
>Use your passkey instead?</a
|
||||
>{m.use_your_passkey_instead()}</a
|
||||
>
|
||||
</div>
|
||||
</SignInWrapper>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import LoginLogoErrorSuccessIndicator from '../../components/login-logo-error-success-indicator.svelte';
|
||||
import { page } from '$app/state';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let { data } = $props();
|
||||
let code = $state(data.code ?? '');
|
||||
@@ -26,7 +27,7 @@
|
||||
try {
|
||||
goto(data.redirect);
|
||||
} catch (e) {
|
||||
error = 'Invalid redirect URL';
|
||||
error = m.invalid_redirect_url();
|
||||
}
|
||||
} catch (e) {
|
||||
error = getAxiosErrorMessage(e);
|
||||
@@ -43,20 +44,20 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Login Code</title>
|
||||
<title>{m.login_code()}</title>
|
||||
</svelte:head>
|
||||
|
||||
<SignInWrapper>
|
||||
<div class="flex justify-center">
|
||||
<LoginLogoErrorSuccessIndicator error={!!error} />
|
||||
</div>
|
||||
<h1 class="font-playfair mt-5 text-4xl font-bold">Login Code</h1>
|
||||
<h1 class="font-playfair mt-5 text-4xl font-bold">{m.login_code()}</h1>
|
||||
{#if error}
|
||||
<p class="text-muted-foreground mt-2">
|
||||
{error}. Please try again.
|
||||
{error}. {m.please_try_again()}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-muted-foreground mt-2">Enter the code you received to sign in.</p>
|
||||
<p class="text-muted-foreground mt-2">{m.enter_the_code_you_received_to_sign_in()}</p>
|
||||
{/if}
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
@@ -65,10 +66,10 @@
|
||||
}}
|
||||
class="w-full max-w-[450px]"
|
||||
>
|
||||
<Input id="Email" class="mt-7" placeholder="Code" bind:value={code} type="text" />
|
||||
<Input id="Email" class="mt-7" placeholder={m.code()} bind:value={code} type="text" />
|
||||
<div class="mt-8 flex justify-stretch gap-2">
|
||||
<Button variant="secondary" class="w-full" href={"/login/alternative" + page.url.search}>Go back</Button>
|
||||
<Button class="w-full" type="submit" {isLoading}>Submit</Button>
|
||||
<Button variant="secondary" class="w-full" href={"/login/alternative" + page.url.search}>{m.go_back()}</Button>
|
||||
<Button class="w-full" type="submit" {isLoading}>{m.submit()}</Button>
|
||||
</div>
|
||||
</form>
|
||||
</SignInWrapper>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import UserService from '$lib/services/user-service';
|
||||
import { fade } from 'svelte/transition';
|
||||
import LoginLogoErrorSuccessIndicator from '../../components/login-logo-error-success-indicator.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
const { data } = $props();
|
||||
|
||||
@@ -21,38 +22,38 @@
|
||||
await userService
|
||||
.requestOneTimeAccessEmail(email, data.redirect)
|
||||
.then(() => (success = true))
|
||||
.catch((e) => (error = e.response?.data.error || 'An unknown error occurred'));
|
||||
.catch((e) => (error = e.response?.data.error || m.an_unknown_error_occurred()));
|
||||
|
||||
isLoading = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Email Login</title>
|
||||
<title>{m.email_login()}</title>
|
||||
</svelte:head>
|
||||
|
||||
<SignInWrapper>
|
||||
<div class="flex justify-center">
|
||||
<LoginLogoErrorSuccessIndicator {success} error={!!error} />
|
||||
</div>
|
||||
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">Email Login</h1>
|
||||
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">{m.email_login()}</h1>
|
||||
{#if error}
|
||||
<p class="text-muted-foreground mt-2" in:fade>
|
||||
{error}. Please try again.
|
||||
{error}. {m.please_try_again()}
|
||||
</p>
|
||||
<div class="mt-10 flex w-full justify-stretch gap-2">
|
||||
<Button variant="secondary" class="w-full" href="/">Go back</Button>
|
||||
<Button class="w-full" onclick={() => (error = undefined)}>Try again</Button>
|
||||
<Button variant="secondary" class="w-full" href="/">{m.go_back()}</Button>
|
||||
<Button class="w-full" onclick={() => (error = undefined)}>{m.try_again()}</Button>
|
||||
</div>
|
||||
{:else if success}
|
||||
<p class="text-muted-foreground mt-2" in:fade>
|
||||
An email has been sent to the provided email, if it exists in the system.
|
||||
{m.an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system()}
|
||||
</p>
|
||||
<div class="mt-8 flex w-full justify-stretch gap-2">
|
||||
<Button variant="secondary" class="w-full" href={'/login/alternative' + page.url.search}
|
||||
>Go back</Button
|
||||
>{m.go_back()}</Button
|
||||
>
|
||||
<Button class="w-full" href={'/login/alternative/code' + page.url.search}>Enter code</Button>
|
||||
<Button class="w-full" href={'/login/alternative/code' + page.url.search}>{m.enter_code()}</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<form
|
||||
@@ -63,14 +64,14 @@
|
||||
class="w-full max-w-[450px]"
|
||||
>
|
||||
<p class="text-muted-foreground mt-2" in:fade>
|
||||
Enter your email address to receive an email with a login code.
|
||||
{m.enter_your_email_address_to_receive_an_email_with_a_login_code()}
|
||||
</p>
|
||||
<Input id="Email" class="mt-7" placeholder="Your email" bind:value={email} />
|
||||
<Input id="Email" class="mt-7" placeholder={m.your_email()} bind:value={email} />
|
||||
<div class="mt-8 flex justify-stretch gap-2">
|
||||
<Button variant="secondary" class="w-full" href={'/login/alternative' + page.url.search}
|
||||
>Go back</Button
|
||||
>{m.go_back()}</Button
|
||||
>
|
||||
<Button class="w-full" type="submit" {isLoading}>Submit</Button>
|
||||
<Button class="w-full" type="submit" {isLoading}>{m.submit()}</Button>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import SignInWrapper from '$lib/components/login-wrapper.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import UserService from '$lib/services/user-service';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store.js';
|
||||
import userStore from '$lib/stores/user-store.js';
|
||||
@@ -33,18 +34,16 @@
|
||||
<LoginLogoErrorSuccessIndicator error={!!error} />
|
||||
</div>
|
||||
<h1 class="font-playfair mt-5 text-4xl font-bold">
|
||||
{`${$appConfigStore.appName} Setup`}
|
||||
{m.appname_setup({ appName: $appConfigStore.appName })}
|
||||
</h1>
|
||||
{#if error}
|
||||
<p class="text-muted-foreground mt-2">
|
||||
{error}. Please try again.
|
||||
{error}. {m.please_try_again()}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-muted-foreground mt-2">
|
||||
You're about to sign in to the initial admin account. Anyone with this link can access the
|
||||
account until a passkey is added. Please set up a passkey as soon as possible to prevent
|
||||
unauthorized access.
|
||||
{m.you_are_about_to_sign_in_to_the_initial_admin_account()}
|
||||
</p>
|
||||
<Button class="mt-5" {isLoading} on:click={authenticate}>Continue</Button>
|
||||
<Button class="mt-5" {isLoading} on:click={authenticate}>{m.continue()}</Button>
|
||||
{/if}
|
||||
</SignInWrapper>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import SignInWrapper from '$lib/components/login-wrapper.svelte';
|
||||
import Logo from '$lib/components/logo.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import WebAuthnService from '$lib/services/webauthn-service';
|
||||
import userStore from '$lib/stores/user-store.js';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util.js';
|
||||
@@ -22,7 +23,7 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Logout</title>
|
||||
<title>{m.logout()}</title>
|
||||
</svelte:head>
|
||||
|
||||
<SignInWrapper>
|
||||
@@ -31,13 +32,13 @@
|
||||
<Logo class="h-10 w-10" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="font-playfair mt-5 text-4xl font-bold">Sign out</h1>
|
||||
<h1 class="font-playfair mt-5 text-4xl font-bold">{m.sign_out()}</h1>
|
||||
|
||||
<p class="text-muted-foreground mt-2">
|
||||
Do you want to sign out of Pocket ID with the account <b>{$userStore?.username}</b>?
|
||||
{@html m.do_you_want_to_sign_out_of_pocketid_with_the_account({ username: $userStore?.username ?? '' })}
|
||||
</p>
|
||||
<div class="mt-10 flex w-full justify-stretch gap-2">
|
||||
<Button class="w-full" variant="secondary" onclick={() => history.back()}>Cancel</Button>
|
||||
<Button class="w-full" {isLoading} onclick={signOut}>Sign out</Button>
|
||||
<Button class="w-full" variant="secondary" onclick={() => history.back()}>{m.cancel()}</Button>
|
||||
<Button class="w-full" {isLoading} onclick={signOut}>{m.sign_out()}</Button>
|
||||
</div>
|
||||
</SignInWrapper>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { LucideExternalLink } from 'lucide-svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { LayoutData } from './$types';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let {
|
||||
children,
|
||||
@@ -16,19 +17,19 @@
|
||||
const { versionInformation } = data;
|
||||
|
||||
let links = $state([
|
||||
{ href: '/settings/account', label: 'My Account' },
|
||||
{ href: '/settings/audit-log', label: 'Audit Log' }
|
||||
{ href: '/settings/account', label: m.my_account() },
|
||||
{ href: '/settings/audit-log', label: m.audit_log() }
|
||||
]);
|
||||
|
||||
if ($userStore?.isAdmin) {
|
||||
links = [
|
||||
// svelte-ignore state_referenced_locally
|
||||
...links,
|
||||
{ href: '/settings/admin/users', label: 'Users' },
|
||||
{ href: '/settings/admin/user-groups', label: 'User Groups' },
|
||||
{ href: '/settings/admin/oidc-clients', label: 'OIDC Clients' },
|
||||
{ href: '/settings/admin/api-keys', label: 'API Keys' },
|
||||
{ href: '/settings/admin/application-configuration', label: 'Application Configuration' }
|
||||
{ href: '/settings/admin/users', label: m.users() },
|
||||
{ href: '/settings/admin/user-groups', label: m.user_groups() },
|
||||
{ href: '/settings/admin/oidc-clients', label: m.oidc_clients() },
|
||||
{ href: '/settings/admin/api-keys', label: m.api_keys() },
|
||||
{ href: '/settings/admin/application-configuration', label: m.application_configuration() }
|
||||
];
|
||||
}
|
||||
</script>
|
||||
@@ -40,7 +41,7 @@
|
||||
>
|
||||
<div class="min-w-[200px] xl:min-w-[250px]">
|
||||
<div class="mx-auto grid w-full gap-2">
|
||||
<h1 class="mb-5 text-3xl font-semibold">Settings</h1>
|
||||
<h1 class="mb-5 text-3xl font-semibold">{m.settings()}</h1>
|
||||
</div>
|
||||
<nav class="text-muted-foreground grid gap-4 text-sm">
|
||||
{#each links as { href, label }}
|
||||
@@ -54,7 +55,7 @@
|
||||
target="_blank"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
Update Pocket ID <LucideExternalLink class="my-auto inline-block h-3 w-3" />
|
||||
{m.update_pocket_id()} <LucideExternalLink class="my-auto inline-block h-3 w-3" />
|
||||
</a>
|
||||
{/if}
|
||||
</nav>
|
||||
@@ -65,7 +66,7 @@
|
||||
</main>
|
||||
<div class="flex flex-col items-center">
|
||||
<p class="text-muted-foreground py-3 text-xs">
|
||||
Powered by <a
|
||||
{m.powered_by()} <a
|
||||
class="text-foreground"
|
||||
href="https://github.com/pocket-id/pocket-id"
|
||||
target="_blank">Pocket ID</a
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import * as Alert from '$lib/components/ui/alert';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import UserService from '$lib/services/user-service';
|
||||
import WebAuthnService from '$lib/services/webauthn-service';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
@@ -13,6 +14,7 @@
|
||||
import { toast } from 'svelte-sonner';
|
||||
import ProfilePictureSettings from '../../../lib/components/form/profile-picture-settings.svelte';
|
||||
import AccountForm from './account-form.svelte';
|
||||
import LocalePicker from './locale-picker.svelte';
|
||||
import LoginCodeModal from './login-code-modal.svelte';
|
||||
import PasskeyList from './passkey-list.svelte';
|
||||
import RenamePasskeyModal from './rename-passkey-modal.svelte';
|
||||
@@ -39,7 +41,7 @@
|
||||
let success = true;
|
||||
await userService
|
||||
.updateCurrent(user)
|
||||
.then(() => toast.success('Account details updated successfully'))
|
||||
.then(() => toast.success(m.account_details_updated_successfully()))
|
||||
.catch((e) => {
|
||||
axiosErrorToast(e);
|
||||
success = false;
|
||||
@@ -51,9 +53,7 @@
|
||||
async function updateProfilePicture(image: File) {
|
||||
await userService
|
||||
.updateCurrentUsersProfilePicture(image)
|
||||
.then(() =>
|
||||
toast.success('Profile picture updated successfully. It may take a few minutes to update.')
|
||||
)
|
||||
.then(() => toast.success(m.profile_picture_updated_successfully()))
|
||||
.catch(axiosErrorToast);
|
||||
}
|
||||
|
||||
@@ -72,24 +72,22 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Account Settings</title>
|
||||
<title>{m.account_settings()}</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if passkeys.length == 0}
|
||||
<Alert.Root variant="warning">
|
||||
<LucideAlertTriangle class="size-4" />
|
||||
<Alert.Title>Passkey missing</Alert.Title>
|
||||
<Alert.Title>{m.passkey_missing()}</Alert.Title>
|
||||
<Alert.Description
|
||||
>Please add a passkey to prevent losing access to your account.</Alert.Description
|
||||
>{m.please_provide_a_passkey_to_prevent_losing_access_to_your_account()}</Alert.Description
|
||||
>
|
||||
</Alert.Root>
|
||||
{:else if passkeys.length == 1}
|
||||
<Alert.Root variant="warning" dismissibleId="single-passkey">
|
||||
<LucideAlertTriangle class="size-4" />
|
||||
<Alert.Title>Single Passkey Configured</Alert.Title>
|
||||
<Alert.Description
|
||||
>It is recommended to add more than one passkey to avoid losing access to your account.</Alert.Description
|
||||
>
|
||||
<Alert.Title>{m.single_passkey_configured()}</Alert.Title>
|
||||
<Alert.Description>{m.it_is_recommended_to_add_more_than_one_passkey()}</Alert.Description>
|
||||
</Alert.Root>
|
||||
{/if}
|
||||
|
||||
@@ -99,7 +97,7 @@
|
||||
>
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Account Details</Card.Title>
|
||||
<Card.Title>{m.account_details()}</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<AccountForm {account} callback={updateAccount} />
|
||||
@@ -122,12 +120,12 @@
|
||||
<Card.Header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title>Passkeys</Card.Title>
|
||||
<Card.Title>{m.passkeys()}</Card.Title>
|
||||
<Card.Description class="mt-1">
|
||||
Manage your passkeys that you can use to authenticate yourself.
|
||||
{m.manage_your_passkeys_that_you_can_use_to_authenticate_yourself()}
|
||||
</Card.Description>
|
||||
</div>
|
||||
<Button size="sm" class="ml-3" on:click={createPasskey}>Add Passkey</Button>
|
||||
<Button size="sm" class="ml-3" on:click={createPasskey}>{m.add_passkey()}</Button>
|
||||
</div>
|
||||
</Card.Header>
|
||||
{#if passkeys.length != 0}
|
||||
@@ -141,12 +139,28 @@
|
||||
<Card.Header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title>Login Code</Card.Title>
|
||||
<Card.Title>{m.login_code()}</Card.Title>
|
||||
<Card.Description class="mt-1">
|
||||
Create a one-time login code to sign in from a different device without a passkey.
|
||||
{m.create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey()}
|
||||
</Card.Description>
|
||||
</div>
|
||||
<Button size="sm" class="ml-auto" on:click={() => (showLoginCodeModal = true)}>Create</Button>
|
||||
<Button size="sm" class="ml-auto" on:click={() => (showLoginCodeModal = true)}
|
||||
>{m.create()}</Button
|
||||
>
|
||||
</div>
|
||||
</Card.Header>
|
||||
</Card.Root>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title>{m.language()}</Card.Title>
|
||||
<Card.Description class="mt-1">
|
||||
{m.select_the_language_you_want_to_use()}
|
||||
</Card.Description>
|
||||
</div>
|
||||
<LocalePicker />
|
||||
</div>
|
||||
</Card.Header>
|
||||
</Card.Root>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import FormInput from '$lib/components/form/form-input.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import type { UserCreate } from '$lib/types/user.type';
|
||||
import { createForm } from '$lib/utils/form-util';
|
||||
import { z } from 'zod';
|
||||
@@ -24,7 +25,7 @@
|
||||
.max(30)
|
||||
.regex(
|
||||
/^[a-z0-9_@.-]+$/,
|
||||
"Username can only contain lowercase letters, numbers, underscores, dots, hyphens, and '@' symbols"
|
||||
m.username_can_only_contain()
|
||||
),
|
||||
email: z.string().email(),
|
||||
isAdmin: z.boolean()
|
||||
@@ -36,7 +37,7 @@
|
||||
const data = form.validate();
|
||||
if (!data) return;
|
||||
isLoading = true;
|
||||
const success = await callback(data);
|
||||
await callback(data);
|
||||
// Reset form if user was successfully created
|
||||
isLoading = false;
|
||||
}
|
||||
@@ -45,21 +46,21 @@
|
||||
<form onsubmit={onSubmit}>
|
||||
<div class="flex flex-col gap-3 sm:flex-row">
|
||||
<div class="w-full">
|
||||
<FormInput label="First name" bind:input={$inputs.firstName} />
|
||||
<FormInput label={m.first_name()} bind:input={$inputs.firstName} />
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<FormInput label="Last name" bind:input={$inputs.lastName} />
|
||||
<FormInput label={m.last_name()} bind:input={$inputs.lastName} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 flex flex-col gap-3 sm:flex-row">
|
||||
<div class="w-full">
|
||||
<FormInput label="Email" bind:input={$inputs.email} />
|
||||
<FormInput label={m.email()} bind:input={$inputs.email} />
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<FormInput label="Username" bind:input={$inputs.username} />
|
||||
<FormInput label={m.username()} bind:input={$inputs.username} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 flex justify-end">
|
||||
<Button {isLoading} type="submit">Save</Button>
|
||||
<Button {isLoading} type="submit">{m.save()}</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
43
frontend/src/routes/settings/account/locale-picker.svelte
Normal file
43
frontend/src/routes/settings/account/locale-picker.svelte
Normal file
@@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
import * as Select from '$lib/components/ui/select';
|
||||
import { getLocale, setLocale, type Locale } from '$lib/paraglide/runtime';
|
||||
import UserService from '$lib/services/user-service';
|
||||
import userStore from '$lib/stores/user-store';
|
||||
|
||||
const userService = new UserService();
|
||||
const currentLocale = getLocale();
|
||||
|
||||
const locales = {
|
||||
'cs-CZ': 'Čeština',
|
||||
'de-DE': 'Deutsch',
|
||||
'en-US': 'English',
|
||||
'fr-FR': 'Français',
|
||||
'nl-NL': 'Nederlands',
|
||||
'ru-RU': 'Русский'
|
||||
};
|
||||
|
||||
function updateLocale(locale: Locale) {
|
||||
setLocale(locale);
|
||||
userService.updateCurrent({
|
||||
...$userStore!,
|
||||
locale
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<Select.Root
|
||||
selected={{
|
||||
label: locales[currentLocale],
|
||||
value: currentLocale
|
||||
}}
|
||||
onSelectedChange={(v) => updateLocale(v!.value)}
|
||||
>
|
||||
<Select.Trigger class="h-9 max-w-[200px]" aria-label="Select locale">
|
||||
<Select.Value>{locales[currentLocale]}</Select.Value>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each Object.entries(locales) as [value, label]}
|
||||
<Select.Item {value}>{label}</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
@@ -3,6 +3,7 @@
|
||||
import CopyToClipboard from '$lib/components/copy-to-clipboard.svelte';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { Separator } from '$lib/components/ui/separator';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import UserService from '$lib/services/user-service';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
|
||||
@@ -37,9 +38,9 @@
|
||||
<Dialog.Root open={!!code} {onOpenChange}>
|
||||
<Dialog.Content class="max-w-md">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Login Code</Dialog.Title>
|
||||
<Dialog.Title>{m.login_code()}</Dialog.Title>
|
||||
<Dialog.Description
|
||||
>Sign in using the following code. The code will expire in 15 minutes.
|
||||
>{m.sign_in_using_the_following_code_the_code_will_expire_in_minutes()}
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
@@ -49,7 +50,7 @@
|
||||
</CopyToClipboard>
|
||||
<div class="text-muted-foreground flex items-center justify-center gap-3">
|
||||
<Separator />
|
||||
<p class="text-nowrap text-xs">or visit</p>
|
||||
<p class="text-nowrap text-xs">{m.or_visit()}</p>
|
||||
<Separator />
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { LucideKeyRound, LucidePencil, LucideTrash } from 'lucide-svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import RenamePasskeyModal from './rename-passkey-modal.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let { passkeys = $bindable() }: { passkeys: Passkey[] } = $props();
|
||||
|
||||
@@ -17,16 +18,16 @@
|
||||
|
||||
async function deletePasskey(passkey: Passkey) {
|
||||
openConfirmDialog({
|
||||
title: `Delete ${passkey.name}`,
|
||||
message: 'Are you sure you want to delete this passkey?',
|
||||
title: m.delete_passkey_name({ passkeyName: passkey.name }),
|
||||
message: m.are_you_sure_you_want_to_delete_this_passkey(),
|
||||
confirm: {
|
||||
label: 'Delete',
|
||||
label: m.delete(),
|
||||
destructive: true,
|
||||
action: async () => {
|
||||
try {
|
||||
await webauthnService.removeCredential(passkey.id);
|
||||
passkeys = await webauthnService.listCredentials();
|
||||
toast.success('Passkey deleted successfully');
|
||||
toast.success(m.passkey_deleted_successfully());
|
||||
} catch (e) {
|
||||
axiosErrorToast(e);
|
||||
}
|
||||
@@ -44,7 +45,7 @@
|
||||
<div>
|
||||
<p>{passkey.name}</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Added on {new Date(passkey.createdAt).toLocaleDateString()}
|
||||
{m.added_on()} {new Date(passkey.createdAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -53,13 +54,13 @@
|
||||
on:click={() => (passkeyToRename = passkey)}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
aria-label="Rename"><LucidePencil class="h-3 w-3" /></Button
|
||||
aria-label={m.rename()}><LucidePencil class="h-3 w-3" /></Button
|
||||
>
|
||||
<Button
|
||||
on:click={() => deletePasskey(passkey)}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
aria-label="Delete"><LucideTrash class="h-3 w-3 text-red-500" /></Button
|
||||
aria-label={m.delete()}><LucideTrash class="h-3 w-3 text-red-500" /></Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import WebAuthnService from '$lib/services/webauthn-service';
|
||||
import type { Passkey } from '$lib/types/passkey.type';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
@@ -35,7 +36,7 @@
|
||||
.updateCredentialName(passkey!.id, name)
|
||||
.then(() => {
|
||||
passkey = null;
|
||||
toast.success('Passkey name updated successfully');
|
||||
toast.success(m.passkey_name_updated_successfully());
|
||||
callback?.();
|
||||
})
|
||||
.catch(axiosErrorToast);
|
||||
@@ -45,16 +46,16 @@
|
||||
<Dialog.Root open={!!passkey} {onOpenChange}>
|
||||
<Dialog.Content class="max-w-md">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Name Passkey</Dialog.Title>
|
||||
<Dialog.Description>Name your passkey to easily identify it later.</Dialog.Description>
|
||||
<Dialog.Title>{m.name_passkey()}</Dialog.Title>
|
||||
<Dialog.Description>{m.name_your_passkey_to_easily_identify_it_later()}</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<form onsubmit={onSubmit}>
|
||||
<div class="grid items-center gap-4 sm:grid-cols-4">
|
||||
<Label for="name" class="sm:text-right">Name</Label>
|
||||
<Label for="name" class="sm:text-right">{m.name()}</Label>
|
||||
<Input id="name" bind:value={name} class="col-span-3" />
|
||||
</div>
|
||||
<Dialog.Footer class="mt-4">
|
||||
<Button type="submit">Save</Button>
|
||||
<Button type="submit">{m.save()}</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import ApiKeyDialog from './api-key-dialog.svelte';
|
||||
import ApiKeyForm from './api-key-form.svelte';
|
||||
import ApiKeyList from './api-key-list.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let { data } = $props();
|
||||
let apiKeys = $state(data.apiKeys);
|
||||
@@ -35,18 +36,18 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>API Keys</title>
|
||||
<title>{m.api_keys()}</title>
|
||||
</svelte:head>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title>Create API Key</Card.Title>
|
||||
<Card.Description>Add a new API key for programmatic access.</Card.Description>
|
||||
<Card.Title>{m.create_api_key()}</Card.Title>
|
||||
<Card.Description>{m.add_a_new_api_key_for_programmatic_access()}</Card.Description>
|
||||
</div>
|
||||
{#if !expandAddApiKey}
|
||||
<Button on:click={() => (expandAddApiKey = true)}>Add API Key</Button>
|
||||
<Button on:click={() => (expandAddApiKey = true)}>{m.add_api_key()}</Button>
|
||||
{:else}
|
||||
<Button class="h-8 p-3" variant="ghost" on:click={() => (expandAddApiKey = false)}>
|
||||
<LucideMinus class="h-5 w-5" />
|
||||
@@ -65,7 +66,7 @@
|
||||
|
||||
<Card.Root class="mt-6">
|
||||
<Card.Header>
|
||||
<Card.Title>Manage API Keys</Card.Title>
|
||||
<Card.Title>{m.manage_api_keys()}</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<ApiKeyList {apiKeys} requestOptions={apiKeysRequestOptions} />
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import CopyToClipboard from '$lib/components/copy-to-clipboard.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import type { ApiKeyResponse } from '$lib/types/api-key.type';
|
||||
|
||||
let {
|
||||
@@ -20,22 +21,22 @@
|
||||
<Dialog.Root open={!!apiKeyResponse} {onOpenChange}>
|
||||
<Dialog.Content class="max-w-md" closeButton={false}>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>API Key Created</Dialog.Title>
|
||||
<Dialog.Title>{m.api_key_created()}</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
For security reasons, this key will only be shown once. Please store it securely.
|
||||
{m.for_security_reasons_this_key_will_only_be_shown_once()}
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
{#if apiKeyResponse}
|
||||
<div>
|
||||
<div class="mb-2 font-medium">Name</div>
|
||||
<div class="mb-2 font-medium">{m.name()}</div>
|
||||
<p class="text-muted-foreground">{apiKeyResponse.apiKey.name}</p>
|
||||
|
||||
{#if apiKeyResponse.apiKey.description}
|
||||
<div class="mb-2 mt-4 font-medium">Description</div>
|
||||
<div class="mb-2 mt-4 font-medium">{m.description()}</div>
|
||||
<p class="text-muted-foreground">{apiKeyResponse.apiKey.description}</p>
|
||||
{/if}
|
||||
|
||||
<div class="mb-2 mt-4 font-medium">API Key</div>
|
||||
<div class="mb-2 mt-4 font-medium">{m.api_key()}</div>
|
||||
<div class="bg-muted rounded-md p-2">
|
||||
<CopyToClipboard value={apiKeyResponse.token}>
|
||||
<span class="break-all font-mono text-sm">{apiKeyResponse.token}</span>
|
||||
@@ -44,7 +45,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
<Dialog.Footer class="mt-3">
|
||||
<Button variant="default" on:click={() => onOpenChange(false)}>Close</Button>
|
||||
<Button variant="default" on:click={() => onOpenChange(false)}>{m.close()}</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import FormInput from '$lib/components/form/form-input.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import type { ApiKeyCreate } from '$lib/types/api-key.type';
|
||||
import { createForm } from '$lib/utils/form-util';
|
||||
import { z } from 'zod';
|
||||
@@ -26,10 +27,10 @@
|
||||
const formSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(3, 'Name must be at least 3 characters')
|
||||
.max(50, 'Name cannot exceed 50 characters'),
|
||||
.min(3, m.name_must_be_at_least_3_characters())
|
||||
.max(50, m.name_cannot_exceed_50_characters()),
|
||||
description: z.string().default(''),
|
||||
expiresAt: z.date().min(new Date(), 'Expiration date must be in the future')
|
||||
expiresAt: z.date().min(new Date(), m.expiration_date_must_be_in_the_future())
|
||||
});
|
||||
|
||||
const { inputs, ...form } = createForm<typeof formSchema>(formSchema, apiKey);
|
||||
@@ -54,25 +55,25 @@
|
||||
<form onsubmit={onSubmit}>
|
||||
<div class="grid grid-cols-1 items-start gap-5 md:grid-cols-2">
|
||||
<FormInput
|
||||
label="Name"
|
||||
label={m.name()}
|
||||
bind:input={$inputs.name}
|
||||
description="Name to identify this API key."
|
||||
description={m.name_to_identify_this_api_key()}
|
||||
/>
|
||||
<FormInput
|
||||
label="Expires At"
|
||||
label={m.expires_at()}
|
||||
type="date"
|
||||
description="When this API key will expire."
|
||||
description={m.when_this_api_key_will_expire()}
|
||||
bind:input={$inputs.expiresAt}
|
||||
/>
|
||||
<div class="col-span-1 md:col-span-2">
|
||||
<FormInput
|
||||
label="Description"
|
||||
description="Optional description to help identify this key's purpose."
|
||||
label={m.description()}
|
||||
description={m.optional_description_to_help_identify_this_keys_purpose()}
|
||||
bind:input={$inputs.description}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 flex justify-end">
|
||||
<Button {isLoading} type="submit">Save</Button>
|
||||
<Button {isLoading} type="submit">{m.save()}</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { openConfirmDialog } from '$lib/components/confirm-dialog';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import ApiKeyService from '$lib/services/api-key-service';
|
||||
import type { ApiKey } from '$lib/types/api-key.type';
|
||||
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
@@ -21,22 +22,22 @@
|
||||
const apiKeyService = new ApiKeyService();
|
||||
|
||||
function formatDate(dateStr: string | undefined) {
|
||||
if (!dateStr) return 'Never';
|
||||
if (!dateStr) return m.never();
|
||||
return new Date(dateStr).toLocaleString();
|
||||
}
|
||||
|
||||
function revokeApiKey(apiKey: ApiKey) {
|
||||
openConfirmDialog({
|
||||
title: 'Revoke API Key',
|
||||
message: `Are you sure you want to revoke the API key "${apiKey.name}"? This will break any integrations using this key.`,
|
||||
title: m.revoke_api_key(),
|
||||
message: m.are_you_sure_you_want_to_revoke_the_api_key_apikeyname({ apiKeyName: apiKey.name }),
|
||||
confirm: {
|
||||
label: 'Revoke',
|
||||
label: m.revoke(),
|
||||
destructive: true,
|
||||
action: async () => {
|
||||
try {
|
||||
await apiKeyService.revoke(apiKey.id);
|
||||
apiKeys = await apiKeyService.list(requestOptions);
|
||||
toast.success('API key revoked successfully');
|
||||
toast.success(m.api_key_revoked_successfully());
|
||||
} catch (e) {
|
||||
axiosErrorToast(e);
|
||||
}
|
||||
@@ -52,11 +53,11 @@
|
||||
onRefresh={async (o) => (apiKeys = await apiKeyService.list(o))}
|
||||
withoutSearch
|
||||
columns={[
|
||||
{ label: 'Name', sortColumn: 'name' },
|
||||
{ label: 'Description' },
|
||||
{ label: 'Expires At', sortColumn: 'expiresAt' },
|
||||
{ label: 'Last Used', sortColumn: 'lastUsedAt' },
|
||||
{ label: 'Actions', hidden: true }
|
||||
{ label: m.name(), sortColumn: 'name' },
|
||||
{ label: m.description() },
|
||||
{ label: m.expires_at(), sortColumn: 'expiresAt' },
|
||||
{ label: m.last_used(), sortColumn: 'lastUsedAt' },
|
||||
{ label: m.actions(), hidden: true }
|
||||
]}
|
||||
>
|
||||
{#snippet rows({ item })}
|
||||
@@ -65,7 +66,7 @@
|
||||
<Table.Cell>{formatDate(item.expiresAt)}</Table.Cell>
|
||||
<Table.Cell>{formatDate(item.lastUsedAt)}</Table.Cell>
|
||||
<Table.Cell class="flex justify-end">
|
||||
<Button on:click={() => revokeApiKey(item)} size="sm" variant="outline" aria-label="Revoke"
|
||||
<Button on:click={() => revokeApiKey(item)} size="sm" variant="outline" aria-label={m.revoke()}
|
||||
><LucideBan class="h-3 w-3 text-red-500" /></Button
|
||||
>
|
||||
</Table.Cell>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import AppConfigGeneralForm from './forms/app-config-general-form.svelte';
|
||||
import AppConfigLdapForm from './forms/app-config-ldap-form.svelte';
|
||||
import UpdateApplicationImages from './update-application-images.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let { data } = $props();
|
||||
let appConfig = $state(data.appConfig);
|
||||
@@ -46,36 +47,35 @@
|
||||
: Promise.resolve();
|
||||
|
||||
await Promise.all([lightLogoPromise, darkLogoPromise, backgroundImagePromise, faviconPromise])
|
||||
.then(() => toast.success('Images updated successfully'))
|
||||
.then(() => toast.success(m.images_updated_successfully()))
|
||||
.catch(axiosErrorToast);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Application Configuration</title>
|
||||
<title>{m.application_configuration()}</title>
|
||||
</svelte:head>
|
||||
|
||||
<CollapsibleCard id="application-configuration-general" title="General" defaultExpanded>
|
||||
<CollapsibleCard id="application-configuration-general" title={m.general()} defaultExpanded>
|
||||
<AppConfigGeneralForm {appConfig} callback={updateAppConfig} />
|
||||
</CollapsibleCard>
|
||||
|
||||
<CollapsibleCard
|
||||
id="application-configuration-email"
|
||||
title="Email"
|
||||
description="Enable email notifications to alert users when a login is detected from a new device or
|
||||
location."
|
||||
title={m.email()}
|
||||
description={m.enable_email_notifications_to_alert_users_when_a_login_is_detected_from_a_new_device_or_location()}
|
||||
>
|
||||
<AppConfigEmailForm {appConfig} callback={updateAppConfig} />
|
||||
</CollapsibleCard>
|
||||
|
||||
<CollapsibleCard
|
||||
id="application-configuration-ldap"
|
||||
title="LDAP"
|
||||
description="Configure LDAP settings to sync users and groups from an LDAP server."
|
||||
title={m.ldap()}
|
||||
description={m.configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server()}
|
||||
>
|
||||
<AppConfigLdapForm {appConfig} callback={updateAppConfig} />
|
||||
</CollapsibleCard>
|
||||
|
||||
<CollapsibleCard id="application-configuration-images" title="Images">
|
||||
<CollapsibleCard id="application-configuration-images" title={m.images()}>
|
||||
<UpdateApplicationImages callback={updateImages} />
|
||||
</CollapsibleCard>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import FileInput from '$lib/components/form/file-input.svelte';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { cn } from '$lib/utils/style';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
@@ -60,7 +61,7 @@
|
||||
<span
|
||||
class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 transform font-medium opacity-0 transition-opacity group-hover:opacity-100"
|
||||
>
|
||||
Update
|
||||
{m.update()}
|
||||
</span>
|
||||
</div>
|
||||
</FileInput>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import Label from '$lib/components/ui/label/label.svelte';
|
||||
import * as Select from '$lib/components/ui/select';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import AppConfigService from '$lib/services/app-config-service';
|
||||
import type { AllAppConfig } from '$lib/types/application-configuration';
|
||||
import { createForm } from '$lib/utils/form-util';
|
||||
@@ -55,7 +56,7 @@
|
||||
appConfig[key] = value;
|
||||
});
|
||||
|
||||
toast.success('Email configuration updated successfully');
|
||||
toast.success(m.email_configuration_updated_successfully());
|
||||
return true;
|
||||
}
|
||||
async function onTestEmail() {
|
||||
@@ -64,11 +65,11 @@
|
||||
|
||||
if (hasChanges) {
|
||||
openConfirmDialog({
|
||||
title: 'Save changes?',
|
||||
title: m.save_changes_question(),
|
||||
message:
|
||||
'You have to save the changes before sending a test email. Do you want to save now?',
|
||||
m.you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now(),
|
||||
confirm: {
|
||||
label: 'Save and send',
|
||||
label: m.save_and_send(),
|
||||
action: async () => {
|
||||
const saved = await onSubmit();
|
||||
if (saved) {
|
||||
@@ -86,9 +87,9 @@
|
||||
isSendingTestEmail = true;
|
||||
await appConfigService
|
||||
.sendTestEmail()
|
||||
.then(() => toast.success('Test email sent successfully to your email address.'))
|
||||
.then(() => toast.success(m.test_email_sent_successfully()))
|
||||
.catch(() =>
|
||||
toast.error('Failed to send test email. Check the server logs for more information.')
|
||||
toast.error(m.failed_to_send_test_email())
|
||||
)
|
||||
.finally(() => (isSendingTestEmail = false));
|
||||
}
|
||||
@@ -96,21 +97,21 @@
|
||||
|
||||
<form onsubmit={onSubmit}>
|
||||
<fieldset disabled={uiConfigDisabled}>
|
||||
<h4 class="text-lg font-semibold">SMTP Configuration</h4>
|
||||
<h4 class="text-lg font-semibold">{m.smtp_configuration()}</h4>
|
||||
<div class="mt-4 grid grid-cols-1 items-end gap-5 md:grid-cols-2">
|
||||
<FormInput label="SMTP Host" bind:input={$inputs.smtpHost} />
|
||||
<FormInput label="SMTP Port" type="number" bind:input={$inputs.smtpPort} />
|
||||
<FormInput label="SMTP User" bind:input={$inputs.smtpUser} />
|
||||
<FormInput label="SMTP Password" type="password" bind:input={$inputs.smtpPassword} />
|
||||
<FormInput label="SMTP From" bind:input={$inputs.smtpFrom} />
|
||||
<FormInput label={m.smtp_host()} bind:input={$inputs.smtpHost} />
|
||||
<FormInput label={m.smtp_port()} type="number" bind:input={$inputs.smtpPort} />
|
||||
<FormInput label={m.smtp_user()} bind:input={$inputs.smtpUser} />
|
||||
<FormInput label={m.smtp_password()} type="password" bind:input={$inputs.smtpPassword} />
|
||||
<FormInput label={m.smtp_from()} bind:input={$inputs.smtpFrom} />
|
||||
<div class="grid gap-2">
|
||||
<Label class="mb-0" for="smtp-tls">SMTP TLS Option</Label>
|
||||
<Label class="mb-0" for="smtp-tls">{m.smtp_tls_option()}</Label>
|
||||
<Select.Root
|
||||
selected={{ value: $inputs.smtpTls.value, label: tlsOptions[$inputs.smtpTls.value] }}
|
||||
onSelectedChange={(v) => ($inputs.smtpTls.value = v!.value)}
|
||||
>
|
||||
<Select.Trigger>
|
||||
<Select.Value placeholder="Email TLS Option" />
|
||||
<Select.Value placeholder={m.email_tls_option()} />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="none" label="None" />
|
||||
@@ -121,31 +122,31 @@
|
||||
</div>
|
||||
<CheckboxWithLabel
|
||||
id="skip-cert-verify"
|
||||
label="Skip Certificate Verification"
|
||||
description="This can be useful for self-signed certificates."
|
||||
label={m.skip_certificate_verification()}
|
||||
description={m.this_can_be_useful_for_selfsigned_certificates()}
|
||||
bind:checked={$inputs.smtpSkipCertVerify.value}
|
||||
/>
|
||||
</div>
|
||||
<h4 class="mt-10 text-lg font-semibold">Enabled Emails</h4>
|
||||
<h4 class="mt-10 text-lg font-semibold">{m.enabled_emails()}</h4>
|
||||
<div class="mt-4 flex flex-col gap-5">
|
||||
<CheckboxWithLabel
|
||||
id="email-login-notification"
|
||||
label="Email Login Notification"
|
||||
description="Send an email to the user when they log in from a new device."
|
||||
label={m.email_login_notification()}
|
||||
description={m.send_an_email_to_the_user_when_they_log_in_from_a_new_device()}
|
||||
bind:checked={$inputs.emailLoginNotificationEnabled.value}
|
||||
/>
|
||||
<CheckboxWithLabel
|
||||
id="email-login"
|
||||
label="Email Login"
|
||||
description="Allows users to sign in with a login code sent to their email. This reduces the security significantly as anyone with access to the user's email can gain entry."
|
||||
label={m.email_login()}
|
||||
description={m.allow_users_to_sign_in_with_a_login_code_sent_to_their_email()}
|
||||
bind:checked={$inputs.emailOneTimeAccessEnabled.value}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="mt-8 flex flex-wrap justify-end gap-3">
|
||||
<Button isLoading={isSendingTestEmail} variant="secondary" onclick={onTestEmail}
|
||||
>Send test email</Button
|
||||
>{m.send_test_email()}</Button
|
||||
>
|
||||
<Button type="submit" disabled={uiConfigDisabled}>Save</Button>
|
||||
<Button type="submit" disabled={uiConfigDisabled}>{m.save()}</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user