mirror of
https://github.com/pocket-id/pocket-id.git
synced 2025-12-10 09:13:31 +03:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5cece3b0e | ||
|
|
d5485238b8 | ||
|
|
ac5a121f66 | ||
|
|
481df3bcb9 | ||
|
|
7677a3de2c | ||
|
|
1f65c01b04 | ||
|
|
d5928f6fea | ||
|
|
bef77ac8dc | ||
|
|
c8eb034c49 | ||
|
|
c77167df46 | ||
|
|
3717a663d9 | ||
|
|
5814549cbe | ||
|
|
2e5d268798 | ||
|
|
4ed312251e | ||
|
|
946c534b08 | ||
|
|
883877adec | ||
|
|
215531d65c | ||
|
|
c0f055c3c0 | ||
|
|
d77044882d | ||
|
|
d6795300b1 | ||
|
|
fd3c76ffa3 | ||
|
|
698bc3a35a | ||
|
|
1bcb50edc3 | ||
|
|
9700afb9cb | ||
|
|
9ce82fb205 | ||
|
|
2935236ace |
6
.github/workflows/build-next.yml
vendored
6
.github/workflows/build-next.yml
vendored
@@ -5,6 +5,10 @@ on:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency:
|
||||
group: build-next-image
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-next:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -58,7 +62,7 @@ jobs:
|
||||
run: npm run build
|
||||
|
||||
- name: Build binaries
|
||||
run: sh scripts/development/build-binaries.sh
|
||||
run: sh scripts/development/build-binaries.sh --docker-only
|
||||
|
||||
- name: Build and push container image
|
||||
id: build-push-image
|
||||
|
||||
25
CHANGELOG.md
25
CHANGELOG.md
@@ -1,3 +1,28 @@
|
||||
## [](https://github.com/pocket-id/pocket-id/compare/v1.3.1...v) (2025-06-19)
|
||||
|
||||
### Features
|
||||
|
||||
* allow setting unix socket mode ([#661](https://github.com/pocket-id/pocket-id/issues/661)) ([7677a3d](https://github.com/pocket-id/pocket-id/commit/7677a3de2c923c11a58bc8c4d1b2121d403a1504))
|
||||
* auto-focus on the login buttons ([#647](https://github.com/pocket-id/pocket-id/issues/647)) ([d679530](https://github.com/pocket-id/pocket-id/commit/d6795300b158b85dd9feadd561b6ecd891f5db0d))
|
||||
* configurable local ipv6 ranges for audit log ([#657](https://github.com/pocket-id/pocket-id/issues/657)) ([d548523](https://github.com/pocket-id/pocket-id/commit/d5485238b8fd4cc566af00eae2b17d69a119f991))
|
||||
* location filter for global audit log ([#662](https://github.com/pocket-id/pocket-id/issues/662)) ([ac5a121](https://github.com/pocket-id/pocket-id/commit/ac5a121f664b8127d0faf30c0f93432f30e7f33a))
|
||||
* ui accent colors ([#643](https://github.com/pocket-id/pocket-id/issues/643)) ([883877a](https://github.com/pocket-id/pocket-id/commit/883877adec6fc3e65bd5a705499449959b894fb5))
|
||||
* use icon instead of text on application image update hover state ([215531d](https://github.com/pocket-id/pocket-id/commit/215531d65c6683609b0b4a5505fdb72696fdb93e))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow images with uppercase file extension ([1bcb50e](https://github.com/pocket-id/pocket-id/commit/1bcb50edc335886dd722a4c69960c48cc3cd1687))
|
||||
* center oidc client images if they are smaller than the box ([946c534](https://github.com/pocket-id/pocket-id/commit/946c534b0877a074a6b658060f9af27e4061397c))
|
||||
* explicitly cache images to prevent unexpected behavior ([2e5d268](https://github.com/pocket-id/pocket-id/commit/2e5d2687982186c12e530492292d49895cb6043a))
|
||||
* reduce duration of animations on login and signin page ([#648](https://github.com/pocket-id/pocket-id/issues/648)) ([d770448](https://github.com/pocket-id/pocket-id/commit/d77044882d5a41da22df1c0099c1eb1f20bcbc5b))
|
||||
* use inline style for dynamic background image URL instead of Tailwind class ([bef77ac](https://github.com/pocket-id/pocket-id/commit/bef77ac8dca2b98b6732677aaafbc28f79d00487))
|
||||
## [](https://github.com/pocket-id/pocket-id/compare/v1.3.0...v) (2025-06-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* change timestamp of `client_credentials.sql` migration ([2935236](https://github.com/pocket-id/pocket-id/commit/2935236acee9c78c2fe6787ec8b5f53ae0eca047))
|
||||
|
||||
## [](https://github.com/pocket-id/pocket-id/compare/v1.2.0...v) (2025-06-09)
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# This Dockerfile embeds a pre-built binary for the given Linux architecture
|
||||
# Binaries must be built using ./scripts/development/build-binaries.sh first
|
||||
# Binaries must be built using ""./scripts/development/build-binaries.sh --docker-only"
|
||||
|
||||
FROM alpine
|
||||
|
||||
|
||||
12
backend/.air.toml
Normal file
12
backend/.air.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
root = "."
|
||||
tmp_dir = ".bin"
|
||||
|
||||
[build]
|
||||
bin = "./.bin/pocket-id"
|
||||
cmd = "CGO_ENABLED=0 go build -o ./.bin/pocket-id ./cmd"
|
||||
exclude_dir = ["resources", ".bin", "data"]
|
||||
exclude_regex = [".*_test\\.go"]
|
||||
stop_on_error = true
|
||||
|
||||
[misc]
|
||||
clean_on_exit = true
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/frontend"
|
||||
@@ -119,6 +121,18 @@ func initRouterInternal(db *gorm.DB, svc *services) (utils.Service, error) {
|
||||
return nil, fmt.Errorf("failed to create %s listener: %w", network, err)
|
||||
}
|
||||
|
||||
// Set the socket mode if using a Unix socket
|
||||
if network == "unix" && common.EnvConfig.UnixSocketMode != "" {
|
||||
mode, err := strconv.ParseUint(common.EnvConfig.UnixSocketMode, 8, 32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse UNIX socket mode '%s': %w", common.EnvConfig.UnixSocketMode, err)
|
||||
}
|
||||
|
||||
if err := os.Chmod(addr, os.FileMode(mode)); err != nil {
|
||||
return nil, fmt.Errorf("failed to set UNIX socket mode '%s': %w", common.EnvConfig.UnixSocketMode, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Service runner function
|
||||
runFn := func(ctx context.Context) error {
|
||||
log.Printf("Server listening on %s", addr)
|
||||
|
||||
@@ -33,9 +33,11 @@ type EnvConfigSchema struct {
|
||||
Port string `env:"PORT"`
|
||||
Host string `env:"HOST"`
|
||||
UnixSocket string `env:"UNIX_SOCKET"`
|
||||
UnixSocketMode string `env:"UNIX_SOCKET_MODE"`
|
||||
MaxMindLicenseKey string `env:"MAXMIND_LICENSE_KEY"`
|
||||
GeoLiteDBPath string `env:"GEOLITE_DB_PATH"`
|
||||
GeoLiteDBUrl string `env:"GEOLITE_DB_URL"`
|
||||
LocalIPv6Ranges string `env:"LOCAL_IPV6_RANGES"`
|
||||
UiConfigDisabled bool `env:"UI_CONFIG_DISABLED"`
|
||||
MetricsEnabled bool `env:"METRICS_ENABLED"`
|
||||
TracingEnabled bool `env:"TRACING_ENABLED"`
|
||||
@@ -53,9 +55,11 @@ var EnvConfig = &EnvConfigSchema{
|
||||
Port: "1411",
|
||||
Host: "0.0.0.0",
|
||||
UnixSocket: "",
|
||||
UnixSocketMode: "",
|
||||
MaxMindLicenseKey: "",
|
||||
GeoLiteDBPath: "data/GeoLite2-City.mmdb",
|
||||
GeoLiteDBUrl: MaxMindGeoLiteCityUrl,
|
||||
LocalIPv6Ranges: "",
|
||||
UiConfigDisabled: false,
|
||||
MetricsEnabled: false,
|
||||
TracingEnabled: false,
|
||||
|
||||
@@ -3,6 +3,7 @@ package controller
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
@@ -247,6 +248,8 @@ func (acc *AppConfigController) getImage(c *gin.Context, name string, imageType
|
||||
mimeType := utils.GetImageMimeType(imageType)
|
||||
|
||||
c.Header("Content-Type", mimeType)
|
||||
|
||||
utils.SetCacheControlHeader(c, 15*time.Minute, 24*time.Hour)
|
||||
c.File(imagePath)
|
||||
}
|
||||
|
||||
|
||||
@@ -89,6 +89,7 @@ func (alc *AuditLogController) listAuditLogsForUserHandler(c *gin.Context) {
|
||||
// @Param filters[userId] query string false "Filter by user ID"
|
||||
// @Param filters[event] query string false "Filter by event type"
|
||||
// @Param filters[clientName] query string false "Filter by client name"
|
||||
// @Param filters[location] query string false "Filter by location type (external or internal)"
|
||||
// @Success 200 {object} dto.Paginated[dto.AuditLogDto]
|
||||
// @Router /api/audit-logs/all [get]
|
||||
func (alc *AuditLogController) listAllAuditLogsHandler(c *gin.Context) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
@@ -545,6 +546,8 @@ func (oc *OidcController) getClientLogoHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
utils.SetCacheControlHeader(c, 15*time.Minute, 12*time.Hour)
|
||||
|
||||
c.Header("Content-Type", mimeType)
|
||||
c.File(imagePath)
|
||||
}
|
||||
|
||||
@@ -250,10 +250,7 @@ func (uc *UserController) getUserProfilePictureHandler(c *gin.Context) {
|
||||
defer picture.Close()
|
||||
}
|
||||
|
||||
_, ok := c.GetQuery("skipCache")
|
||||
if !ok {
|
||||
c.Header("Cache-Control", "public, max-age=900")
|
||||
}
|
||||
utils.SetCacheControlHeader(c, 15*time.Minute, 1*time.Hour)
|
||||
|
||||
c.DataFromReader(http.StatusOK, size, "image/png", picture, nil)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ type AppConfigUpdateDto struct {
|
||||
EmailsVerified string `json:"emailsVerified" binding:"required"`
|
||||
DisableAnimations string `json:"disableAnimations" binding:"required"`
|
||||
AllowOwnAccountEdit string `json:"allowOwnAccountEdit" binding:"required"`
|
||||
AccentColor string `json:"accentColor"`
|
||||
SmtpHost string `json:"smtpHost"`
|
||||
SmtpPort string `json:"smtpPort"`
|
||||
SmtpFrom string `json:"smtpFrom" binding:"omitempty,email"`
|
||||
|
||||
@@ -23,4 +23,5 @@ type AuditLogFilterDto struct {
|
||||
UserID string `form:"filters[userId]"`
|
||||
Event string `form:"filters[event]"`
|
||||
ClientName string `form:"filters[clientName]"`
|
||||
Location string `form:"filters[location]"`
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ type AppConfig struct {
|
||||
AppName AppConfigVariable `key:"appName,public"` // Public
|
||||
SessionDuration AppConfigVariable `key:"sessionDuration"`
|
||||
EmailsVerified AppConfigVariable `key:"emailsVerified"`
|
||||
AccentColor AppConfigVariable `key:"accentColor,public"` // Public
|
||||
DisableAnimations AppConfigVariable `key:"disableAnimations,public"` // Public
|
||||
AllowOwnAccountEdit AppConfigVariable `key:"allowOwnAccountEdit,public"` // Public
|
||||
// Internal
|
||||
|
||||
@@ -68,6 +68,7 @@ func (s *AppConfigService) getDefaultDbConfig() *model.AppConfig {
|
||||
EmailsVerified: model.AppConfigVariable{Value: "false"},
|
||||
DisableAnimations: model.AppConfigVariable{Value: "false"},
|
||||
AllowOwnAccountEdit: model.AppConfigVariable{Value: "true"},
|
||||
AccentColor: model.AppConfigVariable{Value: "default"},
|
||||
// Internal
|
||||
BackgroundImageType: model.AppConfigVariable{Value: "jpg"},
|
||||
LogoLightImageType: model.AppConfigVariable{Value: "svg"},
|
||||
@@ -321,7 +322,7 @@ func (s *AppConfigService) ListAppConfig(showAll bool) []model.AppConfigVariable
|
||||
}
|
||||
|
||||
func (s *AppConfigService) UpdateImage(ctx context.Context, uploadedFile *multipart.FileHeader, imageName string, oldImageType string) (err error) {
|
||||
fileType := utils.GetFileExtension(uploadedFile.Filename)
|
||||
fileType := strings.ToLower(utils.GetFileExtension(uploadedFile.Filename))
|
||||
mimeType := utils.GetImageMimeType(fileType)
|
||||
if mimeType == "" {
|
||||
return &common.FileTypeNotSupportedError{}
|
||||
|
||||
@@ -150,6 +150,14 @@ func (s *AuditLogService) ListAllAuditLogs(ctx context.Context, sortedPagination
|
||||
return nil, utils.PaginationResponse{}, fmt.Errorf("unsupported database dialect: %s", dialect)
|
||||
}
|
||||
}
|
||||
if filters.Location != "" {
|
||||
switch filters.Location {
|
||||
case "external":
|
||||
query = query.Where("country != 'Internal Network'")
|
||||
case "internal":
|
||||
query = query.Where("country = 'Internal Network'")
|
||||
}
|
||||
}
|
||||
|
||||
pagination, err := utils.PaginateAndSort(sortedPaginationRequest, query, &logs)
|
||||
if err != nil {
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -22,9 +23,10 @@ import (
|
||||
)
|
||||
|
||||
type GeoLiteService struct {
|
||||
httpClient *http.Client
|
||||
disableUpdater bool
|
||||
mutex sync.RWMutex
|
||||
httpClient *http.Client
|
||||
disableUpdater bool
|
||||
mutex sync.RWMutex
|
||||
localIPv6Ranges []*net.IPNet
|
||||
}
|
||||
|
||||
var localhostIPNets = []*net.IPNet{
|
||||
@@ -54,9 +56,66 @@ func NewGeoLiteService(httpClient *http.Client) *GeoLiteService {
|
||||
service.disableUpdater = true
|
||||
}
|
||||
|
||||
// Initialize IPv6 local ranges
|
||||
if err := service.initializeIPv6LocalRanges(); err != nil {
|
||||
log.Printf("Warning: Failed to initialize IPv6 local ranges: %v", err)
|
||||
}
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
// initializeIPv6LocalRanges parses the LOCAL_IPV6_RANGES environment variable
|
||||
func (s *GeoLiteService) initializeIPv6LocalRanges() error {
|
||||
rangesEnv := common.EnvConfig.LocalIPv6Ranges
|
||||
if rangesEnv == "" {
|
||||
return nil // No local IPv6 ranges configured
|
||||
}
|
||||
|
||||
ranges := strings.Split(rangesEnv, ",")
|
||||
localRanges := make([]*net.IPNet, 0, len(ranges))
|
||||
|
||||
for _, rangeStr := range ranges {
|
||||
rangeStr = strings.TrimSpace(rangeStr)
|
||||
if rangeStr == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
_, ipNet, err := net.ParseCIDR(rangeStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid IPv6 range '%s': %w", rangeStr, err)
|
||||
}
|
||||
|
||||
// Ensure it's an IPv6 range
|
||||
if ipNet.IP.To4() != nil {
|
||||
return fmt.Errorf("range '%s' is not a valid IPv6 range", rangeStr)
|
||||
}
|
||||
|
||||
localRanges = append(localRanges, ipNet)
|
||||
}
|
||||
|
||||
s.localIPv6Ranges = localRanges
|
||||
|
||||
if len(localRanges) > 0 {
|
||||
log.Printf("Initialized %d IPv6 local ranges", len(localRanges))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isLocalIPv6 checks if the given IPv6 address is within any of the configured local ranges
|
||||
func (s *GeoLiteService) isLocalIPv6(ip net.IP) bool {
|
||||
if ip.To4() != nil {
|
||||
return false // Not an IPv6 address
|
||||
}
|
||||
|
||||
for _, localRange := range s.localIPv6Ranges {
|
||||
if localRange.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *GeoLiteService) DisableUpdater() bool {
|
||||
return s.disableUpdater
|
||||
}
|
||||
@@ -65,6 +124,12 @@ func (s *GeoLiteService) DisableUpdater() bool {
|
||||
func (s *GeoLiteService) GetLocationByIP(ipAddress string) (country, city string, err error) {
|
||||
// Check the IP address against known private IP ranges
|
||||
if ip := net.ParseIP(ipAddress); ip != nil {
|
||||
// Check IPv6 local ranges first
|
||||
if s.isLocalIPv6(ip) {
|
||||
return "Internal Network", "LAN", nil
|
||||
}
|
||||
|
||||
// Check existing IPv4 ranges
|
||||
for _, ipNet := range tailscaleIPNets {
|
||||
if ipNet.Contains(ip) {
|
||||
return "Internal Network", "Tailscale", nil
|
||||
|
||||
231
backend/internal/service/geolite_service_test.go
Normal file
231
backend/internal/service/geolite_service_test.go
Normal file
@@ -0,0 +1,231 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
)
|
||||
|
||||
func TestGeoLiteService_IPv6LocalRanges(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
localRanges string
|
||||
testIP string
|
||||
expectedCountry string
|
||||
expectedCity string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "IPv6 in local range",
|
||||
localRanges: "2001:0db8:abcd:000::/56,2001:0db8:abcd:001::/56",
|
||||
testIP: "2001:0db8:abcd:000::1",
|
||||
expectedCountry: "Internal Network",
|
||||
expectedCity: "LAN",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "IPv6 not in local range",
|
||||
localRanges: "2001:0db8:abcd:000::/56",
|
||||
testIP: "2001:0db8:ffff:000::1",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "Multiple ranges - second range match",
|
||||
localRanges: "2001:0db8:abcd:000::/56,2001:0db8:abcd:001::/56",
|
||||
testIP: "2001:0db8:abcd:001::1",
|
||||
expectedCountry: "Internal Network",
|
||||
expectedCity: "LAN",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Empty local ranges",
|
||||
localRanges: "",
|
||||
testIP: "2001:0db8:abcd:000::1",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "IPv4 private address still works",
|
||||
localRanges: "2001:0db8:abcd:000::/56",
|
||||
testIP: "192.168.1.1",
|
||||
expectedCountry: "Internal Network",
|
||||
expectedCity: "LAN",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "IPv6 loopback",
|
||||
localRanges: "2001:0db8:abcd:000::/56",
|
||||
testIP: "::1",
|
||||
expectedCountry: "Internal Network",
|
||||
expectedCity: "localhost",
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
originalConfig := common.EnvConfig.LocalIPv6Ranges
|
||||
common.EnvConfig.LocalIPv6Ranges = tt.localRanges
|
||||
defer func() {
|
||||
common.EnvConfig.LocalIPv6Ranges = originalConfig
|
||||
}()
|
||||
|
||||
service := NewGeoLiteService(&http.Client{})
|
||||
|
||||
country, city, err := service.GetLocationByIP(tt.testIP)
|
||||
|
||||
if tt.expectError {
|
||||
if err == nil && country != "Internal Network" {
|
||||
t.Errorf("Expected error or internal network classification for external IP")
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error for local IP, got: %v", err)
|
||||
}
|
||||
if country != tt.expectedCountry {
|
||||
t.Errorf("Expected country %s, got %s", tt.expectedCountry, country)
|
||||
}
|
||||
if city != tt.expectedCity {
|
||||
t.Errorf("Expected city %s, got %s", tt.expectedCity, city)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeoLiteService_isLocalIPv6(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
localRanges string
|
||||
testIP string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "Valid IPv6 in range",
|
||||
localRanges: "2001:0db8:abcd:000::/56",
|
||||
testIP: "2001:0db8:abcd:000::1",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Valid IPv6 not in range",
|
||||
localRanges: "2001:0db8:abcd:000::/56",
|
||||
testIP: "2001:0db8:ffff:000::1",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "IPv4 address should return false",
|
||||
localRanges: "2001:0db8:abcd:000::/56",
|
||||
testIP: "192.168.1.1",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "No ranges configured",
|
||||
localRanges: "",
|
||||
testIP: "2001:0db8:abcd:000::1",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Edge of range",
|
||||
localRanges: "2001:0db8:abcd:000::/56",
|
||||
testIP: "2001:0db8:abcd:00ff:ffff:ffff:ffff:ffff",
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
originalConfig := common.EnvConfig.LocalIPv6Ranges
|
||||
common.EnvConfig.LocalIPv6Ranges = tt.localRanges
|
||||
defer func() {
|
||||
common.EnvConfig.LocalIPv6Ranges = originalConfig
|
||||
}()
|
||||
|
||||
service := NewGeoLiteService(&http.Client{})
|
||||
ip := net.ParseIP(tt.testIP)
|
||||
if ip == nil {
|
||||
t.Fatalf("Invalid test IP: %s", tt.testIP)
|
||||
}
|
||||
|
||||
result := service.isLocalIPv6(ip)
|
||||
if result != tt.expected {
|
||||
t.Errorf("Expected %v, got %v for IP %s", tt.expected, result, tt.testIP)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeoLiteService_initializeIPv6LocalRanges(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
envValue string
|
||||
expectError bool
|
||||
expectCount int
|
||||
}{
|
||||
{
|
||||
name: "Valid IPv6 ranges",
|
||||
envValue: "2001:0db8:abcd:000::/56,2001:0db8:abcd:001::/56",
|
||||
expectError: false,
|
||||
expectCount: 2,
|
||||
},
|
||||
{
|
||||
name: "Empty environment variable",
|
||||
envValue: "",
|
||||
expectError: false,
|
||||
expectCount: 0,
|
||||
},
|
||||
{
|
||||
name: "Invalid CIDR notation",
|
||||
envValue: "2001:0db8:abcd:000::/999",
|
||||
expectError: true,
|
||||
expectCount: 0,
|
||||
},
|
||||
{
|
||||
name: "IPv4 range in IPv6 env var",
|
||||
envValue: "192.168.1.0/24",
|
||||
expectError: true,
|
||||
expectCount: 0,
|
||||
},
|
||||
{
|
||||
name: "Mixed valid and invalid ranges",
|
||||
envValue: "2001:0db8:abcd:000::/56,invalid-range",
|
||||
expectError: true,
|
||||
expectCount: 0,
|
||||
},
|
||||
{
|
||||
name: "Whitespace handling",
|
||||
envValue: " 2001:0db8:abcd:000::/56 , 2001:0db8:abcd:001::/56 ",
|
||||
expectError: false,
|
||||
expectCount: 2,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
originalConfig := common.EnvConfig.LocalIPv6Ranges
|
||||
common.EnvConfig.LocalIPv6Ranges = tt.envValue
|
||||
defer func() {
|
||||
common.EnvConfig.LocalIPv6Ranges = originalConfig
|
||||
}()
|
||||
|
||||
service := &GeoLiteService{
|
||||
httpClient: &http.Client{},
|
||||
}
|
||||
|
||||
err := service.initializeIPv6LocalRanges()
|
||||
|
||||
if tt.expectError && err == nil {
|
||||
t.Errorf("Expected error but got none")
|
||||
}
|
||||
if !tt.expectError && err != nil {
|
||||
t.Errorf("Expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
rangeCount := len(service.localIPv6Ranges)
|
||||
|
||||
if rangeCount != tt.expectCount {
|
||||
t.Errorf("Expected %d ranges, got %d", tt.expectCount, rangeCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -820,7 +820,7 @@ func (s *OidcService) GetClientLogo(ctx context.Context, clientID string) (strin
|
||||
}
|
||||
|
||||
func (s *OidcService) UpdateClientLogo(ctx context.Context, clientID string, file *multipart.FileHeader) error {
|
||||
fileType := utils.GetFileExtension(file.Filename)
|
||||
fileType := strings.ToLower(utils.GetFileExtension(file.Filename))
|
||||
if mimeType := utils.GetImageMimeType(fileType); mimeType == "" {
|
||||
return &common.FileTypeNotSupportedError{}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@ package utils
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// BearerAuth returns the value of the bearer token in the Authorization header if present
|
||||
@@ -16,3 +20,14 @@ func BearerAuth(r *http.Request) (string, bool) {
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// SetCacheControlHeader sets the Cache-Control header for the response.
|
||||
func SetCacheControlHeader(ctx *gin.Context, maxAge, staleWhileRevalidate time.Duration) {
|
||||
_, ok := ctx.GetQuery("skipCache")
|
||||
if !ok {
|
||||
maxAgeSeconds := strconv.Itoa(int(maxAge.Seconds()))
|
||||
staleWhileRevalidateSeconds := strconv.Itoa(int(staleWhileRevalidate.Seconds()))
|
||||
ctx.Header("Cache-Control", "public, max-age="+maxAgeSeconds+", stale-while-revalidate="+staleWhileRevalidateSeconds)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DROP INDEX idx_audit_logs_country;
|
||||
@@ -0,0 +1 @@
|
||||
CREATE INDEX idx_audit_logs_country ON audit_logs(country);
|
||||
@@ -0,0 +1 @@
|
||||
DROP INDEX idx_audit_logs_country;
|
||||
@@ -0,0 +1 @@
|
||||
CREATE INDEX idx_audit_logs_country ON audit_logs(country);
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
pocket-id:
|
||||
image: ghcr.io/pocket-id/pocket-id
|
||||
image: ghcr.io/pocket-id/pocket-id:v1
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
ports:
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"my_account": "Můj Účet",
|
||||
"logout": "Odhlásit se",
|
||||
"confirm": "Potvrdit",
|
||||
"docs": "Dokumentace",
|
||||
"key": "Klíč",
|
||||
"value": "Hodnota",
|
||||
"remove_custom_claim": "Odstranit vlastní nárok",
|
||||
@@ -179,7 +180,7 @@
|
||||
"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í.",
|
||||
"emai_login_code_requested_by_user": "Přihlašovací kód e-mailu vyžádaný uživatelem",
|
||||
"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 vstoupit.",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to bypass passkeys by requesting a login code sent to their email. This significantly reduces security as anyone with access to the user's email can gain entry.",
|
||||
"email_login_code_from_admin": "Poslat e-mail přihlašovacímu kódu od administrátora",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "Umožňuje administrátorovi odeslat přihlašovací kód uživateli e-mailem.",
|
||||
"send_test_email": "Odeslat testovací e-mail",
|
||||
@@ -309,7 +310,7 @@
|
||||
"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?",
|
||||
"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": "Obnovit",
|
||||
"reset_to_default": "Obnovit výchozí",
|
||||
"profile_picture_has_been_reset": "Profilový obrázek byl obnoven. Aktualizace může trvat několik minut.",
|
||||
@@ -325,7 +326,7 @@
|
||||
"client_authorization": "Autorizace klienta",
|
||||
"new_client_authorization": "Nová autorizace klienta",
|
||||
"disable_animations": "Zakázat animace",
|
||||
"turn_off_ui_animations": "Vypnout všechny animace v celém administrátorském rozhraní.",
|
||||
"turn_off_ui_animations": "Turn off animations throughout the UI.",
|
||||
"user_disabled": "Účet deaktivován",
|
||||
"disabled_users_cannot_log_in_or_use_services": "Zakázaní uživatelé se nemohou přihlásit nebo používat služby.",
|
||||
"user_disabled_successfully": "Uživatel byl úspěšně deaktivován.",
|
||||
@@ -346,30 +347,36 @@
|
||||
"the_device_has_been_authorized": "Zařízení bylo autorizováno.",
|
||||
"enter_code_displayed_in_previous_step": "Zadejte kód, který byl zobrazen v předchozím kroku.",
|
||||
"authorize": "Autorizovat",
|
||||
"federated_client_credentials": "Federated Client Credentials",
|
||||
"federated_client_credentials_description": "Using federated client credentials, you can authenticate OIDC clients using JWT tokens issued by third-party authorities.",
|
||||
"add_federated_client_credential": "Add Federated Client Credential",
|
||||
"add_another_federated_client_credential": "Add another federated client credential",
|
||||
"federated_client_credentials": "Údaje o klientovi ve federaci",
|
||||
"federated_client_credentials_description": "Pomocí federovaných přihlašovacích údajů klienta můžete ověřit klienty OIDC pomocí JWT tokenů vydaných třetí stranou.",
|
||||
"add_federated_client_credential": "Přidat údaje federovaného klienta",
|
||||
"add_another_federated_client_credential": "Přidat dalšího federovaného klienta",
|
||||
"oidc_allowed_group_count": "Počet povolených skupin",
|
||||
"unrestricted": "Bez omezení",
|
||||
"show_advanced_options": "Show Advanced Options",
|
||||
"hide_advanced_options": "Hide Advanced Options",
|
||||
"oidc_data_preview": "OIDC Data Preview",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_different_users": "Preview the OIDC data that would be sent for different users",
|
||||
"show_advanced_options": "Zobrazit rozšířené možnosti",
|
||||
"hide_advanced_options": "Skrýt rozšířené rožnosti",
|
||||
"oidc_data_preview": "Náhled OIDC dat",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_different_users": "Náhled údajů OIDC, které by měly být odeslány pro různé uživatele",
|
||||
"id_token": "ID Token",
|
||||
"access_token": "Access Token",
|
||||
"userinfo": "Userinfo",
|
||||
"id_token_payload": "ID Token Payload",
|
||||
"access_token_payload": "Access Token Payload",
|
||||
"userinfo_endpoint_response": "Userinfo Endpoint Response",
|
||||
"copy": "Copy",
|
||||
"no_preview_data_available": "No preview data available",
|
||||
"copy_all": "Copy All",
|
||||
"preview": "Preview",
|
||||
"preview_for_user": "Preview for {name} ({email})",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Preview the OIDC data that would be sent for this user",
|
||||
"show": "Show",
|
||||
"select_an_option": "Select an option",
|
||||
"select_user": "Select User",
|
||||
"error": "Error"
|
||||
"copy": "Kopírovat",
|
||||
"no_preview_data_available": "Nejsou k dispozici žádná náhledová data",
|
||||
"copy_all": "Kopírovat vše",
|
||||
"preview": "Náhled",
|
||||
"preview_for_user": "Náhled pro {name} ({email})",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Náhled OIDC dat, která by byla odeslána pro uživatele",
|
||||
"show": "Zobrazit",
|
||||
"select_an_option": "Vyberte možnost",
|
||||
"select_user": "Vyberte uživatele",
|
||||
"error": "Chyba",
|
||||
"select_an_accent_color_to_customize_the_appearance_of_pocket_id": "Select an accent color to customize the appearance of Pocket ID.",
|
||||
"accent_color": "Accent Color",
|
||||
"custom_accent_color": "Custom Accent Color",
|
||||
"custom_accent_color_description": "Enter a custom color using valid CSS color formats (e.g., hex, rgb, hsl).",
|
||||
"color_value": "Color Value",
|
||||
"apply": "Apply"
|
||||
}
|
||||
|
||||
@@ -3,373 +3,380 @@
|
||||
"my_account": "Min konto",
|
||||
"logout": "Log ud",
|
||||
"confirm": "Bekræft",
|
||||
"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",
|
||||
"docs": "Dokumentation",
|
||||
"key": "Nøgle",
|
||||
"value": "Værdi",
|
||||
"remove_custom_claim": "Fjern brugerdefineret claim",
|
||||
"add_custom_claim": "Tilføj brugerdefineret claim",
|
||||
"add_another": "Tilføj endnu en",
|
||||
"select_a_date": "Vælg en dato",
|
||||
"select_file": "Vælg en fil",
|
||||
"profile_picture": "Profilbillede",
|
||||
"profile_picture_is_managed_by_ldap_server": "Profilbilledet administreres af LDAP-serveren og kan ikke ændres her.",
|
||||
"click_profile_picture_to_upload_custom": "Klik på profilbilledet for at uploade et brugerdefineret billede fra dine filer.",
|
||||
"image_should_be_in_format": "Billedet skal være i PNG eller JPEG-format.",
|
||||
"items_per_page": "Emner pr. side",
|
||||
"no_items_found": "Ingen emner fundet",
|
||||
"search": "Søg...",
|
||||
"expand_card": "Expand card",
|
||||
"expand_card": "Udvid kortet",
|
||||
"copied": "Kopieret",
|
||||
"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",
|
||||
"click_to_copy": "Klik for at kopiere",
|
||||
"something_went_wrong": "Noget gik galt",
|
||||
"go_back_to_home": "Gå tilbage til hjem",
|
||||
"dont_have_access_to_your_passkey": "Har du ikke adgang til din adgangsnøgle?",
|
||||
"login_background": "Log ind baggrund",
|
||||
"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.",
|
||||
"login_code": "Loginkode",
|
||||
"create_a_login_code_to_sign_in_without_a_passkey_once": "Opret en loginkode, som brugeren kan bruge til at logge ind uden en adgangsnøgle én gang.",
|
||||
"one_hour": "1 time",
|
||||
"twelve_hours": "12 timer",
|
||||
"one_day": "1 dag",
|
||||
"one_week": "1 uge",
|
||||
"one_month": "1 måned",
|
||||
"expiration": "Expiration",
|
||||
"expiration": "Udløbstid",
|
||||
"generate_code": "Generer kode",
|
||||
"name": "Navn",
|
||||
"browser_unsupported": "Browser unsupported",
|
||||
"this_browser_does_not_support_passkeys": "This browser doesn't support passkeys. Please use an 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 {appName} 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 {appName} 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 don't have access to your passkey, you can sign in using one of the following methods.",
|
||||
"use_your_passkey_instead": "Use your passkey instead?",
|
||||
"browser_unsupported": "Browseren understøttes ikke",
|
||||
"this_browser_does_not_support_passkeys": "Denne browser understøtter ikke adgangsnøgler. Benyt venligst en alternativ login metode.",
|
||||
"an_unknown_error_occurred": "En ukendt fejl opstod",
|
||||
"authentication_process_was_aborted": "Godkendelsesprocessen blev afbrudt",
|
||||
"error_occurred_with_authenticator": "Der opstod en fejl med godkendelsesenheden",
|
||||
"authenticator_does_not_support_discoverable_credentials": "Godkenderen understøtter ikke synlige legitimationsoplysninger",
|
||||
"authenticator_does_not_support_resident_keys": "Godkenderen understøtter ikke gemte nøgler",
|
||||
"passkey_was_previously_registered": "Denne adgangsnøgle er allerede registreret",
|
||||
"authenticator_does_not_support_any_of_the_requested_algorithms": "Godkenderen understøtter ikke nogen af de algoritmer, der anmodes om",
|
||||
"authenticator_timed_out": "Godkenderen overskred tidsgrænsen",
|
||||
"critical_error_occurred_contact_administrator": "En kritisk fejl opstod. Kontakt venligst din administrator.",
|
||||
"sign_in_to": "Log ind på {name}",
|
||||
"client_not_found": "Klient ikke fundet",
|
||||
"client_wants_to_access_the_following_information": "{client} ønsker at få adgang til følgende oplysninger:",
|
||||
"do_you_want_to_sign_in_to_client_with_your_app_name_account": "Vil du logge ind på {client} med din {appName}-konto?",
|
||||
"email": "E-mail",
|
||||
"view_your_email_address": "Se din e-mailadresse",
|
||||
"profile": "Profil",
|
||||
"view_your_profile_information": "Se dine profiloplysninger",
|
||||
"groups": "Grupper",
|
||||
"view_the_groups_you_are_a_member_of": "Se de grupper, du er medlem af",
|
||||
"cancel": "Annuller",
|
||||
"sign_in": "Log ind",
|
||||
"try_again": "Prøv igen",
|
||||
"client_logo": "Klientlogo",
|
||||
"sign_out": "Log ud",
|
||||
"do_you_want_to_sign_out_of_pocketid_with_the_account": "Vil du logge ud af {appName} med kontoen <b>{username}</b>?",
|
||||
"sign_in_to_appname": "Log ind på {appName}",
|
||||
"please_try_to_sign_in_again": "Prøv at logge ind igen.",
|
||||
"authenticate_yourself_with_your_passkey_to_access_the_admin_panel": "Bekræft din identitet med din adgangsnøgle for at få adgang til administrationspanelet.",
|
||||
"authenticate": "Bekræft identitet",
|
||||
"appname_setup": "Opsætning af {appName}",
|
||||
"please_try_again": "Prøv venligst igen.",
|
||||
"you_are_about_to_sign_in_to_the_initial_admin_account": "Du er ved at logge ind på den oprindelige administrator-konto. Enhver med dette link kan få adgang, indtil en adgangsnøgle tilføjes. Opsæt en adgangsnøgle hurtigst muligt for at forhindre uautoriseret adgang.",
|
||||
"continue": "Fortsæt",
|
||||
"alternative_sign_in": "Andre loginmetoder",
|
||||
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "Hvis du ikke har adgang til din adgangsnøgle, kan du logge ind med en af følgende metoder.",
|
||||
"use_your_passkey_instead": "Vil du i stedet bruge din adgangsnøgle?",
|
||||
"email_login": "E-mail 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.",
|
||||
"enter_a_login_code_to_sign_in": "Indtast en loginkode for at logge ind.",
|
||||
"request_a_login_code_via_email": "Anmod om en loginkode via e-mail.",
|
||||
"go_back": "Gå tilbage",
|
||||
"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.",
|
||||
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "En e-mail er blevet sendt til den angivne e-mailadresse, hvis den findes i systemet.",
|
||||
"enter_code": "Indtast kode",
|
||||
"enter_your_email_address_to_receive_an_email_with_a_login_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": "Indtast din e-mailadresse for at modtage en login-kode.\n",
|
||||
"your_email": "Din e-mail",
|
||||
"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",
|
||||
"submit": "Indsend",
|
||||
"enter_the_code_you_received_to_sign_in": "Indtast den kode, du har modtaget, for at logge ind.",
|
||||
"code": "Kode",
|
||||
"invalid_redirect_url": "Ugyldig redirect-URL",
|
||||
"audit_log": "Aktivitetslog",
|
||||
"users": "Brugere",
|
||||
"user_groups": "Brugergrupper",
|
||||
"oidc_clients": "OIDC Clients",
|
||||
"api_keys": "API Keys",
|
||||
"application_configuration": "Application Configuration",
|
||||
"oidc_clients": "OIDC-klienter",
|
||||
"api_keys": "API-nøgler",
|
||||
"application_configuration": "Applikationsindstillinger",
|
||||
"settings": "Indstillinger",
|
||||
"update_pocket_id": "Opdater Pocket ID",
|
||||
"powered_by": "Powered by",
|
||||
"see_your_account_activities_from_the_last_3_months": "See your account activities from the last 3 months.",
|
||||
"powered_by": "Drevet af",
|
||||
"see_your_account_activities_from_the_last_3_months": "Se dine kontoaktiviteter fra de sidste 3 måneder.",
|
||||
"time": "Tid",
|
||||
"event": "Event",
|
||||
"approximate_location": "Approximate Location",
|
||||
"event": "Hændelse",
|
||||
"approximate_location": "Omtrentlig placering",
|
||||
"ip_address": "IP-adresse",
|
||||
"device": "Enhed",
|
||||
"client": "Klient",
|
||||
"unknown": "Ukendt",
|
||||
"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.",
|
||||
"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",
|
||||
"configure_smtp_to_send_emails": "Enable email notifications to alert users when a login is detected from a new device or location.",
|
||||
"account_details_updated_successfully": "Kontodetaljer blev opdateret",
|
||||
"profile_picture_updated_successfully": "Profilbillede opdateret. Det kan tage et par minutter før ændringen vises.",
|
||||
"account_settings": "Kontoindstillinger",
|
||||
"passkey_missing": "Adgangsnøgle mangler",
|
||||
"please_provide_a_passkey_to_prevent_losing_access_to_your_account": "Tilføj en adgangsnøgle for at undgå at miste adgangen til din konto.",
|
||||
"single_passkey_configured": "Én adgangsnøgle er konfigureret",
|
||||
"it_is_recommended_to_add_more_than_one_passkey": "Det anbefales at tilføje mere end én adgangsnøgle for at undgå at miste adgangen til din konto.",
|
||||
"account_details": "Kontooplysninger",
|
||||
"passkeys": "Adgangsnøgler",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "Administrér dine adgangsnøgler, som du kan bruge til at godkende dig selv.",
|
||||
"add_passkey": "Tilføj adgangsnøgle",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "Opret en engangskode for at logge ind fra en anden enhed uden en adgangsnøgle.",
|
||||
"create": "Opret",
|
||||
"first_name": "Fornavn",
|
||||
"last_name": "Efternavn",
|
||||
"username": "Brugernavn",
|
||||
"save": "Gem",
|
||||
"username_can_only_contain": "Brugernavn må kun indeholde små bogstaver, tal, understregninger (_), punktummer (.), bindestreger (-) og @-tegn",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Log ind med nedenstående kode. Koden udløber om 15 minutter.",
|
||||
"or_visit": "eller besøg",
|
||||
"added_on": "Tilføjet den",
|
||||
"rename": "Omdøb",
|
||||
"delete": "Slet",
|
||||
"are_you_sure_you_want_to_delete_this_passkey": "Er du sikker på, at du vil slette denne adgangsnøgle?",
|
||||
"passkey_deleted_successfully": "Adgangsnøgle blev slettet",
|
||||
"delete_passkey_name": "Slet {passkeyName}",
|
||||
"passkey_name_updated_successfully": "Navnet på adgangsnøglen blev opdateret",
|
||||
"name_passkey": "Navngiv adgangsnøgle",
|
||||
"name_your_passkey_to_easily_identify_it_later": "Giv din adgangsnøgle et navn, så du nemt kan genkende den senere.",
|
||||
"create_api_key": "Opret API-nøgle",
|
||||
"add_a_new_api_key_for_programmatic_access": "Tilføj en ny API-nøgle til programmatisk adgang.",
|
||||
"add_api_key": "Tilføj API-nøgle",
|
||||
"manage_api_keys": "Administrér API-nøgler",
|
||||
"api_key_created": "API-nøgle oprettet",
|
||||
"for_security_reasons_this_key_will_only_be_shown_once": "Af sikkerhedshensyn vises denne nøgle kun én gang. Gem den et sikkert sted.",
|
||||
"description": "Beskrivelse",
|
||||
"api_key": "API-nøgle",
|
||||
"close": "Luk",
|
||||
"name_to_identify_this_api_key": "Navn til at identificere denne API-nøgle.",
|
||||
"expires_at": "Udløber den",
|
||||
"when_this_api_key_will_expire": "Hvornår denne API-nøgle udløber.",
|
||||
"optional_description_to_help_identify_this_keys_purpose": "Valgfri beskrivelse for at identificere nøglens formål.",
|
||||
"expiration_date_must_be_in_the_future": "Udløbsdatoen skal ligge i fremtiden",
|
||||
"revoke_api_key": "Tilbagekald API-nøgle",
|
||||
"never": "Aldrig",
|
||||
"revoke": "Tilbagekald",
|
||||
"api_key_revoked_successfully": "API-nøgle blev tilbagekaldt",
|
||||
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "Er du sikker på, at du vil tilbagekalde API-nøglen \"{apiKeyName}\"? Dette vil afbryde alle integrationer, der bruger nøglen.",
|
||||
"last_used": "Sidst brugt",
|
||||
"actions": "Handlinger",
|
||||
"images_updated_successfully": "Billeder blev opdateret",
|
||||
"general": "Generelt",
|
||||
"configure_smtp_to_send_emails": "Aktivér e-mailnotifikationer for at advare brugere, når et login registreres fra en ny enhed eller placering.",
|
||||
"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.",
|
||||
"emai_login_code_requested_by_user": "Email Login Code Requested by User",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to bypass passkeys by requesting a login code sent to their email. This reduces the security significantly as anyone with access to the user's email can gain entry.",
|
||||
"email_login_code_from_admin": "Email Login Code from Admin",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "Allows an admin to send a login code to the user via email.",
|
||||
"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",
|
||||
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Konfigurer LDAP-indstillinger for at synkronisere brugere og grupper fra en LDAP-server",
|
||||
"images": "Billeder",
|
||||
"update": "Opdater",
|
||||
"email_configuration_updated_successfully": "E-mailkonfiguration blev opdateret",
|
||||
"save_changes_question": "Gem ændringer?",
|
||||
"you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "Du skal gemme ændringerne, før du kan sende en test-e-mail. Vil du gemme nu?",
|
||||
"save_and_send": "Gem og send",
|
||||
"test_email_sent_successfully": "Test-e-mail blev sendt til din e-mailadresse.",
|
||||
"failed_to_send_test_email": "Kunne ikke sende test-e-mail. Tjek serverloggen for flere oplysninger.",
|
||||
"smtp_configuration": "SMTP-konfiguration",
|
||||
"smtp_host": "SMTP-vært",
|
||||
"smtp_port": "SMTP-port",
|
||||
"smtp_user": "SMTP-bruger",
|
||||
"smtp_password": "SMTP-adgangskode",
|
||||
"smtp_from": "SMTP-afsender",
|
||||
"smtp_tls_option": "SMTP TLS-indstilling",
|
||||
"email_tls_option": "E-mail TLS-indstilling",
|
||||
"skip_certificate_verification": "Spring certifikatverificering over",
|
||||
"this_can_be_useful_for_selfsigned_certificates": "Dette kan være nyttigt ved selvsignerede certifikater.",
|
||||
"enabled_emails": "Aktiverede e-mails",
|
||||
"email_login_notification": "Notifikation om login via e-mail",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Send en e-mail til brugeren, når de logger ind fra en ny enhed.",
|
||||
"emai_login_code_requested_by_user": "Tillad brugere at anmode om login-koder via e-mail",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Tillader brugere at omgå adgangsnøgler ved at anmode om en login-kode sendt til deres e-mail. Dette reducerer sikkerheden væsentligt, da enhver med adgang til brugerens e-mail kan få adgang.\n",
|
||||
"email_login_code_from_admin": "Tillad administratorer at sende login-koder via e-mail",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "Giver en administrator mulighed for at sende en login-kode til brugeren via e-mail.",
|
||||
"send_test_email": "Send test-e-mail",
|
||||
"application_configuration_updated_successfully": "Applikationsindstillinger blev opdateret",
|
||||
"application_name": "Applikationsnavn",
|
||||
"session_duration": "Sessionsvarighed",
|
||||
"the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again": "Varighed i minutter før brugeren skal logge ind igen.",
|
||||
"enable_self_account_editing": "Aktivér redigering af egen konto",
|
||||
"whether_the_users_should_be_able_to_edit_their_own_account_details": "Om brugere må redigere deres egne kontooplysninger.",
|
||||
"emails_verified": "E-mailadresser verificeret",
|
||||
"whether_the_users_email_should_be_marked_as_verified_for_the_oidc_clients": "Om brugerens e-mail skal markeres som verificeret for OIDC-klienter.",
|
||||
"ldap_configuration_updated_successfully": "LDAP-konfiguration blev opdateret",
|
||||
"ldap_disabled_successfully": "LDAP blev deaktiveret",
|
||||
"ldap_sync_finished": "LDAP-synkronisering fuldført",
|
||||
"client_configuration": "Klientkonfiguration",
|
||||
"ldap_url": "LDAP-URL",
|
||||
"ldap_bind_dn": "LDAP Bind DN",
|
||||
"ldap_bind_password": "LDAP Bind Password",
|
||||
"ldap_bind_password": "LDAP-bindingsadgangskode",
|
||||
"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 have lost it.",
|
||||
"add": "Add",
|
||||
"callback_urls": "Callback URLs",
|
||||
"logout_callback_urls": "Logout Callback URLs",
|
||||
"public_client": "Public Client",
|
||||
"public_clients_description": "Public clients do not have a client secret. They are designed for mobile, web, and native applications where secrets cannot be securely stored.",
|
||||
"user_search_filter": "Brugersøgningsfilter",
|
||||
"the_search_filter_to_use_to_search_or_sync_users": "Søgefilteret der bruges til at finde eller synkronisere brugere.",
|
||||
"groups_search_filter": "Gruppesøgningsfilter",
|
||||
"the_search_filter_to_use_to_search_or_sync_groups": "Søgefilteret der bruges til at finde eller synkronisere grupper.",
|
||||
"attribute_mapping": "Attributtilknytning",
|
||||
"user_unique_identifier_attribute": "Unik brugeridentifikator-attribut",
|
||||
"the_value_of_this_attribute_should_never_change": "Værdien af denne attribut bør aldrig ændres.",
|
||||
"username_attribute": "Brugernavn-attribut",
|
||||
"user_mail_attribute": "E-mail-attribut",
|
||||
"user_first_name_attribute": "Fornavns-attribut",
|
||||
"user_last_name_attribute": "Efternavns-attribut",
|
||||
"user_profile_picture_attribute": "Profilbilled-attribut",
|
||||
"the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image": "Værdien af denne attribut kan være en URL, en binær fil eller et base64-kodet billede.",
|
||||
"group_members_attribute": "Gruppemedlems-attribut",
|
||||
"the_attribute_to_use_for_querying_members_of_a_group": "Attributten der bruges til at hente gruppemedlemmer.",
|
||||
"group_unique_identifier_attribute": "Unik gruppe-ID-attribut",
|
||||
"group_name_attribute": "Gruppenavns-attribut",
|
||||
"admin_group_name": "Administratorgruppe-navn",
|
||||
"members_of_this_group_will_have_admin_privileges_in_pocketid": "Medlemmer af denne gruppe får administratorrettigheder i Pocket ID.",
|
||||
"disable": "Deaktivér",
|
||||
"sync_now": "Synkronisér nu",
|
||||
"enable": "Aktivér",
|
||||
"user_created_successfully": "Bruger blev oprettet",
|
||||
"create_user": "Opret bruger",
|
||||
"add_a_new_user_to_appname": "Tilføj en ny bruger til {appName}",
|
||||
"add_user": "Tilføj bruger",
|
||||
"manage_users": "Administrér brugere",
|
||||
"admin_privileges": "Administratorrettigheder",
|
||||
"admins_have_full_access_to_the_admin_panel": "Administratorer har fuld adgang til administratorpanelet.",
|
||||
"delete_firstname_lastname": "Slet {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_delete_this_user": "Er du sikker på, at du vil slette denne bruger?",
|
||||
"user_deleted_successfully": "Brugeren blev slettet",
|
||||
"role": "Rolle",
|
||||
"source": "Kilde",
|
||||
"admin": "Administrator",
|
||||
"user": "Bruger",
|
||||
"local": "Lokal",
|
||||
"toggle_menu": "Åbn/luk menu",
|
||||
"edit": "Redigér",
|
||||
"user_groups_updated_successfully": "Brugergrupper blev opdateret",
|
||||
"user_updated_successfully": "Bruger blev opdateret",
|
||||
"custom_claims_updated_successfully": "Brugerdefinerede claims blev opdateret",
|
||||
"back": "Tilbage",
|
||||
"user_details_firstname_lastname": "Brugeroplysninger for {firstName} {lastName}",
|
||||
"manage_which_groups_this_user_belongs_to": "Administrér hvilke grupper denne bruger tilhører.",
|
||||
"custom_claims": "Brugerdefinerede claims",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user": "Brugerdefinerede claims er nøgle-værdi-par, der kan bruges til at gemme yderligere information om en bruger. Disse claims vil blive inkluderet i ID-tokenet, hvis scopen ’profile’ er anmodet.",
|
||||
"user_group_created_successfully": "Brugergruppe blev oprettet",
|
||||
"create_user_group": "Opret brugergruppe",
|
||||
"create_a_new_group_that_can_be_assigned_to_users": "Opret en ny gruppe, der kan tildeles brugere.",
|
||||
"add_group": "Tilføj gruppe",
|
||||
"manage_user_groups": "Administrér brugergrupper",
|
||||
"friendly_name": "Kaldenavn",
|
||||
"name_that_will_be_displayed_in_the_ui": "Navn der vises i brugerfladen",
|
||||
"name_that_will_be_in_the_groups_claim": "Navn der vises i “groups”-claimet",
|
||||
"delete_name": "Slet {name}",
|
||||
"are_you_sure_you_want_to_delete_this_user_group": "Er du sikker på, at du vil slette denne brugergruppe?",
|
||||
"user_group_deleted_successfully": "Brugergruppe blev slettet",
|
||||
"user_count": "Antal brugere",
|
||||
"user_group_updated_successfully": "Brugergruppe blev opdateret",
|
||||
"users_updated_successfully": "Brugere blev opdateret",
|
||||
"user_group_details_name": "Detaljer for brugergruppe {name}",
|
||||
"assign_users_to_this_group": "Tildel brugere til denne gruppe.",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized": "Brugerdefinerede claims er nøgle-værdi-par, der bruges til at gemme yderligere information om en bruger. Disse claims vil blive inkluderet i ID-tokenet, hvis scopen ’profile’ anmodes. Brugerdefinerede claims defineret direkte på brugeren har prioritet, hvis der opstår konflikter.",
|
||||
"oidc_client_created_successfully": "OIDC-klient blev oprettet",
|
||||
"create_oidc_client": "Opret OIDC-klient",
|
||||
"add_a_new_oidc_client_to_appname": "Tilføj en ny OIDC-klient til {appName}",
|
||||
"add_oidc_client": "Tilføj OIDC-klient",
|
||||
"manage_oidc_clients": "Administrér OIDC-klienter",
|
||||
"one_time_link": "Engangslink",
|
||||
"use_this_link_to_sign_in_once": "Brug dette link til at logge ind én gang. Det er nødvendigt for brugere, som endnu ikke har tilføjet en adgangsnøgle eller har mistet den.",
|
||||
"add": "Tilføj",
|
||||
"callback_urls": "Callback-URL’er",
|
||||
"logout_callback_urls": "Logout Callback-URL’er",
|
||||
"public_client": "Public-klient",
|
||||
"public_clients_description": "Public-klienter har ikke en klienthemmelighed. De er designet til mobil-, web- og native-apps, hvor hemmeligheder ikke kan opbevares sikkert.",
|
||||
"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",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange er en sikkerhedsfunktion, der beskytter mod CSRF- og authorization code-angreb.",
|
||||
"name_logo": "Logo for {name}",
|
||||
"change_logo": "Skift logo",
|
||||
"upload_logo": "Upload logo",
|
||||
"remove_logo": "Fjern logo",
|
||||
"are_you_sure_you_want_to_delete_this_oidc_client": "Er du sikker på, at du vil slette denne OIDC-klient?",
|
||||
"oidc_client_deleted_successfully": "OIDC-klient blev slettet",
|
||||
"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}",
|
||||
"enabled": "Aktiveret",
|
||||
"disabled": "Deaktiveret",
|
||||
"oidc_client_updated_successfully": "OIDC-klient blev opdateret",
|
||||
"create_new_client_secret": "Opret ny client secret",
|
||||
"are_you_sure_you_want_to_create_a_new_client_secret": "Vil du oprette en ny client secret? Den gamle bliver ugyldig og kan ikke længere bruges.",
|
||||
"generate": "Generér",
|
||||
"new_client_secret_created_successfully": "Ny client secret blev oprettet",
|
||||
"allowed_user_groups_updated_successfully": "Tilladte brugergrupper blev opdateret",
|
||||
"oidc_client_name": "OIDC-klient {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.",
|
||||
"show_more_details": "Vis flere detaljer",
|
||||
"allowed_user_groups": "Tilladte brugergrupper",
|
||||
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "Tilføj brugergrupper til denne klient for at begrænse adgangen til brugere i disse grupper. Hvis ingen brugergrupper er valgt, vil alle brugere have adgang til klienten.",
|
||||
"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.",
|
||||
"personal": "Personal",
|
||||
"light_mode_logo": "Logo til lys tilstand",
|
||||
"dark_mode_logo": "Logo til mørk tilstand",
|
||||
"background_image": "Baggrundsbillede",
|
||||
"language": "Sprog",
|
||||
"reset_profile_picture_question": "Nulstil profilbillede?",
|
||||
"this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default": "Dette vil fjerne det uploadede billede og nulstille profilbilledet til standard. Vil du fortsætte?",
|
||||
"reset": "Nulstil",
|
||||
"reset_to_default": "Nulstil til standard",
|
||||
"profile_picture_has_been_reset": "Profilbilledet er blevet nulstillet. Det kan tage et par minutter at opdatere.",
|
||||
"select_the_language_you_want_to_use": "Vælg det sprog, du vil bruge. Nogle sprog er muligvis ikke fuldt oversat.",
|
||||
"personal": "Personlig",
|
||||
"global": "Global",
|
||||
"all_users": "All Users",
|
||||
"all_events": "All Events",
|
||||
"all_clients": "All Clients",
|
||||
"global_audit_log": "Global Audit Log",
|
||||
"see_all_account_activities_from_the_last_3_months": "See all user activity for the last 3 months.",
|
||||
"token_sign_in": "Token Sign In",
|
||||
"client_authorization": "Client Authorization",
|
||||
"new_client_authorization": "New Client Authorization",
|
||||
"disable_animations": "Disable Animations",
|
||||
"turn_off_ui_animations": "Turn off all animations throughout the Admin UI.",
|
||||
"user_disabled": "Account Disabled",
|
||||
"disabled_users_cannot_log_in_or_use_services": "Disabled users cannot log in or use services.",
|
||||
"user_disabled_successfully": "User has been disabled successfully.",
|
||||
"user_enabled_successfully": "User has been enabled successfully.",
|
||||
"all_users": "Alle brugere",
|
||||
"all_events": "Alle hændelser",
|
||||
"all_clients": "Alle klienter",
|
||||
"global_audit_log": "Global aktivitetslog",
|
||||
"see_all_account_activities_from_the_last_3_months": "Se al brugeraktivitet for de seneste 3 måneder.",
|
||||
"token_sign_in": "Token-login",
|
||||
"client_authorization": "Godkendelse af klient",
|
||||
"new_client_authorization": "Ny klientgodkendelse",
|
||||
"disable_animations": "Deaktiver animationer",
|
||||
"turn_off_ui_animations": "Slå animationer fra for hele brugergrænsefladen.",
|
||||
"user_disabled": "Konto deaktiveret",
|
||||
"disabled_users_cannot_log_in_or_use_services": "Deaktiverede brugere kan ikke logge ind eller bruge tjenester.",
|
||||
"user_disabled_successfully": "Brugeren blev deaktiveret.",
|
||||
"user_enabled_successfully": "Brugeren blev aktiveret.",
|
||||
"status": "Status",
|
||||
"disable_firstname_lastname": "Disable {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_disable_this_user": "Are you sure you want to disable this user? They will not be able to log in or access any services.",
|
||||
"ldap_soft_delete_users": "Keep disabled users from LDAP.",
|
||||
"ldap_soft_delete_users_description": "When enabled, users removed from LDAP will be disabled rather than deleted from the system.",
|
||||
"login_code_email_success": "The login code has been sent to the user.",
|
||||
"send_email": "Send Email",
|
||||
"show_code": "Show Code",
|
||||
"callback_url_description": "URL(s) provided by your client. Will be automatically added if left blank. Wildcards (*) are supported, but best avoided for better security.",
|
||||
"logout_callback_url_description": "URL(s) provided by your client for logout. Wildcards (*) are supported, but best avoided for better security.",
|
||||
"api_key_expiration": "API Key Expiration",
|
||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Send an email to the user when their API key is about to expire.",
|
||||
"authorize_device": "Authorize Device",
|
||||
"the_device_has_been_authorized": "The device has been authorized.",
|
||||
"enter_code_displayed_in_previous_step": "Enter the code that was displayed in the previous step.",
|
||||
"authorize": "Authorize",
|
||||
"federated_client_credentials": "Federated Client Credentials",
|
||||
"federated_client_credentials_description": "Using federated client credentials, you can authenticate OIDC clients using JWT tokens issued by third-party authorities.",
|
||||
"add_federated_client_credential": "Add Federated Client Credential",
|
||||
"add_another_federated_client_credential": "Add another federated client credential",
|
||||
"oidc_allowed_group_count": "Allowed Group Count",
|
||||
"unrestricted": "Unrestricted",
|
||||
"show_advanced_options": "Show Advanced Options",
|
||||
"hide_advanced_options": "Hide Advanced Options",
|
||||
"oidc_data_preview": "OIDC Data Preview",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_different_users": "Preview the OIDC data that would be sent for different users",
|
||||
"id_token": "ID Token",
|
||||
"access_token": "Access Token",
|
||||
"userinfo": "Userinfo",
|
||||
"id_token_payload": "ID Token Payload",
|
||||
"access_token_payload": "Access Token Payload",
|
||||
"userinfo_endpoint_response": "Userinfo Endpoint Response",
|
||||
"copy": "Copy",
|
||||
"no_preview_data_available": "No preview data available",
|
||||
"copy_all": "Copy All",
|
||||
"preview": "Preview",
|
||||
"preview_for_user": "Preview for {name} ({email})",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Preview the OIDC data that would be sent for this user",
|
||||
"show": "Show",
|
||||
"select_an_option": "Select an option",
|
||||
"select_user": "Select User",
|
||||
"error": "Error"
|
||||
"disable_firstname_lastname": "Deaktiver {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_disable_this_user": "Er du sikker på, at du vil deaktivere denne bruger? Brugeren vil ikke kunne logge ind eller få adgang til tjenester.",
|
||||
"ldap_soft_delete_users": "Behold deaktiverede brugere i LDAP.",
|
||||
"ldap_soft_delete_users_description": "Når aktiveret, vil brugere fjernet fra LDAP blive deaktiveret i stedet for at blive slettet fra systemet.",
|
||||
"login_code_email_success": "Loginkoden er sendt til brugeren.",
|
||||
"send_email": "Send e-mail",
|
||||
"show_code": "Vis kode",
|
||||
"callback_url_description": "En eller flere URL’er angivet af din klient. Tilføjes automatisk, hvis feltet er tomt. Wildcards (*) understøttes, men bør undgås af hensyn til sikkerheden.",
|
||||
"logout_callback_url_description": "En eller flere URL’er angivet af din klient til logout. Wildcards (*) understøttes, men bør undgås af hensyn til sikkerheden.",
|
||||
"api_key_expiration": "Udløb af API-nøgle",
|
||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Send en e-mail til brugeren, når deres API-nøgle er ved at udløbe.",
|
||||
"authorize_device": "Godkend enhed",
|
||||
"the_device_has_been_authorized": "Enheden er godkendt.",
|
||||
"enter_code_displayed_in_previous_step": "Indtast koden, der blev vist i det forrige trin.",
|
||||
"authorize": "Godkend",
|
||||
"federated_client_credentials": "Federated klientlegitimationsoplysninger",
|
||||
"federated_client_credentials_description": "Ved hjælp af federated klientlegitimationsoplysninger kan du godkende OIDC-klienter med JWT-tokens udstedt af tredjepartsudbydere.",
|
||||
"add_federated_client_credential": "Tilføj federated klientlegitimation",
|
||||
"add_another_federated_client_credential": "Tilføj endnu en federated klientlegitimation",
|
||||
"oidc_allowed_group_count": "Tilladt antal grupper",
|
||||
"unrestricted": "Ubegrænset",
|
||||
"show_advanced_options": "Vis avancerede indstillinger",
|
||||
"hide_advanced_options": "Skjul avancerede indstillinger",
|
||||
"oidc_data_preview": "Forhåndsvisning af OIDC-data",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_different_users": "Forhåndsvis OIDC-data, der ville blive sendt for forskellige brugere",
|
||||
"id_token": "ID-token",
|
||||
"access_token": "Adgangstoken",
|
||||
"userinfo": "Brugerinfo",
|
||||
"id_token_payload": "ID-token-payload",
|
||||
"access_token_payload": "Adgangstoken-payload",
|
||||
"userinfo_endpoint_response": "Svar fra brugerinfo-endpoint",
|
||||
"copy": "Kopiér",
|
||||
"no_preview_data_available": "Ingen forhåndsvisningsdata tilgængelig",
|
||||
"copy_all": "Kopiér alt",
|
||||
"preview": "Forhåndsvisning",
|
||||
"preview_for_user": "Forhåndsvisning for {name} ({email})",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Forhåndsvis OIDC-data, der ville blive sendt for denne bruger",
|
||||
"show": "Vis",
|
||||
"select_an_option": "Vælg en indstilling",
|
||||
"select_user": "Vælg en bruger",
|
||||
"error": "Fejl",
|
||||
"select_an_accent_color_to_customize_the_appearance_of_pocket_id": "Vælg en accentfarve for at tilpasse udseendet af Pocket ID.",
|
||||
"accent_color": "Accentfarve",
|
||||
"custom_accent_color": "Brugerdefineret accentfarve",
|
||||
"custom_accent_color_description": "Indtast en brugerdefineret farve i et gyldigt CSS-format (f.eks. hex, rgb, hsl).",
|
||||
"color_value": "Farveværdi",
|
||||
"apply": "Anvend"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"my_account": "Mein Konto",
|
||||
"logout": "Abmelden",
|
||||
"confirm": "Bestätigen",
|
||||
"docs": "Dokumentation",
|
||||
"key": "Schlüssel",
|
||||
"value": "Wert",
|
||||
"remove_custom_claim": "Benutzerdefinierten Claim entfernen",
|
||||
@@ -35,7 +36,7 @@
|
||||
"expiration": "Ablaufdatum",
|
||||
"generate_code": "Code erzeugen",
|
||||
"name": "Name",
|
||||
"browser_unsupported": "Browser nicht unterstützt",
|
||||
"browser_unsupported": "Browser wird 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",
|
||||
@@ -179,7 +180,7 @@
|
||||
"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.",
|
||||
"emai_login_code_requested_by_user": "E-Mail-Logincode angefordert vom Benutzer",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Ermöglicht Benutzern, den Passkey zu umgehen, indem sie das Senden eines Logincodes an ihre E-Mail-Adresse anfordern. Dies reduziert die Sicherheit erheblich, da jeder, der Zugriff auf die E-Mail des Benutzers hat, Zugang bekommen kann.",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Ermöglicht Benutzern Passkeys zu umgehen, indem sie einen Login-Code anfordern, der an ihre E-Mail gesendet wurde. Dies verringert die Sicherheit erheblich, da jeder, der Zugriff auf die E-Mail des Benutzers hat, Zugang erhalten kann.",
|
||||
"email_login_code_from_admin": "E-Mail-Logincode von Administratoren",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "Erlaube Administratoren das Senden von Logincodes an den Nutzer via E-Mail.",
|
||||
"send_test_email": "Test-E-Mail senden",
|
||||
@@ -233,7 +234,7 @@
|
||||
"user_deleted_successfully": "Benutzer erfolgreich gelöscht",
|
||||
"role": "Rolle",
|
||||
"source": "Quelle",
|
||||
"admin": "Admin",
|
||||
"admin": "Administrator",
|
||||
"user": "Benutzer",
|
||||
"local": "Lokal",
|
||||
"toggle_menu": "Menü umschalten",
|
||||
@@ -352,24 +353,30 @@
|
||||
"add_another_federated_client_credential": "Add another federated client credential",
|
||||
"oidc_allowed_group_count": "Erlaubte Gruppenanzahl",
|
||||
"unrestricted": "Uneingeschränkt",
|
||||
"show_advanced_options": "Show Advanced Options",
|
||||
"hide_advanced_options": "Hide Advanced Options",
|
||||
"oidc_data_preview": "OIDC Data Preview",
|
||||
"show_advanced_options": "Erweiterte Optionen anzeigen",
|
||||
"hide_advanced_options": "Erweiterte Optionen ausblenden",
|
||||
"oidc_data_preview": "OIDC Daten-Vorschau",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_different_users": "Preview the OIDC data that would be sent for different users",
|
||||
"id_token": "ID Token",
|
||||
"access_token": "Access Token",
|
||||
"userinfo": "Userinfo",
|
||||
"id_token_payload": "ID Token Payload",
|
||||
"access_token_payload": "Access Token Payload",
|
||||
"userinfo_endpoint_response": "Userinfo Endpoint Response",
|
||||
"copy": "Copy",
|
||||
"no_preview_data_available": "No preview data available",
|
||||
"copy_all": "Copy All",
|
||||
"preview": "Preview",
|
||||
"preview_for_user": "Preview for {name} ({email})",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Preview the OIDC data that would be sent for this user",
|
||||
"show": "Show",
|
||||
"select_an_option": "Select an option",
|
||||
"select_user": "Select User",
|
||||
"error": "Error"
|
||||
"userinfo_endpoint_response": "Userinfo Endpoint Antwort",
|
||||
"copy": "Kopieren",
|
||||
"no_preview_data_available": "Keine Vorschaudaten verfügbar",
|
||||
"copy_all": "Alles kopieren",
|
||||
"preview": "Vorschau",
|
||||
"preview_for_user": "Vorschau für {name} ({email})",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Vorschau der OIDC-Daten, für diesen Benutzer",
|
||||
"show": "Anzeigen",
|
||||
"select_an_option": "Wähle eine Option",
|
||||
"select_user": "Benutzer auswählen",
|
||||
"error": "Fehler",
|
||||
"select_an_accent_color_to_customize_the_appearance_of_pocket_id": "Select an accent color to customize the appearance of Pocket ID.",
|
||||
"accent_color": "Accent Color",
|
||||
"custom_accent_color": "Custom Accent Color",
|
||||
"custom_accent_color_description": "Enter a custom color using valid CSS color formats (e.g., hex, rgb, hsl).",
|
||||
"color_value": "Color Value",
|
||||
"apply": "Apply"
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
"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.",
|
||||
"emai_login_code_requested_by_user": "Email Login Code Requested by User",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to bypass passkeys by requesting a login code sent to their email. This reduces the security significantly as anyone with access to the user's email can gain entry.",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to bypass passkeys by requesting a login code sent to their email. This significantly reduces security as anyone with access to the user's email can gain entry.",
|
||||
"email_login_code_from_admin": "Email Login Code from Admin",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "Allows an admin to send a login code to the user via email.",
|
||||
"send_test_email": "Send test email",
|
||||
@@ -310,7 +310,7 @@
|
||||
"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?",
|
||||
"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.",
|
||||
@@ -320,13 +320,14 @@
|
||||
"all_users": "All Users",
|
||||
"all_events": "All Events",
|
||||
"all_clients": "All Clients",
|
||||
"all_locations": "All Locations",
|
||||
"global_audit_log": "Global Audit Log",
|
||||
"see_all_account_activities_from_the_last_3_months": "See all user activity for the last 3 months.",
|
||||
"token_sign_in": "Token Sign In",
|
||||
"client_authorization": "Client Authorization",
|
||||
"new_client_authorization": "New Client Authorization",
|
||||
"disable_animations": "Disable Animations",
|
||||
"turn_off_ui_animations": "Turn off animations troughout the UI.",
|
||||
"turn_off_ui_animations": "Turn off animations throughout the UI.",
|
||||
"user_disabled": "Account Disabled",
|
||||
"disabled_users_cannot_log_in_or_use_services": "Disabled users cannot log in or use services.",
|
||||
"user_disabled_successfully": "User has been disabled successfully.",
|
||||
@@ -372,5 +373,11 @@
|
||||
"show": "Show",
|
||||
"select_an_option": "Select an option",
|
||||
"select_user": "Select User",
|
||||
"error": "Error"
|
||||
"error": "Error",
|
||||
"select_an_accent_color_to_customize_the_appearance_of_pocket_id": "Select an accent color to customize the appearance of Pocket ID.",
|
||||
"accent_color": "Accent Color",
|
||||
"custom_accent_color": "Custom Accent Color",
|
||||
"custom_accent_color_description": "Enter a custom color using valid CSS color formats (e.g., hex, rgb, hsl).",
|
||||
"color_value": "Color Value",
|
||||
"apply": "Apply"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"my_account": "Mi Cuenta",
|
||||
"logout": "Cerrar sesión",
|
||||
"confirm": "Confirmar",
|
||||
"docs": "Docs",
|
||||
"key": "Clave",
|
||||
"value": "Valor",
|
||||
"remove_custom_claim": "Eliminar reclamo personalizado",
|
||||
@@ -179,7 +180,7 @@
|
||||
"email_login_notification": "Email Login Notification",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Enviar un correo electrónico al usuario cuando inicie sesión desde un dispositivo nuevo.",
|
||||
"emai_login_code_requested_by_user": "Código de acceso solicitado por el usuario",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Permite a los usuarios saltarse las claves de acceso solicitando un código de acceso enviado a su correo electrónico. Esto reduce la seguridad significativamente, ya que cualquiera con acceso al correo electrónico del usuario puede obtener acceso.",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to bypass passkeys by requesting a login code sent to their email. This significantly reduces security as anyone with access to the user's email can gain entry.",
|
||||
"email_login_code_from_admin": "Email Login Code from Admin",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "Permite a un administrador enviar un código de acceso al usuario por correo electrónico.",
|
||||
"send_test_email": "Enviar correo de prueba",
|
||||
@@ -309,7 +310,7 @@
|
||||
"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?",
|
||||
"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.",
|
||||
@@ -325,7 +326,7 @@
|
||||
"client_authorization": "Client Authorization",
|
||||
"new_client_authorization": "New Client Authorization",
|
||||
"disable_animations": "Disable Animations",
|
||||
"turn_off_ui_animations": "Turn off all animations throughout the Admin UI.",
|
||||
"turn_off_ui_animations": "Turn off animations throughout the UI.",
|
||||
"user_disabled": "Account Disabled",
|
||||
"disabled_users_cannot_log_in_or_use_services": "Disabled users cannot log in or use services.",
|
||||
"user_disabled_successfully": "User has been disabled successfully.",
|
||||
@@ -371,5 +372,11 @@
|
||||
"show": "Show",
|
||||
"select_an_option": "Select an option",
|
||||
"select_user": "Select User",
|
||||
"error": "Error"
|
||||
"error": "Error",
|
||||
"select_an_accent_color_to_customize_the_appearance_of_pocket_id": "Select an accent color to customize the appearance of Pocket ID.",
|
||||
"accent_color": "Accent Color",
|
||||
"custom_accent_color": "Custom Accent Color",
|
||||
"custom_accent_color_description": "Enter a custom color using valid CSS color formats (e.g., hex, rgb, hsl).",
|
||||
"color_value": "Color Value",
|
||||
"apply": "Apply"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"my_account": "Mon compte",
|
||||
"logout": "Déconnexion",
|
||||
"confirm": "Confirmer",
|
||||
"docs": "Docs",
|
||||
"key": "Clé",
|
||||
"value": "Valeur",
|
||||
"remove_custom_claim": "Remove custom claim",
|
||||
@@ -179,7 +180,7 @@
|
||||
"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.",
|
||||
"emai_login_code_requested_by_user": "Email Login Code Requested by User",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to bypass passkeys by requesting a login code sent to their email. This reduces the security significantly as anyone with access to the user's email can gain entry.",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to bypass passkeys by requesting a login code sent to their email. This significantly reduces security as anyone with access to the user's email can gain entry.",
|
||||
"email_login_code_from_admin": "Email Login Code from Admin",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "Allows an admin to send a login code to the user via email.",
|
||||
"send_test_email": "Send test email",
|
||||
@@ -309,7 +310,7 @@
|
||||
"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 ?",
|
||||
"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": "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.",
|
||||
@@ -325,7 +326,7 @@
|
||||
"client_authorization": "Client Authorization",
|
||||
"new_client_authorization": "New Client Authorization",
|
||||
"disable_animations": "Disable Animations",
|
||||
"turn_off_ui_animations": "Turn off all animations throughout the Admin UI.",
|
||||
"turn_off_ui_animations": "Turn off animations throughout the UI.",
|
||||
"user_disabled": "Account Disabled",
|
||||
"disabled_users_cannot_log_in_or_use_services": "Disabled users cannot log in or use services.",
|
||||
"user_disabled_successfully": "User has been disabled successfully.",
|
||||
@@ -371,5 +372,11 @@
|
||||
"show": "Show",
|
||||
"select_an_option": "Select an option",
|
||||
"select_user": "Select User",
|
||||
"error": "Error"
|
||||
"error": "Error",
|
||||
"select_an_accent_color_to_customize_the_appearance_of_pocket_id": "Select an accent color to customize the appearance of Pocket ID.",
|
||||
"accent_color": "Accent Color",
|
||||
"custom_accent_color": "Custom Accent Color",
|
||||
"custom_accent_color_description": "Enter a custom color using valid CSS color formats (e.g., hex, rgb, hsl).",
|
||||
"color_value": "Color Value",
|
||||
"apply": "Apply"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"my_account": "Il mio account",
|
||||
"logout": "Disconnetti",
|
||||
"confirm": "Conferma",
|
||||
"docs": "Documentazione",
|
||||
"key": "Chiave",
|
||||
"value": "Valore",
|
||||
"remove_custom_claim": "Rimuovi attributo personalizzato",
|
||||
@@ -325,7 +326,7 @@
|
||||
"client_authorization": "Autorizzazione client",
|
||||
"new_client_authorization": "Nuova autorizzazione client",
|
||||
"disable_animations": "Disabilita animazioni",
|
||||
"turn_off_ui_animations": "Disattiva tutte le animazioni nell'interfaccia di amministrazione.",
|
||||
"turn_off_ui_animations": "Disattiva tutte le animazioni della UI.",
|
||||
"user_disabled": "Account disabilitato",
|
||||
"disabled_users_cannot_log_in_or_use_services": "Gli utenti disabilitati non possono accedere o utilizzare i servizi.",
|
||||
"user_disabled_successfully": "Utente disabilitato con successo.",
|
||||
@@ -354,22 +355,28 @@
|
||||
"unrestricted": "Illimitati",
|
||||
"show_advanced_options": "Mostra Opzioni Avanzate",
|
||||
"hide_advanced_options": "Nascondi Opzioni Avanzate",
|
||||
"oidc_data_preview": "OIDC Data Preview",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_different_users": "Preview the OIDC data that would be sent for different users",
|
||||
"oidc_data_preview": "Anteprima Dati OIDC",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_different_users": "Anteprima dei dati OIDC che saranno inviati agli utenti",
|
||||
"id_token": "ID Token",
|
||||
"access_token": "Access Token",
|
||||
"userinfo": "Userinfo",
|
||||
"id_token_payload": "ID Token Payload",
|
||||
"access_token_payload": "Access Token Payload",
|
||||
"userinfo_endpoint_response": "Userinfo Endpoint Response",
|
||||
"copy": "Copy",
|
||||
"no_preview_data_available": "No preview data available",
|
||||
"copy_all": "Copy All",
|
||||
"preview": "Preview",
|
||||
"preview_for_user": "Preview for {name} ({email})",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Preview the OIDC data that would be sent for this user",
|
||||
"show": "Show",
|
||||
"select_an_option": "Select an option",
|
||||
"select_user": "Select User",
|
||||
"error": "Error"
|
||||
"userinfo_endpoint_response": "Risposta Endpoint Userinfo",
|
||||
"copy": "Copia",
|
||||
"no_preview_data_available": "Dati di anteprima non disponibili",
|
||||
"copy_all": "Copia tutto",
|
||||
"preview": "Anteprima",
|
||||
"preview_for_user": "Anteprima per {name} ({email})",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Anteprima dei dati OIDC che saranno inviati per l'utente",
|
||||
"show": "Mostra",
|
||||
"select_an_option": "Seleziona un'opzione",
|
||||
"select_user": "Seleziona utente",
|
||||
"error": "Errore",
|
||||
"select_an_accent_color_to_customize_the_appearance_of_pocket_id": "Seleziona un colore in risalto per personalizzare l'aspetto di Pocket ID.",
|
||||
"accent_color": "Colore in Risalto",
|
||||
"custom_accent_color": "Colore in Risalto Personalizzato",
|
||||
"custom_accent_color_description": "Inserisci un colore personalizzato usando formati di colore CSS validi (es: hex, rgb, hsl).",
|
||||
"color_value": "Valore Colore",
|
||||
"apply": "Applica"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"my_account": "Mijn account",
|
||||
"logout": "Uitloggen",
|
||||
"confirm": "Bevestigen",
|
||||
"docs": "Docs",
|
||||
"key": "Sleutel",
|
||||
"value": "Waarde",
|
||||
"remove_custom_claim": "Aangepaste claim verwijderen",
|
||||
@@ -179,7 +180,7 @@
|
||||
"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.",
|
||||
"emai_login_code_requested_by_user": "Email Login Code Requested by User",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to bypass passkeys by requesting a login code sent to their email. This reduces the security significantly as anyone with access to the user's email can gain entry.",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to bypass passkeys by requesting a login code sent to their email. This significantly reduces security as anyone with access to the user's email can gain entry.",
|
||||
"email_login_code_from_admin": "Email Login Code from Admin",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "Allows an admin to send a login code to the user via email.",
|
||||
"send_test_email": "Test-e-mail verzenden",
|
||||
@@ -309,7 +310,7 @@
|
||||
"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?",
|
||||
"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": "Standaardinstellingen herstellen",
|
||||
"profile_picture_has_been_reset": "Profielfoto is gereset. Het kan enkele minuten duren voordat de wijzigingen zichtbaar zijn.",
|
||||
@@ -325,7 +326,7 @@
|
||||
"client_authorization": "Client autorisatie",
|
||||
"new_client_authorization": "Nieuwe clientautorisatie",
|
||||
"disable_animations": "Disable Animations",
|
||||
"turn_off_ui_animations": "Turn off all animations throughout the Admin UI.",
|
||||
"turn_off_ui_animations": "Turn off animations throughout the UI.",
|
||||
"user_disabled": "Account Disabled",
|
||||
"disabled_users_cannot_log_in_or_use_services": "Disabled users cannot log in or use services.",
|
||||
"user_disabled_successfully": "User has been disabled successfully.",
|
||||
@@ -371,5 +372,11 @@
|
||||
"show": "Show",
|
||||
"select_an_option": "Select an option",
|
||||
"select_user": "Select User",
|
||||
"error": "Error"
|
||||
"error": "Error",
|
||||
"select_an_accent_color_to_customize_the_appearance_of_pocket_id": "Select an accent color to customize the appearance of Pocket ID.",
|
||||
"accent_color": "Accent Color",
|
||||
"custom_accent_color": "Custom Accent Color",
|
||||
"custom_accent_color_description": "Enter a custom color using valid CSS color formats (e.g., hex, rgb, hsl).",
|
||||
"color_value": "Color Value",
|
||||
"apply": "Apply"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"my_account": "Moje konto",
|
||||
"logout": "Wyloguj się",
|
||||
"confirm": "Potwierdź",
|
||||
"docs": "Docs",
|
||||
"key": "Klucz",
|
||||
"value": "Wartość",
|
||||
"remove_custom_claim": "Usuń niestandardowy atrybut",
|
||||
@@ -179,7 +180,7 @@
|
||||
"email_login_notification": "Powiadomienie o logowaniu przez e-mail",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Wyślij e-mail do użytkownika, gdy zaloguje się z nowego urządzenia.",
|
||||
"emai_login_code_requested_by_user": "Kod logowania e-mailem zażądany przez użytkownika",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Pozwól użytkownikom zalogować się za pomocą kodu logowania wysłanego na ich e-mail. Znacząco obniża to bezpieczeństwo, ponieważ każdy, kto ma dostęp do e-maila użytkownika, może uzyskać dostęp.",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to bypass passkeys by requesting a login code sent to their email. This significantly reduces security as anyone with access to the user's email can gain entry.",
|
||||
"email_login_code_from_admin": "Kod logowania e-mailem od administratora",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "Pozwala administratorowi wysłać kod logowania do użytkownika za pomocą e-maila.",
|
||||
"send_test_email": "Wyślij testowy e-mail",
|
||||
@@ -309,7 +310,7 @@
|
||||
"background_image": "Obraz tła",
|
||||
"language": "Język",
|
||||
"reset_profile_picture_question": "Zresetować zdjęcie profilowe?",
|
||||
"this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default": "To usunie przesłany obraz i zresetuje zdjęcie profilowe do domyślnego. Czy chcesz kontynuować?",
|
||||
"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": "Zresetuj",
|
||||
"reset_to_default": "Zresetuj do domyślnych",
|
||||
"profile_picture_has_been_reset": "Zdjęcie profilowe zostało zresetowane. Może to potrwać kilka minut.",
|
||||
@@ -325,7 +326,7 @@
|
||||
"client_authorization": "Autoryzacja klienta",
|
||||
"new_client_authorization": "Nowa autoryzacja klienta",
|
||||
"disable_animations": "Wyłącz animacje",
|
||||
"turn_off_ui_animations": "Wyłącz wszystkie animacje w całym interfejsie administracyjnym.",
|
||||
"turn_off_ui_animations": "Turn off animations throughout the UI.",
|
||||
"user_disabled": "Konto wyłączone",
|
||||
"disabled_users_cannot_log_in_or_use_services": "Wyłączone konta użytkowników nie mogą się logować ani korzystać z usług.",
|
||||
"user_disabled_successfully": "Sukces! Konto zostało wyłączone.",
|
||||
@@ -371,5 +372,11 @@
|
||||
"show": "Show",
|
||||
"select_an_option": "Select an option",
|
||||
"select_user": "Select User",
|
||||
"error": "Error"
|
||||
"error": "Error",
|
||||
"select_an_accent_color_to_customize_the_appearance_of_pocket_id": "Select an accent color to customize the appearance of Pocket ID.",
|
||||
"accent_color": "Accent Color",
|
||||
"custom_accent_color": "Custom Accent Color",
|
||||
"custom_accent_color_description": "Enter a custom color using valid CSS color formats (e.g., hex, rgb, hsl).",
|
||||
"color_value": "Color Value",
|
||||
"apply": "Apply"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"my_account": "Minha Conta",
|
||||
"logout": "Sair",
|
||||
"confirm": "Confirmar",
|
||||
"docs": "Docs",
|
||||
"key": "Chave",
|
||||
"value": "Valor",
|
||||
"remove_custom_claim": "Remove custom claim",
|
||||
@@ -179,7 +180,7 @@
|
||||
"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.",
|
||||
"emai_login_code_requested_by_user": "Email Login Code Requested by User",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to bypass passkeys by requesting a login code sent to their email. This reduces the security significantly as anyone with access to the user's email can gain entry.",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to bypass passkeys by requesting a login code sent to their email. This significantly reduces security as anyone with access to the user's email can gain entry.",
|
||||
"email_login_code_from_admin": "Email Login Code from Admin",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "Allows an admin to send a login code to the user via email.",
|
||||
"send_test_email": "Send test email",
|
||||
@@ -309,7 +310,7 @@
|
||||
"background_image": "Background Image",
|
||||
"language": "Idioma",
|
||||
"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?",
|
||||
"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": "Redefinir",
|
||||
"reset_to_default": "Redefinir para o padrão",
|
||||
"profile_picture_has_been_reset": "Profile picture has been reset. It may take a few minutes to update.",
|
||||
@@ -325,7 +326,7 @@
|
||||
"client_authorization": "Client Authorization",
|
||||
"new_client_authorization": "New Client Authorization",
|
||||
"disable_animations": "Disable Animations",
|
||||
"turn_off_ui_animations": "Turn off all animations throughout the Admin UI.",
|
||||
"turn_off_ui_animations": "Turn off animations throughout the UI.",
|
||||
"user_disabled": "Account Disabled",
|
||||
"disabled_users_cannot_log_in_or_use_services": "Disabled users cannot log in or use services.",
|
||||
"user_disabled_successfully": "User has been disabled successfully.",
|
||||
@@ -371,5 +372,11 @@
|
||||
"show": "Show",
|
||||
"select_an_option": "Select an option",
|
||||
"select_user": "Select User",
|
||||
"error": "Error"
|
||||
"error": "Error",
|
||||
"select_an_accent_color_to_customize_the_appearance_of_pocket_id": "Select an accent color to customize the appearance of Pocket ID.",
|
||||
"accent_color": "Accent Color",
|
||||
"custom_accent_color": "Custom Accent Color",
|
||||
"custom_accent_color_description": "Enter a custom color using valid CSS color formats (e.g., hex, rgb, hsl).",
|
||||
"color_value": "Color Value",
|
||||
"apply": "Apply"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"my_account": "Моя учетная запись",
|
||||
"logout": "Выйти",
|
||||
"confirm": "Подтвердить",
|
||||
"docs": "Документация",
|
||||
"key": "Ключ",
|
||||
"value": "Значение",
|
||||
"remove_custom_claim": "Удалить пользовательский claim",
|
||||
@@ -325,7 +326,7 @@
|
||||
"client_authorization": "Авторизация в клиенте",
|
||||
"new_client_authorization": "Новая авторизация в клиенте",
|
||||
"disable_animations": "Отключить анимации",
|
||||
"turn_off_ui_animations": "Выключить все анимации в интерфейсе администратора.",
|
||||
"turn_off_ui_animations": "Выключить анимации по всему интерфейсу.",
|
||||
"user_disabled": "Аккаунт отключен",
|
||||
"disabled_users_cannot_log_in_or_use_services": "Отключенные пользователи не могут войти или использовать сервисы.",
|
||||
"user_disabled_successfully": "Пользователь успешно отключен.",
|
||||
@@ -346,30 +347,36 @@
|
||||
"the_device_has_been_authorized": "Устройство авторизовано.",
|
||||
"enter_code_displayed_in_previous_step": "Введите код, который был отображен на предыдущем шаге.",
|
||||
"authorize": "Авторизируйте",
|
||||
"federated_client_credentials": "Federated Client Credentials",
|
||||
"federated_client_credentials_description": "Using federated client credentials, you can authenticate OIDC clients using JWT tokens issued by third-party authorities.",
|
||||
"add_federated_client_credential": "Add Federated Client Credential",
|
||||
"add_another_federated_client_credential": "Add another federated client credential",
|
||||
"federated_client_credentials": "Федеративные учетные данные клиента",
|
||||
"federated_client_credentials_description": "Используя федеративные учетные данные клиента, вы можете авторизовывать OIDC клиентов, используя JWT токены, выпущенные третьими сторонами.",
|
||||
"add_federated_client_credential": "Добавить федеративные учетные данные клиента",
|
||||
"add_another_federated_client_credential": "Добавить другие федеративные учетные данные клиента",
|
||||
"oidc_allowed_group_count": "Кол-во разрешенных групп",
|
||||
"unrestricted": "Не ограничено",
|
||||
"show_advanced_options": "Show Advanced Options",
|
||||
"hide_advanced_options": "Hide Advanced Options",
|
||||
"oidc_data_preview": "OIDC Data Preview",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_different_users": "Preview the OIDC data that would be sent for different users",
|
||||
"show_advanced_options": "Показать дополнительные опции",
|
||||
"hide_advanced_options": "Скрыть дополнительные опции",
|
||||
"oidc_data_preview": "Предпросмотр данных OIDC",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_different_users": "Предпросмотр данных OIDC, которые будут отправлены разным пользователям",
|
||||
"id_token": "ID Token",
|
||||
"access_token": "Access Token",
|
||||
"userinfo": "Userinfo",
|
||||
"id_token_payload": "ID Token Payload",
|
||||
"access_token_payload": "Access Token Payload",
|
||||
"userinfo_endpoint_response": "Userinfo Endpoint Response",
|
||||
"copy": "Copy",
|
||||
"no_preview_data_available": "No preview data available",
|
||||
"copy_all": "Copy All",
|
||||
"preview": "Preview",
|
||||
"preview_for_user": "Preview for {name} ({email})",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Preview the OIDC data that would be sent for this user",
|
||||
"show": "Show",
|
||||
"select_an_option": "Select an option",
|
||||
"select_user": "Select User",
|
||||
"error": "Error"
|
||||
"id_token_payload": "Содержимое ID Token",
|
||||
"access_token_payload": "Содержимое Access Token",
|
||||
"userinfo_endpoint_response": "Ответ Userinfo эндпоинта",
|
||||
"copy": "Копировать",
|
||||
"no_preview_data_available": "Предварительный просмотр данных не доступен",
|
||||
"copy_all": "Копировать все",
|
||||
"preview": "Предпросмотр",
|
||||
"preview_for_user": "Предпросмотр для {name} ({email})",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Предпросмотр данных OIDC, которые будут отправлены для этого пользователя",
|
||||
"show": "Показать",
|
||||
"select_an_option": "Выберите опцию",
|
||||
"select_user": "Выбрать пользователя",
|
||||
"error": "Ошибка",
|
||||
"select_an_accent_color_to_customize_the_appearance_of_pocket_id": "Выберите цвет акцента, чтобы настроить внешний вид Pocket ID.",
|
||||
"accent_color": "Цвет акцента",
|
||||
"custom_accent_color": "Пользовательский цвет акцента",
|
||||
"custom_accent_color_description": "Введите пользовательский цвет, используя правильные цветовые форматы CSS (например, hex, rgb, hsl).",
|
||||
"color_value": "Значение цвета",
|
||||
"apply": "Применить"
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"$schema": "",
|
||||
"my_account": "账户",
|
||||
"logout": "登出",
|
||||
"confirm": "确认",
|
||||
"docs": "文档",
|
||||
"key": "Key",
|
||||
"value": "Value",
|
||||
"remove_custom_claim": "删除自定义声明",
|
||||
@@ -309,7 +310,7 @@
|
||||
"background_image": "背景图片",
|
||||
"language": "语言",
|
||||
"reset_profile_picture_question": "重置头像?",
|
||||
"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": "这将删除已上传的图片,并将头像重置为默认。您确定要继续吗?",
|
||||
"reset": "重置",
|
||||
"reset_to_default": "恢复默认设置",
|
||||
"profile_picture_has_been_reset": "头像已重置。可能需要几分钟才能更新。",
|
||||
@@ -325,7 +326,7 @@
|
||||
"client_authorization": "客户端授权",
|
||||
"new_client_authorization": "首次客户端授权",
|
||||
"disable_animations": "关闭动画",
|
||||
"turn_off_ui_animations": "关闭管理界面中的所有动画效果。",
|
||||
"turn_off_ui_animations": "关闭界面中的所有动画效果。",
|
||||
"user_disabled": "账户已禁用",
|
||||
"disabled_users_cannot_log_in_or_use_services": "禁用的用户无法登录或使用服务。",
|
||||
"user_disabled_successfully": "用户已成功禁用。",
|
||||
@@ -354,22 +355,28 @@
|
||||
"unrestricted": "不受限制",
|
||||
"show_advanced_options": "显示高级选项",
|
||||
"hide_advanced_options": "隐藏高级选项",
|
||||
"oidc_data_preview": "OIDC Data Preview",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_different_users": "Preview the OIDC data that would be sent for different users",
|
||||
"oidc_data_preview": "OIDC 数据预览",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_different_users": "预览将发送给不同用户的 OIDC 数据",
|
||||
"id_token": "ID Token",
|
||||
"access_token": "Access Token",
|
||||
"userinfo": "Userinfo",
|
||||
"id_token_payload": "ID Token Payload",
|
||||
"access_token_payload": "Access Token Payload",
|
||||
"userinfo_endpoint_response": "Userinfo Endpoint Response",
|
||||
"copy": "Copy",
|
||||
"no_preview_data_available": "No preview data available",
|
||||
"copy_all": "Copy All",
|
||||
"preview": "Preview",
|
||||
"preview_for_user": "Preview for {name} ({email})",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_this_user": "Preview the OIDC data that would be sent for this user",
|
||||
"show": "Show",
|
||||
"select_an_option": "Select an option",
|
||||
"select_user": "Select User",
|
||||
"error": "Error"
|
||||
"id_token_payload": "ID Token 有效载载",
|
||||
"access_token_payload": "Access Token 有效载载",
|
||||
"userinfo_endpoint_response": "Userinfo 端点响应",
|
||||
"copy": "复制",
|
||||
"no_preview_data_available": "暂无可用的预览数据",
|
||||
"copy_all": "全部复制",
|
||||
"preview": "预览",
|
||||
"preview_for_user": "为 {name} ({email}) 预览",
|
||||
"preview_the_oidc_data_that_would_be_sent_for_this_user": "预览将为此用户发送的 OIDC 数据",
|
||||
"show": "显示",
|
||||
"select_an_option": "请选择",
|
||||
"select_user": "选择用户",
|
||||
"error": "错误",
|
||||
"select_an_accent_color_to_customize_the_appearance_of_pocket_id": "Select an accent color to customize the appearance of Pocket ID.",
|
||||
"accent_color": "Accent Color",
|
||||
"custom_accent_color": "Custom Accent Color",
|
||||
"custom_accent_color_description": "Enter a custom color using valid CSS color formats (e.g., hex, rgb, hsl).",
|
||||
"color_value": "Color Value",
|
||||
"apply": "Apply"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"my_account": "我的帳號",
|
||||
"logout": "登出",
|
||||
"confirm": "確認",
|
||||
"docs": "Docs",
|
||||
"key": "Key",
|
||||
"value": "Value",
|
||||
"remove_custom_claim": "移除自定義 claim",
|
||||
@@ -179,7 +180,7 @@
|
||||
"email_login_notification": "電子郵件登入通知",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "使用者從新裝置登入時寄送電子郵件通知。",
|
||||
"emai_login_code_requested_by_user": "使用者請求電子郵件登入代碼",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "允許使用者請求一組登入代碼並透過電子郵件接收,藉此繞過密碼金鑰驗證。 這將大幅降低安全性,因為只要取得使用者的信箱存取權,就可能登入系統。",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to bypass passkeys by requesting a login code sent to their email. This significantly reduces security as anyone with access to the user's email can gain entry.",
|
||||
"email_login_code_from_admin": "來自管理員的使用者登入代碼",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "允許管理員透過電子郵件向使用者發送登入代碼。",
|
||||
"send_test_email": "發送測試郵件",
|
||||
@@ -309,7 +310,7 @@
|
||||
"background_image": "背景圖片",
|
||||
"language": "語言",
|
||||
"reset_profile_picture_question": "重設個人資料圖片?",
|
||||
"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": "This will remove the uploaded image and reset the profile picture to default. Do you want to continue?",
|
||||
"reset": "重設",
|
||||
"reset_to_default": "重設至預設值",
|
||||
"profile_picture_has_been_reset": "個人資料圖片已經重設。 這可能會花幾分鐘更新。",
|
||||
@@ -325,7 +326,7 @@
|
||||
"client_authorization": "客戶端授權",
|
||||
"new_client_authorization": "新客戶端授權",
|
||||
"disable_animations": "停用動畫",
|
||||
"turn_off_ui_animations": "關閉整個系統中的所有動畫效果。",
|
||||
"turn_off_ui_animations": "Turn off animations throughout the UI.",
|
||||
"user_disabled": "帳戶已停用",
|
||||
"disabled_users_cannot_log_in_or_use_services": "已停用的使用者不能登入或使用服務。",
|
||||
"user_disabled_successfully": "使用者已成功停用。",
|
||||
@@ -371,5 +372,11 @@
|
||||
"show": "Show",
|
||||
"select_an_option": "Select an option",
|
||||
"select_user": "Select User",
|
||||
"error": "Error"
|
||||
"error": "Error",
|
||||
"select_an_accent_color_to_customize_the_appearance_of_pocket_id": "Select an accent color to customize the appearance of Pocket ID.",
|
||||
"accent_color": "Accent Color",
|
||||
"custom_accent_color": "Custom Accent Color",
|
||||
"custom_accent_color_description": "Enter a custom color using valid CSS color formats (e.g., hex, rgb, hsl).",
|
||||
"color_value": "Color Value",
|
||||
"apply": "Apply"
|
||||
}
|
||||
|
||||
11151
frontend/package-lock.json
generated
11151
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pocket-id-frontend",
|
||||
"version": "1.3.0",
|
||||
"version": "1.4.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -13,12 +13,10 @@
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@lucide/svelte": "^0.511.0",
|
||||
"@simplewebauthn/browser": "^13.1.0",
|
||||
"@tailwindcss/vite": "^4.1.7",
|
||||
"axios": "^1.8.2",
|
||||
"clsx": "^2.1.1",
|
||||
"crypto": "^1.0.1",
|
||||
"jose": "^5.9.6",
|
||||
"qrcode": "^1.5.4",
|
||||
"sveltekit-superforms": "^2.23.1",
|
||||
@@ -29,7 +27,8 @@
|
||||
"@inlang/paraglide-js": "^2.0.13",
|
||||
"@inlang/plugin-m-function-matcher": "^2.0.10",
|
||||
"@inlang/plugin-message-format": "^4.0.0",
|
||||
"@internationalized/date": "^3.7.0",
|
||||
"@internationalized/date": "^3.8.2",
|
||||
"@lucide/svelte": "^0.513.0",
|
||||
"@playwright/test": "^1.50.0",
|
||||
"@sveltejs/adapter-static": "^3.0.8",
|
||||
"@sveltejs/kit": "^2.20.7",
|
||||
@@ -37,7 +36,7 @@
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/node": "^22.10.10",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"bits-ui": "^1.5.3",
|
||||
"bits-ui": "^2.5.0",
|
||||
"eslint": "^9.19.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-svelte": "^2.46.1",
|
||||
|
||||
@@ -203,7 +203,7 @@
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.8s ease-out forwards;
|
||||
animation: fadeIn 0.3s ease-out forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@@ -233,7 +233,7 @@
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
animation: slide-bg-container 1.2s cubic-bezier(0.33, 1, 0.68, 1) forwards;
|
||||
animation: slide-bg-container 0.6s cubic-bezier(0.33, 1, 0.68, 1) forwards;
|
||||
}
|
||||
|
||||
@keyframes delayed-fade {
|
||||
@@ -247,5 +247,5 @@
|
||||
}
|
||||
|
||||
.animate-delayed-fade {
|
||||
animation: delayed-fade 1.5s ease-out forwards;
|
||||
animation: delayed-fade 0.5s ease-out forwards;
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
|
||||
<div class="w-full" {...restProps}>
|
||||
<Popover.Root bind:open>
|
||||
<Popover.Trigger {id} class="w-full" >
|
||||
<Popover.Trigger {id} class="w-full">
|
||||
{#snippet child({ props })}
|
||||
<Button
|
||||
{...props}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import * as Avatar from '$lib/components/ui/avatar';
|
||||
import Button from '$lib/components/ui/button/button.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { getProfilePictureUrl } from '$lib/utils/profile-picture-util';
|
||||
import { cachedProfilePicture } from '$lib/utils/cached-image-util';
|
||||
import { LucideLoader, LucideRefreshCw, LucideUpload } from '@lucide/svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { openConfirmDialog } from '../confirm-dialog';
|
||||
@@ -25,7 +25,7 @@
|
||||
onMount(() => {
|
||||
// The "skipCache" query will only be added to the profile picture url on client-side
|
||||
// because of that we need to set the imageDataURL after the component is mounted
|
||||
imageDataURL = getProfilePictureUrl(userId);
|
||||
imageDataURL = cachedProfilePicture.getUrl(userId);
|
||||
});
|
||||
|
||||
async function onImageChange(e: Event) {
|
||||
@@ -41,7 +41,7 @@
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
await updateCallback(file).catch(() => {
|
||||
imageDataURL = getProfilePictureUrl(userId);
|
||||
imageDataURL = cachedProfilePicture.getUrl(userId);
|
||||
});
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
onSelect,
|
||||
oninput,
|
||||
isLoading,
|
||||
disableSearch = false,
|
||||
selectText = m.select_an_option(),
|
||||
...restProps
|
||||
}: HTMLAttributes<HTMLButtonElement> & {
|
||||
@@ -25,6 +26,7 @@
|
||||
oninput?: FormEventHandler<HTMLInputElement>;
|
||||
onSelect?: (value: string) => void;
|
||||
isLoading?: boolean;
|
||||
disableSearch?: boolean;
|
||||
selectText?: string;
|
||||
} = $props();
|
||||
|
||||
@@ -76,13 +78,15 @@
|
||||
</Popover.Trigger>
|
||||
<Popover.Content class="p-0" sameWidth>
|
||||
<Command.Root shouldFilter={false}>
|
||||
<Command.Input
|
||||
placeholder={m.search()}
|
||||
oninput={(e) => {
|
||||
filterItems(e.currentTarget.value);
|
||||
oninput?.(e);
|
||||
}}
|
||||
/>
|
||||
{#if !disableSearch}
|
||||
<Command.Input
|
||||
placeholder={m.search()}
|
||||
oninput={(e) => {
|
||||
filterItems(e.currentTarget.value);
|
||||
oninput?.(e);
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<Command.Empty>
|
||||
{#if isLoading}
|
||||
<div class="flex w-full justify-center">
|
||||
|
||||
39
frontend/src/lib/components/form/switch-with-label.svelte
Normal file
39
frontend/src/lib/components/form/switch-with-label.svelte
Normal file
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { Switch } from '$lib/components/ui/switch/index.js';
|
||||
|
||||
let {
|
||||
id,
|
||||
checked = $bindable(),
|
||||
label,
|
||||
description,
|
||||
disabled = false,
|
||||
onCheckedChange
|
||||
}: {
|
||||
id: string;
|
||||
checked: boolean;
|
||||
label: string;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
onCheckedChange?: (checked: boolean) => void;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div class="items-top flex space-x-2">
|
||||
<Switch
|
||||
{id}
|
||||
{disabled}
|
||||
onCheckedChange={(v) => onCheckedChange && onCheckedChange(v == true)}
|
||||
bind:checked
|
||||
/>
|
||||
<div class="grid gap-1.5 leading-none">
|
||||
<Label for={id} class="mb-0 text-sm leading-none font-medium">
|
||||
{label}
|
||||
</Label>
|
||||
{#if description}
|
||||
<p class="text-muted-foreground text-[0.8rem]">
|
||||
{description}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -5,7 +5,7 @@
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import WebAuthnService from '$lib/services/webauthn-service';
|
||||
import userStore from '$lib/stores/user-store';
|
||||
import { getProfilePictureUrl } from '$lib/utils/profile-picture-util';
|
||||
import { cachedProfilePicture } from '$lib/utils/cached-image-util';
|
||||
import { LucideLogOut, LucideUser } from '@lucide/svelte';
|
||||
|
||||
const webauthnService = new WebAuthnService();
|
||||
@@ -19,7 +19,7 @@
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger
|
||||
><Avatar.Root class="size-9">
|
||||
<Avatar.Image src={getProfilePictureUrl($userStore?.id)} />
|
||||
<Avatar.Image src={cachedProfilePicture.getUrl($userStore!.id)} />
|
||||
</Avatar.Root></DropdownMenu.Trigger
|
||||
>
|
||||
<DropdownMenu.Content class="min-w-40" align="end">
|
||||
|
||||
10
frontend/src/lib/components/image-box.svelte
Normal file
10
frontend/src/lib/components/image-box.svelte
Normal file
@@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils/style';
|
||||
import type { HTMLImgAttributes } from 'svelte/elements';
|
||||
|
||||
let props: HTMLImgAttributes & {} = $props();
|
||||
</script>
|
||||
|
||||
<div class={'bg-muted flex items-center justify-center rounded-2xl p-3'}>
|
||||
<img class={cn('size-24 object-contain', props.class)} {...props} />
|
||||
</div>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { cachedBackgroundImage } from '$lib/utils/cached-image-util';
|
||||
import { cn } from '$lib/utils/style';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { MediaQuery } from 'svelte/reactivity';
|
||||
@@ -34,7 +35,7 @@
|
||||
{#if showAlternativeSignInMethodButton}
|
||||
<div
|
||||
class="mb-4 flex items-center justify-center"
|
||||
style={animate ? 'animation-delay: 1000ms;' : ''}
|
||||
style={animate ? 'animation-delay: 500ms;' : ''}
|
||||
>
|
||||
<a
|
||||
href={page.url.pathname == '/login'
|
||||
@@ -54,7 +55,7 @@
|
||||
<!-- Background image with slide animation -->
|
||||
<div class="{cn(animate && 'animate-slide-bg-container')} absolute top-0 right-0 bottom-0 z-0">
|
||||
<img
|
||||
src="/api/application-configuration/background-image"
|
||||
src={cachedBackgroundImage.getUrl()}
|
||||
class="h-screen rounded-l-[60px] object-cover {animate
|
||||
? 'w-full'
|
||||
: 'w-[calc(100vw-650px)]'}"
|
||||
@@ -64,7 +65,8 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="flex h-screen items-center justify-center bg-[url('/api/application-configuration/background-image')] bg-cover bg-center text-center"
|
||||
class="flex h-screen items-center justify-center bg-cover bg-center text-center"
|
||||
style="background-image: url({cachedBackgroundImage.getUrl()});"
|
||||
>
|
||||
<Card.Root class="mx-3 w-full max-w-md" style={animate ? 'animation-delay: 200ms;' : ''}>
|
||||
<Card.CardContent
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { cachedApplicationLogo } from '$lib/utils/cached-image-util';
|
||||
import { mode } from 'mode-watcher';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let { ...props }: HTMLAttributes<HTMLImageElement> = $props();
|
||||
|
||||
const isDarkMode = $derived(mode.current === 'dark');
|
||||
const isLightMode = $derived(mode.current === 'light');
|
||||
</script>
|
||||
|
||||
<img {...props} src="/api/application-configuration/logo?light={!isDarkMode}" alt={m.logo()} />
|
||||
<img {...props} src={cachedApplicationLogo.getUrl(isLightMode)} alt={m.logo()} />
|
||||
|
||||
@@ -37,11 +37,13 @@
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
isLoading?: boolean;
|
||||
autofocus?: boolean;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import LoaderCircle from '@lucide/svelte/icons/loader-circle';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let {
|
||||
class: className,
|
||||
@@ -52,9 +54,18 @@
|
||||
type = 'button',
|
||||
disabled,
|
||||
isLoading = false,
|
||||
autofocus = false,
|
||||
children,
|
||||
...restProps
|
||||
}: ButtonProps = $props();
|
||||
|
||||
onMount(async () => {
|
||||
// Using autofocus can be bad for a11y, but in the case of Pocket ID is only used responsibly in places where there are not many choices, and on buttons only where there's descriptive text
|
||||
if (autofocus) {
|
||||
// Use setTimeout to make sure the element is showing
|
||||
setTimeout(() => ref?.focus(), 100);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
|
||||
10
frontend/src/lib/components/ui/radio-group/index.ts
Normal file
10
frontend/src/lib/components/ui/radio-group/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import Root from './radio-group.svelte';
|
||||
import Item from './radio-group-item.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
Item,
|
||||
//
|
||||
Root as RadioGroup,
|
||||
Item as RadioGroupItem
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { RadioGroup as RadioGroupPrimitive } from 'bits-ui';
|
||||
import CircleIcon from '@lucide/svelte/icons/circle';
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils/style.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<RadioGroupPrimitive.ItemProps> = $props();
|
||||
</script>
|
||||
|
||||
<RadioGroupPrimitive.Item
|
||||
bind:ref
|
||||
data-slot="radio-group-item"
|
||||
class={cn(
|
||||
'border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ checked })}
|
||||
<div data-slot="radio-group-indicator" class="relative flex items-center justify-center">
|
||||
{#if checked}
|
||||
<CircleIcon
|
||||
class="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</RadioGroupPrimitive.Item>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { RadioGroup as RadioGroupPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils/style.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
value = $bindable(''),
|
||||
...restProps
|
||||
}: RadioGroupPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<RadioGroupPrimitive.Root
|
||||
bind:ref
|
||||
bind:value
|
||||
data-slot="radio-group"
|
||||
class={cn('grid gap-3', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
7
frontend/src/lib/components/ui/switch/index.ts
Normal file
7
frontend/src/lib/components/ui/switch/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from './switch.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Switch
|
||||
};
|
||||
29
frontend/src/lib/components/ui/switch/switch.svelte
Normal file
29
frontend/src/lib/components/ui/switch/switch.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { Switch as SwitchPrimitive } from 'bits-ui';
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils/style.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
checked = $bindable(false),
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SwitchPrimitive.RootProps> = $props();
|
||||
</script>
|
||||
|
||||
<SwitchPrimitive.Root
|
||||
bind:ref
|
||||
bind:checked
|
||||
data-slot="switch"
|
||||
class={cn(
|
||||
'data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 peer inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
<SwitchPrimitive.Thumb
|
||||
data-slot="switch-thumb"
|
||||
class={cn(
|
||||
'bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0'
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitive.Root>
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AllAppConfig, AppConfigRawResponse } from '$lib/types/application-configuration';
|
||||
import { cachedApplicationLogo, cachedBackgroundImage } from '$lib/utils/cached-image-util';
|
||||
import APIService from './api-service';
|
||||
|
||||
export default class AppConfigService extends APIService {
|
||||
@@ -36,6 +37,7 @@ export default class AppConfigService extends APIService {
|
||||
await this.api.put(`/application-configuration/logo`, formData, {
|
||||
params: { light }
|
||||
});
|
||||
cachedApplicationLogo.bustCache(light);
|
||||
}
|
||||
|
||||
async updateBackgroundImage(backgroundImage: File) {
|
||||
@@ -43,6 +45,7 @@ export default class AppConfigService extends APIService {
|
||||
formData.append('file', backgroundImage!);
|
||||
|
||||
await this.api.put(`/application-configuration/background-image`, formData);
|
||||
cachedBackgroundImage.bustCache();
|
||||
}
|
||||
|
||||
async sendTestEmail() {
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
OidcDeviceCodeInfo
|
||||
} from '$lib/types/oidc.type';
|
||||
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
import { cachedOidcClientLogo } from '$lib/utils/cached-image-util';
|
||||
import APIService from './api-service';
|
||||
|
||||
class OidcService extends APIService {
|
||||
@@ -80,10 +81,12 @@ class OidcService extends APIService {
|
||||
formData.append('file', image!);
|
||||
|
||||
await this.api.post(`/oidc/clients/${client.id}/logo`, formData);
|
||||
cachedOidcClientLogo.bustCache(client.id);
|
||||
}
|
||||
|
||||
async removeClientLogo(id: string) {
|
||||
await this.api.delete(`/oidc/clients/${id}/logo`);
|
||||
cachedOidcClientLogo.bustCache(id);
|
||||
}
|
||||
|
||||
async createClientSecret(id: string) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import userStore from '$lib/stores/user-store';
|
||||
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
import type { UserGroup } from '$lib/types/user-group.type';
|
||||
import type { User, UserCreate } from '$lib/types/user.type';
|
||||
import { bustProfilePictureCache } from '$lib/utils/profile-picture-util';
|
||||
import { cachedProfilePicture } from '$lib/utils/cached-image-util';
|
||||
import { get } from 'svelte/store';
|
||||
import APIService from './api-service';
|
||||
|
||||
@@ -52,26 +52,26 @@ export default class UserService extends APIService {
|
||||
const formData = new FormData();
|
||||
formData.append('file', image!);
|
||||
|
||||
bustProfilePictureCache(userId);
|
||||
await this.api.put(`/users/${userId}/profile-picture`, formData);
|
||||
cachedProfilePicture.bustCache(userId);
|
||||
}
|
||||
|
||||
async updateCurrentUsersProfilePicture(image: File) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', image!);
|
||||
|
||||
bustProfilePictureCache(get(userStore)!.id);
|
||||
await this.api.put('/users/me/profile-picture', formData);
|
||||
cachedProfilePicture.bustCache(get(userStore)!.id);
|
||||
}
|
||||
|
||||
async resetCurrentUserProfilePicture() {
|
||||
bustProfilePictureCache(get(userStore)!.id);
|
||||
await this.api.delete(`/users/me/profile-picture`);
|
||||
cachedProfilePicture.bustCache(get(userStore)!.id);
|
||||
}
|
||||
|
||||
async resetProfilePicture(userId: string) {
|
||||
bustProfilePictureCache(userId);
|
||||
await this.api.delete(`/users/${userId}/profile-picture`);
|
||||
cachedProfilePicture.bustCache(userId);
|
||||
}
|
||||
|
||||
async createOneTimeAccessToken(expiresAt: Date, userId: string) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import AppConfigService from '$lib/services/app-config-service';
|
||||
import type { AppConfig } from '$lib/types/application-configuration';
|
||||
import { applyAccentColor } from '$lib/utils/accent-color-util';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
const appConfigStore = writable<AppConfig>();
|
||||
@@ -8,10 +9,11 @@ const appConfigService = new AppConfigService();
|
||||
|
||||
const reload = async () => {
|
||||
const appConfig = await appConfigService.list();
|
||||
appConfigStore.set(appConfig);
|
||||
set(appConfig);
|
||||
};
|
||||
|
||||
const set = (appConfig: AppConfig) => {
|
||||
applyAccentColor(appConfig.accentColor);
|
||||
appConfigStore.set(appConfig);
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ export type AppConfig = {
|
||||
ldapEnabled: boolean;
|
||||
disableAnimations: boolean;
|
||||
uiConfigDisabled: boolean;
|
||||
accentColor: string;
|
||||
};
|
||||
|
||||
export type AllAppConfig = AppConfig & {
|
||||
|
||||
@@ -14,5 +14,6 @@ export type AuditLog = {
|
||||
export type AuditLogFilter = {
|
||||
userId: string;
|
||||
event: string;
|
||||
location: string;
|
||||
clientName: string;
|
||||
};
|
||||
|
||||
58
frontend/src/lib/utils/accent-color-util.ts
Normal file
58
frontend/src/lib/utils/accent-color-util.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
export function applyAccentColor(accentValue: string) {
|
||||
if (accentValue === 'default') {
|
||||
document.documentElement.style.removeProperty('--primary');
|
||||
document.documentElement.style.removeProperty('--primary-foreground');
|
||||
document.documentElement.style.removeProperty('--ring');
|
||||
document.documentElement.style.removeProperty('--sidebar-ring');
|
||||
return;
|
||||
}
|
||||
|
||||
document.documentElement.style.setProperty('--primary', accentValue);
|
||||
|
||||
// Smart foreground color selection based on brightness
|
||||
const foregroundColor = getContrastingForeground(accentValue);
|
||||
document.documentElement.style.setProperty('--primary-foreground', foregroundColor);
|
||||
|
||||
// Create proper ring colors based on input format
|
||||
const ringColor = `color-mix(in srgb, ${accentValue} 50%, transparent)`;
|
||||
document.documentElement.style.setProperty('--ring', ringColor);
|
||||
document.documentElement.style.setProperty('--sidebar-ring', ringColor);
|
||||
}
|
||||
|
||||
function getContrastingForeground(color: string): string {
|
||||
const brightness = getColorBrightness(color);
|
||||
|
||||
// Use white text for dark colors, black text for light colors
|
||||
return brightness < 0.55 ? 'oklch(0.98 0 0)' : 'oklch(0.09 0 0)';
|
||||
}
|
||||
|
||||
function getColorBrightness(color: string): number {
|
||||
// Create a temporary element to get computed color
|
||||
const tempElement = document.createElement('div');
|
||||
tempElement.style.color = color;
|
||||
document.body.appendChild(tempElement);
|
||||
|
||||
const computedColor = window.getComputedStyle(tempElement).color;
|
||||
document.body.removeChild(tempElement);
|
||||
|
||||
// Parse RGB values from computed color
|
||||
const rgbMatch = computedColor.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
|
||||
if (!rgbMatch) {
|
||||
// Fallback: assume medium brightness
|
||||
return 0.5;
|
||||
}
|
||||
|
||||
const [, r, g, b] = rgbMatch.map(Number);
|
||||
|
||||
// Calculate relative luminance using the standard formula
|
||||
// https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html
|
||||
const sR = r / 255;
|
||||
const sG = g / 255;
|
||||
const sB = b / 255;
|
||||
|
||||
const rLinear = sR <= 0.03928 ? sR / 12.92 : Math.pow((sR + 0.055) / 1.055, 2.4);
|
||||
const gLinear = sG <= 0.03928 ? sG / 12.92 : Math.pow((sG + 0.055) / 1.055, 2.4);
|
||||
const bLinear = sB <= 0.03928 ? sB / 12.92 : Math.pow((sB + 0.055) / 1.055, 2.4);
|
||||
|
||||
return 0.2126 * rLinear + 0.7152 * gLinear + 0.0722 * bLinear;
|
||||
}
|
||||
89
frontend/src/lib/utils/cached-image-util.ts
Normal file
89
frontend/src/lib/utils/cached-image-util.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
type SkipCacheUntil = {
|
||||
[key: string]: number;
|
||||
};
|
||||
|
||||
type CachableImage = {
|
||||
getUrl: (...props: any[]) => string;
|
||||
bustCache: (...props: any[]) => void;
|
||||
};
|
||||
|
||||
export const cachedApplicationLogo: CachableImage = {
|
||||
getUrl: (light = true) => {
|
||||
let url = '/api/application-configuration/logo';
|
||||
if (!light) {
|
||||
url += '?light=false';
|
||||
}
|
||||
return getCachedImageUrl(url);
|
||||
},
|
||||
bustCache: (light = true) => {
|
||||
let url = '/api/application-configuration/logo';
|
||||
if (!light) {
|
||||
url += '?light=false';
|
||||
}
|
||||
bustImageCache(url);
|
||||
}
|
||||
};
|
||||
|
||||
export const cachedBackgroundImage: CachableImage = {
|
||||
getUrl: () => getCachedImageUrl('/api/application-configuration/background-image'),
|
||||
bustCache: () => bustImageCache('/api/application-configuration/background-image')
|
||||
};
|
||||
|
||||
export const cachedProfilePicture: CachableImage = {
|
||||
getUrl: (userId: string) => {
|
||||
const url = `/api/users/${userId}/profile-picture.png`;
|
||||
return getCachedImageUrl(url);
|
||||
},
|
||||
bustCache: (userId: string) => {
|
||||
const url = `/api/users/${userId}/profile-picture.png`;
|
||||
bustImageCache(url);
|
||||
}
|
||||
};
|
||||
|
||||
export const cachedOidcClientLogo: CachableImage = {
|
||||
getUrl: (clientId: string) => {
|
||||
const url = `/api/oidc/clients/${clientId}/logo`;
|
||||
return getCachedImageUrl(url);
|
||||
},
|
||||
bustCache: (clientId: string) => {
|
||||
const url = `/api/oidc/clients/${clientId}/logo`;
|
||||
bustImageCache(url);
|
||||
}
|
||||
};
|
||||
|
||||
function getCachedImageUrl(url: string) {
|
||||
const skipCacheUntil = getSkipCacheUntil(url);
|
||||
const skipCache = skipCacheUntil > Date.now();
|
||||
if (skipCache) {
|
||||
const skipCacheParam = new URLSearchParams();
|
||||
skipCacheParam.append('skip-cache', skipCacheUntil.toString());
|
||||
url += '?' + skipCacheParam.toString();
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function bustImageCache(url: string) {
|
||||
const skipCacheUntil: SkipCacheUntil = JSON.parse(
|
||||
localStorage.getItem('skip-cache-until') ?? '{}'
|
||||
);
|
||||
skipCacheUntil[hashKey(url)] = Date.now() + 1000 * 60 * 15; // 15 minutes
|
||||
localStorage.setItem('skip-cache-until', JSON.stringify(skipCacheUntil));
|
||||
}
|
||||
|
||||
function getSkipCacheUntil(url: string) {
|
||||
const skipCacheUntil: SkipCacheUntil = JSON.parse(
|
||||
localStorage.getItem('skip-cache-until') ?? '{}'
|
||||
);
|
||||
return skipCacheUntil[hashKey(url)] ?? 0;
|
||||
}
|
||||
|
||||
function hashKey(key: string): string {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < key.length; i++) {
|
||||
const char = key.charCodeAt(i);
|
||||
hash = (hash << 5) - hash + char;
|
||||
hash = hash & hash;
|
||||
}
|
||||
return Math.abs(hash).toString(36);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export async function createSHA256hash(input: string) {
|
||||
const msgUint8 = new TextEncoder().encode(input); // encode as (utf-8) Uint8Array
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8); // hash the message
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
|
||||
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); // convert bytes to hex string
|
||||
return hashHex;
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
type SkipCacheUntil = {
|
||||
[key: string]: number;
|
||||
};
|
||||
|
||||
export function getProfilePictureUrl(userId?: string) {
|
||||
if (!userId) return '';
|
||||
|
||||
let url = `/api/users/${userId}/profile-picture.png`;
|
||||
|
||||
const skipCacheUntil = getSkipCacheUntil(userId);
|
||||
const skipCache = skipCacheUntil > Date.now();
|
||||
if (skipCache) {
|
||||
const skipCacheParam = new URLSearchParams();
|
||||
skipCacheParam.append('skip-cache', skipCacheUntil.toString());
|
||||
url += '?' + skipCacheParam.toString();
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function getSkipCacheUntil(userId: string) {
|
||||
const skipCacheUntil: SkipCacheUntil = JSON.parse(
|
||||
localStorage.getItem('skip-cache-until') ?? '{}'
|
||||
);
|
||||
return skipCacheUntil[userId] ?? 0;
|
||||
}
|
||||
|
||||
export function bustProfilePictureCache(userId: string) {
|
||||
const skipCacheUntil: SkipCacheUntil = JSON.parse(
|
||||
localStorage.getItem('skip-cache-until') ?? '{}'
|
||||
);
|
||||
skipCacheUntil[userId] = Date.now() + 1000 * 60 * 15; // 15 minutes
|
||||
localStorage.setItem('skip-cache-until', JSON.stringify(skipCacheUntil));
|
||||
}
|
||||
@@ -44,6 +44,17 @@
|
||||
<Header />
|
||||
{@render children()}
|
||||
{/if}
|
||||
<Toaster />
|
||||
<Toaster
|
||||
toastOptions={{
|
||||
classes: {
|
||||
toast: 'border border-primary/30!',
|
||||
title: 'text-foreground',
|
||||
description: 'text-muted-foreground',
|
||||
actionButton: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
cancelButton: 'bg-muted text-muted-foreground hover:bg-muted/80',
|
||||
closeButton: 'text-muted-foreground hover:text-foreground'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ConfirmDialog />
|
||||
<ModeWatcher />
|
||||
|
||||
@@ -138,14 +138,20 @@
|
||||
</Card.Root>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex w-full max-w-[450px] gap-2">
|
||||
<Button onclick={() => history.back()} class="flex-1" variant="secondary">{m.cancel()}</Button
|
||||
>
|
||||
<!-- Flex flow is reversed so the sign in button, which has auto-focus, is the first one in the DOM, for a11y -->
|
||||
<div class="flex w-full max-w-[450px] flex-row-reverse gap-2">
|
||||
{#if !errorMessage}
|
||||
<Button class="flex-1" {isLoading} onclick={authorize}>{m.sign_in()}</Button>
|
||||
<Button class="flex-1" {isLoading} onclick={authorize} autofocus={true}>
|
||||
{m.sign_in()}
|
||||
</Button>
|
||||
{:else}
|
||||
<Button class="flex-1" onclick={() => (errorMessage = null)}>{m.try_again()}</Button>
|
||||
<Button class="flex-1" onclick={() => (errorMessage = null)}>
|
||||
{m.try_again()}
|
||||
</Button>
|
||||
{/if}
|
||||
<Button onclick={() => history.back()} class="flex-1" variant="secondary">
|
||||
{m.cancel()}
|
||||
</Button>
|
||||
</div>
|
||||
</SignInWrapper>
|
||||
{/if}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import CrossAnimated from '$lib/icons/cross-animated.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import type { OidcClientMetaData } from '$lib/types/oidc.type';
|
||||
import { cachedOidcClientLogo } from '$lib/utils/cached-image-util';
|
||||
|
||||
const {
|
||||
success,
|
||||
@@ -60,7 +61,7 @@
|
||||
{:else if client.hasLogo}
|
||||
<img
|
||||
class="size-10"
|
||||
src="/api/oidc/clients/{client.id}/logo"
|
||||
src={cachedOidcClientLogo.getUrl(client.id)}
|
||||
draggable={false}
|
||||
alt={m.client_logo()}
|
||||
/>
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
{m.authenticate_yourself_with_your_passkey_to_access_the_admin_panel()}
|
||||
</p>
|
||||
{/if}
|
||||
<Button class="mt-10" {isLoading} onclick={authenticate}
|
||||
>{error ? m.try_again() : m.authenticate()}</Button
|
||||
>
|
||||
<Button class="mt-10" {isLoading} onclick={authenticate} autofocus={true}>
|
||||
{error ? m.try_again() : m.authenticate()}
|
||||
</Button>
|
||||
</SignInWrapper>
|
||||
|
||||
@@ -26,10 +26,7 @@
|
||||
};
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(3)
|
||||
.max(50),
|
||||
name: z.string().min(3).max(50),
|
||||
description: z.string().default(''),
|
||||
expiresAt: z.date().min(new Date(), m.expiration_date_must_be_in_the_future())
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<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 { LucideUpload } from '@lucide/svelte';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
@@ -44,11 +44,12 @@
|
||||
<Label class="w-52" for={id}>{label}</Label>
|
||||
<FileInput {id} variant="secondary" {accept} onchange={onImageChange}>
|
||||
<div
|
||||
class="{forceColorScheme === 'light'
|
||||
? 'bg-[#F1F1F5]'
|
||||
: forceColorScheme === 'dark'
|
||||
? 'bg-[#27272A]'
|
||||
: 'bg-muted'} group relative flex items-center rounded"
|
||||
class={{
|
||||
'group relative flex items-center rounded': true,
|
||||
'bg-[#F1F1F5]': forceColorScheme === 'light',
|
||||
'bg-[#27272A]': forceColorScheme === 'dark',
|
||||
'bg-muted': !forceColorScheme
|
||||
}}
|
||||
>
|
||||
<img
|
||||
class={cn(
|
||||
@@ -58,11 +59,13 @@
|
||||
src={imageDataURL}
|
||||
alt={label}
|
||||
/>
|
||||
<span
|
||||
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 transform font-medium opacity-0 transition-opacity group-hover:opacity-100"
|
||||
>
|
||||
{m.update()}
|
||||
</span>
|
||||
<LucideUpload
|
||||
class={{
|
||||
'absolute top-1/2 left-1/2 size-5 -translate-x-1/2 -translate-y-1/2 transform font-medium opacity-0 transition-opacity group-hover:opacity-100': true,
|
||||
'text-black': forceColorScheme === 'light',
|
||||
'text-white': forceColorScheme === 'dark'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</FileInput>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
<script lang="ts">
|
||||
import { Label } from '$lib/components/ui/label/index.js';
|
||||
import * as RadioGroup from '$lib/components/ui/radio-group/index.js';
|
||||
import { applyAccentColor } from '$lib/utils/accent-color-util';
|
||||
import { Check, Plus } from '@lucide/svelte';
|
||||
import CustomColorDialog from './custom-accent-color-dialog.svelte';
|
||||
|
||||
let {
|
||||
selectedColor = $bindable(),
|
||||
previousColor
|
||||
}: { selectedColor: string; previousColor: string } = $props();
|
||||
let showCustomColorDialog = $state(false);
|
||||
|
||||
const accentColors = [
|
||||
{ label: 'Default', color: 'default' },
|
||||
{ label: 'Rose', color: 'oklch(0.63 0.2 15)' },
|
||||
{ label: 'Orange', color: 'oklch(0.68 0.2 50)' },
|
||||
{ label: 'Amber', color: 'oklch(0.75 0.18 80)' },
|
||||
{ label: 'Green', color: 'oklch(0.65 0.2 150)' },
|
||||
{ label: 'Teal', color: 'oklch(0.6 0.15 180)' },
|
||||
{ label: 'Blue', color: 'oklch(0.6 0.2 240)' },
|
||||
{ label: 'Purple', color: 'oklch(0.6 0.24 300)' }
|
||||
];
|
||||
|
||||
// Check if current accent color is a custom color (not in predefined list)
|
||||
let isCustomColor = $derived(!accentColors.some((c) => c.color === selectedColor));
|
||||
let isPreviousColorCustom = $derived(!accentColors.some((c) => c.color === previousColor));
|
||||
|
||||
function handleAccentColorChange(accentValue: string) {
|
||||
selectedColor = accentValue;
|
||||
applyAccentColor(accentValue);
|
||||
}
|
||||
|
||||
function handleCustomColorApply(color: string) {
|
||||
handleAccentColorChange(color);
|
||||
}
|
||||
</script>
|
||||
|
||||
<RadioGroup.Root
|
||||
class="flex flex-wrap gap-3"
|
||||
value={isCustomColor ? 'custom' : selectedColor}
|
||||
onValueChange={(value) => {
|
||||
if (value != 'custom') {
|
||||
handleAccentColorChange(value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#each accentColors as accent}
|
||||
{@render colorOption(accent.label, accent.color, selectedColor === accent.color)}
|
||||
{/each}
|
||||
{#if isCustomColor || isPreviousColorCustom}
|
||||
{@render colorOption('Custom', isCustomColor ? selectedColor : previousColor, isCustomColor)}
|
||||
{/if}
|
||||
{@render colorOption('Custom', 'custom', false, true)}
|
||||
</RadioGroup.Root>
|
||||
|
||||
<CustomColorDialog bind:open={showCustomColorDialog} onApply={handleCustomColorApply} />
|
||||
|
||||
{#snippet colorOption(
|
||||
label: string,
|
||||
color: string,
|
||||
isSelected: boolean,
|
||||
isCustomColorSelection = false
|
||||
)}
|
||||
<div class="group/item relative">
|
||||
<RadioGroup.Item id={color} value={color} class="sr-only" />
|
||||
<Label
|
||||
for={color}
|
||||
class="cursor-pointer {isCustomColorSelection ? 'group' : ''}"
|
||||
onclick={() => {
|
||||
if (isCustomColorSelection) {
|
||||
showCustomColorDialog = true;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class={{
|
||||
'relative z-10 size-8 rounded-full border-2 transition-all duration-200 ease-out group-hover/item:z-20 group-hover/item:scale-110': true,
|
||||
'bg-black dark:bg-white': color === 'default'
|
||||
}}
|
||||
style={color !== 'default' ? `background-color: ${color}` : ''}
|
||||
title={label}
|
||||
>
|
||||
{#if isCustomColorSelection}
|
||||
<div
|
||||
class="bg-muted absolute inset-0 flex items-center justify-center rounded-full border-2 border-dashed border-gray-300"
|
||||
>
|
||||
<Plus class="text-muted-foreground size-4" />
|
||||
</div>
|
||||
{:else if isSelected}
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<Check class="size-4 text-white drop-shadow-sm" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div
|
||||
class="text-muted-foreground group-hover/item:text-foreground bg-background absolute top-12 left-1/2 z-20 max-w-0 -translate-x-1/2 transform overflow-hidden rounded-md border px-2 py-1 text-xs whitespace-nowrap opacity-0 shadow-sm transition-all duration-300 ease-out group-hover/item:max-w-[100px] group-hover/item:opacity-100"
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
{/snippet}
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { openConfirmDialog } from '$lib/components/confirm-dialog';
|
||||
import CheckboxWithLabel from '$lib/components/form/checkbox-with-label.svelte';
|
||||
import SwitchWithLabel from '$lib/components/form/switch-with-label.svelte';
|
||||
import FormInput from '$lib/components/form/form-input.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import Label from '$lib/components/ui/label/label.svelte';
|
||||
@@ -121,7 +121,7 @@
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
<CheckboxWithLabel
|
||||
<SwitchWithLabel
|
||||
id="skip-cert-verify"
|
||||
label={m.skip_certificate_verification()}
|
||||
description={m.this_can_be_useful_for_selfsigned_certificates()}
|
||||
@@ -130,26 +130,26 @@
|
||||
</div>
|
||||
<h4 class="mt-10 text-lg font-semibold">{m.enabled_emails()}</h4>
|
||||
<div class="mt-4 flex flex-col gap-5">
|
||||
<CheckboxWithLabel
|
||||
<SwitchWithLabel
|
||||
id="email-login-notification"
|
||||
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
|
||||
<SwitchWithLabel
|
||||
id="email-login-admin"
|
||||
label={m.email_login_code_from_admin()}
|
||||
description={m.allows_an_admin_to_send_a_login_code_to_the_user()}
|
||||
bind:checked={$inputs.emailOneTimeAccessAsAdminEnabled.value}
|
||||
/>
|
||||
<CheckboxWithLabel
|
||||
<SwitchWithLabel
|
||||
id="api-key-expiration"
|
||||
label={m.api_key_expiration()}
|
||||
description={m.send_an_email_to_the_user_when_their_api_key_is_about_to_expire()}
|
||||
bind:checked={$inputs.emailApiKeyExpirationEnabled.value}
|
||||
/>
|
||||
<CheckboxWithLabel
|
||||
<SwitchWithLabel
|
||||
id="email-login-user"
|
||||
label={m.emai_login_code_requested_by_user()}
|
||||
description={m.allow_users_to_sign_in_with_a_login_code_sent_to_their_email()}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import CheckboxWithLabel from '$lib/components/form/checkbox-with-label.svelte';
|
||||
import FormInput from '$lib/components/form/form-input.svelte';
|
||||
import SwitchWithLabel from '$lib/components/form/switch-with-label.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Label } from '$lib/components/ui/label/index.js';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
import type { AllAppConfig } from '$lib/types/application-configuration';
|
||||
@@ -9,6 +10,7 @@
|
||||
import { createForm } from '$lib/utils/form-util';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { z } from 'zod/v4';
|
||||
import AccentColorPicker from './accent-color-picker.svelte';
|
||||
|
||||
let {
|
||||
callback,
|
||||
@@ -25,7 +27,8 @@
|
||||
sessionDuration: appConfig.sessionDuration,
|
||||
emailsVerified: appConfig.emailsVerified,
|
||||
allowOwnAccountEdit: appConfig.allowOwnAccountEdit,
|
||||
disableAnimations: appConfig.disableAnimations
|
||||
disableAnimations: appConfig.disableAnimations,
|
||||
accentColor: appConfig.accentColor
|
||||
};
|
||||
|
||||
const formSchema = z.object({
|
||||
@@ -33,14 +36,17 @@
|
||||
sessionDuration: z.number().min(1).max(43200),
|
||||
emailsVerified: z.boolean(),
|
||||
allowOwnAccountEdit: z.boolean(),
|
||||
disableAnimations: z.boolean()
|
||||
disableAnimations: z.boolean(),
|
||||
accentColor: z.string()
|
||||
});
|
||||
|
||||
const { inputs, ...form } = createForm<typeof formSchema>(formSchema, updatedAppConfig);
|
||||
|
||||
async function onSubmit() {
|
||||
const data = form.validate();
|
||||
if (!data) return;
|
||||
isLoading = true;
|
||||
|
||||
await callback(data).finally(() => (isLoading = false));
|
||||
toast.success(m.application_configuration_updated_successfully());
|
||||
}
|
||||
@@ -56,24 +62,40 @@
|
||||
description={m.the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again()}
|
||||
bind:input={$inputs.sessionDuration}
|
||||
/>
|
||||
<CheckboxWithLabel
|
||||
|
||||
<SwitchWithLabel
|
||||
id="self-account-editing"
|
||||
label={m.enable_self_account_editing()}
|
||||
description={m.whether_the_users_should_be_able_to_edit_their_own_account_details()}
|
||||
bind:checked={$inputs.allowOwnAccountEdit.value}
|
||||
/>
|
||||
<CheckboxWithLabel
|
||||
<SwitchWithLabel
|
||||
id="emails-verified"
|
||||
label={m.emails_verified()}
|
||||
description={m.whether_the_users_email_should_be_marked_as_verified_for_the_oidc_clients()}
|
||||
bind:checked={$inputs.emailsVerified.value}
|
||||
/>
|
||||
<CheckboxWithLabel
|
||||
<SwitchWithLabel
|
||||
id="disable-animations"
|
||||
label={m.disable_animations()}
|
||||
description={m.turn_off_ui_animations()}
|
||||
bind:checked={$inputs.disableAnimations.value}
|
||||
/>
|
||||
|
||||
<div class="space-y-5">
|
||||
<div>
|
||||
<Label class="mb-0 text-sm font-medium">
|
||||
{m.accent_color()}
|
||||
</Label>
|
||||
<p class="text-muted-foreground text-[0.8rem]">
|
||||
{m.select_an_accent_color_to_customize_the_appearance_of_pocket_id()}
|
||||
</p>
|
||||
</div>
|
||||
<AccentColorPicker
|
||||
previousColor={appConfig.accentColor}
|
||||
bind:selectedColor={$inputs.accentColor.value}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 flex justify-end">
|
||||
<Button {isLoading} type="submit">{m.save()}</Button>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import CheckboxWithLabel from '$lib/components/form/checkbox-with-label.svelte';
|
||||
import SwitchWithLabel from '$lib/components/form/switch-with-label.svelte';
|
||||
import FormInput from '$lib/components/form/form-input.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
@@ -140,13 +140,13 @@
|
||||
placeholder="(objectClass=groupOfNames)"
|
||||
bind:input={$inputs.ldapUserGroupSearchFilter}
|
||||
/>
|
||||
<CheckboxWithLabel
|
||||
<SwitchWithLabel
|
||||
id="skip-cert-verify"
|
||||
label={m.skip_certificate_verification()}
|
||||
description={m.this_can_be_useful_for_selfsigned_certificates()}
|
||||
bind:checked={$inputs.ldapSkipCertVerify.value}
|
||||
/>
|
||||
<CheckboxWithLabel
|
||||
<SwitchWithLabel
|
||||
id="ldap-soft-delete-users"
|
||||
label={m.ldap_soft_delete_users()}
|
||||
description={m.ldap_soft_delete_users_description()}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label/index.js';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { preventDefault } from '$lib/utils/event-util';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
onApply
|
||||
}: {
|
||||
open: boolean;
|
||||
onApply: (color: string) => void;
|
||||
} = $props();
|
||||
|
||||
let customColorInput = $state('');
|
||||
|
||||
function applyCustomColor() {
|
||||
if (!isValidColor(customColorInput)) return;
|
||||
onApply(customColorInput);
|
||||
open = false;
|
||||
}
|
||||
|
||||
function isValidColor(color: string): boolean {
|
||||
// Create a temporary element to test if the color is valid
|
||||
const testElement = document.createElement('div');
|
||||
testElement.style.color = color;
|
||||
return testElement.style.color !== '';
|
||||
}
|
||||
|
||||
function onOpenChange(newOpen: boolean) {
|
||||
if (!newOpen) {
|
||||
customColorInput = '';
|
||||
}
|
||||
open = newOpen;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root {open} {onOpenChange}>
|
||||
<Dialog.Content class="max-w-md">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title class="flex items-center gap-2">{m.custom_accent_color()}</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
{m.custom_accent_color_description()}
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={preventDefault(applyCustomColor)}>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<Label for="custom-color-input" class="text-sm font-medium">{m.color_value()}</Label>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-full transition">
|
||||
<Input
|
||||
id="custom-color-input"
|
||||
bind:value={customColorInput}
|
||||
placeholder="#3b82f6"
|
||||
class="mt-1 flex-1"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class={{
|
||||
'border-border mt-1 rounded-lg border-1 transition-all duration-200 ease-in-out': true,
|
||||
'h-9 w-9': isValidColor(customColorInput),
|
||||
'h-0 w-0': !isValidColor(customColorInput)
|
||||
}}
|
||||
style="background-color: {customColorInput}"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog.Footer class="mt-6">
|
||||
<Button variant="secondary" onclick={() => onOpenChange(false)}>{m.cancel()}</Button>
|
||||
<Button type="submit" disabled={!customColorInput || !isValidColor(customColorInput)}
|
||||
>{m.apply()}</Button
|
||||
>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Button from '$lib/components/ui/button/button.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { cachedApplicationLogo, cachedBackgroundImage } from '$lib/utils/cached-image-util';
|
||||
import ApplicationImage from './application-image.svelte';
|
||||
|
||||
let {
|
||||
@@ -34,7 +35,7 @@
|
||||
imageClass="size-32"
|
||||
label={m.light_mode_logo()}
|
||||
bind:image={logoLight}
|
||||
imageURL="/api/application-configuration/logo?light=true"
|
||||
imageURL={cachedApplicationLogo.getUrl(true)}
|
||||
forceColorScheme="light"
|
||||
/>
|
||||
<ApplicationImage
|
||||
@@ -42,7 +43,7 @@
|
||||
imageClass="size-32"
|
||||
label={m.dark_mode_logo()}
|
||||
bind:image={logoDark}
|
||||
imageURL="/api/application-configuration/logo?light=false"
|
||||
imageURL={cachedApplicationLogo.getUrl(false)}
|
||||
forceColorScheme="dark"
|
||||
/>
|
||||
<ApplicationImage
|
||||
@@ -50,7 +51,7 @@
|
||||
imageClass="h-[350px] max-w-[500px]"
|
||||
label={m.background_image()}
|
||||
bind:image={backgroundImage}
|
||||
imageURL="/api/application-configuration/background-image"
|
||||
imageURL={cachedBackgroundImage.getUrl()}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
|
||||
@@ -51,7 +51,6 @@
|
||||
}
|
||||
|
||||
function getFieldError(index: number, field: keyof OidcClientFederatedIdentity): string | null {
|
||||
console.log(federatedIdentities);
|
||||
if (!errors) return null;
|
||||
const path = [index, field];
|
||||
return errors?.filter((e) => e.path[0] == path[0] && e.path[1] == path[1])[0]?.message;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<script lang="ts">
|
||||
import CheckboxWithLabel from '$lib/components/form/checkbox-with-label.svelte';
|
||||
import FileInput from '$lib/components/form/file-input.svelte';
|
||||
import FormInput from '$lib/components/form/form-input.svelte';
|
||||
import SwitchWithLabel from '$lib/components/form/switch-with-label.svelte';
|
||||
import ImageBox from '$lib/components/image-box.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import Label from '$lib/components/ui/label/label.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import type { OidcClient, OidcClientCreateWithLogo } from '$lib/types/oidc.type';
|
||||
import { cachedOidcClientLogo } from '$lib/utils/cached-image-util';
|
||||
import { preventDefault } from '$lib/utils/event-util';
|
||||
import { createForm } from '$lib/utils/form-util';
|
||||
import { cn } from '$lib/utils/style';
|
||||
@@ -27,7 +29,7 @@
|
||||
let showAdvancedOptions = $state(false);
|
||||
let logo = $state<File | null | undefined>();
|
||||
let logoDataURL: string | null = $state(
|
||||
existingClient?.hasLogo ? `/api/oidc/clients/${existingClient!.id}/logo` : null
|
||||
existingClient?.hasLogo ? cachedOidcClientLogo.getUrl(existingClient!.id) : null
|
||||
);
|
||||
|
||||
const client = {
|
||||
@@ -120,13 +122,13 @@
|
||||
bind:callbackURLs={$inputs.logoutCallbackURLs.value}
|
||||
bind:error={$inputs.logoutCallbackURLs.error}
|
||||
/>
|
||||
<CheckboxWithLabel
|
||||
<SwitchWithLabel
|
||||
id="public-client"
|
||||
label={m.public_client()}
|
||||
description={m.public_clients_description()}
|
||||
bind:checked={$inputs.isPublic.value}
|
||||
/>
|
||||
<CheckboxWithLabel
|
||||
<SwitchWithLabel
|
||||
id="pkce"
|
||||
label={m.pkce()}
|
||||
description={m.public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks()}
|
||||
@@ -137,13 +139,11 @@
|
||||
<Label for="logo">{m.logo()}</Label>
|
||||
<div class="mt-2 flex items-end gap-3">
|
||||
{#if logoDataURL}
|
||||
<div class="bg-muted size-32 rounded-2xl p-3">
|
||||
<img
|
||||
class="m-auto max-h-full max-w-full object-contain"
|
||||
src={logoDataURL}
|
||||
alt={m.name_logo({ name: $inputs.name.value })}
|
||||
/>
|
||||
</div>
|
||||
<ImageBox
|
||||
class="size-24"
|
||||
src={logoDataURL}
|
||||
alt={m.name_logo({ name: $inputs.name.value })}
|
||||
/>
|
||||
{/if}
|
||||
<div class="flex flex-col gap-2">
|
||||
<FileInput
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<script lang="ts">
|
||||
import AdvancedTable from '$lib/components/advanced-table.svelte';
|
||||
import { openConfirmDialog } from '$lib/components/confirm-dialog/';
|
||||
import ImageBox from '$lib/components/image-box.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import OIDCService from '$lib/services/oidc-service';
|
||||
import type { OidcClient, OidcClientWithAllowedUserGroupsCount } from '$lib/types/oidc.type';
|
||||
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
import { cachedOidcClientLogo } from '$lib/utils/cached-image-util';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
import { LucidePencil, LucideTrash } from '@lucide/svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
@@ -56,15 +58,11 @@
|
||||
{#snippet rows({ item })}
|
||||
<Table.Cell class="w-8 font-medium">
|
||||
{#if item.hasLogo}
|
||||
<div class="bg-secondary rounded-2xl p-3">
|
||||
<div class="size-8">
|
||||
<img
|
||||
class="m-auto max-h-full max-w-full object-contain"
|
||||
src="/api/oidc/clients/{item.id}/logo"
|
||||
alt={m.name_logo({ name: item.name })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ImageBox
|
||||
class="min-h-8 min-w-8"
|
||||
src={cachedOidcClientLogo.getUrl(item.id)}
|
||||
alt={m.name_logo({ name: item.name })}
|
||||
/>
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
<Table.Cell class="font-medium">{item.name}</Table.Cell>
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
</Card.Root>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Content class="pt-6">
|
||||
<Card.Content>
|
||||
<ProfilePictureSettings
|
||||
userId={user.id}
|
||||
isLdapUser={!!user.ldapId}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import CheckboxWithLabel from '$lib/components/form/checkbox-with-label.svelte';
|
||||
import SwitchWithLabel from '$lib/components/form/switch-with-label.svelte';
|
||||
import FormInput from '$lib/components/form/form-input.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
@@ -62,13 +62,13 @@
|
||||
<FormInput label={m.last_name()} bind:input={$inputs.lastName} />
|
||||
<FormInput label={m.username()} bind:input={$inputs.username} />
|
||||
<FormInput label={m.email()} bind:input={$inputs.email} />
|
||||
<CheckboxWithLabel
|
||||
<SwitchWithLabel
|
||||
id="admin-privileges"
|
||||
label={m.admin_privileges()}
|
||||
description={m.admins_have_full_access_to_the_admin_panel()}
|
||||
bind:checked={$inputs.isAdmin.value}
|
||||
/>
|
||||
<CheckboxWithLabel
|
||||
<SwitchWithLabel
|
||||
id="user-disabled"
|
||||
label={m.user_disabled()}
|
||||
description={m.disabled_users_cannot_log_in_or_use_services()}
|
||||
|
||||
@@ -18,9 +18,15 @@
|
||||
let filters: AuditLogFilter = $state({
|
||||
userId: '',
|
||||
event: '',
|
||||
location: '',
|
||||
clientName: ''
|
||||
});
|
||||
|
||||
const locationTypes = $state({
|
||||
external: 'External Networks',
|
||||
internal: 'Internal Networks'
|
||||
});
|
||||
|
||||
const eventTypes = $state({
|
||||
SIGN_IN: m.sign_in(),
|
||||
TOKEN_SIGN_IN: m.token_sign_in(),
|
||||
@@ -47,7 +53,7 @@
|
||||
>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<div class="mb-6 grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<div class="mb-6 grid grid-cols-1 gap-4 md:grid-cols-4">
|
||||
<div>
|
||||
{#await auditLogService.listUsers()}
|
||||
<Select.Root type="single">
|
||||
@@ -82,6 +88,20 @@
|
||||
bind:value={filters.event}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<SearchableSelect
|
||||
disableSearch={true}
|
||||
class="w-full"
|
||||
items={[
|
||||
{ value: '', label: m.all_locations() },
|
||||
...Object.entries(locationTypes).map(([value, label]) => ({
|
||||
value,
|
||||
label
|
||||
}))
|
||||
]}
|
||||
bind:value={filters.location}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{#await auditLogService.listClientNames()}
|
||||
<Select.Root
|
||||
|
||||
@@ -2,6 +2,15 @@ set -eu
|
||||
cd backend
|
||||
mkdir -p .bin
|
||||
|
||||
# Check for --docker-only flag
|
||||
DOCKER_ONLY=false
|
||||
for arg in "$@"; do
|
||||
if [ "$arg" = "--docker-only" ]; then
|
||||
DOCKER_ONLY=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Function to build for a specific platform
|
||||
build_platform() {
|
||||
target=$1
|
||||
@@ -27,7 +36,7 @@ build_platform() {
|
||||
env_vars="${env_vars} GOARM=${arm_version}"
|
||||
fi
|
||||
|
||||
# Build the binary
|
||||
# Build the binary
|
||||
eval "${env_vars} go build \
|
||||
-ldflags='-X github.com/pocket-id/pocket-id/backend/internal/common.Version=${pocket_id_version} -buildid ${pocket_id_version}' \
|
||||
-o \"${output_dir}\" \
|
||||
@@ -37,22 +46,29 @@ build_platform() {
|
||||
printf "Done\n"
|
||||
}
|
||||
|
||||
# linux builds
|
||||
build_platform "linux-amd64" "linux" "amd64" ""
|
||||
build_platform "linux-386" "linux" "386" ""
|
||||
build_platform "linux-arm64" "linux" "arm64" ""
|
||||
build_platform "linux-armv7" "linux" "arm" "7"
|
||||
if [ "$DOCKER_ONLY" = true ]; then
|
||||
echo "Building for Docker platforms only (arm64 and amd64)..."
|
||||
build_platform "linux-amd64" "linux" "amd64" ""
|
||||
build_platform "linux-arm64" "linux" "arm64" ""
|
||||
else
|
||||
echo "Building for all platforms..."
|
||||
# linux builds
|
||||
build_platform "linux-amd64" "linux" "amd64" ""
|
||||
build_platform "linux-386" "linux" "386" ""
|
||||
build_platform "linux-arm64" "linux" "arm64" ""
|
||||
build_platform "linux-armv7" "linux" "arm" "7"
|
||||
|
||||
# macOS builds
|
||||
build_platform "macos-x64" "darwin" "amd64" ""
|
||||
build_platform "macos-arm64" "darwin" "arm64" ""
|
||||
# macOS builds
|
||||
build_platform "macos-x64" "darwin" "amd64" ""
|
||||
build_platform "macos-arm64" "darwin" "arm64" ""
|
||||
|
||||
# Windows builds
|
||||
build_platform "windows-x64" "windows" "amd64" ""
|
||||
build_platform "windows-arm64" "windows" "arm64" ""
|
||||
# Windows builds
|
||||
build_platform "windows-x64" "windows" "amd64" ""
|
||||
build_platform "windows-arm64" "windows" "arm64" ""
|
||||
|
||||
# FreeBSD builds
|
||||
build_platform "freebsd-amd64" "freebsd" "amd64" ""
|
||||
build_platform "freebsd-arm64" "freebsd" "arm64" ""
|
||||
# FreeBSD builds
|
||||
build_platform "freebsd-amd64" "freebsd" "amd64" ""
|
||||
build_platform "freebsd-arm64" "freebsd" "arm64" ""
|
||||
fi
|
||||
|
||||
echo "Compilation done"
|
||||
|
||||
Reference in New Issue
Block a user