Compare commits

..

15 Commits

Author SHA1 Message Date
Elias Schneider
646f849441 release: 2.1.0 2026-01-04 21:18:24 +01:00
Elias Schneider
20bbd4a06f chore(translations): update translations via Crowdin (#1189) 2026-01-04 21:17:49 +01:00
Justin Moy
2d7e2ec8df feat: process nonce within device authorization flow (#1185)
Co-authored-by: Elias Schneider <login@eliasschneider.com>
2026-01-04 18:18:17 +00:00
Kyle Mendell
72009ced67 feat: add issuer url to oidc client details list (#1197) 2026-01-04 19:04:16 +01:00
Elias Schneider
4881130ead refactor: run SCIM jobs in context of gocron instead of custom implementation 2026-01-04 19:00:18 +01:00
Elias Schneider
d6a7b503ff fix: invalid cookie name for email login code device token 2026-01-03 23:46:44 +01:00
Elias Schneider
3c3916536e release: 2.0.2 2026-01-03 15:16:46 +01:00
Elias Schneider
a24b2afb7b chore: add no-op migration to postgres 2026-01-03 15:12:14 +01:00
Elias Schneider
7c34501055 fix: localhost callback URLs with port don't match correctly 2026-01-03 15:07:56 +01:00
Elias Schneider
ba00f40bd4 fix: allow version downgrade database is dirty 2026-01-03 15:06:39 +01:00
Elias Schneider
2f651adf3b fix: migration fails if users exist with no email address 2026-01-03 15:06:34 +01:00
Elias Schneider
f42ba3bbef release: 2.0.1 2026-01-02 23:50:35 +01:00
Elias Schneider
2341da99e9 fix: restore old input input field size 2026-01-02 23:49:41 +01:00
Elias Schneider
2cce200892 fix: admins imported from LDAP lose admin privileges 2026-01-02 23:42:25 +01:00
Elias Schneider
cd2e9f3a2a chore(docker): bump image tag to v2 2026-01-02 19:21:58 +01:00
62 changed files with 422 additions and 249 deletions

View File

@@ -1 +1 @@
2.0.0
2.1.0

View File

@@ -1,3 +1,47 @@
## v2.1.0
### Bug Fixes
- invalid cookie name for email login code device token ([d6a7b50](https://github.com/pocket-id/pocket-id/commit/d6a7b503ff4571b1291a55a569add3374f5e2d5b) by @stonith404)
### Features
- add issuer url to oidc client details list ([#1197](https://github.com/pocket-id/pocket-id/pull/1197) by @kmendell)
- process nonce within device authorization flow ([#1185](https://github.com/pocket-id/pocket-id/pull/1185) by @justincmoy)
### Other
- run SCIM jobs in context of gocron instead of custom implementation ([4881130](https://github.com/pocket-id/pocket-id/commit/4881130eadcef0642f8a87650b7c36fda453b51b) by @stonith404)
**Full Changelog**: https://github.com/pocket-id/pocket-id/compare/v2.0.2...v2.1.0
## v2.0.2
### Bug Fixes
- migration fails if users exist with no email address ([2f651ad](https://github.com/pocket-id/pocket-id/commit/2f651adf3b4e8d689461da2083c3afcb1eb1d477) by @stonith404)
- allow version downgrade database is dirty ([ba00f40](https://github.com/pocket-id/pocket-id/commit/ba00f40bd4b06f31d251599fcb1db63e902a6987) by @stonith404)
- localhost callback URLs with port don't match correctly ([7c34501](https://github.com/pocket-id/pocket-id/commit/7c345010556f11a593948b2a1ae558b7a8003696) by @stonith404)
### Other
- add no-op migration to postgres ([a24b2af](https://github.com/pocket-id/pocket-id/commit/a24b2afb7b8165bed05976058a8ae797adc245df) by @stonith404)
**Full Changelog**: https://github.com/pocket-id/pocket-id/compare/v2.0.1...v2.0.2
## v2.0.1
### Bug Fixes
- admins imported from LDAP lose admin privileges ([2cce200](https://github.com/pocket-id/pocket-id/commit/2cce2008928081b5e0f0e6bcbc3f43816f082de9) by @stonith404)
- restore old input input field size ([2341da9](https://github.com/pocket-id/pocket-id/commit/2341da99e9716686cf28dd0680d751ae9da0fadc) by @stonith404)
### Other
- bump image tag to `v2` ([cd2e9f3](https://github.com/pocket-id/pocket-id/commit/cd2e9f3a2ad753815ef8da998f9b54853d953a2a) by @stonith404)
**Full Changelog**: https://github.com/pocket-id/pocket-id/compare/v2.0.0...v2.0.1
## v2.0.0
### Bug Fixes

View File

@@ -48,8 +48,13 @@ func Bootstrap(ctx context.Context) error {
return fmt.Errorf("failed to initialize application images: %w", err)
}
scheduler, err := job.NewScheduler()
if err != nil {
return fmt.Errorf("failed to create job scheduler: %w", err)
}
// Create all services
svc, err := initServices(ctx, db, httpClient, imageExtensions, fileStorage)
svc, err := initServices(ctx, db, httpClient, imageExtensions, fileStorage, scheduler)
if err != nil {
return fmt.Errorf("failed to initialize services: %w", err)
}
@@ -74,11 +79,7 @@ func Bootstrap(ctx context.Context) error {
}
shutdownFns = append(shutdownFns, shutdownFn)
// Init the job scheduler
scheduler, err := job.NewScheduler()
if err != nil {
return fmt.Errorf("failed to create job scheduler: %w", err)
}
// Register scheduled jobs
err = registerScheduledJobs(ctx, db, svc, httpClient, scheduler)
if err != nil {
return fmt.Errorf("failed to register scheduled jobs: %w", err)

View File

@@ -35,6 +35,10 @@ func registerScheduledJobs(ctx context.Context, db *gorm.DB, svc *services, http
if err != nil {
return fmt.Errorf("failed to register analytics job in scheduler: %w", err)
}
err = scheduler.RegisterScimJobs(ctx, svc.scimService)
if err != nil {
return fmt.Errorf("failed to register SCIM scheduler job: %w", err)
}
return nil
}

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"github.com/pocket-id/pocket-id/backend/internal/job"
"gorm.io/gorm"
"github.com/pocket-id/pocket-id/backend/internal/service"
@@ -12,28 +13,27 @@ import (
)
type services struct {
appConfigService *service.AppConfigService
appImagesService *service.AppImagesService
emailService *service.EmailService
geoLiteService *service.GeoLiteService
auditLogService *service.AuditLogService
jwtService *service.JwtService
webauthnService *service.WebAuthnService
scimService *service.ScimService
scimSchedulerService *service.ScimSchedulerService
userService *service.UserService
customClaimService *service.CustomClaimService
oidcService *service.OidcService
userGroupService *service.UserGroupService
ldapService *service.LdapService
apiKeyService *service.ApiKeyService
versionService *service.VersionService
fileStorage storage.FileStorage
appLockService *service.AppLockService
appConfigService *service.AppConfigService
appImagesService *service.AppImagesService
emailService *service.EmailService
geoLiteService *service.GeoLiteService
auditLogService *service.AuditLogService
jwtService *service.JwtService
webauthnService *service.WebAuthnService
scimService *service.ScimService
userService *service.UserService
customClaimService *service.CustomClaimService
oidcService *service.OidcService
userGroupService *service.UserGroupService
ldapService *service.LdapService
apiKeyService *service.ApiKeyService
versionService *service.VersionService
fileStorage storage.FileStorage
appLockService *service.AppLockService
}
// Initializes all services
func initServices(ctx context.Context, db *gorm.DB, httpClient *http.Client, imageExtensions map[string]string, fileStorage storage.FileStorage) (svc *services, err error) {
func initServices(ctx context.Context, db *gorm.DB, httpClient *http.Client, imageExtensions map[string]string, fileStorage storage.FileStorage, scheduler *job.Scheduler) (svc *services, err error) {
svc = &services{}
svc.appConfigService, err = service.NewAppConfigService(ctx, db)
@@ -63,20 +63,17 @@ func initServices(ctx context.Context, db *gorm.DB, httpClient *http.Client, ima
return nil, fmt.Errorf("failed to create WebAuthn service: %w", err)
}
svc.oidcService, err = service.NewOidcService(ctx, db, svc.jwtService, svc.appConfigService, svc.auditLogService, svc.customClaimService, svc.webauthnService, httpClient, fileStorage)
svc.scimService = service.NewScimService(db, scheduler, httpClient)
svc.oidcService, err = service.NewOidcService(ctx, db, svc.jwtService, svc.appConfigService, svc.auditLogService, svc.customClaimService, svc.webauthnService, svc.scimService, httpClient, fileStorage)
if err != nil {
return nil, fmt.Errorf("failed to create OIDC service: %w", err)
}
svc.userGroupService = service.NewUserGroupService(db, svc.appConfigService)
svc.userService = service.NewUserService(db, svc.jwtService, svc.auditLogService, svc.emailService, svc.appConfigService, svc.customClaimService, svc.appImagesService, fileStorage)
svc.userGroupService = service.NewUserGroupService(db, svc.appConfigService, svc.scimService)
svc.userService = service.NewUserService(db, svc.jwtService, svc.auditLogService, svc.emailService, svc.appConfigService, svc.customClaimService, svc.appImagesService, svc.scimService, fileStorage)
svc.ldapService = service.NewLdapService(db, httpClient, svc.appConfigService, svc.userService, svc.userGroupService, fileStorage)
svc.apiKeyService = service.NewApiKeyService(db, svc.emailService)
svc.scimService = service.NewScimService(db, httpClient)
svc.scimSchedulerService, err = service.NewScimSchedulerService(ctx, svc.scimService)
if err != nil {
return nil, fmt.Errorf("failed to create SCIM scheduler service: %w", err)
}
svc.versionService = service.NewVersionService(httpClient)

View File

@@ -139,6 +139,7 @@ type OidcDeviceAuthorizationRequestDto struct {
ClientSecret string `form:"client_secret"`
ClientAssertion string `form:"client_assertion"`
ClientAssertionType string `form:"client_assertion_type"`
Nonce string `form:"nonce"`
}
type OidcDeviceAuthorizationResponseDto struct {

View File

@@ -28,7 +28,7 @@ func (s *Scheduler) RegisterAnalyticsJob(ctx context.Context, appConfig *service
appConfig: appConfig,
httpClient: httpClient,
}
return s.registerJob(ctx, "SendHeartbeat", gocron.DurationJob(24*time.Hour), jobs.sendHeartbeat, true)
return s.RegisterJob(ctx, "SendHeartbeat", gocron.DurationJob(24*time.Hour), jobs.sendHeartbeat, true)
}
type AnalyticsJob struct {

View File

@@ -22,7 +22,7 @@ func (s *Scheduler) RegisterApiKeyExpiryJob(ctx context.Context, apiKeyService *
}
// Send every day at midnight
return s.registerJob(ctx, "ExpiredApiKeyEmailJob", gocron.CronJob("0 0 * * *", false), jobs.checkAndNotifyExpiringApiKeys, false)
return s.RegisterJob(ctx, "ExpiredApiKeyEmailJob", gocron.CronJob("0 0 * * *", false), jobs.checkAndNotifyExpiringApiKeys, false)
}
func (j *ApiKeyEmailJobs) checkAndNotifyExpiringApiKeys(ctx context.Context) error {

View File

@@ -21,13 +21,13 @@ func (s *Scheduler) RegisterDbCleanupJobs(ctx context.Context, db *gorm.DB) erro
// Run every 24 hours (but with some jitter so they don't run at the exact same time), and now
def := gocron.DurationRandomJob(24*time.Hour-2*time.Minute, 24*time.Hour+2*time.Minute)
return errors.Join(
s.registerJob(ctx, "ClearWebauthnSessions", def, jobs.clearWebauthnSessions, true),
s.registerJob(ctx, "ClearOneTimeAccessTokens", def, jobs.clearOneTimeAccessTokens, true),
s.registerJob(ctx, "ClearSignupTokens", def, jobs.clearSignupTokens, true),
s.registerJob(ctx, "ClearOidcAuthorizationCodes", def, jobs.clearOidcAuthorizationCodes, true),
s.registerJob(ctx, "ClearOidcRefreshTokens", def, jobs.clearOidcRefreshTokens, true),
s.registerJob(ctx, "ClearReauthenticationTokens", def, jobs.clearReauthenticationTokens, true),
s.registerJob(ctx, "ClearAuditLogs", def, jobs.clearAuditLogs, true),
s.RegisterJob(ctx, "ClearWebauthnSessions", def, jobs.clearWebauthnSessions, true),
s.RegisterJob(ctx, "ClearOneTimeAccessTokens", def, jobs.clearOneTimeAccessTokens, true),
s.RegisterJob(ctx, "ClearSignupTokens", def, jobs.clearSignupTokens, true),
s.RegisterJob(ctx, "ClearOidcAuthorizationCodes", def, jobs.clearOidcAuthorizationCodes, true),
s.RegisterJob(ctx, "ClearOidcRefreshTokens", def, jobs.clearOidcRefreshTokens, true),
s.RegisterJob(ctx, "ClearReauthenticationTokens", def, jobs.clearReauthenticationTokens, true),
s.RegisterJob(ctx, "ClearAuditLogs", def, jobs.clearAuditLogs, true),
)
}

View File

@@ -19,11 +19,11 @@ import (
func (s *Scheduler) RegisterFileCleanupJobs(ctx context.Context, db *gorm.DB, fileStorage storage.FileStorage) error {
jobs := &FileCleanupJobs{db: db, fileStorage: fileStorage}
err := s.registerJob(ctx, "ClearUnusedDefaultProfilePictures", gocron.DurationJob(24*time.Hour), jobs.clearUnusedDefaultProfilePictures, false)
err := s.RegisterJob(ctx, "ClearUnusedDefaultProfilePictures", gocron.DurationJob(24*time.Hour), jobs.clearUnusedDefaultProfilePictures, false)
// Only necessary for file system storage
if fileStorage.Type() == storage.TypeFileSystem {
err = errors.Join(err, s.registerJob(ctx, "ClearOrphanedTempFiles", gocron.DurationJob(12*time.Hour), jobs.clearOrphanedTempFiles, true))
err = errors.Join(err, s.RegisterJob(ctx, "ClearOrphanedTempFiles", gocron.DurationJob(12*time.Hour), jobs.clearOrphanedTempFiles, true))
}
return err

View File

@@ -23,7 +23,7 @@ func (s *Scheduler) RegisterGeoLiteUpdateJobs(ctx context.Context, geoLiteServic
jobs := &GeoLiteUpdateJobs{geoLiteService: geoLiteService}
// Run every 24 hours (and right away)
return s.registerJob(ctx, "UpdateGeoLiteDB", gocron.DurationJob(24*time.Hour), jobs.updateGoeLiteDB, true)
return s.RegisterJob(ctx, "UpdateGeoLiteDB", gocron.DurationJob(24*time.Hour), jobs.updateGoeLiteDB, true)
}
func (j *GeoLiteUpdateJobs) updateGoeLiteDB(ctx context.Context) error {

View File

@@ -18,7 +18,7 @@ func (s *Scheduler) RegisterLdapJobs(ctx context.Context, ldapService *service.L
jobs := &LdapJobs{ldapService: ldapService, appConfigService: appConfigService}
// Register the job to run every hour
return s.registerJob(ctx, "SyncLdap", gocron.DurationJob(time.Hour), jobs.syncLdap, true)
return s.RegisterJob(ctx, "SyncLdap", gocron.DurationJob(time.Hour), jobs.syncLdap, true)
}
func (j *LdapJobs) syncLdap(ctx context.Context) error {

View File

@@ -2,6 +2,7 @@ package job
import (
"context"
"errors"
"fmt"
"log/slog"
@@ -24,6 +25,26 @@ func NewScheduler() (*Scheduler, error) {
}, nil
}
func (s *Scheduler) RemoveJob(name string) error {
jobs := s.scheduler.Jobs()
var errs []error
for _, job := range jobs {
if job.Name() == name {
err := s.scheduler.RemoveJob(job.ID())
if err != nil {
errs = append(errs, fmt.Errorf("failed to unqueue job %q with ID %q: %w", name, job.ID().String(), err))
}
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
// Run the scheduler.
// This function blocks until the context is canceled.
func (s *Scheduler) Run(ctx context.Context) error {
@@ -43,9 +64,10 @@ func (s *Scheduler) Run(ctx context.Context) error {
return nil
}
func (s *Scheduler) registerJob(ctx context.Context, name string, def gocron.JobDefinition, job func(ctx context.Context) error, runImmediately bool) error {
func (s *Scheduler) RegisterJob(ctx context.Context, name string, def gocron.JobDefinition, job func(ctx context.Context) error, runImmediately bool, extraOptions ...gocron.JobOption) error {
jobOptions := []gocron.JobOption{
gocron.WithContext(ctx),
gocron.WithName(name),
gocron.WithEventListeners(
gocron.BeforeJobRuns(func(jobID uuid.UUID, jobName string) {
slog.Info("Starting job",
@@ -73,6 +95,8 @@ func (s *Scheduler) registerJob(ctx context.Context, name string, def gocron.Job
jobOptions = append(jobOptions, gocron.JobOption(gocron.WithStartImmediately()))
}
jobOptions = append(jobOptions, extraOptions...)
_, err := s.scheduler.NewJob(def, gocron.NewTask(job), jobOptions...)
if err != nil {

View File

@@ -0,0 +1,25 @@
package job
import (
"context"
"time"
"github.com/go-co-op/gocron/v2"
"github.com/pocket-id/pocket-id/backend/internal/service"
)
type ScimJobs struct {
scimService *service.ScimService
}
func (s *Scheduler) RegisterScimJobs(ctx context.Context, scimService *service.ScimService) error {
jobs := &ScimJobs{scimService: scimService}
// Register the job to run every hour
return s.RegisterJob(ctx, "SyncScim", gocron.DurationJob(time.Hour), jobs.SyncScim, true)
}
func (j *ScimJobs) SyncScim(ctx context.Context) error {
return j.scimService.SyncAll(ctx)
}

View File

@@ -144,6 +144,7 @@ type OidcDeviceCode struct {
DeviceCode string
UserCode string
Scope string
Nonce string
ExpiresAt datatype.DateTime
IsAuthorized bool

View File

@@ -56,6 +56,7 @@ type OidcService struct {
auditLogService *AuditLogService
customClaimService *CustomClaimService
webAuthnService *WebAuthnService
scimService *ScimService
httpClient *http.Client
jwkCache *jwk.Cache
@@ -70,6 +71,7 @@ func NewOidcService(
auditLogService *AuditLogService,
customClaimService *CustomClaimService,
webAuthnService *WebAuthnService,
scimService *ScimService,
httpClient *http.Client,
fileStorage storage.FileStorage,
) (s *OidcService, err error) {
@@ -80,6 +82,7 @@ func NewOidcService(
auditLogService: auditLogService,
customClaimService: customClaimService,
webAuthnService: webAuthnService,
scimService: scimService,
httpClient: httpClient,
fileStorage: fileStorage,
}
@@ -311,7 +314,7 @@ func (s *OidcService) createTokenFromDeviceCode(ctx context.Context, input dto.O
}
// Explicitly use the input clientID for the audience claim to ensure consistency
idToken, err := s.jwtService.GenerateIDToken(userClaims, input.ClientID, "")
idToken, err := s.jwtService.GenerateIDToken(userClaims, input.ClientID, deviceAuth.Nonce)
if err != nil {
return CreatedTokens{}, err
}
@@ -1088,6 +1091,7 @@ func (s *OidcService) UpdateAllowedUserGroups(ctx context.Context, id string, in
return model.OidcClient{}, err
}
s.scimService.ScheduleSync()
return client, nil
}
@@ -1278,6 +1282,7 @@ func (s *OidcService) CreateDeviceAuthorization(ctx context.Context, input dto.O
ExpiresAt: datatype.DateTime(time.Now().Add(DeviceCodeDuration)),
IsAuthorized: false,
ClientID: client.ID,
Nonce: input.Nonce,
}
if err := s.db.Create(deviceAuth).Error; err != nil {

View File

@@ -1,136 +0,0 @@
package service
import (
"context"
"errors"
"log/slog"
"sync"
"time"
"gorm.io/gorm"
)
// ScimSchedulerService schedules and triggers periodic synchronization
// of SCIM service providers. Each provider is tracked independently,
// and sync operations are run at or after their scheduled time.
type ScimSchedulerService struct {
scimService *ScimService
providerSyncTime map[string]time.Time
mu sync.RWMutex
}
func NewScimSchedulerService(ctx context.Context, scimService *ScimService) (*ScimSchedulerService, error) {
s := &ScimSchedulerService{
scimService: scimService,
providerSyncTime: make(map[string]time.Time),
}
err := s.start(ctx)
return s, err
}
// ScheduleSync forces the given provider to be synced soon by
// moving its next scheduled time to 5 minutes from now.
func (s *ScimSchedulerService) ScheduleSync(providerID string) {
s.setSyncTime(providerID, 5*time.Minute)
}
// start initializes the scheduler and begins the synchronization loop.
// Syncs happen every hour by default, but ScheduleSync can be called to schedule a sync sooner.
func (s *ScimSchedulerService) start(ctx context.Context) error {
if err := s.refreshProviders(ctx); err != nil {
return err
}
go func() {
const (
syncCheckInterval = 5 * time.Second
providerRefreshDelay = time.Minute
)
ticker := time.NewTicker(syncCheckInterval)
defer ticker.Stop()
lastProviderRefresh := time.Now()
for {
select {
case <-ctx.Done():
return
// Runs every 5 seconds to check if any provider is due for sync
case <-ticker.C:
now := time.Now()
if now.Sub(lastProviderRefresh) >= providerRefreshDelay {
err := s.refreshProviders(ctx)
if err != nil {
slog.Error("Error refreshing SCIM service providers",
slog.Any("error", err),
)
} else {
lastProviderRefresh = now
}
}
var due []string
s.mu.RLock()
for providerID, syncTime := range s.providerSyncTime {
if !syncTime.After(now) {
due = append(due, providerID)
}
}
s.mu.RUnlock()
s.syncProviders(ctx, due)
}
}
}()
return nil
}
func (s *ScimSchedulerService) refreshProviders(ctx context.Context) error {
providers, err := s.scimService.ListServiceProviders(ctx)
if err != nil {
return err
}
inAHour := time.Now().Add(time.Hour)
s.mu.Lock()
for _, provider := range providers {
if _, exists := s.providerSyncTime[provider.ID]; !exists {
s.providerSyncTime[provider.ID] = inAHour
}
}
s.mu.Unlock()
return nil
}
func (s *ScimSchedulerService) syncProviders(ctx context.Context, providerIDs []string) {
for _, providerID := range providerIDs {
err := s.scimService.SyncServiceProvider(ctx, providerID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// Remove the provider from the schedule if it no longer exists
s.mu.Lock()
delete(s.providerSyncTime, providerID)
s.mu.Unlock()
} else {
slog.Error("Error syncing SCIM client",
slog.String("provider_id", providerID),
slog.Any("error", err),
)
}
continue
}
// A successful sync schedules the next sync in an hour
s.setSyncTime(providerID, time.Hour)
}
}
func (s *ScimSchedulerService) setSyncTime(providerID string, t time.Duration) {
s.mu.Lock()
s.providerSyncTime[providerID] = time.Now().Add(t)
s.mu.Unlock()
}

View File

@@ -15,6 +15,7 @@ import (
"strings"
"time"
"github.com/go-co-op/gocron/v2"
"github.com/pocket-id/pocket-id/backend/internal/dto"
"github.com/pocket-id/pocket-id/backend/internal/model"
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
@@ -32,6 +33,11 @@ const scimErrorBodyLimit = 4096
type scimSyncAction int
type Scheduler interface {
RegisterJob(ctx context.Context, name string, def gocron.JobDefinition, job func(ctx context.Context) error, runImmediately bool, extraOptions ...gocron.JobOption) error
RemoveJob(name string) error
}
const (
scimActionNone scimSyncAction = iota
scimActionCreated
@@ -48,15 +54,16 @@ type scimSyncStats struct {
// ScimService handles SCIM provisioning to external service providers.
type ScimService struct {
db *gorm.DB
scheduler Scheduler
httpClient *http.Client
}
func NewScimService(db *gorm.DB, httpClient *http.Client) *ScimService {
func NewScimService(db *gorm.DB, scheduler Scheduler, httpClient *http.Client) *ScimService {
if httpClient == nil {
httpClient = &http.Client{Timeout: 20 * time.Second}
}
return &ScimService{db: db, httpClient: httpClient}
return &ScimService{db: db, scheduler: scheduler, httpClient: httpClient}
}
func (s *ScimService) GetServiceProvider(
@@ -132,6 +139,41 @@ func (s *ScimService) DeleteServiceProvider(ctx context.Context, serviceProvider
Error
}
//nolint:contextcheck
func (s *ScimService) ScheduleSync() {
jobName := "ScheduledScimSync"
start := time.Now().Add(5 * time.Minute)
_ = s.scheduler.RemoveJob(jobName)
err := s.scheduler.RegisterJob(
context.Background(), jobName,
gocron.OneTimeJob(gocron.OneTimeJobStartDateTime(start)), s.SyncAll, false)
if err != nil {
slog.Error("Failed to schedule SCIM sync", slog.Any("error", err))
}
}
func (s *ScimService) SyncAll(ctx context.Context) error {
providers, err := s.ListServiceProviders(ctx)
if err != nil {
return err
}
var errs []error
for _, provider := range providers {
if ctx.Err() != nil {
errs = append(errs, ctx.Err())
break
}
if err := s.SyncServiceProvider(ctx, provider.ID); err != nil {
errs = append(errs, fmt.Errorf("failed to sync SCIM provider %s: %w", provider.ID, err))
}
}
return errors.Join(errs...)
}
func (s *ScimService) SyncServiceProvider(ctx context.Context, serviceProviderID string) error {
start := time.Now()
provider, err := s.GetServiceProvider(ctx, serviceProviderID)

View File

@@ -16,11 +16,12 @@ import (
type UserGroupService struct {
db *gorm.DB
scimService *ScimService
appConfigService *AppConfigService
}
func NewUserGroupService(db *gorm.DB, appConfigService *AppConfigService) *UserGroupService {
return &UserGroupService{db: db, appConfigService: appConfigService}
func NewUserGroupService(db *gorm.DB, appConfigService *AppConfigService, scimService *ScimService) *UserGroupService {
return &UserGroupService{db: db, appConfigService: appConfigService, scimService: scimService}
}
func (s *UserGroupService) List(ctx context.Context, name string, listRequestOptions utils.ListRequestOptions) (groups []model.UserGroup, response utils.PaginationResponse, err error) {
@@ -90,7 +91,13 @@ func (s *UserGroupService) Delete(ctx context.Context, id string) error {
return err
}
return tx.Commit().Error
err = tx.Commit().Error
if err != nil {
return err
}
s.scimService.ScheduleSync()
return nil
}
func (s *UserGroupService) Create(ctx context.Context, input dto.UserGroupCreateDto) (group model.UserGroup, err error) {
@@ -118,6 +125,8 @@ func (s *UserGroupService) createInternal(ctx context.Context, input dto.UserGro
}
return model.UserGroup{}, err
}
s.scimService.ScheduleSync()
return group, nil
}
@@ -165,6 +174,8 @@ func (s *UserGroupService) updateInternal(ctx context.Context, id string, input
} else if err != nil {
return model.UserGroup{}, err
}
s.scimService.ScheduleSync()
return group, nil
}
@@ -227,6 +238,7 @@ func (s *UserGroupService) updateUsersInternal(ctx context.Context, id string, u
return model.UserGroup{}, err
}
s.scimService.ScheduleSync()
return group, nil
}
@@ -303,5 +315,6 @@ func (s *UserGroupService) UpdateAllowedOidcClient(ctx context.Context, id strin
return model.UserGroup{}, err
}
s.scimService.ScheduleSync()
return group, nil
}

View File

@@ -37,10 +37,11 @@ type UserService struct {
appConfigService *AppConfigService
customClaimService *CustomClaimService
appImagesService *AppImagesService
scimService *ScimService
fileStorage storage.FileStorage
}
func NewUserService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditLogService, emailService *EmailService, appConfigService *AppConfigService, customClaimService *CustomClaimService, appImagesService *AppImagesService, fileStorage storage.FileStorage) *UserService {
func NewUserService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditLogService, emailService *EmailService, appConfigService *AppConfigService, customClaimService *CustomClaimService, appImagesService *AppImagesService, scimService *ScimService, fileStorage storage.FileStorage) *UserService {
return &UserService{
db: db,
jwtService: jwtService,
@@ -49,6 +50,7 @@ func NewUserService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditL
appConfigService: appConfigService,
customClaimService: customClaimService,
appImagesService: appImagesService,
scimService: scimService,
fileStorage: fileStorage,
}
}
@@ -226,6 +228,7 @@ func (s *UserService) deleteUserInternal(ctx context.Context, tx *gorm.DB, userI
return fmt.Errorf("failed to delete user: %w", err)
}
s.scimService.ScheduleSync()
return nil
}
@@ -309,6 +312,7 @@ func (s *UserService) createUserInternal(ctx context.Context, input dto.UserCrea
}
}
s.scimService.ScheduleSync()
return user, nil
}
@@ -447,6 +451,7 @@ func (s *UserService) updateUserInternal(ctx context.Context, userID string, upd
return user, err
}
s.scimService.ScheduleSync()
return user, nil
}
@@ -663,6 +668,7 @@ func (s *UserService) UpdateUserGroups(ctx context.Context, id string, userGroup
return model.User{}, err
}
s.scimService.ScheduleSync()
return user, nil
}
@@ -753,12 +759,19 @@ func (s *UserService) ResetProfilePicture(ctx context.Context, userID string) er
}
func (s *UserService) disableUserInternal(ctx context.Context, tx *gorm.DB, userID string) error {
return tx.
err := tx.
WithContext(ctx).
Model(&model.User{}).
Where("id = ?", userID).
Update("disabled", true).
Error
if err != nil {
return err
}
s.scimService.ScheduleSync()
return nil
}
func (s *UserService) CreateSignupToken(ctx context.Context, ttl time.Duration, usageLimit int, userGroupIDs []string) (model.SignupToken, error) {

View File

@@ -17,31 +17,38 @@ func GetCallbackURLFromList(urls []string, inputCallbackURL string) (callbackURL
// time of the request for loopback IP redirect URIs, to accommodate
// clients that obtain an available ephemeral port from the operating
// system at the time of the request.
loopbackRedirect := ""
loopbackCallbackURLWithoutPort := ""
u, _ := url.Parse(inputCallbackURL)
if u != nil && u.Scheme == "http" {
host := u.Hostname()
ip := net.ParseIP(host)
if host == "localhost" || (ip != nil && ip.IsLoopback()) {
loopbackRedirect = u.String()
u.Host = host
inputCallbackURL = u.String()
loopbackCallbackURLWithoutPort = u.String()
}
}
for _, pattern := range urls {
// Try the original callback first
matches, err := matchCallbackURL(pattern, inputCallbackURL)
if err != nil {
return "", err
} else if !matches {
continue
}
if matches {
return inputCallbackURL, nil
}
if loopbackRedirect != "" {
return loopbackRedirect, nil
// If we have a loopback variant, try that too
if loopbackCallbackURLWithoutPort != "" {
matches, err = matchCallbackURL(pattern, loopbackCallbackURLWithoutPort)
if err != nil {
return "", err
}
if matches {
return inputCallbackURL, nil
}
}
return inputCallbackURL, nil
}
return "", nil

View File

@@ -392,6 +392,13 @@ func TestGetCallbackURLFromList_LoopbackSpecialHandling(t *testing.T) {
expectedURL: "http://127.0.0.1:8080/callback",
expectMatch: true,
},
{
name: "127.0.0.1 with same port - exact match",
urls: []string{"http://127.0.0.1:8080/callback"},
inputCallbackURL: "http://127.0.0.1:8080/callback",
expectedURL: "http://127.0.0.1:8080/callback",
expectMatch: true,
},
{
name: "127.0.0.1 with different port",
urls: []string{"http://127.0.0.1/callback"},

View File

@@ -8,7 +8,7 @@ import (
var AccessTokenCookieName = "__Host-access_token"
var SessionIdCookieName = "__Host-session"
var DeviceTokenCookieName = "__Host-device_token" //nolint:gosec
var DeviceTokenCookieName = "__Secure-device_token" //nolint:gosec
func init() {
if strings.HasPrefix(common.EnvConfig.AppURL, "http://") {

View File

@@ -38,7 +38,14 @@ func MigrateDatabase(sqlDb *sql.DB) error {
return migrateDatabaseFromGitHub(sqlDb, requiredVersion)
}
if err := m.Migrate(requiredVersion); err != nil && !errors.Is(err, migrate.ErrNoChange) {
err = m.Migrate(requiredVersion)
if err != nil {
if errors.Is(err, migrate.ErrNoChange) {
return nil
}
if errors.As(err, &migrate.ErrDirty{}) {
return fmt.Errorf("database migration failed. Please create an issue on GitHub and temporarely downgrade to the previous version: %w", err)
}
return fmt.Errorf("failed to apply embedded migrations: %w", err)
}
return nil
@@ -98,7 +105,7 @@ func migrateDatabaseFromGitHub(sqlDb *sql.DB, version uint) error {
return fmt.Errorf("failed to create GitHub migration instance: %w", err)
}
if err := m.Migrate(version); err != nil && !errors.Is(err, migrate.ErrNoChange) {
if err := m.Force(int(version)); err != nil && !errors.Is(err, migrate.ErrNoChange) { //nolint:gosec
return fmt.Errorf("failed to apply GitHub migrations: %w", err)
}
return nil

View File

@@ -0,0 +1 @@
UPDATE app_config_variables SET value = 'ldapAttributeAdminGroup' WHERE value = 'ldapAdminGroupName';

View File

@@ -0,0 +1,8 @@
UPDATE app_config_variables
SET key = 'ldapAdminGroupName'
WHERE key = 'ldapAttributeAdminGroup'
AND NOT EXISTS (
SELECT 1
FROM app_config_variables
WHERE key = 'ldapAdminGroupName'
);

View File

@@ -0,0 +1 @@
-- No-op on Postgres

View File

@@ -0,0 +1 @@
-- No-op on Postgres

View File

@@ -0,0 +1 @@
ALTER TABLE oidc_device_codes DROP COLUMN nonce;

View File

@@ -0,0 +1 @@
ALTER TABLE oidc_device_codes ADD COLUMN nonce VARCHAR(255);

View File

@@ -10,8 +10,8 @@ CREATE TABLE users_new
id TEXT NOT NULL PRIMARY KEY,
created_at DATETIME,
username TEXT COLLATE NOCASE NOT NULL UNIQUE,
email TEXT NOT NULL UNIQUE,
first_name TEXT,
email TEXT UNIQUE,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
display_name TEXT NOT NULL,
is_admin BOOLEAN DEFAULT FALSE NOT NULL,

View File

@@ -0,0 +1,7 @@
PRAGMA foreign_keys= OFF;
BEGIN;
UPDATE app_config_variables SET value = 'ldapAttributeAdminGroup' WHERE value = 'ldapAdminGroupName';
COMMIT;
PRAGMA foreign_keys= ON;

View File

@@ -0,0 +1,14 @@
PRAGMA foreign_keys= OFF;
BEGIN;
UPDATE app_config_variables
SET key = 'ldapAdminGroupName'
WHERE key = 'ldapAttributeAdminGroup'
AND NOT EXISTS (
SELECT 1
FROM app_config_variables
WHERE key = 'ldapAdminGroupName'
);
COMMIT;
PRAGMA foreign_keys= ON;

View File

@@ -0,0 +1,52 @@
PRAGMA foreign_keys= OFF;
BEGIN;
CREATE TABLE users_new
(
id TEXT NOT NULL PRIMARY KEY,
created_at DATETIME,
updated_at DATETIME,
username TEXT COLLATE NOCASE NOT NULL UNIQUE,
email TEXT UNIQUE,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
display_name TEXT NOT NULL,
is_admin BOOLEAN DEFAULT FALSE NOT NULL,
ldap_id TEXT UNIQUE,
locale TEXT,
disabled BOOLEAN DEFAULT FALSE NOT NULL
);
INSERT INTO users_new (
id,
created_at,
updated_at,
username,
email,
first_name,
last_name,
display_name,
is_admin,
ldap_id,
locale,
disabled
) SELECT
id,
created_at,
updated_at,
username,
email,
first_name,
last_name,
display_name,
is_admin,
ldap_id,
locale,
disabled FROM users;
DROP TABLE users;
ALTER TABLE users_new RENAME TO users;
COMMIT;
PRAGMA foreign_keys= ON;

View File

@@ -0,0 +1,5 @@
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE oidc_device_codes DROP COLUMN nonce;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,5 @@
PRAGMA foreign_keys=OFF;
BEGIN;
ALTER TABLE oidc_device_codes ADD COLUMN nonce TEXT;
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -1,6 +1,6 @@
services:
pocket-id:
image: ghcr.io/pocket-id/pocket-id:v1
image: ghcr.io/pocket-id/pocket-id:v2
restart: unless-stopped
env_file: .env
ports:

View File

@@ -498,5 +498,6 @@
"scim_sync_successful": "Synchronizace SCIM byla úspěšně dokončena.",
"save_and_sync": "Uložit a synchronizovat",
"scim_save_changes_description": "Před spuštěním synchronizace SCIM je nutné uložit změny. Chcete uložit nyní?",
"scopes": "Rozsah"
"scopes": "Rozsah",
"issuer_url": "URL vydavatele"
}

View File

@@ -498,5 +498,6 @@
"scim_sync_successful": "SCIM-synkroniseringen er gennemført med succes.",
"save_and_sync": "Gem og synkroniser",
"scim_save_changes_description": "Du skal gemme ændringerne, før du starter en SCIM-synkronisering. Vil du gemme nu?",
"scopes": "Omfang"
"scopes": "Omfang",
"issuer_url": "Udsteders URL"
}

View File

@@ -498,5 +498,6 @@
"scim_sync_successful": "Die SCIM-Synchronisierung ist erfolgreich abgeschlossen worden.",
"save_and_sync": "Speichern und synchronisieren",
"scim_save_changes_description": "Du musst die Änderungen speichern, bevor du eine SCIM-Synchronisierung startest. Willst du jetzt speichern?",
"scopes": "Kopfsuchgeräte"
"scopes": "Kopfsuchgeräte",
"issuer_url": "Aussteller-URL"
}

View File

@@ -498,5 +498,6 @@
"scim_sync_successful": "The SCIM sync has been completed successfully.",
"save_and_sync": "Save and Sync",
"scim_save_changes_description": "You have to save the changes before starting a SCIM sync. Do you want to save now?",
"scopes": "Scopes"
"scopes": "Scopes",
"issuer_url": "Issuer URL"
}

View File

@@ -498,5 +498,6 @@
"scim_sync_successful": "La sincronización SCIM se ha completado correctamente.",
"save_and_sync": "Guardar y sincronizar",
"scim_save_changes_description": "Debes guardar los cambios antes de iniciar una sincronización SCIM. ¿Deseas guardar ahora?",
"scopes": "Ámbitos"
"scopes": "Ámbitos",
"issuer_url": "URL del emisor"
}

View File

@@ -498,5 +498,6 @@
"scim_sync_successful": "SCIM-synkronointi on suoritettu onnistuneesti.",
"save_and_sync": "Tallenna ja synkronoi",
"scim_save_changes_description": "Sinun on tallennettava muutokset ennen SCIM-synkronoinnin aloittamista. Haluatko tallentaa nyt?",
"scopes": "Käyttöalueet"
"scopes": "Käyttöalueet",
"issuer_url": "Julkaisijan URL-osoite"
}

View File

@@ -498,5 +498,6 @@
"scim_sync_successful": "La synchronisation SCIM s'est bien passée.",
"save_and_sync": "Enregistrer et synchroniser",
"scim_save_changes_description": "Tu dois enregistrer les changements avant de lancer une synchronisation SCIM. Tu veux enregistrer maintenant ?",
"scopes": "Portées"
"scopes": "Portées",
"issuer_url": "URL de l'émetteur"
}

View File

@@ -498,5 +498,6 @@
"scim_sync_successful": "La sincronizzazione SCIM è andata a buon fine.",
"save_and_sync": "Salva e sincronizza",
"scim_save_changes_description": "Devi salvare le modifiche prima di iniziare una sincronizzazione SCIM. Vuoi salvare adesso?",
"scopes": "Scopi"
"scopes": "Scopi",
"issuer_url": "URL dell'emittente"
}

View File

@@ -498,5 +498,6 @@
"scim_sync_successful": "SCIM同期が正常に完了しました。",
"save_and_sync": "保存と同期",
"scim_save_changes_description": "SCIM同期を開始する前に変更を保存する必要があります。今すぐ保存しますか",
"scopes": "スコープ"
"scopes": "スコープ",
"issuer_url": "発行者URL"
}

View File

@@ -498,5 +498,6 @@
"scim_sync_successful": "SCIM 동기화가 성공적으로 완료되었습니다.",
"save_and_sync": "저장 및 동기화",
"scim_save_changes_description": "SCIM 동기화를 시작하기 전에 변경 사항을 저장해야 합니다. 지금 저장하시겠습니까?",
"scopes": "범위"
"scopes": "범위",
"issuer_url": "발행자 URL"
}

View File

@@ -498,5 +498,6 @@
"scim_sync_successful": "De SCIM-synchronisatie is goed gelukt.",
"save_and_sync": "Opslaan en synchroniseren",
"scim_save_changes_description": "Je moet de wijzigingen opslaan voordat je een SCIM-synchronisatie start. Wil je nu opslaan?",
"scopes": "Scopes"
"scopes": "Scopes",
"issuer_url": "URL van de uitgever"
}

View File

@@ -498,5 +498,6 @@
"scim_sync_successful": "Synchronizacja SCIM została pomyślnie zakończona.",
"save_and_sync": "Zapisz i zsynchronizuj",
"scim_save_changes_description": "Przed rozpoczęciem synchronizacji SCIM należy zapisać zmiany. Czy chcesz zapisać teraz?",
"scopes": "Zakresy"
"scopes": "Zakresy",
"issuer_url": "Adres URL wystawcy"
}

View File

@@ -498,5 +498,6 @@
"scim_sync_successful": "A sincronização SCIM foi concluída com sucesso.",
"save_and_sync": "Salvar e sincronizar",
"scim_save_changes_description": "Você precisa salvar as alterações antes de iniciar uma sincronização SCIM. Quer salvar agora?",
"scopes": "Âmbitos"
"scopes": "Âmbitos",
"issuer_url": "URL do emissor"
}

View File

@@ -498,5 +498,6 @@
"scim_sync_successful": "Синхронизация SCIM прошла без проблем.",
"save_and_sync": "Сохранить и синхронизировать",
"scim_save_changes_description": "Перед тем, как начать синхронизацию SCIM, нужно сохранить изменения. Хочешь сохранить сейчас?",
"scopes": "Области применения"
"scopes": "Области применения",
"issuer_url": "URL эмитента"
}

View File

@@ -498,5 +498,6 @@
"scim_sync_successful": "SCIM-synkroniseringen har slutförts.",
"save_and_sync": "Spara och synkronisera",
"scim_save_changes_description": "Du måste spara ändringarna innan du startar en SCIM-synkronisering. Vill du spara nu?",
"scopes": "Omfattning"
"scopes": "Omfattning",
"issuer_url": "Utfärdarens URL"
}

View File

@@ -498,5 +498,6 @@
"scim_sync_successful": "SCIM senkronizasyonu başarıyla tamamlandı.",
"save_and_sync": "Kaydet ve Senkronize Et",
"scim_save_changes_description": "SCIM senkronizasyonunu başlatmadan önce değişiklikleri kaydetmeniz gerekir. Şimdi kaydetmek ister misiniz?",
"scopes": "Kapsamlar"
"scopes": "Kapsamlar",
"issuer_url": "İhraççı URL"
}

View File

@@ -498,5 +498,6 @@
"scim_sync_successful": "Синхронізація SCIM успішно завершена.",
"save_and_sync": "Зберегти та синхронізувати",
"scim_save_changes_description": "Перед початком синхронізації SCIM необхідно зберегти зміни. Чи хочете ви зберегти зараз?",
"scopes": "Області застосування"
"scopes": "Області застосування",
"issuer_url": "URL емітента"
}

View File

@@ -498,5 +498,6 @@
"scim_sync_successful": "Quá trình đồng bộ hóa SCIM đã hoàn tất thành công.",
"save_and_sync": "Lưu và Đồng bộ hóa",
"scim_save_changes_description": "Bạn phải lưu các thay đổi trước khi bắt đầu đồng bộ hóa SCIM. Bạn có muốn lưu ngay bây giờ không?",
"scopes": "Phạm vi"
"scopes": "Phạm vi",
"issuer_url": "Địa chỉ URL của tổ chức phát hành"
}

View File

@@ -14,7 +14,7 @@
"profile_picture": "头像",
"profile_picture_is_managed_by_ldap_server": "头像由 LDAP 服务器管理,无法在此处更改。",
"click_profile_picture_to_upload_custom": "点击头像来从文件中上传您的自定义头像。",
"image_should_be_in_format": "图片应为 PNG、JPEG 或 WEBP 格式。",
"image_should_be_in_format": "图片格式支持 PNG、JPEG 或 WEBP。",
"items_per_page": "每页条数",
"no_items_found": "这里暂时空空如也",
"select_items": "选择项目……",
@@ -95,7 +95,7 @@
"settings": "设置",
"update_pocket_id": "更新 Pocket ID",
"powered_by": "",
"see_your_recent_account_activities": "查看您账户在配置的保留期内的活动记录。",
"see_your_recent_account_activities": "查看您账户的活动日志。日志保存期已在环境变量中配置。",
"time": "时间",
"event": "事件",
"approximate_location": "大致位置",
@@ -155,7 +155,7 @@
"are_you_sure_you_want_to_revoke_the_api_key_apikeyname": "您确定要撤销 API 密钥 \"{apiKeyName}\" 吗?这将中断使用此密钥的任何集成。",
"last_used": "上次使用时间",
"actions": "操作",
"images_updated_successfully": "图片更新成功。更新过程可能需要几分钟时间。",
"images_updated_successfully": "图片更新成功。可能需要几分钟生效。",
"general": "常规",
"configure_smtp_to_send_emails": "启用电子邮件通知,当检测到来自新设备或新位置的登录时提醒用户。",
"ldap": "LDAP",
@@ -306,16 +306,16 @@
"client_secret": "客户端密钥",
"show_more_details": "显示更多详情",
"allowed_user_groups": "允许的用户组",
"allowed_user_groups_description": "选择允许其成员登录此客户端的用户组。",
"allowed_user_groups_description": "选择允许登录此客户端的用户组。",
"allowed_user_groups_status_unrestricted_description": "未应用任何用户组限制。任何用户均可登录此客户端。",
"unrestrict": "解除限制",
"unrestrict": "取消限制",
"restrict": "限制",
"user_groups_restriction_updated_successfully": "用户组限制已成功更新",
"allowed_user_groups_updated_successfully": "已成功更新允许的用户组",
"favicon": "网站图标",
"light_mode_logo": "浅色模式 Logo",
"dark_mode_logo": "深色模式 Logo",
"email_logo": "电子邮件标",
"email_logo": "电子邮件标",
"background_image": "背景图片",
"language": "语言",
"reset_profile_picture_question": "重置头像?",
@@ -332,7 +332,7 @@
"all_clients": "所有客户端",
"all_locations": "所有地方",
"global_audit_log": "全局日志",
"see_all_recent_account_activities": "查看所有用户在设定保留期内的账户活动。",
"see_all_recent_account_activities": "查看所有用户的活动日志。日志保存期已在环境变量中配置。",
"token_sign_in": "Token 登录",
"client_authorization": "客户端授权",
"new_client_authorization": "首次客户端授权",
@@ -460,7 +460,7 @@
"display_name": "显示名称",
"configure_application_images": "配置应用图标",
"ui_config_disabled_info_title": "用户界面配置已禁用",
"ui_config_disabled_info_description": "由于应用配置设置已通过环境变量设定,用户界面配置已禁用。某些设置可能无法编辑。",
"ui_config_disabled_info_description": "用户界面配置已被环境变量禁用。某些设置可能无法编辑。",
"logo_from_url_description": "粘贴直接图片URLsvg、png、webp格式。<link href=\"https://selfh.st/icons\">可在Selfh.st图标库</link>或<link href=\"https://dashboardicons.com\">仪表盘图标库</link>中查找图标。",
"invalid_url": "无效网址",
"require_user_email": "需要电子邮件地址",
@@ -475,15 +475,15 @@
"light": "浅色",
"dark": "深色",
"system": "系统",
"signup_token_user_groups_description": "自动将这些组分配给使用此令牌注册的用户。",
"signup_token_user_groups_description": "使用此令牌注册时的自动分配给用户的用户组。",
"allowed_oidc_clients": "允许的 OIDC 客户端",
"allowed_oidc_clients_description": "选择允许此用户组成员登录的 OIDC 客户端。",
"unrestrict_oidc_client": "解除限制 {clientName}",
"confirm_unrestrict_oidc_client_description": "您确定要解除对该 OIDC 客户端的限制吗? <b>{clientName}</b>?此操作将移除此客户端的所有组分配,任何用户均可登录。",
"allowed_oidc_clients_description": "此用户组成员有权访问的OIDC 客户端。",
"unrestrict_oidc_client": "取消限制 {clientName}",
"confirm_unrestrict_oidc_client_description": "您确定要取消对该 OIDC 客户端的限制吗? <b>{clientName}</b>?此操作将移除此客户端的所有组分配,任何用户均可登录。",
"allowed_oidc_clients_updated_successfully": "允许的 OIDC 客户端已成功更新",
"yes": "是",
"no": "",
"restricted": "限",
"yes": "是",
"no": "",
"restricted": "限",
"scim_provisioning": "SCIM 配置",
"scim_provisioning_description": "SCIM 配置功能可让您自动为 OIDC 客户端创建和删除用户及组。更多详情请参<link href='https://pocket-id.org/docs/configuration/scim'>阅文档</link>。",
"scim_endpoint": "SCIM 端点",
@@ -498,5 +498,6 @@
"scim_sync_successful": "SCIM 同步已成功完成。",
"save_and_sync": "保存并同步",
"scim_save_changes_description": "在开始 SCIM 同步之前,您必须先保存更改。是否现在保存?",
"scopes": "Scopes"
"scopes": "Scopes",
"issuer_url": "发行者网址"
}

View File

@@ -498,5 +498,6 @@
"scim_sync_successful": "SCIM 同步已成功完成。",
"save_and_sync": "儲存與同步",
"scim_save_changes_description": "您必須在開始 SCIM 同步前儲存變更。現在要儲存嗎?",
"scopes": "範圍"
"scopes": "範圍",
"issuer_url": "發行者網址"
}

View File

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

View File

@@ -53,7 +53,7 @@
)}"
>
<div class="flex h-full w-full flex-col overflow-hidden">
<div class="relative flex grow flex-col items-center justify-center overflow-auto">
<div class="relative flex grow flex-col items-center justify-center overflow-auto p-1">
{@render children()}
</div>
{#if showAlternativeSignInMethodButton}

View File

@@ -24,7 +24,7 @@
bind:this={ref}
data-slot="input"
class={cn(
'selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground flex h-8 w-full min-w-0 rounded-md border bg-transparent px-3 py-2 text-sm font-medium shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-2 text-base font-medium shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
className
@@ -39,7 +39,7 @@
bind:this={ref}
data-slot="input"
class={cn(
'border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground flex h-8 w-full min-w-0 rounded-md border px-3 py-1 text-sm shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
className

View File

@@ -38,6 +38,7 @@
const backNavigation = backNavigate('/settings/admin/oidc-clients');
const setupDetails = $state({
[m.issuer_url()]: `https://${page.url.host}`,
[m.authorization_url()]: `https://${page.url.host}/authorize`,
[m.oidc_discovery_url()]: `https://${page.url.host}/.well-known/openid-configuration`,
[m.token_url()]: `https://${page.url.host}/api/oidc/token`,
@@ -207,14 +208,14 @@
<Card.Content>
<div class="flex flex-col">
<div class="mb-2 flex flex-col sm:flex-row sm:items-center">
<Field.Label class="w-50">{m.client_id()}</Field.Label>
<Field.Label class="w-52">{m.client_id()}</Field.Label>
<CopyToClipboard value={client.id}>
<span class="text-muted-foreground text-sm" data-testid="client-id"> {client.id}</span>
</CopyToClipboard>
</div>
{#if !client.isPublic}
<div class="mt-1 mb-2 flex flex-col sm:flex-row sm:items-center">
<Field.Label class="w-50">{m.client_secret()}</Field.Label>
<Field.Label class="w-52">{m.client_secret()}</Field.Label>
{#if $clientSecretStore}
<CopyToClipboard value={$clientSecretStore}>
<span class="text-muted-foreground text-sm" data-testid="client-secret">
@@ -240,8 +241,8 @@
{#if showAllDetails}
<div transition:slide>
{#each Object.entries(setupDetails) as [key, value]}
<div class="mb-5 flex flex-col sm:flex-row sm:items-center">
<Field.Label class="w-50">{key}</Field.Label>
<div class="mb-2 flex flex-col sm:flex-row sm:items-center">
<Field.Label class="w-52">{key}</Field.Label>
<CopyToClipboard {value}>
<span class="text-muted-foreground text-sm">{value}</span>
</CopyToClipboard>