mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-03-02 00:40:21 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbacdb5bf0 | ||
|
|
f4c6cff461 | ||
|
|
0b9cbf47e3 | ||
|
|
bda178c2bb | ||
|
|
6bd6cefaa6 | ||
|
|
83be1e0b49 | ||
|
|
cf3fe0be84 | ||
|
|
ec76e1c111 | ||
|
|
6004f84845 | ||
|
|
3ec98736cf | ||
|
|
ce24372c57 | ||
|
|
4614769b84 | ||
|
|
86d2b5f59f | ||
|
|
1efd1d182d | ||
|
|
0a24ab8001 | ||
|
|
02cacba5c5 | ||
|
|
38653e2aa4 |
24
CHANGELOG.md
24
CHANGELOG.md
@@ -1,3 +1,27 @@
|
||||
## [](https://github.com/pocket-id/pocket-id/compare/v0.51.0...v) (2025-05-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow LDAP users to update their locale ([0b9cbf4](https://github.com/pocket-id/pocket-id/commit/0b9cbf47e36a332cfd854aa92e761264fb3e4795))
|
||||
* last name still showing as required on account form ([#492](https://github.com/pocket-id/pocket-id/issues/492)) ([cf3fe0b](https://github.com/pocket-id/pocket-id/commit/cf3fe0be84f6365f5d4eb08c1b47905962a48a0d))
|
||||
* non admin users weren't able to call the end session endpoint ([6bd6cef](https://github.com/pocket-id/pocket-id/commit/6bd6cefaa6dc571a319a6a1c2b2facc2404eadd3))
|
||||
|
||||
## [](https://github.com/pocket-id/pocket-id/compare/v0.50.0...v) (2025-04-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* new login code card position for mobile devices ([#452](https://github.com/pocket-id/pocket-id/issues/452)) ([02cacba](https://github.com/pocket-id/pocket-id/commit/02cacba5c5524481684cb0e1790811df113a9481))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* do not require PKCE for public clients ([ce24372](https://github.com/pocket-id/pocket-id/commit/ce24372c571cc3b277095dc6a4107663d64f45b3))
|
||||
* hide global audit log switch for non admin users ([1efd1d1](https://github.com/pocket-id/pocket-id/commit/1efd1d182dbb6190d3c7e27034426c9e48781b4a))
|
||||
* return correct error message if user isn't authorized ([86d2b5f](https://github.com/pocket-id/pocket-id/commit/86d2b5f59f26cb944017826cbd8df915cdc986f1))
|
||||
* updating scopes of an authorized client fails with Postgres ([0a24ab8](https://github.com/pocket-id/pocket-id/commit/0a24ab80010eb5a15d99915802c6698274a5c57c))
|
||||
|
||||
## [](https://github.com/pocket-id/pocket-id/compare/v0.49.0...v) (2025-04-27)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/bootstrap"
|
||||
)
|
||||
|
||||
@@ -9,5 +11,8 @@ import (
|
||||
// @description.markdown
|
||||
|
||||
func main() {
|
||||
bootstrap.Bootstrap()
|
||||
err := bootstrap.Bootstrap()
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@ func initApplicationImages() {
|
||||
log.Fatalf("Error copying file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func imageAlreadyExists(fileName string, destinationFiles []os.DirEntry) bool {
|
||||
@@ -55,6 +54,11 @@ func imageAlreadyExists(fileName string, destinationFiles []os.DirEntry) bool {
|
||||
}
|
||||
|
||||
func getImageNameWithoutExtension(fileName string) string {
|
||||
splitted := strings.Split(fileName, ".")
|
||||
return strings.Join(splitted[:len(splitted)-1], ".")
|
||||
idx := strings.LastIndexByte(fileName, '.')
|
||||
if idx < 1 {
|
||||
// No dot found, or fileName starts with a dot
|
||||
return fileName
|
||||
}
|
||||
|
||||
return fileName[:idx]
|
||||
}
|
||||
|
||||
@@ -2,23 +2,69 @@ package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/job"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils/signals"
|
||||
)
|
||||
|
||||
func Bootstrap() {
|
||||
ctx := context.TODO()
|
||||
func Bootstrap() error {
|
||||
// Get a context that is canceled when the application is stopping
|
||||
ctx := signals.SignalContext(context.Background())
|
||||
|
||||
initApplicationImages()
|
||||
|
||||
// Perform migrations for changes
|
||||
migrateConfigDBConnstring()
|
||||
|
||||
db := newDatabase()
|
||||
appConfigService := service.NewAppConfigService(ctx, db)
|
||||
|
||||
migrateKey()
|
||||
|
||||
initRouter(ctx, db, appConfigService)
|
||||
// Connect to the database
|
||||
db := newDatabase()
|
||||
|
||||
// Create all services
|
||||
svc, err := initServices(ctx, db)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize services: %w", err)
|
||||
}
|
||||
|
||||
// Init the job scheduler
|
||||
scheduler, err := job.NewScheduler()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create job scheduler: %w", err)
|
||||
}
|
||||
err = registerScheduledJobs(ctx, db, svc, scheduler)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to register scheduled jobs: %w", err)
|
||||
}
|
||||
|
||||
// Init the router
|
||||
router := initRouter(db, svc)
|
||||
|
||||
// Run all background serivces
|
||||
// This call blocks until the context is canceled
|
||||
err = utils.
|
||||
NewServiceRunner(router, scheduler.Run).
|
||||
Run(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run services: %w", err)
|
||||
}
|
||||
|
||||
// Invoke all shutdown functions
|
||||
// We give these a timeout of 5s
|
||||
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer shutdownCancel()
|
||||
err = utils.
|
||||
// TODO: Add shutdown services here
|
||||
NewServiceRunner().
|
||||
Run(shutdownCtx)
|
||||
if err != nil {
|
||||
log.Printf("Error shutting down services: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -12,9 +12,9 @@ import (
|
||||
|
||||
// When building for E2E tests, add the e2etest controller
|
||||
func init() {
|
||||
registerTestControllers = []func(apiGroup *gin.RouterGroup, db *gorm.DB, appConfigService *service.AppConfigService, jwtService *service.JwtService){
|
||||
func(apiGroup *gin.RouterGroup, db *gorm.DB, appConfigService *service.AppConfigService, jwtService *service.JwtService) {
|
||||
testService := service.NewTestService(db, appConfigService, jwtService)
|
||||
registerTestControllers = []func(apiGroup *gin.RouterGroup, db *gorm.DB, svc *services){
|
||||
func(apiGroup *gin.RouterGroup, db *gorm.DB, svc *services) {
|
||||
testService := service.NewTestService(db, svc.appConfigService, svc.jwtService)
|
||||
controller.NewTestController(apiGroup, testService)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,25 +2,35 @@ package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/controller"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/job"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/middleware"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils/systemd"
|
||||
"golang.org/x/time/rate"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/controller"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/middleware"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils/systemd"
|
||||
)
|
||||
|
||||
// This is used to register additional controllers for tests
|
||||
var registerTestControllers []func(apiGroup *gin.RouterGroup, db *gorm.DB, appConfigService *service.AppConfigService, jwtService *service.JwtService)
|
||||
var registerTestControllers []func(apiGroup *gin.RouterGroup, db *gorm.DB, svc *services)
|
||||
|
||||
func initRouter(ctx context.Context, db *gorm.DB, appConfigService *service.AppConfigService) {
|
||||
func initRouter(db *gorm.DB, svc *services) utils.Service {
|
||||
runner, err := initRouterInternal(db, svc)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to init router: %v", err)
|
||||
}
|
||||
return runner
|
||||
}
|
||||
|
||||
func initRouterInternal(db *gorm.DB, svc *services) (utils.Service, error) {
|
||||
// Set the appropriate Gin mode based on the environment
|
||||
switch common.EnvConfig.AppEnv {
|
||||
case "production":
|
||||
@@ -34,23 +44,6 @@ func initRouter(ctx context.Context, db *gorm.DB, appConfigService *service.AppC
|
||||
r := gin.Default()
|
||||
r.Use(gin.Logger())
|
||||
|
||||
// Initialize services
|
||||
emailService, err := service.NewEmailService(appConfigService, db)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to create email service: %v", err)
|
||||
}
|
||||
|
||||
geoLiteService := service.NewGeoLiteService(ctx)
|
||||
auditLogService := service.NewAuditLogService(db, appConfigService, emailService, geoLiteService)
|
||||
jwtService := service.NewJwtService(appConfigService)
|
||||
webauthnService := service.NewWebAuthnService(db, jwtService, auditLogService, appConfigService)
|
||||
userService := service.NewUserService(db, jwtService, auditLogService, emailService, appConfigService)
|
||||
customClaimService := service.NewCustomClaimService(db)
|
||||
oidcService := service.NewOidcService(db, jwtService, appConfigService, auditLogService, customClaimService)
|
||||
userGroupService := service.NewUserGroupService(db, appConfigService)
|
||||
ldapService := service.NewLdapService(db, appConfigService, userService, userGroupService)
|
||||
apiKeyService := service.NewApiKeyService(db, emailService)
|
||||
|
||||
rateLimitMiddleware := middleware.NewRateLimitMiddleware()
|
||||
|
||||
// Setup global middleware
|
||||
@@ -58,51 +51,83 @@ func initRouter(ctx context.Context, db *gorm.DB, appConfigService *service.AppC
|
||||
r.Use(middleware.NewErrorHandlerMiddleware().Add())
|
||||
r.Use(rateLimitMiddleware.Add(rate.Every(time.Second), 60))
|
||||
|
||||
job.RegisterLdapJobs(ctx, ldapService, appConfigService)
|
||||
job.RegisterDbCleanupJobs(ctx, db)
|
||||
job.RegisterFileCleanupJobs(ctx, db)
|
||||
job.RegisterApiKeyExpiryJob(ctx, apiKeyService, appConfigService)
|
||||
|
||||
// Initialize middleware for specific routes
|
||||
authMiddleware := middleware.NewAuthMiddleware(apiKeyService, userService, jwtService)
|
||||
authMiddleware := middleware.NewAuthMiddleware(svc.apiKeyService, svc.userService, svc.jwtService)
|
||||
fileSizeLimitMiddleware := middleware.NewFileSizeLimitMiddleware()
|
||||
|
||||
// Set up API routes
|
||||
apiGroup := r.Group("/api")
|
||||
controller.NewApiKeyController(apiGroup, authMiddleware, apiKeyService)
|
||||
controller.NewWebauthnController(apiGroup, authMiddleware, middleware.NewRateLimitMiddleware(), webauthnService, appConfigService)
|
||||
controller.NewOidcController(apiGroup, authMiddleware, fileSizeLimitMiddleware, oidcService, jwtService)
|
||||
controller.NewUserController(apiGroup, authMiddleware, middleware.NewRateLimitMiddleware(), userService, appConfigService)
|
||||
controller.NewAppConfigController(apiGroup, authMiddleware, appConfigService, emailService, ldapService)
|
||||
controller.NewAuditLogController(apiGroup, auditLogService, authMiddleware)
|
||||
controller.NewUserGroupController(apiGroup, authMiddleware, userGroupService)
|
||||
controller.NewCustomClaimController(apiGroup, authMiddleware, customClaimService)
|
||||
controller.NewApiKeyController(apiGroup, authMiddleware, svc.apiKeyService)
|
||||
controller.NewWebauthnController(apiGroup, authMiddleware, middleware.NewRateLimitMiddleware(), svc.webauthnService, svc.appConfigService)
|
||||
controller.NewOidcController(apiGroup, authMiddleware, fileSizeLimitMiddleware, svc.oidcService, svc.jwtService)
|
||||
controller.NewUserController(apiGroup, authMiddleware, middleware.NewRateLimitMiddleware(), svc.userService, svc.appConfigService)
|
||||
controller.NewAppConfigController(apiGroup, authMiddleware, svc.appConfigService, svc.emailService, svc.ldapService)
|
||||
controller.NewAuditLogController(apiGroup, svc.auditLogService, authMiddleware)
|
||||
controller.NewUserGroupController(apiGroup, authMiddleware, svc.userGroupService)
|
||||
controller.NewCustomClaimController(apiGroup, authMiddleware, svc.customClaimService)
|
||||
|
||||
// Add test controller in non-production environments
|
||||
if common.EnvConfig.AppEnv != "production" {
|
||||
for _, f := range registerTestControllers {
|
||||
f(apiGroup, db, appConfigService, jwtService)
|
||||
f(apiGroup, db, svc)
|
||||
}
|
||||
}
|
||||
|
||||
// Set up base routes
|
||||
baseGroup := r.Group("/")
|
||||
controller.NewWellKnownController(baseGroup, jwtService)
|
||||
controller.NewWellKnownController(baseGroup, svc.jwtService)
|
||||
|
||||
// Get the listener
|
||||
l, err := net.Listen("tcp", common.EnvConfig.Host+":"+common.EnvConfig.Port)
|
||||
// Set up the server
|
||||
srv := &http.Server{
|
||||
Addr: net.JoinHostPort(common.EnvConfig.Host, common.EnvConfig.Port),
|
||||
MaxHeaderBytes: 1 << 20,
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
Handler: r,
|
||||
}
|
||||
|
||||
// Set up the listener
|
||||
listener, err := net.Listen("tcp", srv.Addr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return nil, fmt.Errorf("failed to create TCP listener: %w", err)
|
||||
}
|
||||
|
||||
// Notify systemd that we are ready
|
||||
if err := systemd.SdNotifyReady(); err != nil {
|
||||
log.Println("Unable to notify systemd that the service is ready: ", err)
|
||||
// continue to serve anyway since it's not that important
|
||||
// Service runner function
|
||||
runFn := func(ctx context.Context) error {
|
||||
log.Printf("Server listening on %s", srv.Addr)
|
||||
|
||||
// Start the server in a background goroutine
|
||||
go func() {
|
||||
defer listener.Close()
|
||||
|
||||
// Next call blocks until the server is shut down
|
||||
srvErr := srv.Serve(listener)
|
||||
if srvErr != http.ErrServerClosed {
|
||||
log.Fatalf("Error starting app server: %v", srvErr)
|
||||
}
|
||||
}()
|
||||
|
||||
// Notify systemd that we are ready
|
||||
err = systemd.SdNotifyReady()
|
||||
if err != nil {
|
||||
// Log the error only
|
||||
log.Printf("[WARN] Unable to notify systemd that the service is ready: %v", err)
|
||||
}
|
||||
|
||||
// Block until the context is canceled
|
||||
<-ctx.Done()
|
||||
|
||||
// Handle graceful shutdown
|
||||
// Note we use the background context here as ctx has been canceled already
|
||||
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
shutdownErr := srv.Shutdown(shutdownCtx) //nolint:contextcheck
|
||||
shutdownCancel()
|
||||
if shutdownErr != nil {
|
||||
// Log the error only (could be context canceled)
|
||||
log.Printf("[WARN] App server shutdown error: %v", shutdownErr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Serve requests
|
||||
if err := r.RunListener(l); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return runFn, nil
|
||||
}
|
||||
|
||||
35
backend/internal/bootstrap/scheduler_bootstrap.go
Normal file
35
backend/internal/bootstrap/scheduler_bootstrap.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/job"
|
||||
)
|
||||
|
||||
func registerScheduledJobs(ctx context.Context, db *gorm.DB, svc *services, scheduler *job.Scheduler) error {
|
||||
err := scheduler.RegisterLdapJobs(ctx, svc.ldapService, svc.appConfigService)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to register LDAP jobs in scheduler: %w", err)
|
||||
}
|
||||
err = scheduler.RegisterGeoLiteUpdateJobs(ctx, svc.geoLiteService)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to register GeoLite DB update service: %w", err)
|
||||
}
|
||||
err = scheduler.RegisterDbCleanupJobs(ctx, db)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to register DB cleanup jobs in scheduler: %w", err)
|
||||
}
|
||||
err = scheduler.RegisterFileCleanupJobs(ctx, db)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to register file cleanup jobs in scheduler: %w", err)
|
||||
}
|
||||
err = scheduler.RegisterApiKeyExpiryJob(ctx, svc.apiKeyService, svc.appConfigService)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to register API key expiration jobs in scheduler: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
51
backend/internal/bootstrap/services_bootstrap.go
Normal file
51
backend/internal/bootstrap/services_bootstrap.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||
)
|
||||
|
||||
type services struct {
|
||||
appConfigService *service.AppConfigService
|
||||
emailService *service.EmailService
|
||||
geoLiteService *service.GeoLiteService
|
||||
auditLogService *service.AuditLogService
|
||||
jwtService *service.JwtService
|
||||
webauthnService *service.WebAuthnService
|
||||
userService *service.UserService
|
||||
customClaimService *service.CustomClaimService
|
||||
oidcService *service.OidcService
|
||||
userGroupService *service.UserGroupService
|
||||
ldapService *service.LdapService
|
||||
apiKeyService *service.ApiKeyService
|
||||
}
|
||||
|
||||
// Initializes all services
|
||||
// The context should be used by services only for initialization, and not for running
|
||||
func initServices(initCtx context.Context, db *gorm.DB) (svc *services, err error) {
|
||||
svc = &services{}
|
||||
|
||||
svc.appConfigService = service.NewAppConfigService(initCtx, db)
|
||||
|
||||
svc.emailService, err = service.NewEmailService(db, svc.appConfigService)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create email service: %w", err)
|
||||
}
|
||||
|
||||
svc.geoLiteService = service.NewGeoLiteService()
|
||||
svc.auditLogService = service.NewAuditLogService(db, svc.appConfigService, svc.emailService, svc.geoLiteService)
|
||||
svc.jwtService = service.NewJwtService(svc.appConfigService)
|
||||
svc.userService = service.NewUserService(db, svc.jwtService, svc.auditLogService, svc.emailService, svc.appConfigService)
|
||||
svc.customClaimService = service.NewCustomClaimService(db)
|
||||
svc.oidcService = service.NewOidcService(db, svc.jwtService, svc.appConfigService, svc.auditLogService, svc.customClaimService)
|
||||
svc.userGroupService = service.NewUserGroupService(db, svc.appConfigService)
|
||||
svc.ldapService = service.NewLdapService(db, svc.appConfigService, svc.userService, svc.userGroupService)
|
||||
svc.apiKeyService = service.NewApiKeyService(db, svc.emailService)
|
||||
svc.webauthnService = service.NewWebAuthnService(db, svc.jwtService, svc.auditLogService, svc.appConfigService)
|
||||
|
||||
return svc, nil
|
||||
}
|
||||
@@ -30,8 +30,8 @@ func NewOidcController(group *gin.RouterGroup, authMiddleware *middleware.AuthMi
|
||||
group.POST("/oidc/token", oc.createTokensHandler)
|
||||
group.GET("/oidc/userinfo", oc.userInfoHandler)
|
||||
group.POST("/oidc/userinfo", oc.userInfoHandler)
|
||||
group.POST("/oidc/end-session", authMiddleware.WithSuccessOptional().Add(), oc.EndSessionHandler)
|
||||
group.GET("/oidc/end-session", authMiddleware.WithSuccessOptional().Add(), oc.EndSessionHandler)
|
||||
group.POST("/oidc/end-session", authMiddleware.WithAdminNotRequired().WithSuccessOptional().Add(), oc.EndSessionHandler)
|
||||
group.GET("/oidc/end-session", authMiddleware.WithAdminNotRequired().WithSuccessOptional().Add(), oc.EndSessionHandler)
|
||||
group.POST("/oidc/introspect", oc.introspectTokenHandler)
|
||||
|
||||
group.GET("/oidc/clients", authMiddleware.Add(), oc.listClientsHandler)
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"github.com/go-co-op/gocron/v2"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||
)
|
||||
|
||||
@@ -13,20 +12,13 @@ type ApiKeyEmailJobs struct {
|
||||
appConfigService *service.AppConfigService
|
||||
}
|
||||
|
||||
func RegisterApiKeyExpiryJob(ctx context.Context, apiKeyService *service.ApiKeyService, appConfigService *service.AppConfigService) {
|
||||
func (s *Scheduler) RegisterApiKeyExpiryJob(ctx context.Context, apiKeyService *service.ApiKeyService, appConfigService *service.AppConfigService) error {
|
||||
jobs := &ApiKeyEmailJobs{
|
||||
apiKeyService: apiKeyService,
|
||||
appConfigService: appConfigService,
|
||||
}
|
||||
|
||||
scheduler, err := gocron.NewScheduler()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create a new scheduler: %v", err)
|
||||
}
|
||||
|
||||
registerJob(ctx, scheduler, "ExpiredApiKeyEmailJob", "0 0 * * *", jobs.checkAndNotifyExpiringApiKeys)
|
||||
|
||||
scheduler.Start()
|
||||
return s.registerJob(ctx, "ExpiredApiKeyEmailJob", "0 0 * * *", jobs.checkAndNotifyExpiringApiKeys)
|
||||
}
|
||||
|
||||
func (j *ApiKeyEmailJobs) checkAndNotifyExpiringApiKeys(ctx context.Context) error {
|
||||
|
||||
@@ -2,30 +2,25 @@ package job
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/go-co-op/gocron/v2"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
||||
)
|
||||
|
||||
func RegisterDbCleanupJobs(ctx context.Context, db *gorm.DB) {
|
||||
scheduler, err := gocron.NewScheduler()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create a new scheduler: %s", err)
|
||||
}
|
||||
|
||||
func (s *Scheduler) RegisterDbCleanupJobs(ctx context.Context, db *gorm.DB) error {
|
||||
jobs := &DbCleanupJobs{db: db}
|
||||
|
||||
registerJob(ctx, scheduler, "ClearWebauthnSessions", "0 3 * * *", jobs.clearWebauthnSessions)
|
||||
registerJob(ctx, scheduler, "ClearOneTimeAccessTokens", "0 3 * * *", jobs.clearOneTimeAccessTokens)
|
||||
registerJob(ctx, scheduler, "ClearOidcAuthorizationCodes", "0 3 * * *", jobs.clearOidcAuthorizationCodes)
|
||||
registerJob(ctx, scheduler, "ClearOidcRefreshTokens", "0 3 * * *", jobs.clearOidcRefreshTokens)
|
||||
registerJob(ctx, scheduler, "ClearAuditLogs", "0 3 * * *", jobs.clearAuditLogs)
|
||||
scheduler.Start()
|
||||
return errors.Join(
|
||||
s.registerJob(ctx, "ClearWebauthnSessions", "0 3 * * *", jobs.clearWebauthnSessions),
|
||||
s.registerJob(ctx, "ClearOneTimeAccessTokens", "0 3 * * *", jobs.clearOneTimeAccessTokens),
|
||||
s.registerJob(ctx, "ClearOidcAuthorizationCodes", "0 3 * * *", jobs.clearOidcAuthorizationCodes),
|
||||
s.registerJob(ctx, "ClearOidcRefreshTokens", "0 3 * * *", jobs.clearOidcRefreshTokens),
|
||||
s.registerJob(ctx, "ClearAuditLogs", "0 3 * * *", jobs.clearAuditLogs),
|
||||
)
|
||||
}
|
||||
|
||||
type DbCleanupJobs struct {
|
||||
|
||||
@@ -8,24 +8,16 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/go-co-op/gocron/v2"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||
)
|
||||
|
||||
func RegisterFileCleanupJobs(ctx context.Context, db *gorm.DB) {
|
||||
scheduler, err := gocron.NewScheduler()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create a new scheduler: %s", err)
|
||||
}
|
||||
|
||||
func (s *Scheduler) RegisterFileCleanupJobs(ctx context.Context, db *gorm.DB) error {
|
||||
jobs := &FileCleanupJobs{db: db}
|
||||
|
||||
registerJob(ctx, scheduler, "ClearUnusedDefaultProfilePictures", "0 2 * * 0", jobs.clearUnusedDefaultProfilePictures)
|
||||
|
||||
scheduler.Start()
|
||||
return s.registerJob(ctx, "ClearUnusedDefaultProfilePictures", "0 2 * * 0", jobs.clearUnusedDefaultProfilePictures)
|
||||
}
|
||||
|
||||
type FileCleanupJobs struct {
|
||||
|
||||
45
backend/internal/job/geoloite_update_job.go
Normal file
45
backend/internal/job/geoloite_update_job.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||
)
|
||||
|
||||
type GeoLiteUpdateJobs struct {
|
||||
geoLiteService *service.GeoLiteService
|
||||
}
|
||||
|
||||
func (s *Scheduler) RegisterGeoLiteUpdateJobs(ctx context.Context, geoLiteService *service.GeoLiteService) error {
|
||||
// Check if the service needs periodic updating
|
||||
if geoLiteService.DisableUpdater() {
|
||||
// Nothing to do
|
||||
return nil
|
||||
}
|
||||
|
||||
jobs := &GeoLiteUpdateJobs{geoLiteService: geoLiteService}
|
||||
|
||||
// Register the job to run every day, at 5 minutes past midnight
|
||||
err := s.registerJob(ctx, "UpdateGeoLiteDB", "5 * */1 * *", jobs.updateGoeLiteDB)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Run the job immediately on startup, with a 1s delay
|
||||
go func() {
|
||||
time.Sleep(time.Second)
|
||||
err = jobs.updateGoeLiteDB(ctx)
|
||||
if err != nil {
|
||||
// Log the error only, but don't return it
|
||||
log.Printf("Failed to Update GeoLite database: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *GeoLiteUpdateJobs) updateGoeLiteDB(ctx context.Context) error {
|
||||
return j.geoLiteService.UpdateDatabase(ctx)
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"github.com/go-co-op/gocron/v2"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func registerJob(ctx context.Context, scheduler gocron.Scheduler, name string, interval string, job func(ctx context.Context) error) {
|
||||
_, err := scheduler.NewJob(
|
||||
gocron.CronJob(interval, false),
|
||||
gocron.NewTask(job),
|
||||
gocron.WithContext(ctx),
|
||||
gocron.WithEventListeners(
|
||||
gocron.AfterJobRuns(func(jobID uuid.UUID, jobName string) {
|
||||
log.Printf("Job %q run successfully", name)
|
||||
}),
|
||||
gocron.AfterJobRunsWithError(func(jobID uuid.UUID, jobName string, err error) {
|
||||
log.Printf("Job %q failed with error: %v", name, err)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to register job %q: %v", name, err)
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"github.com/go-co-op/gocron/v2"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||
)
|
||||
|
||||
@@ -13,24 +12,23 @@ type LdapJobs struct {
|
||||
appConfigService *service.AppConfigService
|
||||
}
|
||||
|
||||
func RegisterLdapJobs(ctx context.Context, ldapService *service.LdapService, appConfigService *service.AppConfigService) {
|
||||
func (s *Scheduler) RegisterLdapJobs(ctx context.Context, ldapService *service.LdapService, appConfigService *service.AppConfigService) error {
|
||||
jobs := &LdapJobs{ldapService: ldapService, appConfigService: appConfigService}
|
||||
|
||||
scheduler, err := gocron.NewScheduler()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create a new scheduler: %v", err)
|
||||
}
|
||||
|
||||
// Register the job to run every hour
|
||||
registerJob(ctx, scheduler, "SyncLdap", "0 * * * *", jobs.syncLdap)
|
||||
err := s.registerJob(ctx, "SyncLdap", "0 * * * *", jobs.syncLdap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Run the job immediately on startup
|
||||
err = jobs.syncLdap(ctx)
|
||||
if err != nil {
|
||||
// Log the error only, but don't return it
|
||||
log.Printf("Failed to sync LDAP: %v", err)
|
||||
}
|
||||
|
||||
scheduler.Start()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *LdapJobs) syncLdap(ctx context.Context) error {
|
||||
|
||||
66
backend/internal/job/scheduler.go
Normal file
66
backend/internal/job/scheduler.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/go-co-op/gocron/v2"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Scheduler struct {
|
||||
scheduler gocron.Scheduler
|
||||
}
|
||||
|
||||
func NewScheduler() (*Scheduler, error) {
|
||||
scheduler, err := gocron.NewScheduler()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create a new scheduler: %w", err)
|
||||
}
|
||||
|
||||
return &Scheduler{
|
||||
scheduler: scheduler,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Run the scheduler.
|
||||
// This function blocks until the context is canceled.
|
||||
func (s *Scheduler) Run(ctx context.Context) error {
|
||||
log.Println("Starting job scheduler")
|
||||
s.scheduler.Start()
|
||||
|
||||
// Block until context is canceled
|
||||
<-ctx.Done()
|
||||
|
||||
err := s.scheduler.Shutdown()
|
||||
if err != nil {
|
||||
log.Printf("[WARN] Error shutting down job scheduler: %v", err)
|
||||
} else {
|
||||
log.Println("Job scheduler shut down")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Scheduler) registerJob(ctx context.Context, name string, interval string, job func(ctx context.Context) error) error {
|
||||
_, err := s.scheduler.NewJob(
|
||||
gocron.CronJob(interval, false),
|
||||
gocron.NewTask(job),
|
||||
gocron.WithContext(ctx),
|
||||
gocron.WithEventListeners(
|
||||
gocron.AfterJobRuns(func(jobID uuid.UUID, jobName string) {
|
||||
log.Printf("Job %q run successfully", name)
|
||||
}),
|
||||
gocron.AfterJobRunsWithError(func(jobID uuid.UUID, jobName string, err error) {
|
||||
log.Printf("Job %q failed with error: %v", name, err)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to register job %q: %w", name, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||
)
|
||||
|
||||
@@ -69,6 +72,13 @@ func (m *AuthMiddleware) Add() gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// If JWT auth failed and the error is not a NotSignedInError, abort the request
|
||||
if !errors.Is(err, &common.NotSignedInError{}) {
|
||||
c.Abort()
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// JWT auth failed, try API key auth
|
||||
userID, isAdmin, err = m.apiKeyMiddleware.Verify(c, m.options.AdminRequired)
|
||||
if err == nil {
|
||||
|
||||
@@ -26,12 +26,12 @@ type AppConfigService struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewAppConfigService(ctx context.Context, db *gorm.DB) *AppConfigService {
|
||||
func NewAppConfigService(initCtx context.Context, db *gorm.DB) *AppConfigService {
|
||||
service := &AppConfigService{
|
||||
db: db,
|
||||
}
|
||||
|
||||
err := service.LoadDbConfig(ctx)
|
||||
err := service.LoadDbConfig(initCtx)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize app config service: %v", err)
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ type EmailService struct {
|
||||
textTemplates map[string]*ttemplate.Template
|
||||
}
|
||||
|
||||
func NewEmailService(appConfigService *AppConfigService, db *gorm.DB) (*EmailService, error) {
|
||||
func NewEmailService(db *gorm.DB, appConfigService *AppConfigService) (*EmailService, error) {
|
||||
htmlTemplates, err := email.PrepareHTMLTemplates(emailTemplatesPaths)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("prepare html templates: %w", err)
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
|
||||
type GeoLiteService struct {
|
||||
disableUpdater bool
|
||||
mutex sync.Mutex
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
var localhostIPNets = []*net.IPNet{
|
||||
@@ -42,25 +42,22 @@ var tailscaleIPNets = []*net.IPNet{
|
||||
}
|
||||
|
||||
// NewGeoLiteService initializes a new GeoLiteService instance and starts a goroutine to update the GeoLite2 City database.
|
||||
func NewGeoLiteService(ctx context.Context) *GeoLiteService {
|
||||
func NewGeoLiteService() *GeoLiteService {
|
||||
service := &GeoLiteService{}
|
||||
|
||||
if common.EnvConfig.MaxMindLicenseKey == "" && common.EnvConfig.GeoLiteDBUrl == common.MaxMindGeoLiteCityUrl {
|
||||
// Warn the user, and disable the updater.
|
||||
// Warn the user, and disable the periodic updater
|
||||
log.Println("MAXMIND_LICENSE_KEY environment variable is empty. The GeoLite2 City database won't be updated.")
|
||||
service.disableUpdater = true
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := service.updateDatabase(ctx)
|
||||
if err != nil {
|
||||
log.Printf("Failed to update GeoLite2 City database: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
func (s *GeoLiteService) DisableUpdater() bool {
|
||||
return s.disableUpdater
|
||||
}
|
||||
|
||||
// GetLocationByIP returns the country and city of the given IP address.
|
||||
func (s *GeoLiteService) GetLocationByIP(ipAddress string) (country, city string, err error) {
|
||||
// Check the IP address against known private IP ranges
|
||||
@@ -83,8 +80,8 @@ func (s *GeoLiteService) GetLocationByIP(ipAddress string) (country, city string
|
||||
}
|
||||
|
||||
// Race condition between reading and writing the database.
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
s.mutex.RLock()
|
||||
defer s.mutex.RUnlock()
|
||||
|
||||
db, err := maxminddb.Open(common.EnvConfig.GeoLiteDBPath)
|
||||
if err != nil {
|
||||
@@ -92,7 +89,10 @@ func (s *GeoLiteService) GetLocationByIP(ipAddress string) (country, city string
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
addr := netip.MustParseAddr(ipAddress)
|
||||
addr, err := netip.ParseAddr(ipAddress)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to parse IP address: %w", err)
|
||||
}
|
||||
|
||||
var record struct {
|
||||
City struct {
|
||||
@@ -112,18 +112,13 @@ func (s *GeoLiteService) GetLocationByIP(ipAddress string) (country, city string
|
||||
}
|
||||
|
||||
// UpdateDatabase checks the age of the database and updates it if it's older than 14 days.
|
||||
func (s *GeoLiteService) updateDatabase(parentCtx context.Context) error {
|
||||
if s.disableUpdater {
|
||||
// Avoid updating the GeoLite2 City database.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *GeoLiteService) UpdateDatabase(parentCtx context.Context) error {
|
||||
if s.isDatabaseUpToDate() {
|
||||
log.Println("GeoLite2 City database is up-to-date.")
|
||||
log.Println("GeoLite2 City database is up-to-date")
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Println("Updating GeoLite2 City database...")
|
||||
log.Println("Updating GeoLite2 City database")
|
||||
downloadUrl := fmt.Sprintf(common.EnvConfig.GeoLiteDBUrl, common.EnvConfig.MaxMindLicenseKey)
|
||||
|
||||
ctx, cancel := context.WithTimeout(parentCtx, 10*time.Minute)
|
||||
@@ -145,7 +140,8 @@ func (s *GeoLiteService) updateDatabase(parentCtx context.Context) error {
|
||||
}
|
||||
|
||||
// Extract the database file directly to the target path
|
||||
if err := s.extractDatabase(resp.Body); err != nil {
|
||||
err = s.extractDatabase(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to extract database: %w", err)
|
||||
}
|
||||
|
||||
@@ -179,10 +175,9 @@ func (s *GeoLiteService) extractDatabase(reader io.Reader) error {
|
||||
// Iterate over the files in the tar archive
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
if err == io.EOF {
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to read tar archive: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/jwt"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
@@ -94,24 +96,8 @@ func (s *OidcService) Authorize(ctx context.Context, input dto.AuthorizeOidcClie
|
||||
|
||||
// If the user has not authorized the client, create a new authorization in the database
|
||||
if !hasAuthorizedClient {
|
||||
userAuthorizedClient := model.UserAuthorizedOidcClient{
|
||||
UserID: userID,
|
||||
ClientID: input.ClientID,
|
||||
Scope: input.Scope,
|
||||
}
|
||||
|
||||
err = tx.
|
||||
WithContext(ctx).
|
||||
Create(&userAuthorizedClient).
|
||||
Error
|
||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||
// The client has already been authorized but with a different scope so we need to update the scope
|
||||
if err := tx.
|
||||
WithContext(ctx).
|
||||
Model(&userAuthorizedClient).Update("scope", input.Scope).Error; err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
} else if err != nil {
|
||||
err := s.createAuthorizedClientInternal(ctx, userID, input.ClientID, input.Scope, tx)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
@@ -201,7 +187,7 @@ func (s *OidcService) createTokenFromDeviceCode(ctx context.Context, deviceCode,
|
||||
tx.Rollback()
|
||||
}()
|
||||
|
||||
_, err = s.VerifyClientCredentials(ctx, clientID, clientSecret, tx)
|
||||
_, err = s.verifyClientCredentialsInternal(ctx, clientID, clientSecret, tx)
|
||||
if err != nil {
|
||||
return "", "", "", 0, err
|
||||
}
|
||||
@@ -269,7 +255,7 @@ func (s *OidcService) createTokenFromAuthorizationCode(ctx context.Context, code
|
||||
tx.Rollback()
|
||||
}()
|
||||
|
||||
client, err := s.VerifyClientCredentials(ctx, clientID, clientSecret, tx)
|
||||
client, err := s.verifyClientCredentialsInternal(ctx, clientID, clientSecret, tx)
|
||||
if err != nil {
|
||||
return "", "", "", 0, err
|
||||
}
|
||||
@@ -342,7 +328,7 @@ func (s *OidcService) createTokenFromRefreshToken(ctx context.Context, refreshTo
|
||||
tx.Rollback()
|
||||
}()
|
||||
|
||||
_, err = s.VerifyClientCredentials(ctx, clientID, clientSecret, tx)
|
||||
_, err = s.verifyClientCredentialsInternal(ctx, clientID, clientSecret, tx)
|
||||
if err != nil {
|
||||
return "", "", 0, err
|
||||
}
|
||||
@@ -401,7 +387,7 @@ func (s *OidcService) IntrospectToken(ctx context.Context, clientID, clientSecre
|
||||
return introspectDto, &common.OidcMissingClientCredentialsError{}
|
||||
}
|
||||
|
||||
_, err = s.VerifyClientCredentials(ctx, clientID, clientSecret, s.db)
|
||||
_, err = s.verifyClientCredentialsInternal(ctx, clientID, clientSecret, s.db)
|
||||
if err != nil {
|
||||
return introspectDto, err
|
||||
}
|
||||
@@ -520,7 +506,7 @@ func (s *OidcService) CreateClient(ctx context.Context, input dto.OidcClientCrea
|
||||
LogoutCallbackURLs: input.LogoutCallbackURLs,
|
||||
CreatedByID: userID,
|
||||
IsPublic: input.IsPublic,
|
||||
PkceEnabled: input.IsPublic || input.PkceEnabled,
|
||||
PkceEnabled: input.PkceEnabled,
|
||||
}
|
||||
|
||||
err := s.db.
|
||||
@@ -999,7 +985,7 @@ func (s *OidcService) getCallbackURL(urls []string, inputCallbackURL string) (ca
|
||||
}
|
||||
|
||||
func (s *OidcService) CreateDeviceAuthorization(ctx context.Context, input dto.OidcDeviceAuthorizationRequestDto) (*dto.OidcDeviceAuthorizationResponseDto, error) {
|
||||
client, err := s.VerifyClientCredentials(ctx, input.ClientID, input.ClientSecret, s.db)
|
||||
client, err := s.verifyClientCredentialsInternal(ctx, input.ClientID, input.ClientSecret, s.db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1095,23 +1081,11 @@ func (s *OidcService) VerifyDeviceCode(ctx context.Context, userCode string, use
|
||||
}
|
||||
|
||||
if !hasAuthorizedClient {
|
||||
userAuthorizedClient := model.UserAuthorizedOidcClient{
|
||||
UserID: userID,
|
||||
ClientID: deviceAuth.ClientID,
|
||||
Scope: deviceAuth.Scope,
|
||||
err := s.createAuthorizedClientInternal(ctx, userID, deviceAuth.ClientID, deviceAuth.Scope, tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.WithContext(ctx).Create(&userAuthorizedClient).Error; err != nil {
|
||||
if !errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||
return err
|
||||
}
|
||||
// If duplicate, update scope
|
||||
if err := tx.WithContext(ctx).Model(&model.UserAuthorizedOidcClient{}).
|
||||
Where("user_id = ? AND client_id = ?", userID, deviceAuth.ClientID).
|
||||
Update("scope", deviceAuth.Scope).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
s.auditLogService.Create(ctx, model.AuditLogEventNewDeviceCodeAuthorization, ipAddress, userAgent, userID, model.AuditLogData{"clientName": deviceAuth.Client.Name}, tx)
|
||||
} else {
|
||||
s.auditLogService.Create(ctx, model.AuditLogEventDeviceCodeAuthorization, ipAddress, userAgent, userID, model.AuditLogData{"clientName": deviceAuth.Client.Name}, tx)
|
||||
@@ -1188,7 +1162,25 @@ func (s *OidcService) createRefreshToken(ctx context.Context, clientID string, u
|
||||
return refreshToken, nil
|
||||
}
|
||||
|
||||
func (s *OidcService) VerifyClientCredentials(ctx context.Context, clientID, clientSecret string, tx *gorm.DB) (model.OidcClient, error) {
|
||||
func (s *OidcService) createAuthorizedClientInternal(ctx context.Context, userID string, clientID string, scope string, tx *gorm.DB) error {
|
||||
userAuthorizedClient := model.UserAuthorizedOidcClient{
|
||||
UserID: userID,
|
||||
ClientID: clientID,
|
||||
Scope: scope,
|
||||
}
|
||||
|
||||
err := tx.WithContext(ctx).
|
||||
Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "user_id"}, {Name: "client_id"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"scope"}),
|
||||
}).
|
||||
Create(&userAuthorizedClient).
|
||||
Error
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *OidcService) verifyClientCredentialsInternal(ctx context.Context, clientID, clientSecret string, tx *gorm.DB) (model.OidcClient, error) {
|
||||
if clientID == "" {
|
||||
return model.OidcClient{}, &common.OidcMissingClientCredentialsError{}
|
||||
}
|
||||
|
||||
@@ -262,13 +262,13 @@ func (s *UserService) createUserInternal(ctx context.Context, input dto.UserCrea
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *UserService) UpdateUser(ctx context.Context, userID string, updatedUser dto.UserCreateDto, updateOwnUser bool, allowLdapUpdate bool) (model.User, error) {
|
||||
func (s *UserService) UpdateUser(ctx context.Context, userID string, updatedUser dto.UserCreateDto, updateOwnUser bool, isLdapSync bool) (model.User, error) {
|
||||
tx := s.db.Begin()
|
||||
defer func() {
|
||||
tx.Rollback()
|
||||
}()
|
||||
|
||||
user, err := s.updateUserInternal(ctx, userID, updatedUser, updateOwnUser, allowLdapUpdate, tx)
|
||||
user, err := s.updateUserInternal(ctx, userID, updatedUser, updateOwnUser, isLdapSync, tx)
|
||||
if err != nil {
|
||||
return model.User{}, err
|
||||
}
|
||||
@@ -292,19 +292,23 @@ func (s *UserService) updateUserInternal(ctx context.Context, userID string, upd
|
||||
return model.User{}, err
|
||||
}
|
||||
|
||||
// Disallow updating the user if it is an LDAP group and LDAP is enabled
|
||||
if !isLdapSync && user.LdapID != nil && s.appConfigService.GetDbConfig().LdapEnabled.IsTrue() {
|
||||
return model.User{}, &common.LdapUserUpdateError{}
|
||||
}
|
||||
// Check if this is an LDAP user and LDAP is enabled
|
||||
isLdapUser := user.LdapID != nil && s.appConfigService.GetDbConfig().LdapEnabled.IsTrue()
|
||||
|
||||
user.FirstName = updatedUser.FirstName
|
||||
user.LastName = updatedUser.LastName
|
||||
user.Email = updatedUser.Email
|
||||
user.Username = updatedUser.Username
|
||||
user.Locale = updatedUser.Locale
|
||||
if !updateOwnUser {
|
||||
user.IsAdmin = updatedUser.IsAdmin
|
||||
user.Disabled = updatedUser.Disabled
|
||||
// For LDAP users, only allow updating the locale unless it's an LDAP sync
|
||||
if !isLdapSync && isLdapUser {
|
||||
// Only update the locale for LDAP users
|
||||
user.Locale = updatedUser.Locale
|
||||
} else {
|
||||
user.FirstName = updatedUser.FirstName
|
||||
user.LastName = updatedUser.LastName
|
||||
user.Email = updatedUser.Email
|
||||
user.Username = updatedUser.Username
|
||||
user.Locale = updatedUser.Locale
|
||||
if !updateOwnUser {
|
||||
user.IsAdmin = updatedUser.IsAdmin
|
||||
user.Disabled = updatedUser.Disabled
|
||||
}
|
||||
}
|
||||
|
||||
err = tx.
|
||||
|
||||
58
backend/internal/utils/servicerunner.go
Normal file
58
backend/internal/utils/servicerunner.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// Source:
|
||||
// https://github.com/ItalyPaleAle/traefik-forward-auth/blob/v3.5.1/pkg/utils/servicerunner.go
|
||||
// Copyright (c) 2018, Thom Seddon & Contributors Copyright (c) 2023, Alessandro Segala & Contributors
|
||||
// License: MIT (https://github.com/ItalyPaleAle/traefik-forward-auth/blob/v3.5.1/LICENSE.md)
|
||||
|
||||
// Service is a background service
|
||||
type Service func(ctx context.Context) error
|
||||
|
||||
// ServiceRunner oversees a number of services running in background
|
||||
type ServiceRunner struct {
|
||||
services []Service
|
||||
}
|
||||
|
||||
// NewServiceRunner creates a new ServiceRunner
|
||||
func NewServiceRunner(services ...Service) *ServiceRunner {
|
||||
return &ServiceRunner{
|
||||
services: services,
|
||||
}
|
||||
}
|
||||
|
||||
// Run all background services
|
||||
func (r *ServiceRunner) Run(ctx context.Context) error {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
errCh := make(chan error)
|
||||
for _, service := range r.services {
|
||||
go func(service Service) {
|
||||
// Run the service
|
||||
rErr := service(ctx)
|
||||
|
||||
// Ignore context canceled errors here as they generally indicate that the service is stopping
|
||||
if rErr != nil && !errors.Is(rErr, context.Canceled) {
|
||||
errCh <- rErr
|
||||
return
|
||||
}
|
||||
errCh <- nil
|
||||
}(service)
|
||||
}
|
||||
|
||||
// Wait for all services to return
|
||||
errs := make([]error, 0)
|
||||
for range len(r.services) {
|
||||
err := <-errCh
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
125
backend/internal/utils/servicerunner_test.go
Normal file
125
backend/internal/utils/servicerunner_test.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Source:
|
||||
// https://github.com/ItalyPaleAle/traefik-forward-auth/blob/v3.5.1/pkg/utils/servicerunner.go
|
||||
// Copyright (c) 2018, Thom Seddon & Contributors Copyright (c) 2023, Alessandro Segala & Contributors
|
||||
// License: MIT (https://github.com/ItalyPaleAle/traefik-forward-auth/blob/v3.5.1/LICENSE.md)
|
||||
|
||||
func TestServiceRunner_Run(t *testing.T) {
|
||||
t.Run("successful services", func(t *testing.T) {
|
||||
// Create a service that just returns no error after 0.2s
|
||||
successService := func(ctx context.Context) error {
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a service runner with two success services
|
||||
runner := NewServiceRunner(successService, successService)
|
||||
|
||||
// Run the services with a timeout to avoid hanging if something goes wrong
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Run should return nil when all services succeed
|
||||
err := runner.Run(ctx)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("service with error", func(t *testing.T) {
|
||||
// Create a service that returns an error
|
||||
expectedErr := errors.New("service failed")
|
||||
errorService := func(ctx context.Context) error {
|
||||
return expectedErr
|
||||
}
|
||||
|
||||
// Create a service runner with one error service and one success service
|
||||
successService := func(ctx context.Context) error {
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
return nil
|
||||
}
|
||||
|
||||
runner := NewServiceRunner(errorService, successService)
|
||||
|
||||
// Run the services with a timeout
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Run should return the error
|
||||
err := runner.Run(ctx)
|
||||
require.Error(t, err)
|
||||
|
||||
// The error should contain our expected error
|
||||
require.ErrorIs(t, err, expectedErr)
|
||||
})
|
||||
|
||||
t.Run("context canceled", func(t *testing.T) {
|
||||
// Create a service that waits until context is canceled
|
||||
waitingService := func(ctx context.Context) error {
|
||||
<-ctx.Done()
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
// Create another service that returns no error quickly
|
||||
quickService := func(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
runner := NewServiceRunner(waitingService, quickService)
|
||||
|
||||
// Create a context that we can cancel
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
// Run the runner in a goroutine
|
||||
errCh := make(chan error)
|
||||
go func() {
|
||||
errCh <- runner.Run(ctx)
|
||||
}()
|
||||
|
||||
// Cancel the context to trigger service shutdown
|
||||
cancel()
|
||||
|
||||
// Wait for the runner to finish with a timeout
|
||||
select {
|
||||
case err := <-errCh:
|
||||
require.NoError(t, err, "expected nil error (context.Canceled should be ignored)")
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("test timed out waiting for runner to finish")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("multiple errors", func(t *testing.T) {
|
||||
// Create two services that return different errors
|
||||
err1 := errors.New("error 1")
|
||||
err2 := errors.New("error 2")
|
||||
|
||||
service1 := func(ctx context.Context) error {
|
||||
return err1
|
||||
}
|
||||
service2 := func(ctx context.Context) error {
|
||||
return err2
|
||||
}
|
||||
|
||||
runner := NewServiceRunner(service1, service2)
|
||||
|
||||
// Run the services
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Run should join all errors
|
||||
err := runner.Run(ctx)
|
||||
require.Error(t, err)
|
||||
|
||||
// Check that both errors are included
|
||||
require.ErrorIs(t, err, err1)
|
||||
require.ErrorIs(t, err, err2)
|
||||
})
|
||||
}
|
||||
40
backend/internal/utils/signals/signal.go
Normal file
40
backend/internal/utils/signals/signal.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package signals
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
/*
|
||||
This code is adapted from:
|
||||
https://github.com/kubernetes-sigs/controller-runtime/blob/8499b67e316a03b260c73f92d0380de8cd2e97a1/pkg/manager/signals/signal.go
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
License: Apache2 (https://github.com/kubernetes-sigs/controller-runtime/blob/8499b67e316a03b260c73f92d0380de8cd2e97a1/LICENSE)
|
||||
*/
|
||||
|
||||
var onlyOneSignalHandler = make(chan struct{})
|
||||
|
||||
// SignalContext returns a context that is canceled when the application receives an interrupt signal.
|
||||
// A second signal forces an immediate shutdown.
|
||||
func SignalContext(parentCtx context.Context) context.Context {
|
||||
close(onlyOneSignalHandler) // Panics when called twice
|
||||
|
||||
ctx, cancel := context.WithCancel(parentCtx)
|
||||
|
||||
sigCh := make(chan os.Signal, 2)
|
||||
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-sigCh
|
||||
log.Println("Received interrupt signal. Shutting down…")
|
||||
cancel()
|
||||
|
||||
<-sigCh
|
||||
log.Println("Received a second interrupt signal. Forcing an immediate shutdown.")
|
||||
os.Exit(1)
|
||||
}()
|
||||
|
||||
return ctx
|
||||
}
|
||||
@@ -276,7 +276,7 @@
|
||||
"callback_urls": "URL zpětného volání",
|
||||
"logout_callback_urls": "URL zpětného volání při odhlášení",
|
||||
"public_client": "Veřejný klient",
|
||||
"public_clients_do_not_have_a_client_secret_and_use_pkce_instead": "Veřejní klienti nemají client secret a místo toho používají PKCE. Povolte to, pokud je váš klient SPA nebo mobilní aplikace.",
|
||||
"public_clients_description": "Veřejní klienti nemají client secret a místo toho používají PKCE. Povolte to, pokud je váš klient SPA nebo mobilní aplikace.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Exchange je bezpečnostní funkce, která zabraňuje útokům CSRF a narušení autorizačních kódů.",
|
||||
"name_logo": "Logo {name}",
|
||||
@@ -342,5 +342,9 @@
|
||||
"show_code": "Zobrazit kód",
|
||||
"callback_url_description": "URL poskytnuté klientem. Klientské zástupné znaky (*) jsou podporovány, ale raději se jim vyhýbejte, pro lepší bezpečnost.",
|
||||
"api_key_expiration": "Vypršení platnosti API klíče",
|
||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Pošlete uživateli e-mail, jakmile jejich API klíč brzy vyprší."
|
||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Pošlete uživateli e-mail, jakmile jejich API klíč brzy vyprší.",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -276,7 +276,7 @@
|
||||
"callback_urls": "Callback URLs",
|
||||
"logout_callback_urls": "Abmelde Callback URLs",
|
||||
"public_client": "Öffentlicher Client",
|
||||
"public_clients_do_not_have_a_client_secret_and_use_pkce_instead": "Öffentliche Clients haben kein Client-Geheimnis und verwenden stattdessen PKCE. Aktiviere dies, wenn dein Client eine SPA oder mobile App ist.",
|
||||
"public_clients_description": "Öffentliche Clients haben kein Client-Geheimnis und verwenden stattdessen PKCE. Aktiviere dies, wenn dein Client eine SPA oder mobile App ist.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Der Public Key Code Exchange (öffentlicher Schlüsselaustausch) ist eine Sicherheitsfunktion, um CSRF Angriffe und Angriffe zum Abfangen von Autorisierungscodes zu verhindern.",
|
||||
"name_logo": "{name} Logo",
|
||||
@@ -327,20 +327,24 @@
|
||||
"client_authorization": "Client-Autorisierung",
|
||||
"new_client_authorization": "Neue Client-Autorisierung",
|
||||
"disable_animations": "Animationen deaktivieren",
|
||||
"turn_off_all_animations_throughout_the_admin_ui": "Schalte alle Animationen im Admin UI aus.",
|
||||
"turn_off_all_animations_throughout_the_admin_ui": "Deaktiviert alle Animationen in der Benutzeroberfläche.",
|
||||
"user_disabled": "Account deaktiviert",
|
||||
"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.",
|
||||
"disabled_users_cannot_log_in_or_use_services": "Deaktivierte Benutzer können sich nicht anmelden oder Dienste nutzen.",
|
||||
"user_disabled_successfully": "Der Benutzer wurde erfolgreich deaktiviert.",
|
||||
"user_enabled_successfully": "Der Benutzer wurde erfolgreich aktiviert.",
|
||||
"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. 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."
|
||||
"disable_firstname_lastname": "Deaktiviere {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_disable_this_user": "Bist du sicher, dass du diesen Benutzer deaktivieren möchtest? Er kann sich dann nicht mehr anmelden, oder auf Dienste zugreifen.",
|
||||
"ldap_soft_delete_users": "Deaktivierte Benutzer von LDAP behalten.",
|
||||
"ldap_soft_delete_users_description": "Wenn aktiviert, werden vom LDAP gelöschte Benutzer deaktivert und nicht aus dem System gelöscht.",
|
||||
"login_code_email_success": "Der Login-Code wurde an den Benutzer gesendet.",
|
||||
"send_email": "E-Mail senden",
|
||||
"show_code": "Code anzeigen",
|
||||
"callback_url_description": "URL(s) die von deinem Client bereitgestellt werden. Wildcards (*) werden unterstützt, sollten für bessere Sicherheit jedoch lieber vermieden werden.",
|
||||
"api_key_expiration": "API Key Ablauf",
|
||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Sende eine E-Mail an den Benutzer, wenn sein API Key ablaufen wird.",
|
||||
"authorize_device": "Gerät autorisieren",
|
||||
"the_device_has_been_authorized": "Das Gerät wurde autorisiert.",
|
||||
"enter_code_displayed_in_previous_step": "Gib den Code ein, der im vorherigen Schritt angezeigt wurde.",
|
||||
"authorize": "Autorisieren"
|
||||
}
|
||||
|
||||
@@ -276,7 +276,7 @@
|
||||
"callback_urls": "Callback URLs",
|
||||
"logout_callback_urls": "Logout Callback URLs",
|
||||
"public_client": "Public Client",
|
||||
"public_clients_do_not_have_a_client_secret_and_use_pkce_instead": "Public clients do not have a client secret and use PKCE instead. Enable this if your client is a SPA or mobile app.",
|
||||
"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.",
|
||||
"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",
|
||||
|
||||
@@ -1,108 +1,108 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"my_account": "My Account",
|
||||
"logout": "Logout",
|
||||
"confirm": "Confirm",
|
||||
"key": "Key",
|
||||
"value": "Value",
|
||||
"remove_custom_claim": "Remove custom claim",
|
||||
"add_custom_claim": "Add custom claim",
|
||||
"add_another": "Add another",
|
||||
"select_a_date": "Select a date",
|
||||
"select_file": "Select File",
|
||||
"profile_picture": "Profile Picture",
|
||||
"profile_picture_is_managed_by_ldap_server": "The profile picture is managed by the LDAP server and cannot be changed here.",
|
||||
"click_profile_picture_to_upload_custom": "Click on the profile picture to upload a custom one from your files.",
|
||||
"image_should_be_in_format": "The image should be in PNG or JPEG format.",
|
||||
"items_per_page": "Items per page",
|
||||
"no_items_found": "No items found",
|
||||
"search": "Search...",
|
||||
"expand_card": "Expand card",
|
||||
"copied": "Copied",
|
||||
"click_to_copy": "Click to copy",
|
||||
"something_went_wrong": "Something went wrong",
|
||||
"go_back_to_home": "Go back to home",
|
||||
"dont_have_access_to_your_passkey": "Don't have access to your passkey?",
|
||||
"login_background": "Login background",
|
||||
"my_account": "Mi Cuenta",
|
||||
"logout": "Cerrar sesión",
|
||||
"confirm": "Confirmar",
|
||||
"key": "Clave",
|
||||
"value": "Valor",
|
||||
"remove_custom_claim": "Eliminar reclamo personalizado",
|
||||
"add_custom_claim": "Añadir reclamo personalizado",
|
||||
"add_another": "Añadir otro",
|
||||
"select_a_date": "Seleccione una fecha",
|
||||
"select_file": "Seleccione Archivo:",
|
||||
"profile_picture": "Foto de perfil",
|
||||
"profile_picture_is_managed_by_ldap_server": "La imagen de perfil es administrada por el servidor LDAP y no puede ser cambiada aquí.",
|
||||
"click_profile_picture_to_upload_custom": "Haga clic en la imagen de perfil para subir una personalizada desde sus archivos.",
|
||||
"image_should_be_in_format": "La imagen debe ser en formato PNG o JPEG.",
|
||||
"items_per_page": "Elementos por página",
|
||||
"no_items_found": "No se encontraron elementos",
|
||||
"search": "Buscar...",
|
||||
"expand_card": "Ampliar tarjeta",
|
||||
"copied": "Copiado",
|
||||
"click_to_copy": "Haz clic para copiar",
|
||||
"something_went_wrong": "Algo ha salido mal",
|
||||
"go_back_to_home": "Volver al Inicio",
|
||||
"dont_have_access_to_your_passkey": "¿No tiene acceso a su Passkey?",
|
||||
"login_background": "Fondo de página de acceso",
|
||||
"logo": "Logo",
|
||||
"login_code": "Login Code",
|
||||
"create_a_login_code_to_sign_in_without_a_passkey_once": "Create a login code that the user can use to sign in without a passkey once.",
|
||||
"one_hour": "1 hour",
|
||||
"twelve_hours": "12 hours",
|
||||
"one_day": "1 day",
|
||||
"one_week": "1 week",
|
||||
"one_month": "1 month",
|
||||
"expiration": "Expiration",
|
||||
"generate_code": "Generate Code",
|
||||
"name": "Name",
|
||||
"browser_unsupported": "Browser unsupported",
|
||||
"this_browser_does_not_support_passkeys": "This browser doesn't support passkeys. Please 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 <b>{appName}</b> account?",
|
||||
"email": "Email",
|
||||
"view_your_email_address": "View your email address",
|
||||
"profile": "Profile",
|
||||
"view_your_profile_information": "View your profile information",
|
||||
"groups": "Groups",
|
||||
"view_the_groups_you_are_a_member_of": "View the groups you are a member of",
|
||||
"cancel": "Cancel",
|
||||
"sign_in": "Sign in",
|
||||
"try_again": "Try again",
|
||||
"client_logo": "Client Logo",
|
||||
"sign_out": "Sign out",
|
||||
"do_you_want_to_sign_out_of_pocketid_with_the_account": "Do you want to sign out of Pocket ID with the account <b>{username}</b>?",
|
||||
"sign_in_to_appname": "Sign in to {appName}",
|
||||
"please_try_to_sign_in_again": "Please try to sign in again.",
|
||||
"authenticate_yourself_with_your_passkey_to_access_the_admin_panel": "Authenticate yourself with your passkey to access the admin panel.",
|
||||
"authenticate": "Authenticate",
|
||||
"appname_setup": "{appName} Setup",
|
||||
"please_try_again": "Please try again.",
|
||||
"you_are_about_to_sign_in_to_the_initial_admin_account": "You're about to sign in to the initial admin account. Anyone with this link can access the account until a passkey is added. Please set up a passkey as soon as possible to prevent unauthorized access.",
|
||||
"continue": "Continue",
|
||||
"alternative_sign_in": "Alternative Sign In",
|
||||
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "If you 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?",
|
||||
"email_login": "Email Login",
|
||||
"enter_a_login_code_to_sign_in": "Enter a login code to sign in.",
|
||||
"request_a_login_code_via_email": "Request a login code via email.",
|
||||
"go_back": "Go back",
|
||||
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "An email has been sent to the provided email, if it exists in the system.",
|
||||
"enter_code": "Enter code",
|
||||
"enter_your_email_address_to_receive_an_email_with_a_login_code": "Enter your email address to receive an email with a login code.",
|
||||
"your_email": "Your email",
|
||||
"submit": "Submit",
|
||||
"enter_the_code_you_received_to_sign_in": "Enter the code you received to sign in.",
|
||||
"code": "Code",
|
||||
"invalid_redirect_url": "Invalid redirect URL",
|
||||
"audit_log": "Audit Log",
|
||||
"users": "Users",
|
||||
"user_groups": "User Groups",
|
||||
"oidc_clients": "OIDC Clients",
|
||||
"api_keys": "API Keys",
|
||||
"application_configuration": "Application Configuration",
|
||||
"settings": "Settings",
|
||||
"update_pocket_id": "Update Pocket ID",
|
||||
"powered_by": "Powered by",
|
||||
"see_your_account_activities_from_the_last_3_months": "See your account activities from the last 3 months.",
|
||||
"time": "Time",
|
||||
"event": "Event",
|
||||
"approximate_location": "Approximate Location",
|
||||
"ip_address": "IP Address",
|
||||
"device": "Device",
|
||||
"client": "Client",
|
||||
"unknown": "Unknown",
|
||||
"account_details_updated_successfully": "Account details updated successfully",
|
||||
"login_code": "Código de inicio de sesión",
|
||||
"create_a_login_code_to_sign_in_without_a_passkey_once": "Crear un código de acceso que el usuario pueda utilizar para iniciar sesión sin un Passkey una vez.",
|
||||
"one_hour": "1 hora",
|
||||
"twelve_hours": "12 horas",
|
||||
"one_day": "1 día",
|
||||
"one_week": "1 semana",
|
||||
"one_month": "1 mes",
|
||||
"expiration": "Expiración",
|
||||
"generate_code": "Gerar Código",
|
||||
"name": "Nombre",
|
||||
"browser_unsupported": "Navegador no soportado",
|
||||
"this_browser_does_not_support_passkeys": "Este navegador no soporta Passkeys. Por favor, utilice un método de inicio de sesión alternativo.",
|
||||
"an_unknown_error_occurred": "Ocurrió un error desconocido",
|
||||
"authentication_process_was_aborted": "El proceso de autenticación fue abortado",
|
||||
"error_occurred_with_authenticator": "Ha ocurrido un error con el autenticador",
|
||||
"authenticator_does_not_support_discoverable_credentials": "El autenticador no soporta credenciales detectables",
|
||||
"authenticator_does_not_support_resident_keys": "El autenticador no soporta claves residentes",
|
||||
"passkey_was_previously_registered": "Esta Passkey ha sido registrado previamente",
|
||||
"authenticator_does_not_support_any_of_the_requested_algorithms": "El autenticador no soporta ninguno de los algoritmos solicitados",
|
||||
"authenticator_timed_out": "Se agotó el tiempo de espera del autenticador",
|
||||
"critical_error_occurred_contact_administrator": "Ha ocurrido un error crítico. Por favor, contacte a su administrador.",
|
||||
"sign_in_to": "Iniciar sesión en {name}",
|
||||
"client_not_found": "Cliente no encontrado",
|
||||
"client_wants_to_access_the_following_information": "<b>{client}</b> quiere acceder a la siguiente información:",
|
||||
"do_you_want_to_sign_in_to_client_with_your_app_name_account": "¿Quieres iniciar sesión en <b>{client}</b> con tu cuenta <b>{appName}</b>?",
|
||||
"email": "Correo electrónico",
|
||||
"view_your_email_address": "Ver su dirección de correo electrónico",
|
||||
"profile": "Perfil",
|
||||
"view_your_profile_information": "Ver información de su perfil",
|
||||
"groups": "Grupos",
|
||||
"view_the_groups_you_are_a_member_of": "Ver los grupos de los que usted es miembro",
|
||||
"cancel": "Cancelar",
|
||||
"sign_in": "Iniciar sesión",
|
||||
"try_again": "Intentar de nuevo",
|
||||
"client_logo": "Logo del cliente",
|
||||
"sign_out": "Cerrar sesión",
|
||||
"do_you_want_to_sign_out_of_pocketid_with_the_account": "¿Quieres cerrar sesión de Pocket ID con la cuenta <b>{username}</b>?",
|
||||
"sign_in_to_appname": "Iniciar sesión en {appName}",
|
||||
"please_try_to_sign_in_again": "Por favor, intente iniciar sesión de nuevo.",
|
||||
"authenticate_yourself_with_your_passkey_to_access_the_admin_panel": "Autenticar con tu Passkey para acceder al panel de administración.",
|
||||
"authenticate": "Autenticar",
|
||||
"appname_setup": "Configuración de {appName}",
|
||||
"please_try_again": "Por favor intente nuevamente.",
|
||||
"you_are_about_to_sign_in_to_the_initial_admin_account": "Estás a punto de iniciar sesión en la cuenta de administrador inicial. Cualquiera con este enlace puede acceder a la cuenta hasta que se agregue un Passkey. Por favor, configure un Passkey lo antes posible para evitar acceso no autorizado.",
|
||||
"continue": "Continuar",
|
||||
"alternative_sign_in": "Inicio de sesión alternativa",
|
||||
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "Si no tiene acceso a su Passkey, puede iniciar sesión usando uno de los siguientes métodos.",
|
||||
"use_your_passkey_instead": "¿Utilizar su Passkey en su lugar?",
|
||||
"email_login": "Ingreso con Email",
|
||||
"enter_a_login_code_to_sign_in": "Introduzca un código de acceso para iniciar sesión.",
|
||||
"request_a_login_code_via_email": "Solicitar un código de acceso por correo electrónico.",
|
||||
"go_back": "Volver atrás",
|
||||
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "Se ha enviado un correo electrónico al correo proporcionado, si existe en el sistema.",
|
||||
"enter_code": "Ingresa el código",
|
||||
"enter_your_email_address_to_receive_an_email_with_a_login_code": "Introduzca su dirección de correo electrónico para recibir un correo electrónico con un código de acceso.",
|
||||
"your_email": "Su correo electrónico",
|
||||
"submit": "Enviar",
|
||||
"enter_the_code_you_received_to_sign_in": "Ingrese el código que recibió para iniciar sesión.",
|
||||
"code": "Código",
|
||||
"invalid_redirect_url": "URL de redirección no válido",
|
||||
"audit_log": "Registro de Auditoría",
|
||||
"users": "Usuarios",
|
||||
"user_groups": "Grupos de usuario",
|
||||
"oidc_clients": "Clientes OIDC",
|
||||
"api_keys": "Llaves API",
|
||||
"application_configuration": "Configuración de la aplicación",
|
||||
"settings": "Configuración",
|
||||
"update_pocket_id": "Actualizar Pocket ID",
|
||||
"powered_by": "Producido por Pocket ID",
|
||||
"see_your_account_activities_from_the_last_3_months": "Vea las actividad de su cuenta de los últimos 3 meses.",
|
||||
"time": "Tiempo",
|
||||
"event": "Evento",
|
||||
"approximate_location": "Ubicación aproximada",
|
||||
"ip_address": "Dirección IP",
|
||||
"device": "Dispositivo",
|
||||
"client": "Cliente",
|
||||
"unknown": "Desconocido",
|
||||
"account_details_updated_successfully": "Detalles de la cuenta actualizados exitosamente",
|
||||
"profile_picture_updated_successfully": "Profile picture updated successfully. It may take a few minutes to update.",
|
||||
"account_settings": "Account Settings",
|
||||
"passkey_missing": "Passkey missing",
|
||||
@@ -276,7 +276,7 @@
|
||||
"callback_urls": "Callback URLs",
|
||||
"logout_callback_urls": "Logout Callback URLs",
|
||||
"public_client": "Public Client",
|
||||
"public_clients_do_not_have_a_client_secret_and_use_pkce_instead": "Public clients do not have a client secret and use PKCE instead. Enable this if your client is a SPA or mobile app.",
|
||||
"public_clients_description": "Public clients do not have a client secret and use PKCE instead. Enable this if your client is a SPA or mobile app.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange is a security feature to prevent CSRF and authorization code interception attacks.",
|
||||
"name_logo": "{name} logo",
|
||||
@@ -342,5 +342,9 @@
|
||||
"show_code": "Show Code",
|
||||
"callback_url_description": "URL(s) provided by your client. 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."
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -276,7 +276,7 @@
|
||||
"callback_urls": "URL de callback",
|
||||
"logout_callback_urls": "URL de callback de déconnexion",
|
||||
"public_client": "Client public",
|
||||
"public_clients_do_not_have_a_client_secret_and_use_pkce_instead": "Les clients publics n'ont pas de secret client et utilisent PKCE à la place. Activez cette option si votre client est une application SPA ou une application mobile.",
|
||||
"public_clients_description": "Les clients publics n'ont pas de secret client et utilisent PKCE à la place. Activez cette option si votre client est une application SPA ou une application mobile.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Le Public Key Code Exchange est une fonctionnalité de sécurité conçue pour prévenir les attaques CSRF et l’interception de code d’autorisation.",
|
||||
"name_logo": "Logo {name}",
|
||||
@@ -342,5 +342,9 @@
|
||||
"show_code": "Show Code",
|
||||
"callback_url_description": "URL(s) provided by your client. 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."
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
"application_configuration": "Configurazione dell'applicazione",
|
||||
"settings": "Impostazioni",
|
||||
"update_pocket_id": "Aggiorna Pocket ID",
|
||||
"powered_by": "Alimentato da",
|
||||
"powered_by": "Powered by",
|
||||
"see_your_account_activities_from_the_last_3_months": "Visualizza le attività del tuo account degli ultimi 3 mesi.",
|
||||
"time": "Ora",
|
||||
"event": "Evento",
|
||||
@@ -276,7 +276,7 @@
|
||||
"callback_urls": "URL di callback",
|
||||
"logout_callback_urls": "URL di callback per il logout",
|
||||
"public_client": "Client pubblico",
|
||||
"public_clients_do_not_have_a_client_secret_and_use_pkce_instead": "I client pubblici non hanno un client secret e utilizzano PKCE. Abilita questa opzione se il tuo client è una SPA o un'app mobile.",
|
||||
"public_clients_description": "I client pubblici non hanno un client secret e utilizzano PKCE. Abilita questa opzione se il tuo client è una SPA o un'app mobile.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Il Public Key Code Exchange è una funzionalità di sicurezza per prevenire attacchi CSRF e intercettazione del codice di autorizzazione.",
|
||||
"name_logo": "Logo di {name}",
|
||||
@@ -342,5 +342,9 @@
|
||||
"show_code": "Mostra codice",
|
||||
"callback_url_description": "URL forniti dal tuo client. Wildcard (*) sono supportati, ma meglio evitarli per una migliore sicurezza.",
|
||||
"api_key_expiration": "Scadenza Chiave API",
|
||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Invia un'email all'utente quando la sua chiave API sta per scadere."
|
||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Invia un'email all'utente quando la sua chiave API sta per scadere.",
|
||||
"authorize_device": "Autorizza Dispositivo",
|
||||
"the_device_has_been_authorized": "Il dispositivo è stato autorizzato.",
|
||||
"enter_code_displayed_in_previous_step": "Inserisci il codice visualizzato nel passaggio precedente.",
|
||||
"authorize": "Autorizza"
|
||||
}
|
||||
|
||||
@@ -276,7 +276,7 @@
|
||||
"callback_urls": "Callback-URL's",
|
||||
"logout_callback_urls": "Callback-URL's voor afmelden",
|
||||
"public_client": "Publieke client",
|
||||
"public_clients_do_not_have_a_client_secret_and_use_pkce_instead": "Publieke clients hebben geen client secret en gebruiken in plaats daarvan PKCE. Schakel dit in als uw client een SPA of mobiele app is.",
|
||||
"public_clients_description": "Publieke clients hebben geen client secret en gebruiken in plaats daarvan PKCE. Schakel dit in als uw client een SPA of mobiele app is.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange is een beveiligingsfunctie om CSRF- en autorisatiecode-onderscheppingsaanvallen te voorkomen.",
|
||||
"name_logo": "{name} logo",
|
||||
@@ -342,5 +342,9 @@
|
||||
"show_code": "Show Code",
|
||||
"callback_url_description": "URL(s) provided by your client. 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."
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -276,7 +276,7 @@
|
||||
"callback_urls": "Callback URLs",
|
||||
"logout_callback_urls": "Logout Callback URLs",
|
||||
"public_client": "Public Client",
|
||||
"public_clients_do_not_have_a_client_secret_and_use_pkce_instead": "Public clients do not have a client secret and use PKCE instead. Enable this if your client is a SPA or mobile app.",
|
||||
"public_clients_description": "Public clients do not have a client secret and use PKCE instead. Enable this if your client is a SPA or mobile app.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange is a security feature to prevent CSRF and authorization code interception attacks.",
|
||||
"name_logo": "{name} logo",
|
||||
@@ -342,5 +342,9 @@
|
||||
"show_code": "Show Code",
|
||||
"callback_url_description": "URL(s) provided by your client. 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."
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -276,7 +276,7 @@
|
||||
"callback_urls": "Callback URLs",
|
||||
"logout_callback_urls": "Logout Callback URLs",
|
||||
"public_client": "Public Client",
|
||||
"public_clients_do_not_have_a_client_secret_and_use_pkce_instead": "Public clients do not have a client secret and use PKCE instead. Enable this if your client is a SPA or mobile app.",
|
||||
"public_clients_description": "Public clients do not have a client secret and use PKCE instead. Enable this if your client is a SPA or mobile app.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange is a security feature to prevent CSRF and authorization code interception attacks.",
|
||||
"name_logo": "{name} logo",
|
||||
@@ -342,5 +342,9 @@
|
||||
"show_code": "Show Code",
|
||||
"callback_url_description": "URL(s) provided by your client. 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."
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -276,7 +276,7 @@
|
||||
"callback_urls": "Callback URLs",
|
||||
"logout_callback_urls": "Logout Callback URLs",
|
||||
"public_client": "Публичный клиент",
|
||||
"public_clients_do_not_have_a_client_secret_and_use_pkce_instead": "Публичные клиенты не имеют клиентского секрета и вместо этого используют PKCE. Включите, если ваш клиент является SPA или мобильным приложением.",
|
||||
"public_clients_description": "Публичные клиенты не имеют клиентского секрета и вместо этого используют PKCE. Включите, если ваш клиент является SPA или мобильным приложением.",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange — это функция безопасности для предотвращения атак CSRF и перехвата кода авторизации.",
|
||||
"name_logo": "Логотип {name}",
|
||||
@@ -342,5 +342,9 @@
|
||||
"show_code": "Показать код",
|
||||
"callback_url_description": "URL-адреса, предоставленные клиентом. Поддерживаются wildcard-адреса (*), но лучше всего избегать их для лучшей безопасности.",
|
||||
"api_key_expiration": "Истечение срока действия API ключа",
|
||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Отправлять пользователю письмо, когда истечет срок действия API ключа."
|
||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Отправлять пользователю письмо, когда истечет срок действия API ключа.",
|
||||
"authorize_device": "Авторизовать устройство",
|
||||
"the_device_has_been_authorized": "Устройство авторизовано.",
|
||||
"enter_code_displayed_in_previous_step": "Введите код, который был отображен на предыдущем шаге.",
|
||||
"authorize": "Авторизируйте"
|
||||
}
|
||||
|
||||
@@ -1,346 +1,346 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"my_account": "账户",
|
||||
"logout": "登出",
|
||||
"confirm": "确认",
|
||||
"key": "Key",
|
||||
"value": "Value",
|
||||
"remove_custom_claim": "移除自定义声明",
|
||||
"add_custom_claim": "添加自定义声明",
|
||||
"add_another": "添加另一个",
|
||||
"select_a_date": "选择日期",
|
||||
"select_file": "选择文件",
|
||||
"profile_picture": "头像",
|
||||
"profile_picture_is_managed_by_ldap_server": "头像由 LDAP 服务器管理,无法在此处更改。",
|
||||
"click_profile_picture_to_upload_custom": "点击头像来从文件中上传您的自定义头像。",
|
||||
"image_should_be_in_format": "图片应为 PNG 或 JPEG 格式。",
|
||||
"items_per_page": "每页条数",
|
||||
"no_items_found": "🌱 这里暂时空空如也。",
|
||||
"search": "搜索...",
|
||||
"expand_card": "展开卡片",
|
||||
"copied": "已复制",
|
||||
"click_to_copy": "点击复制",
|
||||
"something_went_wrong": "出了点问题",
|
||||
"go_back_to_home": "返回首页",
|
||||
"dont_have_access_to_your_passkey": "无法使用您的通行密钥?试试其他登录方式。",
|
||||
"login_background": "登录页背景图",
|
||||
"logo": "Logo",
|
||||
"login_code": "临时登录码",
|
||||
"create_a_login_code_to_sign_in_without_a_passkey_once": "创建一个临时登录码,用户可以使用它一次性登录而无需通行密钥。",
|
||||
"one_hour": "1 小时",
|
||||
"twelve_hours": "12 小时",
|
||||
"one_day": "1 天",
|
||||
"one_week": "1 周",
|
||||
"one_month": "1 个月",
|
||||
"expiration": "到期时间",
|
||||
"generate_code": "生成代码",
|
||||
"name": "名称",
|
||||
"browser_unsupported": "浏览器不支持",
|
||||
"this_browser_does_not_support_passkeys": "此浏览器不支持通行密钥。请使用其他登录方式。",
|
||||
"an_unknown_error_occurred": "发生未知错误",
|
||||
"authentication_process_was_aborted": "认证过程被中止",
|
||||
"error_occurred_with_authenticator": "认证器发生错误",
|
||||
"authenticator_does_not_support_discoverable_credentials": "认证器不支持可发现的凭据",
|
||||
"authenticator_does_not_support_resident_keys": "认证器不支持常驻密钥",
|
||||
"passkey_was_previously_registered": "此通行密钥之前已注册",
|
||||
"authenticator_does_not_support_any_of_the_requested_algorithms": "认证器不支持任何请求的算法",
|
||||
"authenticator_timed_out": "认证器超时",
|
||||
"critical_error_occurred_contact_administrator": "发生严重错误。请联系您的管理员。",
|
||||
"sign_in_to": "登录到 {name}",
|
||||
"client_not_found": "客户端未找到",
|
||||
"client_wants_to_access_the_following_information": "<b>{client}</b> 希望访问以下信息:",
|
||||
"do_you_want_to_sign_in_to_client_with_your_app_name_account": "您是否希望使用您的 <b>{appName}</b> 账户登录到 <b>{client}</b>?",
|
||||
"email": "电子邮件",
|
||||
"view_your_email_address": "查看您的电子邮件地址",
|
||||
"profile": "个人资料",
|
||||
"view_your_profile_information": "查看您的个人资料信息",
|
||||
"groups": "群组",
|
||||
"view_the_groups_you_are_a_member_of": "查看您所属的群组",
|
||||
"cancel": "取消",
|
||||
"sign_in": "登录",
|
||||
"try_again": "重试",
|
||||
"client_logo": "客户端标志",
|
||||
"sign_out": "登出",
|
||||
"do_you_want_to_sign_out_of_pocketid_with_the_account": "您是否希望使用账户 <b>{username}</b> 登出 Pocket ID?",
|
||||
"sign_in_to_appname": "登录到 {appName}",
|
||||
"please_try_to_sign_in_again": "请尝试重新登录。",
|
||||
"authenticate_yourself_with_your_passkey_to_access_the_admin_panel": "使用通行密钥或通过临时登录码进行登录",
|
||||
"authenticate": "登录",
|
||||
"appname_setup": "{appName} 设置",
|
||||
"please_try_again": "请重试。",
|
||||
"you_are_about_to_sign_in_to_the_initial_admin_account": "您即将登录到初始管理员账户。在此添加通行密钥之前,任何拥有此链接的人都可以访问该账户。请尽快设置通行密钥以防止未经授权的访问。",
|
||||
"continue": "继续",
|
||||
"alternative_sign_in": "替代登录方式",
|
||||
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "如果您无法访问您的通行密钥,可以使用以下方法之一登录。",
|
||||
"use_your_passkey_instead": "改用您的通行密钥?",
|
||||
"email_login": "电子邮件登录",
|
||||
"enter_a_login_code_to_sign_in": "输入一次性登录码以登录。",
|
||||
"request_a_login_code_via_email": "通过电子邮件请求登录代码。",
|
||||
"go_back": "返回",
|
||||
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "如果系统中存在提供的电子邮件地址,则已发送一封电子邮件。",
|
||||
"enter_code": "输入登录码",
|
||||
"enter_your_email_address_to_receive_an_email_with_a_login_code": "输入您的电子邮件地址以接收包含登录代码的电子邮件。",
|
||||
"your_email": "您的电子邮件",
|
||||
"submit": "提交",
|
||||
"enter_the_code_you_received_to_sign_in": "输入您收到的登录码以登录。",
|
||||
"code": "Code",
|
||||
"invalid_redirect_url": "无效的重定向 URL",
|
||||
"audit_log": "日志",
|
||||
"users": "用户",
|
||||
"user_groups": "用户组",
|
||||
"oidc_clients": "OIDC 客户端",
|
||||
"api_keys": "API 密钥",
|
||||
"application_configuration": "设置",
|
||||
"settings": "设置",
|
||||
"update_pocket_id": "更新 Pocket ID",
|
||||
"powered_by": "Powered by",
|
||||
"see_your_account_activities_from_the_last_3_months": "查看过去 3 个月的账户活动。",
|
||||
"time": "时间",
|
||||
"event": "事件",
|
||||
"approximate_location": "大致位置",
|
||||
"ip_address": "IP 地址",
|
||||
"device": "设备",
|
||||
"client": "客户端",
|
||||
"unknown": "未知",
|
||||
"account_details_updated_successfully": "账户详细信息更新成功",
|
||||
"profile_picture_updated_successfully": "头像更新成功。可能需要几分钟才能更新。",
|
||||
"account_settings": "账户设置",
|
||||
"passkey_missing": "尚未绑定通行密钥",
|
||||
"please_provide_a_passkey_to_prevent_losing_access_to_your_account": "请添加通行密钥以防止失去对账户的访问。",
|
||||
"single_passkey_configured": "已添加一个通行密钥",
|
||||
"it_is_recommended_to_add_more_than_one_passkey": "建议添加多个通行密钥以避免失去对账户的访问。",
|
||||
"account_details": "账户详情",
|
||||
"passkeys": "通行密钥",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "管理您可以用来进行身份验证的通行密钥。",
|
||||
"add_passkey": "添加通行密钥",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "创建一次性登录码,以便从不同设备登录而无需通行密钥。",
|
||||
"create": "创建",
|
||||
"first_name": "名字",
|
||||
"last_name": "姓氏",
|
||||
"username": "用户名",
|
||||
"save": "保存",
|
||||
"username_can_only_contain": "用户名只能包含小写字母、数字、下划线、点、连字符和 '@' 符号",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "使用以下代码登录。代码将在 15 分钟后过期。",
|
||||
"or_visit": "或访问",
|
||||
"added_on": "添加于",
|
||||
"rename": "重命名",
|
||||
"delete": "删除",
|
||||
"are_you_sure_you_want_to_delete_this_passkey": "您确定要删除此通行密钥吗?",
|
||||
"passkey_deleted_successfully": "通行密钥删除成功",
|
||||
"delete_passkey_name": "删除 {passkeyName}",
|
||||
"passkey_name_updated_successfully": "通行密钥名称更新成功",
|
||||
"name_passkey": "重命名通行密钥",
|
||||
"name_your_passkey_to_easily_identify_it_later": "为您的通行密钥命名,以便以后轻松识别。",
|
||||
"create_api_key": "创建 API 密钥",
|
||||
"add_a_new_api_key_for_programmatic_access": "添加一个新的 API 密钥用于编程访问。",
|
||||
"add_api_key": "添加 API 密钥",
|
||||
"manage_api_keys": "管理 API 密钥",
|
||||
"api_key_created": "API 密钥已创建",
|
||||
"for_security_reasons_this_key_will_only_be_shown_once": "出于安全原因,此密钥只会显示一次。请妥善保存。",
|
||||
"description": "描述",
|
||||
"api_key": "API 密钥",
|
||||
"close": "关闭",
|
||||
"name_to_identify_this_api_key": "用于识别此 API 密钥的名称。",
|
||||
"expires_at": "过期时间",
|
||||
"when_this_api_key_will_expire": "此 API 密钥的过期时间。",
|
||||
"optional_description_to_help_identify_this_keys_purpose": "可选描述,帮助识别此密钥的用途。",
|
||||
"name_must_be_at_least_3_characters": "名称必须至少为 3 个字符",
|
||||
"name_cannot_exceed_50_characters": "名称不能超过 50 个字符",
|
||||
"expiration_date_must_be_in_the_future": "过期日期必须是未来的日期",
|
||||
"revoke_api_key": "撤销 API 密钥",
|
||||
"never": "永不",
|
||||
"revoke": "撤销",
|
||||
"api_key_revoked_successfully": "API 密钥撤销成功",
|
||||
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "您确定要撤销 API 密钥 \"{apiKeyName}\" 吗?这将中断使用此密钥的任何集成。",
|
||||
"last_used": "最后使用",
|
||||
"actions": "操作",
|
||||
"images_updated_successfully": "图片更新成功",
|
||||
"general": "常规",
|
||||
"configure_smtp_to_send_emails": "启用电子邮件通知,以便在新设备或位置检测到登录时提醒用户。",
|
||||
"ldap": "LDAP",
|
||||
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "配置 LDAP 设置以从 LDAP 服务器同步用户和群组。",
|
||||
"images": "图片",
|
||||
"update": "更新",
|
||||
"email_configuration_updated_successfully": "电子邮件配置更新成功",
|
||||
"save_changes_question": "保存更改?",
|
||||
"you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "在发送测试电子邮件之前,您必须保存更改。是否现在保存?",
|
||||
"save_and_send": "保存并发送",
|
||||
"test_email_sent_successfully": "测试电子邮件已成功发送到您的电子邮件地址。",
|
||||
"failed_to_send_test_email": "发送测试电子邮件失败。请检查服务器日志以获取更多信息。",
|
||||
"smtp_configuration": "SMTP 配置",
|
||||
"smtp_host": "SMTP 主机",
|
||||
"smtp_port": "SMTP 端口",
|
||||
"smtp_user": "SMTP 用户",
|
||||
"smtp_password": "SMTP 密码",
|
||||
"smtp_from": "SMTP 发件人",
|
||||
"smtp_tls_option": "SMTP TLS 选项",
|
||||
"email_tls_option": "电子邮件 TLS 选项",
|
||||
"skip_certificate_verification": "跳过证书验证",
|
||||
"this_can_be_useful_for_selfsigned_certificates": "这对于自签名证书很有用。",
|
||||
"enabled_emails": "启用的电子邮件",
|
||||
"email_login_notification": "电子邮件登录通知",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "当用户从新设备登录时,向其发送电子邮件。",
|
||||
"emai_login_code_requested_by_user": "用户请求的电子邮件登录代码",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "允许用户通过发送到其电子邮件的登录代码登录。这会显著降低安全性,因为任何有权访问用户电子邮件的人都可以进入。",
|
||||
"email_login_code_from_admin": "管理员发送的电子邮件登录代码",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "允许管理员通过电子邮件向用户发送登录代码。",
|
||||
"send_test_email": "发送测试电子邮件",
|
||||
"application_configuration_updated_successfully": "应用配置更新成功",
|
||||
"application_name": "应用名称",
|
||||
"session_duration": "会话持续时间",
|
||||
"the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again": "用户需要再次登录之前的会话持续时间(分钟)。",
|
||||
"enable_self_account_editing": "启用自助账户编辑",
|
||||
"whether_the_users_should_be_able_to_edit_their_own_account_details": "用户是否应能够编辑自己的账户详细信息。",
|
||||
"emails_verified": "已验证的邮箱地址",
|
||||
"whether_the_users_email_should_be_marked_as_verified_for_the_oidc_clients": "用户的电子邮件是否应标记为已验证,适用于 OIDC 客户端。",
|
||||
"ldap_configuration_updated_successfully": "LDAP 配置更新成功",
|
||||
"ldap_disabled_successfully": "LDAP 禁用成功",
|
||||
"ldap_sync_finished": "LDAP 同步完成",
|
||||
"client_configuration": "客户端配置",
|
||||
"ldap_url": "LDAP URL",
|
||||
"ldap_bind_dn": "LDAP Bind DN",
|
||||
"ldap_bind_password": "LDAP Bind Password",
|
||||
"ldap_base_dn": "LDAP Base DN",
|
||||
"user_search_filter": "User Search Filter",
|
||||
"the_search_filter_to_use_to_search_or_sync_users": "用于搜索/同步用户的搜索过滤器。",
|
||||
"groups_search_filter": "Groups Search Filter",
|
||||
"the_search_filter_to_use_to_search_or_sync_groups": "用于搜索/同步群组的搜索过滤器。",
|
||||
"attribute_mapping": "属性映射",
|
||||
"user_unique_identifier_attribute": "User Unique Identifier Attribute",
|
||||
"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": "此属性的值可以是 URL、二进制或 base64 编码的图像。",
|
||||
"group_members_attribute": "Group Members Attribute",
|
||||
"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": "此群组的成员将在 Pocket ID 中拥有管理员权限。",
|
||||
"disable": "禁用",
|
||||
"sync_now": "立即同步",
|
||||
"enable": "启用",
|
||||
"user_created_successfully": "用户创建成功",
|
||||
"create_user": "创建用户",
|
||||
"add_a_new_user_to_appname": "向 {appName} 添加新用户",
|
||||
"add_user": "添加用户",
|
||||
"manage_users": "管理用户",
|
||||
"admin_privileges": "管理员权限",
|
||||
"admins_have_full_access_to_the_admin_panel": "管理员拥有管理面板的完全访问权限。",
|
||||
"delete_firstname_lastname": "删除 {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_delete_this_user": "您确定要删除此用户吗?",
|
||||
"user_deleted_successfully": "用户删除成功",
|
||||
"role": "角色",
|
||||
"source": "来源",
|
||||
"admin": "管理员",
|
||||
"user": "用户",
|
||||
"local": "本地",
|
||||
"toggle_menu": "切换菜单",
|
||||
"edit": "编辑",
|
||||
"user_groups_updated_successfully": "用户组更新成功",
|
||||
"user_updated_successfully": "用户更新成功",
|
||||
"custom_claims_updated_successfully": "自定义声明更新成功",
|
||||
"back": "返回",
|
||||
"user_details_firstname_lastname": "用户详情 {firstName} {lastName}",
|
||||
"manage_which_groups_this_user_belongs_to": "管理此用户所属的群组。",
|
||||
"custom_claims": "自定义声明",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user": "自定义声明是键值对,可用于存储有关用户的额外信息。如果请求了 \"profile\" 范围,这些声明将包含在 ID Token 中。",
|
||||
"user_group_created_successfully": "用户组创建成功",
|
||||
"create_user_group": "创建用户组",
|
||||
"create_a_new_group_that_can_be_assigned_to_users": "创建一个可以分配给用户的新群组。",
|
||||
"add_group": "添加群组",
|
||||
"manage_user_groups": "管理用户组",
|
||||
"friendly_name": "显示名称",
|
||||
"name_that_will_be_displayed_in_the_ui": "将在用户界面中显示的名称",
|
||||
"name_that_will_be_in_the_groups_claim": "将在 \"groups\" 声明中显示的名称",
|
||||
"delete_name": "删除 {name}",
|
||||
"are_you_sure_you_want_to_delete_this_user_group": "您确定要删除此用户组吗?",
|
||||
"user_group_deleted_successfully": "用户组删除成功",
|
||||
"user_count": "用户数",
|
||||
"user_group_updated_successfully": "用户组更新成功",
|
||||
"users_updated_successfully": "用户更新成功",
|
||||
"user_group_details_name": "用户组详情 {name}",
|
||||
"assign_users_to_this_group": "将用户分配到此群组。",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized": "自定义声明是键值对,可用于存储有关用户的额外信息。如果请求了 'profile' 范围,这些声明将包含在 ID 令牌中。如果存在冲突,用户上定义的自定义声明将优先。",
|
||||
"oidc_client_created_successfully": "OIDC 客户端创建成功",
|
||||
"create_oidc_client": "创建 OIDC 客户端",
|
||||
"add_a_new_oidc_client_to_appname": "向 {appName} 添加新的 OIDC 客户端。",
|
||||
"add_oidc_client": "添加 OIDC 客户端",
|
||||
"manage_oidc_clients": "管理 OIDC 客户端",
|
||||
"one_time_link": "一次性链接",
|
||||
"use_this_link_to_sign_in_once": "使用此链接一次性登录。这对于尚未添加通行密钥或丢失通行密钥的用户是必要的。",
|
||||
"add": "添加",
|
||||
"callback_urls": "Callback URL",
|
||||
"logout_callback_urls": "Logout Callback URL",
|
||||
"public_client": "公共客户端",
|
||||
"public_clients_do_not_have_a_client_secret_and_use_pkce_instead": "公共客户端没有客户端密钥,而是使用 PKCE。如果您的客户端是 SPA 或移动应用,请启用此选项。",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "公钥代码交换是一种安全功能,可防止 CSRF 和授权代码拦截攻击。",
|
||||
"name_logo": "{name} Logo",
|
||||
"change_logo": "更改 Logo",
|
||||
"upload_logo": "上传 Logo",
|
||||
"remove_logo": "移除 Logo",
|
||||
"are_you_sure_you_want_to_delete_this_oidc_client": "您确定要删除此 OIDC 客户端吗?",
|
||||
"oidc_client_deleted_successfully": "OIDC 客户端删除成功",
|
||||
"authorization_url": "Authorization URL",
|
||||
"oidc_discovery_url": "OIDC Discovery URL",
|
||||
"token_url": "Token URL",
|
||||
"userinfo_url": "Userinfo URL",
|
||||
"logout_url": "Logout URL",
|
||||
"certificate_url": "Certificate URL",
|
||||
"enabled": "已启用",
|
||||
"disabled": "已禁用",
|
||||
"oidc_client_updated_successfully": "OIDC 客户端更新成功",
|
||||
"create_new_client_secret": "创建新的客户端密钥",
|
||||
"are_you_sure_you_want_to_create_a_new_client_secret": "您确定要创建新的客户端密钥吗?旧的密钥将被失效。",
|
||||
"generate": "生成",
|
||||
"new_client_secret_created_successfully": "新客户端密钥创建成功",
|
||||
"allowed_user_groups_updated_successfully": "允许的用户组更新成功",
|
||||
"oidc_client_name": "OIDC 客户端 {name}",
|
||||
"client_id": "客户端 ID",
|
||||
"client_secret": "客户端密钥",
|
||||
"show_more_details": "显示更多详情",
|
||||
"allowed_user_groups": "允许的用户组",
|
||||
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "将用户组添加到此客户端以限制访问,仅允许这些组中的用户。如果未选择用户组,所有用户都将有权访问此客户端。",
|
||||
"favicon": "网站图标",
|
||||
"light_mode_logo": "浅色模式 Logo",
|
||||
"dark_mode_logo": "深色模式 Logo",
|
||||
"background_image": "背景图片",
|
||||
"language": "语言",
|
||||
"reset_profile_picture_question": "重置头像?",
|
||||
"this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default": "这将移除已上传的图片,并将头像重置为默认值。您是否要继续?",
|
||||
"reset": "重置",
|
||||
"reset_to_default": "重置为默认",
|
||||
"profile_picture_has_been_reset": "头像已重置。可能需要几分钟才能更新。",
|
||||
"select_the_language_you_want_to_use": "选择您要使用的语言。某些语言可能未完全翻译。",
|
||||
"personal": "个人",
|
||||
"global": "全局",
|
||||
"all_users": "所有用户",
|
||||
"all_events": "所有事件",
|
||||
"all_clients": "所有客户端",
|
||||
"global_audit_log": "全局日志",
|
||||
"see_all_account_activities_from_the_last_3_months": "查看过去 3 个月的所有用户活动。",
|
||||
"token_sign_in": "Token 登录",
|
||||
"client_authorization": "客户端授权",
|
||||
"new_client_authorization": "首次客户端授权",
|
||||
"disable_animations": "禁用动画",
|
||||
"turn_off_all_animations_throughout_the_admin_ui": "关闭管理用户界面中的所有动画。",
|
||||
"user_disabled": "账户已禁用",
|
||||
"disabled_users_cannot_log_in_or_use_services": "禁用的用户无法登录或使用服务。",
|
||||
"user_disabled_successfully": "用户已成功禁用。",
|
||||
"user_enabled_successfully": "用户已成功启用。",
|
||||
"status": "状态",
|
||||
"disable_firstname_lastname": "禁用 {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_disable_this_user": "您确定要禁用此用户吗?他们将无法登录或访问任何服务。",
|
||||
"ldap_soft_delete_users": "保留来自 LDAP 的禁用用户。",
|
||||
"ldap_soft_delete_users_description": "启用后,从 LDAP 中移除的用户将被禁用,而不是从系统中删除。",
|
||||
"login_code_email_success": "登录代码已发送给用户。",
|
||||
"send_email": "发送电子邮件",
|
||||
"show_code": "显示登录码",
|
||||
"callback_url_description": "由您的客户端提供的 URL。支持通配符 (*),但为了更好的安全性最好避免使用。",
|
||||
"api_key_expiration": "API 密钥过期",
|
||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "当用户的 API 密钥即将过期时,向其发送电子邮件。"
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"my_account": "账户",
|
||||
"logout": "登出",
|
||||
"confirm": "确认",
|
||||
"key": "Key",
|
||||
"value": "Value",
|
||||
"remove_custom_claim": "移除自定义声明",
|
||||
"add_custom_claim": "添加自定义声明",
|
||||
"add_another": "添加另一个",
|
||||
"select_a_date": "选择日期",
|
||||
"select_file": "选择文件",
|
||||
"profile_picture": "头像",
|
||||
"profile_picture_is_managed_by_ldap_server": "头像由 LDAP 服务器管理,无法在此处更改。",
|
||||
"click_profile_picture_to_upload_custom": "点击头像来从文件中上传您的自定义头像。",
|
||||
"image_should_be_in_format": "图片应为 PNG 或 JPEG 格式。",
|
||||
"items_per_page": "每页条数",
|
||||
"no_items_found": "🌱 这里暂时空空如也。",
|
||||
"search": "搜索...",
|
||||
"expand_card": "展开卡片",
|
||||
"copied": "已复制",
|
||||
"click_to_copy": "点击复制",
|
||||
"something_went_wrong": "出了点问题",
|
||||
"go_back_to_home": "返回首页",
|
||||
"dont_have_access_to_your_passkey": "无法使用您的通行密钥?试试其他登录方式。",
|
||||
"login_background": "登录页背景图",
|
||||
"logo": "Logo",
|
||||
"login_code": "临时登录码",
|
||||
"create_a_login_code_to_sign_in_without_a_passkey_once": "创建一个临时登录码,用户可以使用它一次性登录而无需通行密钥。",
|
||||
"one_hour": "1 小时",
|
||||
"twelve_hours": "12 小时",
|
||||
"one_day": "1 天",
|
||||
"one_week": "1 周",
|
||||
"one_month": "1 个月",
|
||||
"expiration": "到期时间",
|
||||
"generate_code": "生成代码",
|
||||
"name": "名称",
|
||||
"browser_unsupported": "浏览器不支持",
|
||||
"this_browser_does_not_support_passkeys": "此浏览器不支持通行密钥。请使用其他登录方式。",
|
||||
"an_unknown_error_occurred": "发生未知错误",
|
||||
"authentication_process_was_aborted": "认证过程被中止",
|
||||
"error_occurred_with_authenticator": "认证器发生错误",
|
||||
"authenticator_does_not_support_discoverable_credentials": "认证器不支持可发现的凭据",
|
||||
"authenticator_does_not_support_resident_keys": "认证器不支持常驻密钥",
|
||||
"passkey_was_previously_registered": "此通行密钥之前已注册",
|
||||
"authenticator_does_not_support_any_of_the_requested_algorithms": "认证器不支持任何请求的算法",
|
||||
"authenticator_timed_out": "认证器超时",
|
||||
"critical_error_occurred_contact_administrator": "发生严重错误。请联系您的管理员。",
|
||||
"sign_in_to": "登录到 {name}",
|
||||
"client_not_found": "客户端未找到",
|
||||
"client_wants_to_access_the_following_information": "<b>{client}</b> 希望访问以下信息:",
|
||||
"do_you_want_to_sign_in_to_client_with_your_app_name_account": "您是否希望使用您的 <b>{appName}</b> 账户登录到 <b>{client}</b>?",
|
||||
"email": "电子邮件",
|
||||
"view_your_email_address": "查看您的电子邮件地址",
|
||||
"profile": "个人资料",
|
||||
"view_your_profile_information": "查看您的个人资料信息",
|
||||
"groups": "群组",
|
||||
"view_the_groups_you_are_a_member_of": "查看您所属的群组",
|
||||
"cancel": "取消",
|
||||
"sign_in": "登录",
|
||||
"try_again": "重试",
|
||||
"client_logo": "客户端标志",
|
||||
"sign_out": "登出",
|
||||
"do_you_want_to_sign_out_of_pocketid_with_the_account": "您是否希望使用账户 <b>{username}</b> 登出 Pocket ID?",
|
||||
"sign_in_to_appname": "登录到 {appName}",
|
||||
"please_try_to_sign_in_again": "请尝试重新登录。",
|
||||
"authenticate_yourself_with_your_passkey_to_access_the_admin_panel": "使用通行密钥或通过临时登录码进行登录",
|
||||
"authenticate": "登录",
|
||||
"appname_setup": "{appName} 设置",
|
||||
"please_try_again": "请重试。",
|
||||
"you_are_about_to_sign_in_to_the_initial_admin_account": "您即将登录到初始管理员账户。在此添加通行密钥之前,任何拥有此链接的人都可以访问该账户。请尽快设置通行密钥以防止未经授权的访问。",
|
||||
"continue": "继续",
|
||||
"alternative_sign_in": "替代登录方式",
|
||||
"if_you_do_not_have_access_to_your_passkey_you_can_sign_in_using_one_of_the_following_methods": "如果您无法访问您的通行密钥,可以使用以下方法之一登录。",
|
||||
"use_your_passkey_instead": "改用您的通行密钥?",
|
||||
"email_login": "电子邮件登录",
|
||||
"enter_a_login_code_to_sign_in": "输入一次性登录码以登录。",
|
||||
"request_a_login_code_via_email": "通过电子邮件请求登录代码。",
|
||||
"go_back": "返回",
|
||||
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "如果系统中存在提供的电子邮件地址,则已发送一封电子邮件。",
|
||||
"enter_code": "输入登录码",
|
||||
"enter_your_email_address_to_receive_an_email_with_a_login_code": "输入您的电子邮件地址以接收包含登录代码的电子邮件。",
|
||||
"your_email": "您的电子邮件",
|
||||
"submit": "提交",
|
||||
"enter_the_code_you_received_to_sign_in": "输入您收到的登录码以登录。",
|
||||
"code": "Code",
|
||||
"invalid_redirect_url": "无效的重定向 URL",
|
||||
"audit_log": "日志",
|
||||
"users": "用户",
|
||||
"user_groups": "用户组",
|
||||
"oidc_clients": "OIDC 客户端",
|
||||
"api_keys": "API 密钥",
|
||||
"application_configuration": "设置",
|
||||
"settings": "设置",
|
||||
"update_pocket_id": "更新 Pocket ID",
|
||||
"powered_by": "Powered by",
|
||||
"see_your_account_activities_from_the_last_3_months": "查看过去 3 个月的账户活动。",
|
||||
"time": "时间",
|
||||
"event": "事件",
|
||||
"approximate_location": "大致位置",
|
||||
"ip_address": "IP 地址",
|
||||
"device": "设备",
|
||||
"client": "客户端",
|
||||
"unknown": "未知",
|
||||
"account_details_updated_successfully": "账户详细信息更新成功",
|
||||
"profile_picture_updated_successfully": "头像更新成功。可能需要几分钟才能更新。",
|
||||
"account_settings": "账户设置",
|
||||
"passkey_missing": "尚未绑定通行密钥",
|
||||
"please_provide_a_passkey_to_prevent_losing_access_to_your_account": "请添加通行密钥以防止失去对账户的访问。",
|
||||
"single_passkey_configured": "已添加一个通行密钥",
|
||||
"it_is_recommended_to_add_more_than_one_passkey": "建议添加多个通行密钥以避免失去对账户的访问。",
|
||||
"account_details": "账户详情",
|
||||
"passkeys": "通行密钥",
|
||||
"manage_your_passkeys_that_you_can_use_to_authenticate_yourself": "管理您可以用来进行身份验证的通行密钥。",
|
||||
"add_passkey": "添加通行密钥",
|
||||
"create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey": "创建一次性登录码,以便从不同设备登录而无需通行密钥。",
|
||||
"create": "创建",
|
||||
"first_name": "名字",
|
||||
"last_name": "姓氏",
|
||||
"username": "用户名",
|
||||
"save": "保存",
|
||||
"username_can_only_contain": "用户名只能包含小写字母、数字、下划线、点、连字符和 '@' 符号",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "使用以下代码登录。代码将在 15 分钟后过期。",
|
||||
"or_visit": "或访问",
|
||||
"added_on": "添加于",
|
||||
"rename": "重命名",
|
||||
"delete": "删除",
|
||||
"are_you_sure_you_want_to_delete_this_passkey": "您确定要删除此通行密钥吗?",
|
||||
"passkey_deleted_successfully": "通行密钥删除成功",
|
||||
"delete_passkey_name": "删除 {passkeyName}",
|
||||
"passkey_name_updated_successfully": "通行密钥名称更新成功",
|
||||
"name_passkey": "重命名通行密钥",
|
||||
"name_your_passkey_to_easily_identify_it_later": "为您的通行密钥命名,以便以后轻松识别。",
|
||||
"create_api_key": "创建 API 密钥",
|
||||
"add_a_new_api_key_for_programmatic_access": "添加一个新的 API 密钥用于编程访问。",
|
||||
"add_api_key": "添加 API 密钥",
|
||||
"manage_api_keys": "管理 API 密钥",
|
||||
"api_key_created": "API 密钥已创建",
|
||||
"for_security_reasons_this_key_will_only_be_shown_once": "出于安全原因,此密钥只会显示一次。请妥善保存。",
|
||||
"description": "描述",
|
||||
"api_key": "API 密钥",
|
||||
"close": "关闭",
|
||||
"name_to_identify_this_api_key": "用于识别此 API 密钥的名称。",
|
||||
"expires_at": "过期时间",
|
||||
"when_this_api_key_will_expire": "此 API 密钥的过期时间。",
|
||||
"optional_description_to_help_identify_this_keys_purpose": "可选描述,帮助识别此密钥的用途。",
|
||||
"name_must_be_at_least_3_characters": "名称必须至少为 3 个字符",
|
||||
"name_cannot_exceed_50_characters": "名称不能超过 50 个字符",
|
||||
"expiration_date_must_be_in_the_future": "过期日期必须是未来的日期",
|
||||
"revoke_api_key": "撤销 API 密钥",
|
||||
"never": "永不",
|
||||
"revoke": "撤销",
|
||||
"api_key_revoked_successfully": "API 密钥撤销成功",
|
||||
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "您确定要撤销 API 密钥 \"{apiKeyName}\" 吗?这将中断使用此密钥的任何集成。",
|
||||
"last_used": "最后使用",
|
||||
"actions": "操作",
|
||||
"images_updated_successfully": "图片更新成功",
|
||||
"general": "常规",
|
||||
"configure_smtp_to_send_emails": "启用电子邮件通知,以便在新设备或位置检测到登录时提醒用户。",
|
||||
"ldap": "LDAP",
|
||||
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "配置 LDAP 设置以从 LDAP 服务器同步用户和群组。",
|
||||
"images": "图片",
|
||||
"update": "更新",
|
||||
"email_configuration_updated_successfully": "电子邮件配置更新成功",
|
||||
"save_changes_question": "保存更改?",
|
||||
"you_have_to_save_the_changes_before_sending_a_test_email_do_you_want_to_save_now": "在发送测试电子邮件之前,您必须保存更改。是否现在保存?",
|
||||
"save_and_send": "保存并发送",
|
||||
"test_email_sent_successfully": "测试电子邮件已成功发送到您的电子邮件地址。",
|
||||
"failed_to_send_test_email": "发送测试电子邮件失败。请检查服务器日志以获取更多信息。",
|
||||
"smtp_configuration": "SMTP 配置",
|
||||
"smtp_host": "SMTP 主机",
|
||||
"smtp_port": "SMTP 端口",
|
||||
"smtp_user": "SMTP 用户",
|
||||
"smtp_password": "SMTP 密码",
|
||||
"smtp_from": "SMTP 发件人",
|
||||
"smtp_tls_option": "SMTP TLS 选项",
|
||||
"email_tls_option": "电子邮件 TLS 选项",
|
||||
"skip_certificate_verification": "跳过证书验证",
|
||||
"this_can_be_useful_for_selfsigned_certificates": "这对于自签名证书很有用。",
|
||||
"enabled_emails": "启用的电子邮件",
|
||||
"email_login_notification": "电子邮件登录通知",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "当用户从新设备登录时,向其发送电子邮件。",
|
||||
"emai_login_code_requested_by_user": "用户请求的电子邮件登录代码",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "允许用户通过发送到其电子邮件的登录代码登录。这会显著降低安全性,因为任何有权访问用户电子邮件的人都可以进入。",
|
||||
"email_login_code_from_admin": "管理员发送的电子邮件登录代码",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "允许管理员通过电子邮件向用户发送登录代码。",
|
||||
"send_test_email": "发送测试电子邮件",
|
||||
"application_configuration_updated_successfully": "应用配置更新成功",
|
||||
"application_name": "应用名称",
|
||||
"session_duration": "会话持续时间",
|
||||
"the_duration_of_a_session_in_minutes_before_the_user_has_to_sign_in_again": "用户需要再次登录之前的会话持续时间(分钟)。",
|
||||
"enable_self_account_editing": "启用自助账户编辑",
|
||||
"whether_the_users_should_be_able_to_edit_their_own_account_details": "用户是否应能够编辑自己的账户详细信息。",
|
||||
"emails_verified": "已验证的邮箱地址",
|
||||
"whether_the_users_email_should_be_marked_as_verified_for_the_oidc_clients": "用户的电子邮件是否应标记为已验证,适用于 OIDC 客户端。",
|
||||
"ldap_configuration_updated_successfully": "LDAP 配置更新成功",
|
||||
"ldap_disabled_successfully": "LDAP 禁用成功",
|
||||
"ldap_sync_finished": "LDAP 同步完成",
|
||||
"client_configuration": "客户端配置",
|
||||
"ldap_url": "LDAP URL",
|
||||
"ldap_bind_dn": "LDAP Bind DN",
|
||||
"ldap_bind_password": "LDAP Bind Password",
|
||||
"ldap_base_dn": "LDAP Base DN",
|
||||
"user_search_filter": "User Search Filter",
|
||||
"the_search_filter_to_use_to_search_or_sync_users": "用于搜索/同步用户的搜索过滤器。",
|
||||
"groups_search_filter": "Groups Search Filter",
|
||||
"the_search_filter_to_use_to_search_or_sync_groups": "用于搜索/同步群组的搜索过滤器。",
|
||||
"attribute_mapping": "属性映射",
|
||||
"user_unique_identifier_attribute": "User Unique Identifier Attribute",
|
||||
"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": "此属性的值可以是 URL、二进制或 base64 编码的图像。",
|
||||
"group_members_attribute": "Group Members Attribute",
|
||||
"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": "此群组的成员将在 Pocket ID 中拥有管理员权限。",
|
||||
"disable": "禁用",
|
||||
"sync_now": "立即同步",
|
||||
"enable": "启用",
|
||||
"user_created_successfully": "用户创建成功",
|
||||
"create_user": "创建用户",
|
||||
"add_a_new_user_to_appname": "向 {appName} 添加新用户",
|
||||
"add_user": "添加用户",
|
||||
"manage_users": "管理用户",
|
||||
"admin_privileges": "管理员权限",
|
||||
"admins_have_full_access_to_the_admin_panel": "管理员拥有管理面板的完全访问权限。",
|
||||
"delete_firstname_lastname": "删除 {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_delete_this_user": "您确定要删除此用户吗?",
|
||||
"user_deleted_successfully": "用户删除成功",
|
||||
"role": "角色",
|
||||
"source": "来源",
|
||||
"admin": "管理员",
|
||||
"user": "用户",
|
||||
"local": "本地",
|
||||
"toggle_menu": "切换菜单",
|
||||
"edit": "编辑",
|
||||
"user_groups_updated_successfully": "用户组更新成功",
|
||||
"user_updated_successfully": "用户更新成功",
|
||||
"custom_claims_updated_successfully": "自定义声明更新成功",
|
||||
"back": "返回",
|
||||
"user_details_firstname_lastname": "用户详情 {firstName} {lastName}",
|
||||
"manage_which_groups_this_user_belongs_to": "管理此用户所属的群组。",
|
||||
"custom_claims": "自定义声明",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user": "自定义声明是键值对,可用于存储有关用户的额外信息。如果请求了 \"profile\" 范围,这些声明将包含在 ID Token 中。",
|
||||
"user_group_created_successfully": "用户组创建成功",
|
||||
"create_user_group": "创建用户组",
|
||||
"create_a_new_group_that_can_be_assigned_to_users": "创建一个可以分配给用户的新群组。",
|
||||
"add_group": "添加群组",
|
||||
"manage_user_groups": "管理用户组",
|
||||
"friendly_name": "显示名称",
|
||||
"name_that_will_be_displayed_in_the_ui": "将在用户界面中显示的名称",
|
||||
"name_that_will_be_in_the_groups_claim": "将在 \"groups\" 声明中显示的名称",
|
||||
"delete_name": "删除 {name}",
|
||||
"are_you_sure_you_want_to_delete_this_user_group": "您确定要删除此用户组吗?",
|
||||
"user_group_deleted_successfully": "用户组删除成功",
|
||||
"user_count": "用户数",
|
||||
"user_group_updated_successfully": "用户组更新成功",
|
||||
"users_updated_successfully": "用户更新成功",
|
||||
"user_group_details_name": "用户组详情 {name}",
|
||||
"assign_users_to_this_group": "将用户分配到此群组。",
|
||||
"custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized": "自定义声明是键值对,可用于存储有关用户的额外信息。如果请求了 'profile' 范围,这些声明将包含在 ID 令牌中。如果存在冲突,用户上定义的自定义声明将优先。",
|
||||
"oidc_client_created_successfully": "OIDC 客户端创建成功",
|
||||
"create_oidc_client": "创建 OIDC 客户端",
|
||||
"add_a_new_oidc_client_to_appname": "向 {appName} 添加新的 OIDC 客户端。",
|
||||
"add_oidc_client": "添加 OIDC 客户端",
|
||||
"manage_oidc_clients": "管理 OIDC 客户端",
|
||||
"one_time_link": "一次性链接",
|
||||
"use_this_link_to_sign_in_once": "使用此链接一次性登录。这对于尚未添加通行密钥或丢失通行密钥的用户是必要的。",
|
||||
"add": "添加",
|
||||
"callback_urls": "Callback URL",
|
||||
"logout_callback_urls": "Logout Callback URL",
|
||||
"public_client": "公共客户端",
|
||||
"public_clients_description": "公共客户端没有客户端密钥,而是使用 PKCE。如果您的客户端是 SPA 或移动应用,请启用此选项。",
|
||||
"pkce": "PKCE",
|
||||
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "公钥代码交换是一种安全功能,可防止 CSRF 和授权代码拦截攻击。",
|
||||
"name_logo": "{name} Logo",
|
||||
"change_logo": "更改 Logo",
|
||||
"upload_logo": "上传 Logo",
|
||||
"remove_logo": "移除 Logo",
|
||||
"are_you_sure_you_want_to_delete_this_oidc_client": "您确定要删除此 OIDC 客户端吗?",
|
||||
"oidc_client_deleted_successfully": "OIDC 客户端删除成功",
|
||||
"authorization_url": "Authorization URL",
|
||||
"oidc_discovery_url": "OIDC Discovery URL",
|
||||
"token_url": "Token URL",
|
||||
"userinfo_url": "Userinfo URL",
|
||||
"logout_url": "Logout URL",
|
||||
"certificate_url": "Certificate URL",
|
||||
"enabled": "已启用",
|
||||
"disabled": "已禁用",
|
||||
"oidc_client_updated_successfully": "OIDC 客户端更新成功",
|
||||
"create_new_client_secret": "创建新的客户端密钥",
|
||||
"are_you_sure_you_want_to_create_a_new_client_secret": "您确定要创建新的客户端密钥吗?旧的密钥将被失效。",
|
||||
"generate": "生成",
|
||||
"new_client_secret_created_successfully": "新客户端密钥创建成功",
|
||||
"allowed_user_groups_updated_successfully": "允许的用户组更新成功",
|
||||
"oidc_client_name": "OIDC 客户端 {name}",
|
||||
"client_id": "客户端 ID",
|
||||
"client_secret": "客户端密钥",
|
||||
"show_more_details": "显示更多详情",
|
||||
"allowed_user_groups": "允许的用户组",
|
||||
"add_user_groups_to_this_client_to_restrict_access_to_users_in_these_groups": "将用户组添加到此客户端以限制访问,仅允许这些组中的用户。如果未选择用户组,所有用户都将有权访问此客户端。",
|
||||
"favicon": "网站图标",
|
||||
"light_mode_logo": "浅色模式 Logo",
|
||||
"dark_mode_logo": "深色模式 Logo",
|
||||
"background_image": "背景图片",
|
||||
"language": "语言",
|
||||
"reset_profile_picture_question": "重置头像?",
|
||||
"this_will_remove_the_uploaded_image_and_reset_the_profile_picture_to_default": "这将移除已上传的图片,并将头像重置为默认值。您是否要继续?",
|
||||
"reset": "重置",
|
||||
"reset_to_default": "重置为默认",
|
||||
"profile_picture_has_been_reset": "头像已重置。可能需要几分钟才能更新。",
|
||||
"select_the_language_you_want_to_use": "选择您要使用的语言。某些语言可能未完全翻译。",
|
||||
"personal": "个人",
|
||||
"global": "全局",
|
||||
"all_users": "所有用户",
|
||||
"all_events": "所有事件",
|
||||
"all_clients": "所有客户端",
|
||||
"global_audit_log": "全局日志",
|
||||
"see_all_account_activities_from_the_last_3_months": "查看过去 3 个月的所有用户活动。",
|
||||
"token_sign_in": "Token 登录",
|
||||
"client_authorization": "客户端授权",
|
||||
"new_client_authorization": "首次客户端授权",
|
||||
"disable_animations": "禁用动画",
|
||||
"turn_off_all_animations_throughout_the_admin_ui": "关闭管理用户界面中的所有动画。",
|
||||
"user_disabled": "账户已禁用",
|
||||
"disabled_users_cannot_log_in_or_use_services": "禁用的用户无法登录或使用服务。",
|
||||
"user_disabled_successfully": "用户已成功禁用。",
|
||||
"user_enabled_successfully": "用户已成功启用。",
|
||||
"status": "状态",
|
||||
"disable_firstname_lastname": "禁用 {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_disable_this_user": "您确定要禁用此用户吗?他们将无法登录或访问任何服务。",
|
||||
"ldap_soft_delete_users": "保留来自 LDAP 的禁用用户。",
|
||||
"ldap_soft_delete_users_description": "启用后,从 LDAP 中移除的用户将被禁用,而不是从系统中删除。",
|
||||
"login_code_email_success": "登录代码已发送给用户。",
|
||||
"send_email": "发送电子邮件",
|
||||
"show_code": "显示登录码",
|
||||
"callback_url_description": "由您的客户端提供的 URL。支持通配符 (*),但为了更好的安全性最好避免使用。",
|
||||
"api_key_expiration": "API 密钥过期",
|
||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "当用户的 API 密钥即将过期时,向其发送电子邮件。"
|
||||
}
|
||||
|
||||
10989
frontend/package-lock.json
generated
10989
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pocket-id-frontend",
|
||||
"version": "0.50.0",
|
||||
"version": "0.51.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -57,6 +57,6 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.21.0",
|
||||
"vite": "^6.2.6"
|
||||
"vite": "^6.3.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
onInput,
|
||||
...restProps
|
||||
}: HTMLAttributes<HTMLDivElement> & {
|
||||
input?: FormInput<string | boolean | number | Date>;
|
||||
input?: FormInput<string | boolean | number | Date | undefined>;
|
||||
label?: string;
|
||||
description?: string;
|
||||
placeholder?: string;
|
||||
|
||||
@@ -93,6 +93,30 @@
|
||||
</Alert.Root>
|
||||
{/if}
|
||||
|
||||
|
||||
<!-- Login code card mobile -->
|
||||
<div class="block sm:hidden">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<div class="flex flex-col items-start justify-between gap-3 sm:flex-row sm:items-center">
|
||||
<div>
|
||||
<Card.Title>
|
||||
<RectangleEllipsis class="text-primary/80 h-5 w-5" />
|
||||
{m.login_code()}
|
||||
</Card.Title>
|
||||
<Card.Description>
|
||||
{m.create_a_one_time_login_code_to_sign_in_from_a_different_device_without_a_passkey()}
|
||||
</Card.Description>
|
||||
</div>
|
||||
|
||||
<Button variant="outline" class="w-full" on:click={() => (showLoginCodeModal = true)}>
|
||||
{m.create()}
|
||||
</Button>
|
||||
</div>
|
||||
</Card.Header>
|
||||
</Card.Root>
|
||||
</div>
|
||||
|
||||
<!-- Account details card -->
|
||||
<fieldset
|
||||
disabled={!$appConfigStore.allowOwnAccountEdit ||
|
||||
@@ -143,8 +167,9 @@
|
||||
</Card.Root>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Login code card -->
|
||||
<div>
|
||||
<div class="hidden sm:block">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<div class="flex flex-col items-start justify-between gap-3 sm:flex-row sm:items-center">
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
import type { UserCreate } from '$lib/types/user.type';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
import { createForm } from '$lib/utils/form-util';
|
||||
import { BookUser } from 'lucide-svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
@@ -29,7 +28,7 @@
|
||||
|
||||
const formSchema = z.object({
|
||||
firstName: z.string().min(1).max(50),
|
||||
lastName: z.string().min(1).max(50),
|
||||
lastName: z.string().max(50).optional(),
|
||||
username: z
|
||||
.string()
|
||||
.min(2)
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
callbackURLs: existingClient?.callbackURLs || [''],
|
||||
logoutCallbackURLs: existingClient?.logoutCallbackURLs || [],
|
||||
isPublic: existingClient?.isPublic || false,
|
||||
pkceEnabled: existingClient?.isPublic == true || existingClient?.pkceEnabled || false
|
||||
pkceEnabled: existingClient?.pkceEnabled || false
|
||||
};
|
||||
|
||||
const formSchema = z.object({
|
||||
@@ -98,17 +98,13 @@
|
||||
<CheckboxWithLabel
|
||||
id="public-client"
|
||||
label={m.public_client()}
|
||||
description={m.public_clients_do_not_have_a_client_secret_and_use_pkce_instead()}
|
||||
onCheckedChange={(v) => {
|
||||
if (v == true) form.setValue('pkceEnabled', true);
|
||||
}}
|
||||
description={m.public_clients_description()}
|
||||
bind:checked={$inputs.isPublic.value}
|
||||
/>
|
||||
<CheckboxWithLabel
|
||||
id="pkce"
|
||||
label={m.pkce()}
|
||||
description={m.public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks()}
|
||||
disabled={$inputs.isPublic.value}
|
||||
bind:checked={$inputs.pkceEnabled.value}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import UserGroupSelection from '$lib/components/user-group-selection.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import CustomClaimService from '$lib/services/custom-claim-service';
|
||||
import UserService from '$lib/services/user-service';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
@@ -14,7 +15,6 @@
|
||||
import { LucideChevronLeft } from 'lucide-svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import UserForm from '../user-form.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let { data } = $props();
|
||||
let user = $state({
|
||||
@@ -75,7 +75,7 @@
|
||||
<title
|
||||
>{m.user_details_firstname_lastname({
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName
|
||||
lastName: user.lastName ?? ''
|
||||
})}</title
|
||||
>
|
||||
</svelte:head>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
async function deleteUser(user: User) {
|
||||
openConfirmDialog({
|
||||
title: m.delete_firstname_lastname({ firstName: user.firstName, lastName: user.lastName }),
|
||||
title: m.delete_firstname_lastname({ firstName: user.firstName, lastName: user.lastName ?? "" }),
|
||||
message: m.are_you_sure_you_want_to_delete_this_user(),
|
||||
confirm: {
|
||||
label: m.delete(),
|
||||
@@ -67,7 +67,7 @@
|
||||
|
||||
async function disableUser(user: User) {
|
||||
openConfirmDialog({
|
||||
title: m.disable_firstname_lastname({ firstName: user.firstName, lastName: user.lastName }),
|
||||
title: m.disable_firstname_lastname({ firstName: user.firstName, lastName: user.lastName ?? "" }),
|
||||
message: m.are_you_sure_you_want_to_disable_this_user(),
|
||||
confirm: {
|
||||
label: m.disable(),
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import AuditLogList from '$lib/components/audit-log-list.svelte';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import userStore from '$lib/stores/user-store';
|
||||
import { LogsIcon } from 'lucide-svelte';
|
||||
import AuditLogSwitcher from './audit-log-switcher.svelte';
|
||||
|
||||
@@ -13,7 +14,9 @@
|
||||
<title>{m.audit_log()}</title>
|
||||
</svelte:head>
|
||||
|
||||
<AuditLogSwitcher currentPage="personal" />
|
||||
{#if $userStore?.isAdmin}
|
||||
<AuditLogSwitcher currentPage="personal" />
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<Card.Root>
|
||||
|
||||
Reference in New Issue
Block a user