Compare commits

..

9 Commits

Author SHA1 Message Date
Elias Schneider
6004f84845 release: 0.51.0 2025-04-28 11:15:52 +02:00
Alessandro (Ale) Segala
3ec98736cf refactor: graceful shutdown for server (#482) 2025-04-28 11:13:50 +02:00
Elias Schneider
ce24372c57 fix: do not require PKCE for public clients 2025-04-28 11:02:35 +02:00
Elias Schneider
4614769b84 refactor: reorganize imports 2025-04-28 10:49:54 +02:00
Elias Schneider
86d2b5f59f fix: return correct error message if user isn't authorized 2025-04-28 10:39:17 +02:00
Elias Schneider
1efd1d182d fix: hide global audit log switch for non admin users 2025-04-28 10:38:53 +02:00
Elias Schneider
0a24ab8001 fix: updating scopes of an authorized client fails with Postgres 2025-04-28 09:29:18 +02:00
James18232
02cacba5c5 feat: new login code card position for mobile devices (#452)
Co-authored-by: Kyle Mendell <kmendell@ofkm.us>
2025-04-28 04:04:48 +00:00
Elias Schneider
38653e2aa4 chore(translations): update translations via Crowdin (#485) 2025-04-27 23:00:37 -05:00
29 changed files with 814 additions and 618 deletions

View File

@@ -1 +1 @@
0.50.0 0.51.0

View File

@@ -1,3 +1,18 @@
## [](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) ## [](https://github.com/pocket-id/pocket-id/compare/v0.49.0...v) (2025-04-27)

View File

@@ -38,7 +38,6 @@ func initApplicationImages() {
log.Fatalf("Error copying file: %v", err) log.Fatalf("Error copying file: %v", err)
} }
} }
} }
func imageAlreadyExists(fileName string, destinationFiles []os.DirEntry) bool { 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 { func getImageNameWithoutExtension(fileName string) string {
splitted := strings.Split(fileName, ".") idx := strings.LastIndexByte(fileName, '.')
return strings.Join(splitted[:len(splitted)-1], ".") if idx < 1 {
// No dot found, or fileName starts with a dot
return fileName
}
return fileName[:idx]
} }

View File

@@ -6,10 +6,12 @@ import (
_ "github.com/golang-migrate/migrate/v4/source/file" _ "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/service"
"github.com/pocket-id/pocket-id/backend/internal/utils/signals"
) )
func Bootstrap() { func Bootstrap() {
ctx := context.TODO() // Get a context that is canceled when the application is stopping
ctx := signals.SignalContext(context.Background())
initApplicationImages() initApplicationImages()

View File

@@ -2,8 +2,10 @@ package bootstrap
import ( import (
"context" "context"
"fmt"
"log" "log"
"net" "net"
"net/http"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -21,6 +23,13 @@ import (
var registerTestControllers []func(apiGroup *gin.RouterGroup, db *gorm.DB, appConfigService *service.AppConfigService, jwtService *service.JwtService) var registerTestControllers []func(apiGroup *gin.RouterGroup, db *gorm.DB, appConfigService *service.AppConfigService, jwtService *service.JwtService)
func initRouter(ctx context.Context, db *gorm.DB, appConfigService *service.AppConfigService) { func initRouter(ctx context.Context, db *gorm.DB, appConfigService *service.AppConfigService) {
err := initRouterInternal(ctx, db, appConfigService)
if err != nil {
log.Fatalf("failed to init router: %v", err)
}
}
func initRouterInternal(ctx context.Context, db *gorm.DB, appConfigService *service.AppConfigService) error {
// Set the appropriate Gin mode based on the environment // Set the appropriate Gin mode based on the environment
switch common.EnvConfig.AppEnv { switch common.EnvConfig.AppEnv {
case "production": case "production":
@@ -37,7 +46,7 @@ func initRouter(ctx context.Context, db *gorm.DB, appConfigService *service.AppC
// Initialize services // Initialize services
emailService, err := service.NewEmailService(appConfigService, db) emailService, err := service.NewEmailService(appConfigService, db)
if err != nil { if err != nil {
log.Fatalf("Unable to create email service: %v", err) return fmt.Errorf("unable to create email service: %w", err)
} }
geoLiteService := service.NewGeoLiteService(ctx) geoLiteService := service.NewGeoLiteService(ctx)
@@ -58,10 +67,30 @@ func initRouter(ctx context.Context, db *gorm.DB, appConfigService *service.AppC
r.Use(middleware.NewErrorHandlerMiddleware().Add()) r.Use(middleware.NewErrorHandlerMiddleware().Add())
r.Use(rateLimitMiddleware.Add(rate.Every(time.Second), 60)) r.Use(rateLimitMiddleware.Add(rate.Every(time.Second), 60))
job.RegisterLdapJobs(ctx, ldapService, appConfigService) scheduler, err := job.NewScheduler()
job.RegisterDbCleanupJobs(ctx, db) if err != nil {
job.RegisterFileCleanupJobs(ctx, db) return fmt.Errorf("failed to create job scheduler: %w", err)
job.RegisterApiKeyExpiryJob(ctx, apiKeyService, appConfigService) }
err = scheduler.RegisterLdapJobs(ctx, ldapService, appConfigService)
if err != nil {
return fmt.Errorf("failed to register LDAP jobs in scheduler: %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, apiKeyService, appConfigService)
if err != nil {
return fmt.Errorf("failed to register API key expiration jobs in scheduler: %w", err)
}
// Run the scheduler in a background goroutine, until the context is canceled
go scheduler.Run(ctx)
// Initialize middleware for specific routes // Initialize middleware for specific routes
authMiddleware := middleware.NewAuthMiddleware(apiKeyService, userService, jwtService) authMiddleware := middleware.NewAuthMiddleware(apiKeyService, userService, jwtService)
@@ -89,20 +118,52 @@ func initRouter(ctx context.Context, db *gorm.DB, appConfigService *service.AppC
baseGroup := r.Group("/") baseGroup := r.Group("/")
controller.NewWellKnownController(baseGroup, jwtService) controller.NewWellKnownController(baseGroup, jwtService)
// Get the listener // Set up the server
l, err := net.Listen("tcp", common.EnvConfig.Host+":"+common.EnvConfig.Port) srv := &http.Server{
if err != nil { Addr: net.JoinHostPort(common.EnvConfig.Host, common.EnvConfig.Port),
log.Fatal(err) MaxHeaderBytes: 1 << 20,
ReadHeaderTimeout: 10 * time.Second,
Handler: r,
} }
// Set up the listener
listener, err := net.Listen("tcp", srv.Addr)
if err != nil {
return fmt.Errorf("failed to create TCP listener: %w", err)
}
log.Printf("Server listening on %s", srv.Addr)
// Notify systemd that we are ready // Notify systemd that we are ready
if err := systemd.SdNotifyReady(); err != nil { err = systemd.SdNotifyReady()
log.Println("Unable to notify systemd that the service is ready: ", err) if err != nil {
log.Printf("[WARN] Unable to notify systemd that the service is ready: %v", err)
// continue to serve anyway since it's not that important // continue to serve anyway since it's not that important
} }
// Serve requests // Start the server in a background goroutine
if err := r.RunListener(l); err != nil { go func() {
log.Fatal(err) 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)
} }
}()
// 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
} }

View File

@@ -4,7 +4,6 @@ import (
"context" "context"
"log" "log"
"github.com/go-co-op/gocron/v2"
"github.com/pocket-id/pocket-id/backend/internal/service" "github.com/pocket-id/pocket-id/backend/internal/service"
) )
@@ -13,20 +12,13 @@ type ApiKeyEmailJobs struct {
appConfigService *service.AppConfigService 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{ jobs := &ApiKeyEmailJobs{
apiKeyService: apiKeyService, apiKeyService: apiKeyService,
appConfigService: appConfigService, appConfigService: appConfigService,
} }
scheduler, err := gocron.NewScheduler() return s.registerJob(ctx, "ExpiredApiKeyEmailJob", "0 0 * * *", jobs.checkAndNotifyExpiringApiKeys)
if err != nil {
log.Fatalf("Failed to create a new scheduler: %v", err)
}
registerJob(ctx, scheduler, "ExpiredApiKeyEmailJob", "0 0 * * *", jobs.checkAndNotifyExpiringApiKeys)
scheduler.Start()
} }
func (j *ApiKeyEmailJobs) checkAndNotifyExpiringApiKeys(ctx context.Context) error { func (j *ApiKeyEmailJobs) checkAndNotifyExpiringApiKeys(ctx context.Context) error {

View File

@@ -2,30 +2,25 @@ package job
import ( import (
"context" "context"
"log" "errors"
"time" "time"
"github.com/go-co-op/gocron/v2"
"gorm.io/gorm" "gorm.io/gorm"
"github.com/pocket-id/pocket-id/backend/internal/model" "github.com/pocket-id/pocket-id/backend/internal/model"
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types" datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
) )
func RegisterDbCleanupJobs(ctx context.Context, db *gorm.DB) { func (s *Scheduler) RegisterDbCleanupJobs(ctx context.Context, db *gorm.DB) error {
scheduler, err := gocron.NewScheduler()
if err != nil {
log.Fatalf("Failed to create a new scheduler: %s", err)
}
jobs := &DbCleanupJobs{db: db} jobs := &DbCleanupJobs{db: db}
registerJob(ctx, scheduler, "ClearWebauthnSessions", "0 3 * * *", jobs.clearWebauthnSessions) return errors.Join(
registerJob(ctx, scheduler, "ClearOneTimeAccessTokens", "0 3 * * *", jobs.clearOneTimeAccessTokens) s.registerJob(ctx, "ClearWebauthnSessions", "0 3 * * *", jobs.clearWebauthnSessions),
registerJob(ctx, scheduler, "ClearOidcAuthorizationCodes", "0 3 * * *", jobs.clearOidcAuthorizationCodes) s.registerJob(ctx, "ClearOneTimeAccessTokens", "0 3 * * *", jobs.clearOneTimeAccessTokens),
registerJob(ctx, scheduler, "ClearOidcRefreshTokens", "0 3 * * *", jobs.clearOidcRefreshTokens) s.registerJob(ctx, "ClearOidcAuthorizationCodes", "0 3 * * *", jobs.clearOidcAuthorizationCodes),
registerJob(ctx, scheduler, "ClearAuditLogs", "0 3 * * *", jobs.clearAuditLogs) s.registerJob(ctx, "ClearOidcRefreshTokens", "0 3 * * *", jobs.clearOidcRefreshTokens),
scheduler.Start() s.registerJob(ctx, "ClearAuditLogs", "0 3 * * *", jobs.clearAuditLogs),
)
} }
type DbCleanupJobs struct { type DbCleanupJobs struct {

View File

@@ -8,24 +8,16 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/go-co-op/gocron/v2"
"gorm.io/gorm" "gorm.io/gorm"
"github.com/pocket-id/pocket-id/backend/internal/common" "github.com/pocket-id/pocket-id/backend/internal/common"
"github.com/pocket-id/pocket-id/backend/internal/model" "github.com/pocket-id/pocket-id/backend/internal/model"
) )
func RegisterFileCleanupJobs(ctx context.Context, db *gorm.DB) { func (s *Scheduler) RegisterFileCleanupJobs(ctx context.Context, db *gorm.DB) error {
scheduler, err := gocron.NewScheduler()
if err != nil {
log.Fatalf("Failed to create a new scheduler: %s", err)
}
jobs := &FileCleanupJobs{db: db} jobs := &FileCleanupJobs{db: db}
registerJob(ctx, scheduler, "ClearUnusedDefaultProfilePictures", "0 2 * * 0", jobs.clearUnusedDefaultProfilePictures) return s.registerJob(ctx, "ClearUnusedDefaultProfilePictures", "0 2 * * 0", jobs.clearUnusedDefaultProfilePictures)
scheduler.Start()
} }
type FileCleanupJobs struct { type FileCleanupJobs struct {

View File

@@ -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)
}
}

View File

@@ -4,7 +4,6 @@ import (
"context" "context"
"log" "log"
"github.com/go-co-op/gocron/v2"
"github.com/pocket-id/pocket-id/backend/internal/service" "github.com/pocket-id/pocket-id/backend/internal/service"
) )
@@ -13,24 +12,23 @@ type LdapJobs struct {
appConfigService *service.AppConfigService 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} 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 // 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 // Run the job immediately on startup
err = jobs.syncLdap(ctx) err = jobs.syncLdap(ctx)
if err != nil { if err != nil {
// Log the error only, but don't return it
log.Printf("Failed to sync LDAP: %v", err) log.Printf("Failed to sync LDAP: %v", err)
} }
scheduler.Start() return nil
} }
func (j *LdapJobs) syncLdap(ctx context.Context) error { func (j *LdapJobs) syncLdap(ctx context.Context) error {

View File

@@ -0,0 +1,64 @@
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) {
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")
}
}
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
}

View File

@@ -1,7 +1,10 @@
package middleware package middleware
import ( import (
"errors"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/pocket-id/pocket-id/backend/internal/common"
"github.com/pocket-id/pocket-id/backend/internal/service" "github.com/pocket-id/pocket-id/backend/internal/service"
) )
@@ -69,6 +72,13 @@ func (m *AuthMiddleware) Add() gin.HandlerFunc {
return 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 // JWT auth failed, try API key auth
userID, isAdmin, err = m.apiKeyMiddleware.Verify(c, m.options.AdminRequired) userID, isAdmin, err = m.apiKeyMiddleware.Verify(c, m.options.AdminRequired)
if err == nil { if err == nil {

View File

@@ -15,6 +15,8 @@ import (
"strings" "strings"
"time" "time"
"gorm.io/gorm/clause"
"github.com/lestrrat-go/jwx/v3/jwt" "github.com/lestrrat-go/jwx/v3/jwt"
"github.com/pocket-id/pocket-id/backend/internal/common" "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 the user has not authorized the client, create a new authorization in the database
if !hasAuthorizedClient { if !hasAuthorizedClient {
userAuthorizedClient := model.UserAuthorizedOidcClient{ err := s.createAuthorizedClientInternal(ctx, userID, input.ClientID, input.Scope, tx)
UserID: userID, if err != nil {
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 {
return "", "", err return "", "", err
} }
} }
@@ -201,7 +187,7 @@ func (s *OidcService) createTokenFromDeviceCode(ctx context.Context, deviceCode,
tx.Rollback() tx.Rollback()
}() }()
_, err = s.VerifyClientCredentials(ctx, clientID, clientSecret, tx) _, err = s.verifyClientCredentialsInternal(ctx, clientID, clientSecret, tx)
if err != nil { if err != nil {
return "", "", "", 0, err return "", "", "", 0, err
} }
@@ -269,7 +255,7 @@ func (s *OidcService) createTokenFromAuthorizationCode(ctx context.Context, code
tx.Rollback() tx.Rollback()
}() }()
client, err := s.VerifyClientCredentials(ctx, clientID, clientSecret, tx) client, err := s.verifyClientCredentialsInternal(ctx, clientID, clientSecret, tx)
if err != nil { if err != nil {
return "", "", "", 0, err return "", "", "", 0, err
} }
@@ -342,7 +328,7 @@ func (s *OidcService) createTokenFromRefreshToken(ctx context.Context, refreshTo
tx.Rollback() tx.Rollback()
}() }()
_, err = s.VerifyClientCredentials(ctx, clientID, clientSecret, tx) _, err = s.verifyClientCredentialsInternal(ctx, clientID, clientSecret, tx)
if err != nil { if err != nil {
return "", "", 0, err return "", "", 0, err
} }
@@ -401,7 +387,7 @@ func (s *OidcService) IntrospectToken(ctx context.Context, clientID, clientSecre
return introspectDto, &common.OidcMissingClientCredentialsError{} return introspectDto, &common.OidcMissingClientCredentialsError{}
} }
_, err = s.VerifyClientCredentials(ctx, clientID, clientSecret, s.db) _, err = s.verifyClientCredentialsInternal(ctx, clientID, clientSecret, s.db)
if err != nil { if err != nil {
return introspectDto, err return introspectDto, err
} }
@@ -520,7 +506,7 @@ func (s *OidcService) CreateClient(ctx context.Context, input dto.OidcClientCrea
LogoutCallbackURLs: input.LogoutCallbackURLs, LogoutCallbackURLs: input.LogoutCallbackURLs,
CreatedByID: userID, CreatedByID: userID,
IsPublic: input.IsPublic, IsPublic: input.IsPublic,
PkceEnabled: input.IsPublic || input.PkceEnabled, PkceEnabled: input.PkceEnabled,
} }
err := s.db. 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) { 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 { if err != nil {
return nil, err return nil, err
} }
@@ -1095,23 +1081,11 @@ func (s *OidcService) VerifyDeviceCode(ctx context.Context, userCode string, use
} }
if !hasAuthorizedClient { if !hasAuthorizedClient {
userAuthorizedClient := model.UserAuthorizedOidcClient{ err := s.createAuthorizedClientInternal(ctx, userID, deviceAuth.ClientID, deviceAuth.Scope, tx)
UserID: userID, if err != nil {
ClientID: deviceAuth.ClientID, return err
Scope: deviceAuth.Scope,
} }
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) s.auditLogService.Create(ctx, model.AuditLogEventNewDeviceCodeAuthorization, ipAddress, userAgent, userID, model.AuditLogData{"clientName": deviceAuth.Client.Name}, tx)
} else { } else {
s.auditLogService.Create(ctx, model.AuditLogEventDeviceCodeAuthorization, ipAddress, userAgent, userID, model.AuditLogData{"clientName": deviceAuth.Client.Name}, tx) 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 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 == "" { if clientID == "" {
return model.OidcClient{}, &common.OidcMissingClientCredentialsError{} return model.OidcClient{}, &common.OidcMissingClientCredentialsError{}
} }

View 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
}

View File

@@ -276,7 +276,7 @@
"callback_urls": "URL zpětného volání", "callback_urls": "URL zpětného volání",
"logout_callback_urls": "URL zpětného volání při odhlášení", "logout_callback_urls": "URL zpětného volání při odhlášení",
"public_client": "Veřejný klient", "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", "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ů.", "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}", "name_logo": "Logo {name}",
@@ -342,5 +342,9 @@
"show_code": "Zobrazit kód", "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.", "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", "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"
} }

View File

@@ -276,7 +276,7 @@
"callback_urls": "Callback URLs", "callback_urls": "Callback URLs",
"logout_callback_urls": "Abmelde Callback URLs", "logout_callback_urls": "Abmelde Callback URLs",
"public_client": "Öffentlicher Client", "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", "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.", "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", "name_logo": "{name} Logo",
@@ -327,20 +327,24 @@
"client_authorization": "Client-Autorisierung", "client_authorization": "Client-Autorisierung",
"new_client_authorization": "Neue Client-Autorisierung", "new_client_authorization": "Neue Client-Autorisierung",
"disable_animations": "Animationen deaktivieren", "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", "user_disabled": "Account deaktiviert",
"disabled_users_cannot_log_in_or_use_services": "Disabled users cannot log in or use services.", "disabled_users_cannot_log_in_or_use_services": "Deaktivierte Benutzer können sich nicht anmelden oder Dienste nutzen.",
"user_disabled_successfully": "User has been disabled successfully.", "user_disabled_successfully": "Der Benutzer wurde erfolgreich deaktiviert.",
"user_enabled_successfully": "User has been enabled successfully.", "user_enabled_successfully": "Der Benutzer wurde erfolgreich aktiviert.",
"status": "Status", "status": "Status",
"disable_firstname_lastname": "Disable {firstName} {lastName}", "disable_firstname_lastname": "Deaktiviere {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.", "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": "Keep disabled users from LDAP.", "ldap_soft_delete_users": "Deaktivierte Benutzer von LDAP behalten.",
"ldap_soft_delete_users_description": "When enabled, users removed from LDAP will be disabled rather than deleted from the system.", "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": "The login code has been sent to the user.", "login_code_email_success": "Der Login-Code wurde an den Benutzer gesendet.",
"send_email": "Send Email", "send_email": "E-Mail senden",
"show_code": "Show Code", "show_code": "Code anzeigen",
"callback_url_description": "URL(s) provided by your client. Wildcards (*) are supported, but best avoided for better security.", "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 Expiration", "api_key_expiration": "API Key Ablauf",
"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": "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"
} }

View File

@@ -276,7 +276,7 @@
"callback_urls": "Callback URLs", "callback_urls": "Callback URLs",
"logout_callback_urls": "Logout Callback URLs", "logout_callback_urls": "Logout Callback URLs",
"public_client": "Public Client", "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", "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.", "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", "name_logo": "{name} logo",

View File

@@ -1,108 +1,108 @@
{ {
"$schema": "https://inlang.com/schema/inlang-message-format", "$schema": "https://inlang.com/schema/inlang-message-format",
"my_account": "My Account", "my_account": "Mi Cuenta",
"logout": "Logout", "logout": "Cerrar sesión",
"confirm": "Confirm", "confirm": "Confirmar",
"key": "Key", "key": "Clave",
"value": "Value", "value": "Valor",
"remove_custom_claim": "Remove custom claim", "remove_custom_claim": "Eliminar reclamo personalizado",
"add_custom_claim": "Add custom claim", "add_custom_claim": "Añadir reclamo personalizado",
"add_another": "Add another", "add_another": "Añadir otro",
"select_a_date": "Select a date", "select_a_date": "Seleccione una fecha",
"select_file": "Select File", "select_file": "Seleccione Archivo:",
"profile_picture": "Profile Picture", "profile_picture": "Foto de perfil",
"profile_picture_is_managed_by_ldap_server": "The profile picture is managed by the LDAP server and cannot be changed here.", "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": "Click on the profile picture to upload a custom one from your files.", "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": "The image should be in PNG or JPEG format.", "image_should_be_in_format": "La imagen debe ser en formato PNG o JPEG.",
"items_per_page": "Items per page", "items_per_page": "Elementos por página",
"no_items_found": "No items found", "no_items_found": "No se encontraron elementos",
"search": "Search...", "search": "Buscar...",
"expand_card": "Expand card", "expand_card": "Ampliar tarjeta",
"copied": "Copied", "copied": "Copiado",
"click_to_copy": "Click to copy", "click_to_copy": "Haz clic para copiar",
"something_went_wrong": "Something went wrong", "something_went_wrong": "Algo ha salido mal",
"go_back_to_home": "Go back to home", "go_back_to_home": "Volver al Inicio",
"dont_have_access_to_your_passkey": "Don't have access to your passkey?", "dont_have_access_to_your_passkey": "¿No tiene acceso a su Passkey?",
"login_background": "Login background", "login_background": "Fondo de página de acceso",
"logo": "Logo", "logo": "Logo",
"login_code": "Login Code", "login_code": "Código de inicio de sesión",
"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.", "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 hour", "one_hour": "1 hora",
"twelve_hours": "12 hours", "twelve_hours": "12 horas",
"one_day": "1 day", "one_day": "1 día",
"one_week": "1 week", "one_week": "1 semana",
"one_month": "1 month", "one_month": "1 mes",
"expiration": "Expiration", "expiration": "Expiración",
"generate_code": "Generate Code", "generate_code": "Gerar Código",
"name": "Name", "name": "Nombre",
"browser_unsupported": "Browser unsupported", "browser_unsupported": "Navegador no soportado",
"this_browser_does_not_support_passkeys": "This browser doesn't support passkeys. Please use an alternative sign in method.", "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": "An unknown error occurred", "an_unknown_error_occurred": "Ocurrió un error desconocido",
"authentication_process_was_aborted": "The authentication process was aborted", "authentication_process_was_aborted": "El proceso de autenticación fue abortado",
"error_occurred_with_authenticator": "An error occurred with the authenticator", "error_occurred_with_authenticator": "Ha ocurrido un error con el autenticador",
"authenticator_does_not_support_discoverable_credentials": "The authenticator does not support discoverable credentials", "authenticator_does_not_support_discoverable_credentials": "El autenticador no soporta credenciales detectables",
"authenticator_does_not_support_resident_keys": "The authenticator does not support resident keys", "authenticator_does_not_support_resident_keys": "El autenticador no soporta claves residentes",
"passkey_was_previously_registered": "This passkey was previously registered", "passkey_was_previously_registered": "Esta Passkey ha sido registrado previamente",
"authenticator_does_not_support_any_of_the_requested_algorithms": "The authenticator does not support any of the requested algorithms", "authenticator_does_not_support_any_of_the_requested_algorithms": "El autenticador no soporta ninguno de los algoritmos solicitados",
"authenticator_timed_out": "The authenticator timed out", "authenticator_timed_out": "Se agotó el tiempo de espera del autenticador",
"critical_error_occurred_contact_administrator": "A critical error occurred. Please contact your administrator.", "critical_error_occurred_contact_administrator": "Ha ocurrido un error crítico. Por favor, contacte a su administrador.",
"sign_in_to": "Sign in to {name}", "sign_in_to": "Iniciar sesión en {name}",
"client_not_found": "Client not found", "client_not_found": "Cliente no encontrado",
"client_wants_to_access_the_following_information": "<b>{client}</b> wants to access the following information:", "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": "Do you want to sign in to <b>{client}</b> with your <b>{appName}</b> account?", "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": "Email", "email": "Correo electrónico",
"view_your_email_address": "View your email address", "view_your_email_address": "Ver su dirección de correo electrónico",
"profile": "Profile", "profile": "Perfil",
"view_your_profile_information": "View your profile information", "view_your_profile_information": "Ver información de su perfil",
"groups": "Groups", "groups": "Grupos",
"view_the_groups_you_are_a_member_of": "View the groups you are a member of", "view_the_groups_you_are_a_member_of": "Ver los grupos de los que usted es miembro",
"cancel": "Cancel", "cancel": "Cancelar",
"sign_in": "Sign in", "sign_in": "Iniciar sesión",
"try_again": "Try again", "try_again": "Intentar de nuevo",
"client_logo": "Client Logo", "client_logo": "Logo del cliente",
"sign_out": "Sign out", "sign_out": "Cerrar sesión",
"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>?", "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": "Sign in to {appName}", "sign_in_to_appname": "Iniciar sesión en {appName}",
"please_try_to_sign_in_again": "Please try to sign in again.", "please_try_to_sign_in_again": "Por favor, intente iniciar sesión de nuevo.",
"authenticate_yourself_with_your_passkey_to_access_the_admin_panel": "Authenticate yourself with your passkey to access the admin panel.", "authenticate_yourself_with_your_passkey_to_access_the_admin_panel": "Autenticar con tu Passkey para acceder al panel de administración.",
"authenticate": "Authenticate", "authenticate": "Autenticar",
"appname_setup": "{appName} Setup", "appname_setup": "Configuración de {appName}",
"please_try_again": "Please try again.", "please_try_again": "Por favor intente nuevamente.",
"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.", "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": "Continue", "continue": "Continuar",
"alternative_sign_in": "Alternative Sign In", "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": "If you don't have access to your passkey, you can sign in using one of the following methods.", "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": "Use your passkey instead?", "use_your_passkey_instead": "¿Utilizar su Passkey en su lugar?",
"email_login": "Email Login", "email_login": "Ingreso con Email",
"enter_a_login_code_to_sign_in": "Enter a login code to sign in.", "enter_a_login_code_to_sign_in": "Introduzca un código de acceso para iniciar sesión.",
"request_a_login_code_via_email": "Request a login code via email.", "request_a_login_code_via_email": "Solicitar un código de acceso por correo electrónico.",
"go_back": "Go back", "go_back": "Volver atrás",
"an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "An email has been sent to the provided email, if it exists in the system.", "an_email_has_been_sent_to_the_provided_email_if_it_exists_in_the_system": "Se ha enviado un correo electrónico al correo proporcionado, si existe en el sistema.",
"enter_code": "Enter code", "enter_code": "Ingresa el código",
"enter_your_email_address_to_receive_an_email_with_a_login_code": "Enter your email address to receive an email with a login code.", "enter_your_email_address_to_receive_an_email_with_a_login_code": "Introduzca su dirección de correo electrónico para recibir un correo electrónico con un código de acceso.",
"your_email": "Your email", "your_email": "Su correo electrónico",
"submit": "Submit", "submit": "Enviar",
"enter_the_code_you_received_to_sign_in": "Enter the code you received to sign in.", "enter_the_code_you_received_to_sign_in": "Ingrese el código que recibió para iniciar sesión.",
"code": "Code", "code": "Código",
"invalid_redirect_url": "Invalid redirect URL", "invalid_redirect_url": "URL de redirección no válido",
"audit_log": "Audit Log", "audit_log": "Registro de Auditoría",
"users": "Users", "users": "Usuarios",
"user_groups": "User Groups", "user_groups": "Grupos de usuario",
"oidc_clients": "OIDC Clients", "oidc_clients": "Clientes OIDC",
"api_keys": "API Keys", "api_keys": "Llaves API",
"application_configuration": "Application Configuration", "application_configuration": "Configuración de la aplicación",
"settings": "Settings", "settings": "Configuración",
"update_pocket_id": "Update Pocket ID", "update_pocket_id": "Actualizar Pocket ID",
"powered_by": "Powered by", "powered_by": "Producido por Pocket ID",
"see_your_account_activities_from_the_last_3_months": "See your account activities from the last 3 months.", "see_your_account_activities_from_the_last_3_months": "Vea las actividad de su cuenta de los últimos 3 meses.",
"time": "Time", "time": "Tiempo",
"event": "Event", "event": "Evento",
"approximate_location": "Approximate Location", "approximate_location": "Ubicación aproximada",
"ip_address": "IP Address", "ip_address": "Dirección IP",
"device": "Device", "device": "Dispositivo",
"client": "Client", "client": "Cliente",
"unknown": "Unknown", "unknown": "Desconocido",
"account_details_updated_successfully": "Account details updated successfully", "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.", "profile_picture_updated_successfully": "Profile picture updated successfully. It may take a few minutes to update.",
"account_settings": "Account Settings", "account_settings": "Account Settings",
"passkey_missing": "Passkey missing", "passkey_missing": "Passkey missing",
@@ -276,7 +276,7 @@
"callback_urls": "Callback URLs", "callback_urls": "Callback URLs",
"logout_callback_urls": "Logout Callback URLs", "logout_callback_urls": "Logout Callback URLs",
"public_client": "Public Client", "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", "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.", "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", "name_logo": "{name} logo",
@@ -342,5 +342,9 @@
"show_code": "Show Code", "show_code": "Show Code",
"callback_url_description": "URL(s) provided by your client. Wildcards (*) are supported, but best avoided for better security.", "callback_url_description": "URL(s) provided by your client. Wildcards (*) are supported, but best avoided for better security.",
"api_key_expiration": "API Key Expiration", "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"
} }

View File

@@ -276,7 +276,7 @@
"callback_urls": "URL de callback", "callback_urls": "URL de callback",
"logout_callback_urls": "URL de callback de déconnexion", "logout_callback_urls": "URL de callback de déconnexion",
"public_client": "Client public", "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", "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 linterception de code dautorisation.", "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 linterception de code dautorisation.",
"name_logo": "Logo {name}", "name_logo": "Logo {name}",
@@ -342,5 +342,9 @@
"show_code": "Show Code", "show_code": "Show Code",
"callback_url_description": "URL(s) provided by your client. Wildcards (*) are supported, but best avoided for better security.", "callback_url_description": "URL(s) provided by your client. Wildcards (*) are supported, but best avoided for better security.",
"api_key_expiration": "API Key Expiration", "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"
} }

View File

@@ -276,7 +276,7 @@
"callback_urls": "URL di callback", "callback_urls": "URL di callback",
"logout_callback_urls": "URL di callback per il logout", "logout_callback_urls": "URL di callback per il logout",
"public_client": "Client pubblico", "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", "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.", "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}", "name_logo": "Logo di {name}",
@@ -342,5 +342,9 @@
"show_code": "Mostra codice", "show_code": "Mostra codice",
"callback_url_description": "URL forniti dal tuo client. Wildcard (*) sono supportati, ma meglio evitarli per una migliore sicurezza.", "callback_url_description": "URL forniti dal tuo client. Wildcard (*) sono supportati, ma meglio evitarli per una migliore sicurezza.",
"api_key_expiration": "Scadenza Chiave API", "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"
} }

View File

@@ -276,7 +276,7 @@
"callback_urls": "Callback-URL's", "callback_urls": "Callback-URL's",
"logout_callback_urls": "Callback-URL's voor afmelden", "logout_callback_urls": "Callback-URL's voor afmelden",
"public_client": "Publieke client", "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", "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.", "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", "name_logo": "{name} logo",
@@ -342,5 +342,9 @@
"show_code": "Show Code", "show_code": "Show Code",
"callback_url_description": "URL(s) provided by your client. Wildcards (*) are supported, but best avoided for better security.", "callback_url_description": "URL(s) provided by your client. Wildcards (*) are supported, but best avoided for better security.",
"api_key_expiration": "API Key Expiration", "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"
} }

View File

@@ -276,7 +276,7 @@
"callback_urls": "Callback URLs", "callback_urls": "Callback URLs",
"logout_callback_urls": "Logout Callback URLs", "logout_callback_urls": "Logout Callback URLs",
"public_client": "Public Client", "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", "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.", "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", "name_logo": "{name} logo",
@@ -342,5 +342,9 @@
"show_code": "Show Code", "show_code": "Show Code",
"callback_url_description": "URL(s) provided by your client. Wildcards (*) are supported, but best avoided for better security.", "callback_url_description": "URL(s) provided by your client. Wildcards (*) are supported, but best avoided for better security.",
"api_key_expiration": "API Key Expiration", "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"
} }

View File

@@ -276,7 +276,7 @@
"callback_urls": "Callback URLs", "callback_urls": "Callback URLs",
"logout_callback_urls": "Logout Callback URLs", "logout_callback_urls": "Logout Callback URLs",
"public_client": "Public Client", "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", "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.", "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", "name_logo": "{name} logo",
@@ -342,5 +342,9 @@
"show_code": "Show Code", "show_code": "Show Code",
"callback_url_description": "URL(s) provided by your client. Wildcards (*) are supported, but best avoided for better security.", "callback_url_description": "URL(s) provided by your client. Wildcards (*) are supported, but best avoided for better security.",
"api_key_expiration": "API Key Expiration", "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"
} }

View File

@@ -276,7 +276,7 @@
"callback_urls": "Callback URLs", "callback_urls": "Callback URLs",
"logout_callback_urls": "Logout Callback URLs", "logout_callback_urls": "Logout Callback URLs",
"public_client": "Публичный клиент", "public_client": "Публичный клиент",
"public_clients_do_not_have_a_client_secret_and_use_pkce_instead": "Публичные клиенты не имеют клиентского секрета и вместо этого используют PKCE. Включите, если ваш клиент является SPA или мобильным приложением.", "public_clients_description": "Публичные клиенты не имеют клиентского секрета и вместо этого используют PKCE. Включите, если ваш клиент является SPA или мобильным приложением.",
"pkce": "PKCE", "pkce": "PKCE",
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange — это функция безопасности для предотвращения атак CSRF и перехвата кода авторизации.", "public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "Public Key Code Exchange — это функция безопасности для предотвращения атак CSRF и перехвата кода авторизации.",
"name_logo": "Логотип {name}", "name_logo": "Логотип {name}",
@@ -342,5 +342,9 @@
"show_code": "Показать код", "show_code": "Показать код",
"callback_url_description": "URL-адреса, предоставленные клиентом. Поддерживаются wildcard-адреса (*), но лучше всего избегать их для лучшей безопасности.", "callback_url_description": "URL-адреса, предоставленные клиентом. Поддерживаются wildcard-адреса (*), но лучше всего избегать их для лучшей безопасности.",
"api_key_expiration": "Истечение срока действия API ключа", "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": "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": "Авторизируйте"
} }

View File

@@ -276,7 +276,7 @@
"callback_urls": "Callback URL", "callback_urls": "Callback URL",
"logout_callback_urls": "Logout Callback URL", "logout_callback_urls": "Logout Callback URL",
"public_client": "公共客户端", "public_client": "公共客户端",
"public_clients_do_not_have_a_client_secret_and_use_pkce_instead": "公共客户端没有客户端密钥,而是使用 PKCE。如果您的客户端是 SPA 或移动应用,请启用此选项。", "public_clients_description": "公共客户端没有客户端密钥,而是使用 PKCE。如果您的客户端是 SPA 或移动应用,请启用此选项。",
"pkce": "PKCE", "pkce": "PKCE",
"public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "公钥代码交换是一种安全功能,可防止 CSRF 和授权代码拦截攻击。", "public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks": "公钥代码交换是一种安全功能,可防止 CSRF 和授权代码拦截攻击。",
"name_logo": "{name} Logo", "name_logo": "{name} Logo",

View File

@@ -1,6 +1,6 @@
{ {
"name": "pocket-id-frontend", "name": "pocket-id-frontend",
"version": "0.50.0", "version": "0.51.0",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@@ -93,6 +93,30 @@
</Alert.Root> </Alert.Root>
{/if} {/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 --> <!-- Account details card -->
<fieldset <fieldset
disabled={!$appConfigStore.allowOwnAccountEdit || disabled={!$appConfigStore.allowOwnAccountEdit ||
@@ -143,8 +167,9 @@
</Card.Root> </Card.Root>
</div> </div>
<!-- Login code card --> <!-- Login code card -->
<div> <div class="hidden sm:block">
<Card.Root> <Card.Root>
<Card.Header> <Card.Header>
<div class="flex flex-col items-start justify-between gap-3 sm:flex-row sm:items-center"> <div class="flex flex-col items-start justify-between gap-3 sm:flex-row sm:items-center">

View File

@@ -33,7 +33,7 @@
callbackURLs: existingClient?.callbackURLs || [''], callbackURLs: existingClient?.callbackURLs || [''],
logoutCallbackURLs: existingClient?.logoutCallbackURLs || [], logoutCallbackURLs: existingClient?.logoutCallbackURLs || [],
isPublic: existingClient?.isPublic || false, isPublic: existingClient?.isPublic || false,
pkceEnabled: existingClient?.isPublic == true || existingClient?.pkceEnabled || false pkceEnabled: existingClient?.pkceEnabled || false
}; };
const formSchema = z.object({ const formSchema = z.object({
@@ -98,17 +98,13 @@
<CheckboxWithLabel <CheckboxWithLabel
id="public-client" id="public-client"
label={m.public_client()} label={m.public_client()}
description={m.public_clients_do_not_have_a_client_secret_and_use_pkce_instead()} description={m.public_clients_description()}
onCheckedChange={(v) => {
if (v == true) form.setValue('pkceEnabled', true);
}}
bind:checked={$inputs.isPublic.value} bind:checked={$inputs.isPublic.value}
/> />
<CheckboxWithLabel <CheckboxWithLabel
id="pkce" id="pkce"
label={m.pkce()} label={m.pkce()}
description={m.public_key_code_exchange_is_a_security_feature_to_prevent_csrf_and_authorization_code_interception_attacks()} 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} bind:checked={$inputs.pkceEnabled.value}
/> />
</div> </div>

View File

@@ -2,6 +2,7 @@
import AuditLogList from '$lib/components/audit-log-list.svelte'; import AuditLogList from '$lib/components/audit-log-list.svelte';
import * as Card from '$lib/components/ui/card'; import * as Card from '$lib/components/ui/card';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import userStore from '$lib/stores/user-store';
import { LogsIcon } from 'lucide-svelte'; import { LogsIcon } from 'lucide-svelte';
import AuditLogSwitcher from './audit-log-switcher.svelte'; import AuditLogSwitcher from './audit-log-switcher.svelte';
@@ -13,7 +14,9 @@
<title>{m.audit_log()}</title> <title>{m.audit_log()}</title>
</svelte:head> </svelte:head>
<AuditLogSwitcher currentPage="personal" /> {#if $userStore?.isAdmin}
<AuditLogSwitcher currentPage="personal" />
{/if}
<div> <div>
<Card.Root> <Card.Root>