Compare commits

...

7 Commits

Author SHA1 Message Date
Elias Schneider
b59e35cb59 release: 2.4.0 2026-03-07 18:36:42 +01:00
Elias Schneider
a675d075d1 fix: use URL keyboard type for callback URL inputs 2026-03-07 18:34:57 +01:00
Alessandro (Ale) Segala
2f56d16f98 fix: various fixes in background jobs (#1362)
Co-authored-by: Elias Schneider <login@eliasschneider.com>
2026-03-07 17:07:26 +00:00
Elias Schneider
f4eb8db509 tests: fix wrong seed data in database.json 2026-03-07 18:00:46 +01:00
Elias Schneider
e7bd66d1a7 tests: fix wrong seed data 2026-03-07 17:51:44 +01:00
Elias Schneider
1d06817065 chore(translations): update translations via Crowdin (#1352) 2026-03-07 17:15:01 +01:00
Ken Watanabe
34890235ba Merge commit from fork 2026-03-07 16:59:25 +01:00
49 changed files with 467 additions and 184 deletions

View File

@@ -1 +1 @@
2.3.0 2.4.0

View File

@@ -1,3 +1,35 @@
## v2.4.0
### Bug Fixes
- improve wildcard matching by using `go-urlpattern` ([#1332](https://github.com/pocket-id/pocket-id/pull/1332) by @stonith404)
- federated client credentials not working if sub ≠ client_id ([#1342](https://github.com/pocket-id/pocket-id/pull/1342) by @ItalyPaleAle)
- handle IPv6 addresses in callback URLs ([#1355](https://github.com/pocket-id/pocket-id/pull/1355) by @ItalyPaleAle)
- wildcard callback URLs blocked by browser-native URL validation ([#1359](https://github.com/pocket-id/pocket-id/pull/1359) by @Copilot)
- one-time-access-token route should get user ID from URL only ([#1358](https://github.com/pocket-id/pocket-id/pull/1358) by @ItalyPaleAle)
- various fixes in background jobs ([#1362](https://github.com/pocket-id/pocket-id/pull/1362) by @ItalyPaleAle)
- use URL keyboard type for callback URL inputs ([a675d07](https://github.com/pocket-id/pocket-id/commit/a675d075d1ab9b7ff8160f1cfc35bc0ea1f1980a) by @stonith404)
### Features
- allow first name and display name to be optional ([#1288](https://github.com/pocket-id/pocket-id/pull/1288) by @taoso)
### Other
- bump svelte from 5.53.2 to 5.53.5 in the npm_and_yarn group across 1 directory ([#1348](https://github.com/pocket-id/pocket-id/pull/1348) by @dependabot[bot])
- bump @sveltejs/kit from 2.53.0 to 2.53.3 in the npm_and_yarn group across 1 directory ([#1349](https://github.com/pocket-id/pocket-id/pull/1349) by @dependabot[bot])
- update AAGUIDs ([#1354](https://github.com/pocket-id/pocket-id/pull/1354) by @github-actions[bot])
- add Português files ([01141b8](https://github.com/pocket-id/pocket-id/commit/01141b8c0f2e96a40fd876d3206e49a694fd12c4) by @kmendell)
- add Latvian files ([e0fc4cc](https://github.com/pocket-id/pocket-id/commit/e0fc4cc01bd51e5a97e46aad78a493a668049220) by @kmendell)
- fix wrong seed data ([e7bd66d](https://github.com/pocket-id/pocket-id/commit/e7bd66d1a77c89dde542b4385ba01dc0d432e434) by @stonith404)
- fix wrong seed data in `database.json` ([f4eb8db](https://github.com/pocket-id/pocket-id/commit/f4eb8db50993edacd90e919b39a5c6d9dd4924c7) by @stonith404)
### Performance Improvements
- frontend performance optimizations ([#1344](https://github.com/pocket-id/pocket-id/pull/1344) by @ItalyPaleAle)
**Full Changelog**: https://github.com/pocket-id/pocket-id/compare/v2.3.0...v2.4.0
## v2.3.0 ## v2.3.0
### Bug Fixes ### Bug Fixes

View File

@@ -28,7 +28,7 @@ func (s *Scheduler) RegisterAnalyticsJob(ctx context.Context, appConfig *service
appConfig: appConfig, appConfig: appConfig,
httpClient: httpClient, 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, service.RegisterJobOpts{RunImmediately: true})
} }
type AnalyticsJob struct { type AnalyticsJob struct {

View File

@@ -22,7 +22,7 @@ func (s *Scheduler) RegisterApiKeyExpiryJob(ctx context.Context, apiKeyService *
} }
// Send every day at midnight // 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, service.RegisterJobOpts{})
} }
func (j *ApiKeyEmailJobs) checkAndNotifyExpiringApiKeys(ctx context.Context) error { func (j *ApiKeyEmailJobs) checkAndNotifyExpiringApiKeys(ctx context.Context) error {
@@ -42,7 +42,11 @@ func (j *ApiKeyEmailJobs) checkAndNotifyExpiringApiKeys(ctx context.Context) err
} }
err = j.apiKeyService.SendApiKeyExpiringSoonEmail(ctx, key) err = j.apiKeyService.SendApiKeyExpiringSoonEmail(ctx, key)
if err != nil { if err != nil {
slog.ErrorContext(ctx, "Failed to send expiring API key notification email", slog.String("key", key.ID), slog.Any("error", err)) slog.ErrorContext(ctx, "Failed to send expiring API key notification email",
slog.String("key", key.ID),
slog.String("user", key.User.ID),
slog.Any("error", err),
)
} }
} }
return nil return nil

View File

@@ -7,28 +7,37 @@ import (
"log/slog" "log/slog"
"time" "time"
"github.com/go-co-op/gocron/v2" backoff "github.com/cenkalti/backoff/v5"
"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"
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types" datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
"github.com/pocket-id/pocket-id/backend/internal/service"
) )
func (s *Scheduler) RegisterDbCleanupJobs(ctx context.Context, db *gorm.DB) error { func (s *Scheduler) RegisterDbCleanupJobs(ctx context.Context, db *gorm.DB) error {
jobs := &DbCleanupJobs{db: db} jobs := &DbCleanupJobs{db: db}
// Run every 24 hours (but with some jitter so they don't run at the exact same time), and now newBackOff := func() *backoff.ExponentialBackOff {
def := gocron.DurationRandomJob(24*time.Hour-2*time.Minute, 24*time.Hour+2*time.Minute) bo := backoff.NewExponentialBackOff()
bo.Multiplier = 4
bo.RandomizationFactor = 0.1
bo.InitialInterval = time.Second
bo.MaxInterval = 45 * time.Second
return bo
}
// Use exponential backoff for each DB cleanup job so transient query failures are retried automatically rather than causing an immediate job failure
return errors.Join( return errors.Join(
s.RegisterJob(ctx, "ClearWebauthnSessions", def, jobs.clearWebauthnSessions, true), s.RegisterJob(ctx, "ClearWebauthnSessions", jobDefWithJitter(24*time.Hour), jobs.clearWebauthnSessions, service.RegisterJobOpts{RunImmediately: true, BackOff: newBackOff()}),
s.RegisterJob(ctx, "ClearOneTimeAccessTokens", def, jobs.clearOneTimeAccessTokens, true), s.RegisterJob(ctx, "ClearOneTimeAccessTokens", jobDefWithJitter(24*time.Hour), jobs.clearOneTimeAccessTokens, service.RegisterJobOpts{RunImmediately: true, BackOff: newBackOff()}),
s.RegisterJob(ctx, "ClearSignupTokens", def, jobs.clearSignupTokens, true), s.RegisterJob(ctx, "ClearSignupTokens", jobDefWithJitter(24*time.Hour), jobs.clearSignupTokens, service.RegisterJobOpts{RunImmediately: true, BackOff: newBackOff()}),
s.RegisterJob(ctx, "ClearEmailVerificationTokens", def, jobs.clearEmailVerificationTokens, true), s.RegisterJob(ctx, "ClearEmailVerificationTokens", jobDefWithJitter(24*time.Hour), jobs.clearEmailVerificationTokens, service.RegisterJobOpts{RunImmediately: true, BackOff: newBackOff()}),
s.RegisterJob(ctx, "ClearOidcAuthorizationCodes", def, jobs.clearOidcAuthorizationCodes, true), s.RegisterJob(ctx, "ClearOidcAuthorizationCodes", jobDefWithJitter(24*time.Hour), jobs.clearOidcAuthorizationCodes, service.RegisterJobOpts{RunImmediately: true, BackOff: newBackOff()}),
s.RegisterJob(ctx, "ClearOidcRefreshTokens", def, jobs.clearOidcRefreshTokens, true), s.RegisterJob(ctx, "ClearOidcRefreshTokens", jobDefWithJitter(24*time.Hour), jobs.clearOidcRefreshTokens, service.RegisterJobOpts{RunImmediately: true, BackOff: newBackOff()}),
s.RegisterJob(ctx, "ClearReauthenticationTokens", def, jobs.clearReauthenticationTokens, true), s.RegisterJob(ctx, "ClearReauthenticationTokens", jobDefWithJitter(24*time.Hour), jobs.clearReauthenticationTokens, service.RegisterJobOpts{RunImmediately: true, BackOff: newBackOff()}),
s.RegisterJob(ctx, "ClearAuditLogs", def, jobs.clearAuditLogs, true), s.RegisterJob(ctx, "ClearAuditLogs", jobDefWithJitter(24*time.Hour), jobs.clearAuditLogs, service.RegisterJobOpts{RunImmediately: true, BackOff: newBackOff()}),
) )
} }

View File

@@ -13,20 +13,26 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
"github.com/pocket-id/pocket-id/backend/internal/model" "github.com/pocket-id/pocket-id/backend/internal/model"
"github.com/pocket-id/pocket-id/backend/internal/service"
"github.com/pocket-id/pocket-id/backend/internal/storage" "github.com/pocket-id/pocket-id/backend/internal/storage"
) )
func (s *Scheduler) RegisterFileCleanupJobs(ctx context.Context, db *gorm.DB, fileStorage storage.FileStorage) error { func (s *Scheduler) RegisterFileCleanupJobs(ctx context.Context, db *gorm.DB, fileStorage storage.FileStorage) error {
jobs := &FileCleanupJobs{db: db, fileStorage: fileStorage} jobs := &FileCleanupJobs{db: db, fileStorage: fileStorage}
err := s.RegisterJob(ctx, "ClearUnusedDefaultProfilePictures", gocron.DurationJob(24*time.Hour), jobs.clearUnusedDefaultProfilePictures, false) var errs []error
errs = append(errs,
s.RegisterJob(ctx, "ClearUnusedDefaultProfilePictures", gocron.DurationJob(24*time.Hour), jobs.clearUnusedDefaultProfilePictures, service.RegisterJobOpts{}),
)
// Only necessary for file system storage // Only necessary for file system storage
if fileStorage.Type() == storage.TypeFileSystem { if fileStorage.Type() == storage.TypeFileSystem {
err = errors.Join(err, s.RegisterJob(ctx, "ClearOrphanedTempFiles", gocron.DurationJob(12*time.Hour), jobs.clearOrphanedTempFiles, true)) errs = append(errs,
s.RegisterJob(ctx, "ClearOrphanedTempFiles", gocron.DurationJob(12*time.Hour), jobs.clearOrphanedTempFiles, service.RegisterJobOpts{RunImmediately: true}),
)
} }
return err return errors.Join(errs...)
} }
type FileCleanupJobs struct { type FileCleanupJobs struct {
@@ -68,7 +74,8 @@ func (j *FileCleanupJobs) clearUnusedDefaultProfilePictures(ctx context.Context)
// If these initials aren't used by any user, delete the file // If these initials aren't used by any user, delete the file
if _, ok := initialsInUse[initials]; !ok { if _, ok := initialsInUse[initials]; !ok {
filePath := path.Join(defaultPicturesDir, filename) filePath := path.Join(defaultPicturesDir, filename)
if err := j.fileStorage.Delete(ctx, filePath); err != nil { err = j.fileStorage.Delete(ctx, filePath)
if err != nil {
slog.ErrorContext(ctx, "Failed to delete unused default profile picture", slog.String("path", filePath), slog.Any("error", err)) slog.ErrorContext(ctx, "Failed to delete unused default profile picture", slog.String("path", filePath), slog.Any("error", err))
} else { } else {
filesDeleted++ filesDeleted++
@@ -95,8 +102,9 @@ func (j *FileCleanupJobs) clearOrphanedTempFiles(ctx context.Context) error {
return nil return nil
} }
if err := j.fileStorage.Delete(ctx, p.Path); err != nil { rErr := j.fileStorage.Delete(ctx, p.Path)
slog.ErrorContext(ctx, "Failed to delete temp file", slog.String("path", p.Path), slog.Any("error", err)) if rErr != nil {
slog.ErrorContext(ctx, "Failed to delete temp file", slog.String("path", p.Path), slog.Any("error", rErr))
return nil return nil
} }
deleted++ deleted++

View File

@@ -23,7 +23,7 @@ func (s *Scheduler) RegisterGeoLiteUpdateJobs(ctx context.Context, geoLiteServic
jobs := &GeoLiteUpdateJobs{geoLiteService: geoLiteService} jobs := &GeoLiteUpdateJobs{geoLiteService: geoLiteService}
// Run every 24 hours (and right away) // 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, service.RegisterJobOpts{RunImmediately: true})
} }
func (j *GeoLiteUpdateJobs) updateGoeLiteDB(ctx context.Context) error { func (j *GeoLiteUpdateJobs) updateGoeLiteDB(ctx context.Context) error {

View File

@@ -4,8 +4,6 @@ import (
"context" "context"
"time" "time"
"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"
) )
@@ -17,8 +15,8 @@ type LdapJobs struct {
func (s *Scheduler) RegisterLdapJobs(ctx context.Context, ldapService *service.LdapService, appConfigService *service.AppConfigService) error { 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}
// Register the job to run every hour // Register the job to run every hour (with some jitter)
return s.RegisterJob(ctx, "SyncLdap", gocron.DurationJob(time.Hour), jobs.syncLdap, true) return s.RegisterJob(ctx, "SyncLdap", jobDefWithJitter(time.Hour), jobs.syncLdap, service.RegisterJobOpts{RunImmediately: true})
} }
func (j *LdapJobs) syncLdap(ctx context.Context) error { func (j *LdapJobs) syncLdap(ctx context.Context) error {

View File

@@ -5,9 +5,13 @@ import (
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
"time"
backoff "github.com/cenkalti/backoff/v5"
"github.com/go-co-op/gocron/v2" "github.com/go-co-op/gocron/v2"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/pocket-id/pocket-id/backend/internal/service"
) )
type Scheduler struct { type Scheduler struct {
@@ -33,18 +37,14 @@ func (s *Scheduler) RemoveJob(name string) error {
if job.Name() == name { if job.Name() == name {
err := s.scheduler.RemoveJob(job.ID()) err := s.scheduler.RemoveJob(job.ID())
if err != nil { if err != nil {
errs = append(errs, fmt.Errorf("failed to unqueue job %q with ID %q: %w", name, job.ID().String(), err)) errs = append(errs, fmt.Errorf("failed to dequeue job %q with ID %q: %w", name, job.ID().String(), err))
} }
} }
} }
if len(errs) > 0 {
return errors.Join(errs...) return errors.Join(errs...)
} }
return nil
}
// Run the scheduler. // Run the scheduler.
// This function blocks until the context is canceled. // This function blocks until the context is canceled.
func (s *Scheduler) Run(ctx context.Context) error { func (s *Scheduler) Run(ctx context.Context) error {
@@ -64,7 +64,29 @@ func (s *Scheduler) Run(ctx context.Context) error {
return nil return nil
} }
func (s *Scheduler) RegisterJob(ctx context.Context, name string, def gocron.JobDefinition, job func(ctx context.Context) error, runImmediately bool, extraOptions ...gocron.JobOption) error { func (s *Scheduler) RegisterJob(ctx context.Context, name string, def gocron.JobDefinition, jobFn func(ctx context.Context) error, opts service.RegisterJobOpts) error {
// If a BackOff strategy is provided, wrap the job with retry logic
if opts.BackOff != nil {
origJob := jobFn
jobFn = func(ctx context.Context) error {
_, err := backoff.Retry(
ctx,
func() (struct{}, error) {
return struct{}{}, origJob(ctx)
},
backoff.WithBackOff(opts.BackOff),
backoff.WithNotify(func(err error, d time.Duration) {
slog.WarnContext(ctx, "Job failed, retrying",
slog.String("name", name),
slog.Any("error", err),
slog.Duration("retryIn", d),
)
}),
)
return err
}
}
jobOptions := []gocron.JobOption{ jobOptions := []gocron.JobOption{
gocron.WithContext(ctx), gocron.WithContext(ctx),
gocron.WithName(name), gocron.WithName(name),
@@ -91,13 +113,13 @@ func (s *Scheduler) RegisterJob(ctx context.Context, name string, def gocron.Job
), ),
} }
if runImmediately { if opts.RunImmediately {
jobOptions = append(jobOptions, gocron.JobOption(gocron.WithStartImmediately())) jobOptions = append(jobOptions, gocron.JobOption(gocron.WithStartImmediately()))
} }
jobOptions = append(jobOptions, extraOptions...) jobOptions = append(jobOptions, opts.ExtraOptions...)
_, err := s.scheduler.NewJob(def, gocron.NewTask(job), jobOptions...) _, err := s.scheduler.NewJob(def, gocron.NewTask(jobFn), jobOptions...)
if err != nil { if err != nil {
return fmt.Errorf("failed to register job %q: %w", name, err) return fmt.Errorf("failed to register job %q: %w", name, err)
@@ -105,3 +127,9 @@ func (s *Scheduler) RegisterJob(ctx context.Context, name string, def gocron.Job
return nil return nil
} }
func jobDefWithJitter(interval time.Duration) gocron.JobDefinition {
const jitter = 5 * time.Minute
return gocron.DurationRandomJob(interval-jitter, interval+jitter)
}

View File

@@ -16,8 +16,8 @@ type ScimJobs struct {
func (s *Scheduler) RegisterScimJobs(ctx context.Context, scimService *service.ScimService) error { func (s *Scheduler) RegisterScimJobs(ctx context.Context, scimService *service.ScimService) error {
jobs := &ScimJobs{scimService: scimService} jobs := &ScimJobs{scimService: scimService}
// Register the job to run every hour // Register the job to run every hour (with some jitter)
return s.RegisterJob(ctx, "SyncScim", gocron.DurationJob(time.Hour), jobs.SyncScim, true) return s.RegisterJob(ctx, "SyncScim", gocron.DurationJob(time.Hour), jobs.SyncScim, service.RegisterJobOpts{RunImmediately: true})
} }
func (j *ScimJobs) SyncScim(ctx context.Context) error { func (j *ScimJobs) SyncScim(ctx context.Context) error {

View File

@@ -3,6 +3,7 @@ package service
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"time" "time"
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types" datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
@@ -205,36 +206,33 @@ func (s *ApiKeyService) ListExpiringApiKeys(ctx context.Context, daysAhead int)
} }
func (s *ApiKeyService) SendApiKeyExpiringSoonEmail(ctx context.Context, apiKey model.ApiKey) error { func (s *ApiKeyService) SendApiKeyExpiringSoonEmail(ctx context.Context, apiKey model.ApiKey) error {
user := apiKey.User if apiKey.User.Email == nil {
if user.ID == "" {
if err := s.db.WithContext(ctx).First(&user, "id = ?", apiKey.UserID).Error; err != nil {
return err
}
}
if user.Email == nil {
return &common.UserEmailNotSetError{} return &common.UserEmailNotSetError{}
} }
err := SendEmail(ctx, s.emailService, email.Address{ err := SendEmail(ctx, s.emailService, email.Address{
Name: user.FullName(), Name: apiKey.User.FullName(),
Email: *user.Email, Email: *apiKey.User.Email,
}, ApiKeyExpiringSoonTemplate, &ApiKeyExpiringSoonTemplateData{ }, ApiKeyExpiringSoonTemplate, &ApiKeyExpiringSoonTemplateData{
ApiKeyName: apiKey.Name, ApiKeyName: apiKey.Name,
ExpiresAt: apiKey.ExpiresAt.ToTime(), ExpiresAt: apiKey.ExpiresAt.ToTime(),
Name: user.FirstName, Name: apiKey.User.FirstName,
}) })
if err != nil { if err != nil {
return err return fmt.Errorf("error sending notification email: %w", err)
} }
// Mark the API key as having had an expiration email sent // Mark the API key as having had an expiration email sent
return s.db.WithContext(ctx). err = s.db.WithContext(ctx).
Model(&model.ApiKey{}). Model(&model.ApiKey{}).
Where("id = ?", apiKey.ID). Where("id = ?", apiKey.ID).
Update("expiration_email_sent", true). Update("expiration_email_sent", true).
Error Error
if err != nil {
return fmt.Errorf("error recording expiration sent email in database: %w", err)
}
return nil
} }
func (s *ApiKeyService) initStaticApiKeyUser(ctx context.Context) (user model.User, err error) { func (s *ApiKeyService) initStaticApiKeyUser(ctx context.Context) (user model.User, err error) {

View File

@@ -73,7 +73,10 @@ func (lv *lockValue) Unmarshal(raw string) error {
// Acquire obtains the lock. When force is true, the lock is stolen from any existing owner. // Acquire obtains the lock. When force is true, the lock is stolen from any existing owner.
// If the lock is forcefully acquired, it blocks until the previous lock has expired. // If the lock is forcefully acquired, it blocks until the previous lock has expired.
func (s *AppLockService) Acquire(ctx context.Context, force bool) (waitUntil time.Time, err error) { func (s *AppLockService) Acquire(ctx context.Context, force bool) (waitUntil time.Time, err error) {
tx := s.db.Begin() tx := s.db.WithContext(ctx).Begin()
if tx.Error != nil {
return time.Time{}, fmt.Errorf("begin lock transaction: %w", tx.Error)
}
defer func() { defer func() {
tx.Rollback() tx.Rollback()
}() }()
@@ -174,7 +177,8 @@ func (s *AppLockService) RunRenewal(ctx context.Context) error {
case <-ctx.Done(): case <-ctx.Done():
return nil return nil
case <-ticker.C: case <-ticker.C:
if err := s.renew(ctx); err != nil { err := s.renew(ctx)
if err != nil {
return fmt.Errorf("renew lock: %w", err) return fmt.Errorf("renew lock: %w", err)
} }
} }
@@ -183,8 +187,10 @@ func (s *AppLockService) RunRenewal(ctx context.Context) error {
// Release releases the lock if it is held by this process. // Release releases the lock if it is held by this process.
func (s *AppLockService) Release(ctx context.Context) error { func (s *AppLockService) Release(ctx context.Context) error {
opCtx, cancel := context.WithTimeout(ctx, 3*time.Second) db, err := s.db.DB()
defer cancel() if err != nil {
return fmt.Errorf("failed to get DB connection: %w", err)
}
var query string var query string
switch s.db.Name() { switch s.db.Name() {
@@ -204,12 +210,20 @@ func (s *AppLockService) Release(ctx context.Context) error {
return fmt.Errorf("unsupported database dialect: %s", s.db.Name()) return fmt.Errorf("unsupported database dialect: %s", s.db.Name())
} }
res := s.db.WithContext(opCtx).Exec(query, lockKey, s.lockID) opCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
if res.Error != nil { defer cancel()
return fmt.Errorf("release lock failed: %w", res.Error)
res, err := db.ExecContext(opCtx, query, lockKey, s.lockID)
if err != nil {
return fmt.Errorf("release lock failed: %w", err)
} }
if res.RowsAffected == 0 { count, err := res.RowsAffected()
if err != nil {
return fmt.Errorf("failed to count affected rows: %w", err)
}
if count == 0 {
slog.Warn("Application lock not held by this process, cannot release", slog.Warn("Application lock not held by this process, cannot release",
slog.Int64("process_id", s.processID), slog.Int64("process_id", s.processID),
slog.String("host_id", s.hostID), slog.String("host_id", s.hostID),
@@ -225,6 +239,11 @@ func (s *AppLockService) Release(ctx context.Context) error {
// renew tries to renew the lock, retrying up to renewRetries times (sleeping 1s between attempts). // renew tries to renew the lock, retrying up to renewRetries times (sleeping 1s between attempts).
func (s *AppLockService) renew(ctx context.Context) error { func (s *AppLockService) renew(ctx context.Context) error {
db, err := s.db.DB()
if err != nil {
return fmt.Errorf("failed to get DB connection: %w", err)
}
var lastErr error var lastErr error
for attempt := 1; attempt <= renewRetries; attempt++ { for attempt := 1; attempt <= renewRetries; attempt++ {
now := time.Now() now := time.Now()
@@ -265,23 +284,37 @@ func (s *AppLockService) renew(ctx context.Context) error {
} }
opCtx, cancel := context.WithTimeout(ctx, 3*time.Second) opCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
res := s.db.WithContext(opCtx).Exec(query, raw, lockKey, s.lockID, nowUnix) res, err := db.ExecContext(opCtx, query, raw, lockKey, s.lockID, nowUnix)
cancel() cancel()
switch { // Query succeeded, but may have updated 0 rows
case res.Error != nil: if err == nil {
lastErr = fmt.Errorf("lock renewal failed: %w", res.Error) count, err := res.RowsAffected()
case res.RowsAffected == 0: if err != nil {
// Must be after checking res.Error return fmt.Errorf("failed to count affected rows: %w", err)
}
// If no rows were updated, we lost the lock
if count == 0 {
return ErrLockLost return ErrLockLost
default: }
// All good
slog.Debug("Renewed application lock", slog.Debug("Renewed application lock",
slog.Int64("process_id", s.processID), slog.Int64("process_id", s.processID),
slog.String("host_id", s.hostID), slog.String("host_id", s.hostID),
slog.Duration("duration", time.Since(now)),
) )
return nil return nil
} }
// If we're here, we have an error that can be retried
slog.Debug("Application lock renewal attempt failed",
slog.Any("error", err),
slog.Duration("duration", time.Since(now)),
)
lastErr = fmt.Errorf("lock renewal failed: %w", err)
// Wait before next attempt or cancel if context is done // Wait before next attempt or cancel if context is done
if attempt < renewRetries { if attempt < renewRetries {
select { select {

View File

@@ -49,6 +49,23 @@ func readLockValue(t *testing.T, db *gorm.DB) lockValue {
return value return value
} }
func lockDatabaseForWrite(t *testing.T, db *gorm.DB) *gorm.DB {
t.Helper()
tx := db.Begin()
require.NoError(t, tx.Error)
// Keep a write transaction open to block other queries.
err := tx.Exec(
`INSERT INTO kv (key, value) VALUES (?, ?) ON CONFLICT(key) DO NOTHING`,
lockKey,
`{"expires_at":0}`,
).Error
require.NoError(t, err)
return tx
}
func TestAppLockServiceAcquire(t *testing.T) { func TestAppLockServiceAcquire(t *testing.T) {
t.Run("creates new lock when none exists", func(t *testing.T) { t.Run("creates new lock when none exists", func(t *testing.T) {
db := testutils.NewDatabaseForTest(t) db := testutils.NewDatabaseForTest(t)
@@ -99,6 +116,66 @@ func TestAppLockServiceAcquire(t *testing.T) {
require.Equal(t, service.hostID, stored.HostID) require.Equal(t, service.hostID, stored.HostID)
require.Greater(t, stored.ExpiresAt, time.Now().Unix()) require.Greater(t, stored.ExpiresAt, time.Now().Unix())
}) })
t.Run("force acquisition returns wait duration when stealing active lock", func(t *testing.T) {
db := testutils.NewDatabaseForTest(t)
service := newTestAppLockService(t, db)
existing := lockValue{
ProcessID: 99,
HostID: "other-host",
LockID: "other-lock-id",
ExpiresAt: time.Now().Add(ttl).Unix(),
}
insertLock(t, db, existing)
waitUntil, err := service.Acquire(context.Background(), true)
require.NoError(t, err)
require.WithinDuration(t, time.Unix(existing.ExpiresAt, 0), waitUntil, time.Second)
})
t.Run("force acquisition does not wait when lock id is unchanged", func(t *testing.T) {
db := testutils.NewDatabaseForTest(t)
service := newTestAppLockService(t, db)
insertLock(t, db, lockValue{
ProcessID: 99,
HostID: "other-host",
LockID: service.lockID,
ExpiresAt: time.Now().Add(ttl).Unix(),
})
waitUntil, err := service.Acquire(context.Background(), true)
require.NoError(t, err)
require.True(t, waitUntil.IsZero())
})
t.Run("returns error when existing lock value is invalid JSON", func(t *testing.T) {
db := testutils.NewDatabaseForTest(t)
service := newTestAppLockService(t, db)
raw := "this-is-not-json"
err := db.Create(&model.KV{Key: lockKey, Value: &raw}).Error
require.NoError(t, err)
_, err = service.Acquire(context.Background(), false)
require.ErrorContains(t, err, "decode existing lock value")
})
t.Run("returns context deadline exceeded when database is locked", func(t *testing.T) {
db := testutils.NewDatabaseForTest(t)
service := newTestAppLockService(t, db)
tx := lockDatabaseForWrite(t, db)
defer tx.Rollback()
ctx, cancel := context.WithTimeout(context.Background(), 150*time.Millisecond)
defer cancel()
_, err := service.Acquire(ctx, false)
require.ErrorIs(t, err, context.DeadlineExceeded)
require.ErrorContains(t, err, "begin lock transaction")
})
} }
func TestAppLockServiceRelease(t *testing.T) { func TestAppLockServiceRelease(t *testing.T) {
@@ -134,6 +211,24 @@ func TestAppLockServiceRelease(t *testing.T) {
stored := readLockValue(t, db) stored := readLockValue(t, db)
require.Equal(t, existing, stored) require.Equal(t, existing, stored)
}) })
t.Run("returns context deadline exceeded when database is locked", func(t *testing.T) {
db := testutils.NewDatabaseForTest(t)
service := newTestAppLockService(t, db)
_, err := service.Acquire(context.Background(), false)
require.NoError(t, err)
tx := lockDatabaseForWrite(t, db)
defer tx.Rollback()
ctx, cancel := context.WithTimeout(context.Background(), 150*time.Millisecond)
defer cancel()
err = service.Release(ctx)
require.ErrorIs(t, err, context.DeadlineExceeded)
require.ErrorContains(t, err, "release lock failed")
})
} }
func TestAppLockServiceRenew(t *testing.T) { func TestAppLockServiceRenew(t *testing.T) {
@@ -186,4 +281,21 @@ func TestAppLockServiceRenew(t *testing.T) {
err = service.renew(context.Background()) err = service.renew(context.Background())
require.ErrorIs(t, err, ErrLockLost) require.ErrorIs(t, err, ErrLockLost)
}) })
t.Run("returns context deadline exceeded when database is locked", func(t *testing.T) {
db := testutils.NewDatabaseForTest(t)
service := newTestAppLockService(t, db)
_, err := service.Acquire(context.Background(), false)
require.NoError(t, err)
tx := lockDatabaseForWrite(t, db)
defer tx.Rollback()
ctx, cancel := context.WithTimeout(context.Background(), 150*time.Millisecond)
defer cancel()
err = service.renew(ctx)
require.ErrorIs(t, err, context.DeadlineExceeded)
})
} }

View File

@@ -258,7 +258,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
Nonce: "nonce", Nonce: "nonce",
ExpiresAt: datatype.DateTime(time.Now().Add(1 * time.Hour)), ExpiresAt: datatype.DateTime(time.Now().Add(1 * time.Hour)),
UserID: users[1].ID, UserID: users[1].ID,
ClientID: oidcClients[2].ID, ClientID: oidcClients[3].ID,
}, },
} }
for _, authCode := range authCodes { for _, authCode := range authCodes {

View File

@@ -150,7 +150,8 @@ func SendEmail[V any](ctx context.Context, srv *EmailService, toEmail email.Addr
} }
// Send the email // Send the email
if err := srv.sendEmailContent(client, toEmail, c); err != nil { err = srv.sendEmailContent(client, toEmail, c)
if err != nil {
return fmt.Errorf("send email content: %w", err) return fmt.Errorf("send email content: %w", err)
} }

View File

@@ -404,7 +404,7 @@ func (s *OidcService) createTokenFromAuthorizationCode(ctx context.Context, inpu
} }
} }
if authorizationCodeMetaData.ClientID != input.ClientID && authorizationCodeMetaData.ExpiresAt.ToTime().Before(time.Now()) { if authorizationCodeMetaData.ClientID != input.ClientID || authorizationCodeMetaData.ExpiresAt.ToTime().Before(time.Now()) {
return CreatedTokens{}, &common.OidcInvalidAuthorizationCodeError{} return CreatedTokens{}, &common.OidcInvalidAuthorizationCodeError{}
} }

View File

@@ -0,0 +1,25 @@
package service
import (
"context"
backoff "github.com/cenkalti/backoff/v5"
"github.com/go-co-op/gocron/v2"
)
// RegisterJobOpts holds optional configuration for registering a scheduled job.
type RegisterJobOpts struct {
// RunImmediately runs the job immediately after registration.
RunImmediately bool
// ExtraOptions are additional gocron job options.
ExtraOptions []gocron.JobOption
// BackOff is an optional backoff strategy. If non-nil, the job will be wrapped
// with automatic retry logic using the provided backoff on transient failures.
BackOff backoff.BackOff
}
// Scheduler is an interface for registering and managing background jobs.
type Scheduler interface {
RegisterJob(ctx context.Context, name string, def gocron.JobDefinition, job func(ctx context.Context) error, opts RegisterJobOpts) error
RemoveJob(name string) error
}

View File

@@ -34,11 +34,6 @@ const scimErrorBodyLimit = 4096
type scimSyncAction int 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 ( const (
scimActionNone scimSyncAction = iota scimActionNone scimSyncAction = iota
scimActionCreated scimActionCreated
@@ -149,7 +144,7 @@ func (s *ScimService) ScheduleSync() {
err := s.scheduler.RegisterJob( err := s.scheduler.RegisterJob(
context.Background(), jobName, context.Background(), jobName,
gocron.OneTimeJob(gocron.OneTimeJobStartDateTime(start)), s.SyncAll, false) gocron.OneTimeJob(gocron.OneTimeJobStartDateTime(start)), s.SyncAll, RegisterJobOpts{})
if err != nil { if err != nil {
slog.Error("Failed to schedule SCIM sync", slog.Any("error", err)) slog.Error("Failed to schedule SCIM sync", slog.Any("error", err))
@@ -168,7 +163,8 @@ func (s *ScimService) SyncAll(ctx context.Context) error {
errs = append(errs, ctx.Err()) errs = append(errs, ctx.Err())
break break
} }
if err := s.SyncServiceProvider(ctx, provider.ID); err != nil { err = s.SyncServiceProvider(ctx, provider.ID)
if err != nil {
errs = append(errs, fmt.Errorf("failed to sync SCIM provider %s: %w", provider.ID, err)) errs = append(errs, fmt.Errorf("failed to sync SCIM provider %s: %w", provider.ID, err))
} }
} }
@@ -210,26 +206,20 @@ func (s *ScimService) SyncServiceProvider(ctx context.Context, serviceProviderID
} }
var errs []error var errs []error
var userStats scimSyncStats
var groupStats scimSyncStats
// Sync users first, so that groups can reference them // Sync users first, so that groups can reference them
if stats, err := s.syncUsers(ctx, provider, users, &userResources); err != nil { userStats, err := s.syncUsers(ctx, provider, users, &userResources)
errs = append(errs, err) if err != nil {
userStats = stats errs = append(errs, err)
} else { }
userStats = stats
} groupStats, err := s.syncGroups(ctx, provider, groups, groupResources.Resources, userResources.Resources)
stats, err := s.syncGroups(ctx, provider, groups, groupResources.Resources, userResources.Resources)
if err != nil { if err != nil {
errs = append(errs, err) errs = append(errs, err)
groupStats = stats
} else {
groupStats = stats
} }
if len(errs) > 0 { if len(errs) > 0 {
err = errors.Join(errs...)
slog.WarnContext(ctx, "SCIM sync completed with errors", slog.WarnContext(ctx, "SCIM sync completed with errors",
slog.String("provider_id", provider.ID), slog.String("provider_id", provider.ID),
slog.Int("error_count", len(errs)), slog.Int("error_count", len(errs)),
@@ -240,12 +230,14 @@ func (s *ScimService) SyncServiceProvider(ctx context.Context, serviceProviderID
slog.Int("groups_updated", groupStats.Updated), slog.Int("groups_updated", groupStats.Updated),
slog.Int("groups_deleted", groupStats.Deleted), slog.Int("groups_deleted", groupStats.Deleted),
slog.Duration("duration", time.Since(start)), slog.Duration("duration", time.Since(start)),
slog.Any("error", err),
) )
return errors.Join(errs...) return err
} }
provider.LastSyncedAt = new(datatype.DateTime(time.Now())) provider.LastSyncedAt = new(datatype.DateTime(time.Now()))
if err := s.db.WithContext(ctx).Save(&provider).Error; err != nil { err = s.db.WithContext(ctx).Save(&provider).Error
if err != nil {
return err return err
} }
@@ -273,7 +265,7 @@ func (s *ScimService) syncUsers(
// Update or create users // Update or create users
for _, u := range users { for _, u := range users {
existing := getResourceByExternalID[dto.ScimUser](u.ID, resourceList.Resources) existing := getResourceByExternalID(u.ID, resourceList.Resources)
action, created, err := s.syncUser(ctx, provider, u, existing) action, created, err := s.syncUser(ctx, provider, u, existing)
if created != nil && existing == nil { if created != nil && existing == nil {
@@ -434,7 +426,7 @@ func (s *ScimService) syncGroup(
// Prepare group members // Prepare group members
members := make([]dto.ScimGroupMember, len(group.Users)) members := make([]dto.ScimGroupMember, len(group.Users))
for i, user := range group.Users { for i, user := range group.Users {
userResource := getResourceByExternalID[dto.ScimUser](user.ID, userResources) userResource := getResourceByExternalID(user.ID, userResources)
if userResource == nil { if userResource == nil {
// Groups depend on user IDs already being provisioned // Groups depend on user IDs already being provisioned
return scimActionNone, fmt.Errorf("cannot sync group %s: user %s is not provisioned in SCIM provider", group.ID, user.ID) return scimActionNone, fmt.Errorf("cannot sync group %s: user %s is not provisioned in SCIM provider", group.ID, user.ID)

View File

@@ -1 +1 @@
{{define "root"}}<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html dir="ltr" lang="en"><head><link rel="preload" as="image" href="{{.LogoURL}}"/><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/></head><body style="background-color:#FBFBFB"><!--$--><!--html--><!--head--><!--body--><table border="0" width="100%" cellPadding="0" cellSpacing="0" role="presentation" align="center"><tbody><tr><td style="padding:50px;background-color:#FBFBFB;font-family:Arial, sans-serif"><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="max-width:37.5em;width:500px;margin:0 auto"><tbody><tr style="width:100%"><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><table align="left" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="margin-bottom:16px"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column" style="width:50px"><img alt="{{.AppName}}" height="32" src="{{.LogoURL}}" style="display:block;outline:none;border:none;text-decoration:none;width:32px;height:32px;vertical-align:middle" width="32"/></td><td data-id="__react-email-column"><p style="font-size:23px;line-height:24px;font-weight:bold;margin:0;padding:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{.AppName}}</p></td></tr></tbody></table></td></tr></tbody></table><div style="background-color:white;padding:24px;border-radius:10px;box-shadow:0 1px 4px 0px rgba(0, 0, 0, 0.1)"><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column"><h1 style="font-size:20px;font-weight:bold;margin:0">API Key Expiring Soon</h1></td><td align="right" data-id="__react-email-column"><p style="font-size:12px;line-height:24px;background-color:#ffd966;color:#7f6000;padding:1px 12px;border-radius:50px;display:inline-block;margin:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">Warning</p></td></tr></tbody></table><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px">Hello <!-- -->{{.Data.Name}}<!-- -->, <br/>This is a reminder that your API key <strong>{{.Data.APIKeyName}}</strong> <!-- -->will expire on <strong>{{.Data.ExpiresAt.Format "2006-01-02 15:04:05 MST"}}</strong>.</p><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px">Please generate a new API key if you need continued access.</p></div></td></tr></tbody></table></td></tr></tbody></table><!--/$--></body></html>{{end}} {{define "root"}}<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html dir="ltr" lang="en"><head><link rel="preload" as="image" href="{{.LogoURL}}"/><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><meta name="x-apple-disable-message-reformatting"/></head><body style="background-color:#FBFBFB"><!--$--><!--html--><!--head--><!--body--><table border="0" width="100%" cellPadding="0" cellSpacing="0" role="presentation" align="center"><tbody><tr><td style="padding:50px;background-color:#FBFBFB;font-family:Arial, sans-serif"><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="max-width:37.5em;width:500px;margin:0 auto"><tbody><tr style="width:100%"><td><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody><tr><td><table align="left" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="margin-bottom:16px"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column" style="width:50px"><img alt="{{.AppName}}" height="32" src="{{.LogoURL}}" style="display:block;outline:none;border:none;text-decoration:none;width:32px;height:32px;vertical-align:middle" width="32"/></td><td data-id="__react-email-column"><p style="font-size:23px;line-height:24px;font-weight:bold;margin:0;padding:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">{{.AppName}}</p></td></tr></tbody></table></td></tr></tbody></table><div style="background-color:white;padding:24px;border-radius:10px;box-shadow:0 1px 4px 0px rgba(0, 0, 0, 0.1)"><table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation"><tbody style="width:100%"><tr style="width:100%"><td data-id="__react-email-column"><h1 style="font-size:20px;font-weight:bold;margin:0">API Key Expiring Soon</h1></td><td align="right" data-id="__react-email-column"><p style="font-size:12px;line-height:24px;background-color:#ffd966;color:#7f6000;padding:1px 12px;border-radius:50px;display:inline-block;margin:0;margin-top:0;margin-bottom:0;margin-left:0;margin-right:0">Warning</p></td></tr></tbody></table><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px">Hello <!-- -->{{.Data.Name}}<!-- -->, <br/>This is a reminder that your API key <strong>{{.Data.ApiKeyName}}</strong> <!-- -->will expire on <strong>{{.Data.ExpiresAt.Format "2006-01-02 15:04:05 MST"}}</strong>.</p><p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px">Please generate a new API key if you need continued access.</p></div></td></tr></tbody></table></td></tr></tbody></table><!--/$--></body></html>{{end}}

View File

@@ -6,6 +6,6 @@ API KEY EXPIRING SOON
Warning Warning
Hello {{.Data.Name}}, Hello {{.Data.Name}},
This is a reminder that your API key {{.Data.APIKeyName}} will expire on {{.Data.ExpiresAt.Format "2006-01-02 15:04:05 MST"}}. This is a reminder that your API key {{.Data.ApiKeyName}} will expire on {{.Data.ExpiresAt.Format "2006-01-02 15:04:05 MST"}}.
Please generate a new API key if you need continued access.{{end}} Please generate a new API key if you need continued access.{{end}}

View File

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

View File

@@ -0,0 +1,6 @@
CREATE INDEX IF NOT EXISTS idx_webauthn_sessions_expires_at ON webauthn_sessions (expires_at);
CREATE INDEX IF NOT EXISTS idx_one_time_access_tokens_expires_at ON one_time_access_tokens (expires_at);
CREATE INDEX IF NOT EXISTS idx_oidc_authorization_codes_expires_at ON oidc_authorization_codes (expires_at);
CREATE INDEX IF NOT EXISTS idx_oidc_refresh_tokens_expires_at ON oidc_refresh_tokens (expires_at);
CREATE INDEX IF NOT EXISTS idx_reauthentication_tokens_expires_at ON reauthentication_tokens (expires_at);
CREATE INDEX IF NOT EXISTS idx_email_verification_tokens_expires_at ON email_verification_tokens (expires_at);

View File

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

View File

@@ -0,0 +1,12 @@
PRAGMA foreign_keys= OFF;
BEGIN;
CREATE INDEX IF NOT EXISTS idx_webauthn_sessions_expires_at ON webauthn_sessions (expires_at);
CREATE INDEX IF NOT EXISTS idx_one_time_access_tokens_expires_at ON one_time_access_tokens (expires_at);
CREATE INDEX IF NOT EXISTS idx_oidc_authorization_codes_expires_at ON oidc_authorization_codes (expires_at);
CREATE INDEX IF NOT EXISTS idx_oidc_refresh_tokens_expires_at ON oidc_refresh_tokens (expires_at);
CREATE INDEX IF NOT EXISTS idx_reauthentication_tokens_expires_at ON reauthentication_tokens (expires_at);
CREATE INDEX IF NOT EXISTS idx_email_verification_tokens_expires_at ON email_verification_tokens (expires_at);
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -40,7 +40,7 @@ ApiKeyExpiringEmail.TemplateProps = {
...sharedTemplateProps, ...sharedTemplateProps,
data: { data: {
name: "{{.Data.Name}}", name: "{{.Data.Name}}",
apiKeyName: "{{.Data.APIKeyName}}", apiKeyName: "{{.Data.ApiKeyName}}",
expiresAt: '{{.Data.ExpiresAt.Format "2006-01-02 15:04:05 MST"}}', expiresAt: '{{.Data.ExpiresAt.Format "2006-01-02 15:04:05 MST"}}',
}, },
}; };

View File

@@ -365,6 +365,7 @@
"enter_code_displayed_in_previous_step": "Zadejte kód, který byl zobrazen v předchozím kroku.", "enter_code_displayed_in_previous_step": "Zadejte kód, který byl zobrazen v předchozím kroku.",
"authorize": "Autorizovat", "authorize": "Autorizovat",
"federated_client_credentials": "Údaje o klientovi ve federaci", "federated_client_credentials": "Údaje o klientovi ve federaci",
"federated_client_credentials_description": "Federované klientské přihlašovací údaje umožňují ověřování klientů OIDC bez správy dlouhodobých tajných klíčů. Využívají tokeny JWT vydané třetími stranami pro klientská tvrzení, např. tokeny identity pracovního zatížení.",
"add_federated_client_credential": "Přidat údaje federovaného klienta", "add_federated_client_credential": "Přidat údaje federovaného klienta",
"add_another_federated_client_credential": "Přidat dalšího federovaného klienta", "add_another_federated_client_credential": "Přidat dalšího federovaného klienta",
"oidc_allowed_group_count": "Počet povolených skupin", "oidc_allowed_group_count": "Počet povolených skupin",

View File

@@ -356,7 +356,7 @@
"login_code_email_success": "Loginkoden er sendt til brugeren.", "login_code_email_success": "Loginkoden er sendt til brugeren.",
"send_email": "Send e-mail", "send_email": "Send e-mail",
"show_code": "Vis kode", "show_code": "Vis kode",
"callback_url_description": "URL(er) angivet af din klient. Tilføjes automatisk, hvis feltet efterlades tomt. <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>Jokertegn</link> understøttes.", "callback_url_description": "URL(er) angivet af din klient. Tilføjes automatisk, hvis feltet efterlades tomt. <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>Wildcards</link> understøttes.",
"logout_callback_url_description": "URL(er) angivet af din klient til logout. <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>Wildcards</link> understøttes.", "logout_callback_url_description": "URL(er) angivet af din klient til logout. <link href='https://pocket-id.org/docs/advanced/callback-url-wildcards'>Wildcards</link> understøttes.",
"api_key_expiration": "Udløb af API-nøgle", "api_key_expiration": "Udløb af API-nøgle",
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Send en e-mail til brugeren, når deres API-nøgle er ved at udløbe.", "send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Send en e-mail til brugeren, når deres API-nøgle er ved at udløbe.",
@@ -365,6 +365,7 @@
"enter_code_displayed_in_previous_step": "Indtast koden, der blev vist i det forrige trin.", "enter_code_displayed_in_previous_step": "Indtast koden, der blev vist i det forrige trin.",
"authorize": "Godkend", "authorize": "Godkend",
"federated_client_credentials": "Federated klientlegitimationsoplysninger", "federated_client_credentials": "Federated klientlegitimationsoplysninger",
"federated_client_credentials_description": "Federerede klientlegitimationsoplysninger gør det muligt at autentificere OIDC-klienter uden at skulle administrere langvarige hemmeligheder. De udnytter JWT-tokens udstedt af tredjepartsmyndigheder til klientpåstande, f.eks. identitetstokens for arbejdsbelastning.",
"add_federated_client_credential": "Tilføj federated klientlegitimation", "add_federated_client_credential": "Tilføj federated klientlegitimation",
"add_another_federated_client_credential": "Tilføj endnu en federated klientlegitimation", "add_another_federated_client_credential": "Tilføj endnu en federated klientlegitimation",
"oidc_allowed_group_count": "Tilladt antal grupper", "oidc_allowed_group_count": "Tilladt antal grupper",
@@ -445,7 +446,7 @@
"no_apps_available": "Ingen apps tilgængelige", "no_apps_available": "Ingen apps tilgængelige",
"contact_your_administrator_for_app_access": "Kontakt din administrator for at få adgang til applikationer.", "contact_your_administrator_for_app_access": "Kontakt din administrator for at få adgang til applikationer.",
"launch": "Start", "launch": "Start",
"client_launch_url": "Kundens lancerings-URL", "client_launch_url": "Start-URL til klient",
"client_launch_url_description": "Den URL, der åbnes, når en bruger starter appen fra siden Mine apps.", "client_launch_url_description": "Den URL, der åbnes, når en bruger starter appen fra siden Mine apps.",
"client_name_description": "Navnet på den klient, der vises i Pocket ID-brugergrænsefladen.", "client_name_description": "Navnet på den klient, der vises i Pocket ID-brugergrænsefladen.",
"revoke_access": "Tilbagekald adgang", "revoke_access": "Tilbagekald adgang",

View File

@@ -365,6 +365,7 @@
"enter_code_displayed_in_previous_step": "Gib den Code ein, der im vorherigen Schritt angezeigt wurde.", "enter_code_displayed_in_previous_step": "Gib den Code ein, der im vorherigen Schritt angezeigt wurde.",
"authorize": "Autorisieren", "authorize": "Autorisieren",
"federated_client_credentials": "Federated Client Credentials", "federated_client_credentials": "Federated Client Credentials",
"federated_client_credentials_description": "Mit föderierten Client-Anmeldeinfos kann man OIDC-Clients authentifizieren, ohne sich um langlebige Geheimnisse kümmern zu müssen. Sie nutzen JWT-Token, die von Drittanbietern für Client-Assertions ausgestellt werden, z. B. Workload-Identitätstoken.",
"add_federated_client_credential": "Föderierte Client-Anmeldeinfos hinzufügen", "add_federated_client_credential": "Föderierte Client-Anmeldeinfos hinzufügen",
"add_another_federated_client_credential": "Weitere Anmeldeinformationen für einen Verbundclient hinzufügen", "add_another_federated_client_credential": "Weitere Anmeldeinformationen für einen Verbundclient hinzufügen",
"oidc_allowed_group_count": "Erlaubte Gruppenanzahl", "oidc_allowed_group_count": "Erlaubte Gruppenanzahl",

View File

@@ -365,6 +365,7 @@
"enter_code_displayed_in_previous_step": "Introduce el código que se mostró en el paso anterior.", "enter_code_displayed_in_previous_step": "Introduce el código que se mostró en el paso anterior.",
"authorize": "Autorizar", "authorize": "Autorizar",
"federated_client_credentials": "Credenciales de cliente federadas", "federated_client_credentials": "Credenciales de cliente federadas",
"federated_client_credentials_description": "Las credenciales de cliente federadas permiten autenticar clientes OIDC sin gestionar secretos de larga duración. Aprovechan los tokens JWT emitidos por autoridades externas para las afirmaciones de los clientes, por ejemplo, tokens de identidad de carga de trabajo.",
"add_federated_client_credential": "Añadir credenciales de cliente federado", "add_federated_client_credential": "Añadir credenciales de cliente federado",
"add_another_federated_client_credential": "Añadir otra credencial de cliente federado", "add_another_federated_client_credential": "Añadir otra credencial de cliente federado",
"oidc_allowed_group_count": "Recuento de grupos permitidos", "oidc_allowed_group_count": "Recuento de grupos permitidos",

View File

@@ -1,6 +1,6 @@
{ {
"$schema": "https://inlang.com/schema/inlang-message-format", "$schema": "https://inlang.com/schema/inlang-message-format",
"my_account": "My Account", "my_account": "Minu konto",
"logout": "Logout", "logout": "Logout",
"confirm": "Confirm", "confirm": "Confirm",
"docs": "Docs", "docs": "Docs",
@@ -365,6 +365,7 @@
"enter_code_displayed_in_previous_step": "Enter the code that was displayed in the previous step.", "enter_code_displayed_in_previous_step": "Enter the code that was displayed in the previous step.",
"authorize": "Authorize", "authorize": "Authorize",
"federated_client_credentials": "Federated Client Credentials", "federated_client_credentials": "Federated Client Credentials",
"federated_client_credentials_description": "Föderatiivsed kliendi autentimisandmed võimaldavad OIDC-kliente autentida ilma pikaajalisi salajasi andmeid haldamata. Need kasutavad kolmandate osapoolte poolt väljastatud JWT-tokeneid kliendi kinnituste jaoks, nt töökoormuse identiteeditokeneid.",
"add_federated_client_credential": "Add Federated Client Credential", "add_federated_client_credential": "Add Federated Client Credential",
"add_another_federated_client_credential": "Add another federated client credential", "add_another_federated_client_credential": "Add another federated client credential",
"oidc_allowed_group_count": "Allowed Group Count", "oidc_allowed_group_count": "Allowed Group Count",

View File

@@ -365,6 +365,7 @@
"enter_code_displayed_in_previous_step": "Syötä edellisessä vaiheessa näkynyt koodi.", "enter_code_displayed_in_previous_step": "Syötä edellisessä vaiheessa näkynyt koodi.",
"authorize": "Salli", "authorize": "Salli",
"federated_client_credentials": "Federoidut asiakastunnukset", "federated_client_credentials": "Federoidut asiakastunnukset",
"federated_client_credentials_description": "Yhdistetyt asiakastunnistetiedot mahdollistavat OIDC-asiakkaiden todentamisen ilman pitkäaikaisten salaisuuksien hallintaa. Ne hyödyntävät kolmansien osapuolten viranomaisten myöntämiä JWT-tunnuksia asiakastodistuksiin, esimerkiksi työkuorman tunnistetunnuksiin.",
"add_federated_client_credential": "Lisää federoitu asiakastunnus", "add_federated_client_credential": "Lisää federoitu asiakastunnus",
"add_another_federated_client_credential": "Lisää toinen federoitu asiakastunnus", "add_another_federated_client_credential": "Lisää toinen federoitu asiakastunnus",
"oidc_allowed_group_count": "Sallittujen ryhmien määrä", "oidc_allowed_group_count": "Sallittujen ryhmien määrä",

View File

@@ -365,6 +365,7 @@
"enter_code_displayed_in_previous_step": "Entrez le code affiché à l'étape précédente.", "enter_code_displayed_in_previous_step": "Entrez le code affiché à l'étape précédente.",
"authorize": "Autoriser", "authorize": "Autoriser",
"federated_client_credentials": "Identifiants client fédérés", "federated_client_credentials": "Identifiants client fédérés",
"federated_client_credentials_description": "Les informations d'identification client fédérées permettent d'authentifier les clients OIDC sans avoir à gérer des secrets à long terme. Elles utilisent des jetons JWT émis par des autorités tierces pour les assertions client, par exemple des jetons d'identité de charge de travail.",
"add_federated_client_credential": "Ajouter un identifiant client fédéré", "add_federated_client_credential": "Ajouter un identifiant client fédéré",
"add_another_federated_client_credential": "Ajouter un autre identifiant client fédéré", "add_another_federated_client_credential": "Ajouter un autre identifiant client fédéré",
"oidc_allowed_group_count": "Nombre de groupes autorisés", "oidc_allowed_group_count": "Nombre de groupes autorisés",

View File

@@ -365,6 +365,7 @@
"enter_code_displayed_in_previous_step": "Inserisci il codice visualizzato nel passaggio precedente.", "enter_code_displayed_in_previous_step": "Inserisci il codice visualizzato nel passaggio precedente.",
"authorize": "Autorizza", "authorize": "Autorizza",
"federated_client_credentials": "Identità Federate", "federated_client_credentials": "Identità Federate",
"federated_client_credentials_description": "Le credenziali client federate ti permettono di autenticare i client OIDC senza dover gestire segreti a lungo termine. Usano i token JWT rilasciati da autorità terze per le asserzioni dei client, tipo i token di identità del carico di lavoro.",
"add_federated_client_credential": "Aggiungi Identità Federata", "add_federated_client_credential": "Aggiungi Identità Federata",
"add_another_federated_client_credential": "Aggiungi un'altra identità federata", "add_another_federated_client_credential": "Aggiungi un'altra identità federata",
"oidc_allowed_group_count": "Numero Gruppi Consentiti", "oidc_allowed_group_count": "Numero Gruppi Consentiti",

View File

@@ -365,6 +365,7 @@
"enter_code_displayed_in_previous_step": "前のステップで表示されたコードを入力してください。", "enter_code_displayed_in_previous_step": "前のステップで表示されたコードを入力してください。",
"authorize": "Authorize", "authorize": "Authorize",
"federated_client_credentials": "連携クライアントの資格情報", "federated_client_credentials": "連携クライアントの資格情報",
"federated_client_credentials_description": "フェデレーテッドクライアント認証情報は、長期にわたるシークレットを管理せずにOIDCクライアントを認証することを可能にします。これらは、クライアントアサーションワークロードIDトークンのためにサードパーティ機関が発行するJWTトークンを活用します。",
"add_federated_client_credential": "Add Federated Client Credential", "add_federated_client_credential": "Add Federated Client Credential",
"add_another_federated_client_credential": "Add another federated client credential", "add_another_federated_client_credential": "Add another federated client credential",
"oidc_allowed_group_count": "許可されたグループ数", "oidc_allowed_group_count": "許可されたグループ数",

View File

@@ -365,6 +365,7 @@
"enter_code_displayed_in_previous_step": "이전 단계에 표시된 코드를 입력하세요.", "enter_code_displayed_in_previous_step": "이전 단계에 표시된 코드를 입력하세요.",
"authorize": "승인", "authorize": "승인",
"federated_client_credentials": "연동 클라이언트 자격 증명", "federated_client_credentials": "연동 클라이언트 자격 증명",
"federated_client_credentials_description": "연방 클라이언트 자격 증명은 장기 비밀을 관리하지 않고도 OIDC 클라이언트를 인증할 수 있게 합니다. 이는 클라이언트 어설션(예: 워크로드 신원 토큰)을 위해 제3자 기관이 발급한 JWT 토큰을 활용합니다.",
"add_federated_client_credential": "연동 클라이언트 자격 증명 추가", "add_federated_client_credential": "연동 클라이언트 자격 증명 추가",
"add_another_federated_client_credential": "다른 연동 클라이언트 자격 증명 추가", "add_another_federated_client_credential": "다른 연동 클라이언트 자격 증명 추가",
"oidc_allowed_group_count": "허용된 그룹 수", "oidc_allowed_group_count": "허용된 그룹 수",

View File

@@ -365,6 +365,7 @@
"enter_code_displayed_in_previous_step": "Voer de code in die in de vorige stap werd getoond.", "enter_code_displayed_in_previous_step": "Voer de code in die in de vorige stap werd getoond.",
"authorize": "Autoriseren", "authorize": "Autoriseren",
"federated_client_credentials": "Federatieve clientreferenties", "federated_client_credentials": "Federatieve clientreferenties",
"federated_client_credentials_description": "Met federatieve klantgegevens kun je OIDC-klanten verifiëren zonder dat je langdurige geheimen hoeft te beheren. Ze gebruiken JWT-tokens die door externe instanties zijn uitgegeven voor klantverklaringen, zoals tokens voor werkbelastingidentiteit.",
"add_federated_client_credential": "Federatieve clientreferenties toevoegen", "add_federated_client_credential": "Federatieve clientreferenties toevoegen",
"add_another_federated_client_credential": "Voeg nog een federatieve clientreferentie toe", "add_another_federated_client_credential": "Voeg nog een federatieve clientreferentie toe",
"oidc_allowed_group_count": "Aantal groepen met toegang", "oidc_allowed_group_count": "Aantal groepen met toegang",

View File

@@ -365,6 +365,7 @@
"enter_code_displayed_in_previous_step": "Enter the code that was displayed in the previous step.", "enter_code_displayed_in_previous_step": "Enter the code that was displayed in the previous step.",
"authorize": "Authorize", "authorize": "Authorize",
"federated_client_credentials": "Federated Client Credentials", "federated_client_credentials": "Federated Client Credentials",
"federated_client_credentials_description": "Federated client credentials allow authenticating OIDC clients without managing long-lived secrets. They leverage JWT tokens issued by third-party authorities for client assertions, e.g. workload identity tokens.",
"add_federated_client_credential": "Add Federated Client Credential", "add_federated_client_credential": "Add Federated Client Credential",
"add_another_federated_client_credential": "Add another federated client credential", "add_another_federated_client_credential": "Add another federated client credential",
"oidc_allowed_group_count": "Allowed Group Count", "oidc_allowed_group_count": "Allowed Group Count",

View File

@@ -365,6 +365,7 @@
"enter_code_displayed_in_previous_step": "Wprowadź kod wyświetlony w poprzednim kroku.", "enter_code_displayed_in_previous_step": "Wprowadź kod wyświetlony w poprzednim kroku.",
"authorize": "Autoryzuj", "authorize": "Autoryzuj",
"federated_client_credentials": "Połączone poświadczenia klienta", "federated_client_credentials": "Połączone poświadczenia klienta",
"federated_client_credentials_description": "Połączone poświadczenia klienta umożliwiają uwierzytelnianie klientów OIDC bez konieczności zarządzania długotrwałymi sekretami. Wykorzystują one tokeny JWT wydane przez zewnętrzne organy do potwierdzania tożsamości klientów, np. tokeny tożsamości obciążenia.",
"add_federated_client_credential": "Dodaj poświadczenia klienta federacyjnego", "add_federated_client_credential": "Dodaj poświadczenia klienta federacyjnego",
"add_another_federated_client_credential": "Dodaj kolejne poświadczenia klienta federacyjnego", "add_another_federated_client_credential": "Dodaj kolejne poświadczenia klienta federacyjnego",
"oidc_allowed_group_count": "Dopuszczalna liczba grup", "oidc_allowed_group_count": "Dopuszczalna liczba grup",

View File

@@ -365,6 +365,7 @@
"enter_code_displayed_in_previous_step": "Digite o código que apareceu na etapa anterior.", "enter_code_displayed_in_previous_step": "Digite o código que apareceu na etapa anterior.",
"authorize": "Autorizar", "authorize": "Autorizar",
"federated_client_credentials": "Credenciais de Cliente Federadas", "federated_client_credentials": "Credenciais de Cliente Federadas",
"federated_client_credentials_description": "As credenciais federadas do cliente permitem autenticar clientes OIDC sem precisar gerenciar segredos de longa duração. Elas usam tokens JWT emitidos por autoridades terceirizadas para afirmações do cliente, tipo tokens de identidade de carga de trabalho.",
"add_federated_client_credential": "Adicionar credencial de cliente federado", "add_federated_client_credential": "Adicionar credencial de cliente federado",
"add_another_federated_client_credential": "Adicionar outra credencial de cliente federado", "add_another_federated_client_credential": "Adicionar outra credencial de cliente federado",
"oidc_allowed_group_count": "Total de grupos permitidos", "oidc_allowed_group_count": "Total de grupos permitidos",

View File

@@ -365,6 +365,7 @@
"enter_code_displayed_in_previous_step": "Введите код, который был отображен на предыдущем шаге.", "enter_code_displayed_in_previous_step": "Введите код, который был отображен на предыдущем шаге.",
"authorize": "Авторизовать", "authorize": "Авторизовать",
"federated_client_credentials": "Федеративные учетные данные клиента", "federated_client_credentials": "Федеративные учетные данные клиента",
"federated_client_credentials_description": "Федеративные учетные данные клиента позволяют аутентифицировать клиентов OIDC без необходимости управления долгосрочными секретами. Они используют токены JWT, выданные сторонними органами для утверждений клиента, например токены идентификации рабочей нагрузки.",
"add_federated_client_credential": "Добавить федеративные учетные данные клиента", "add_federated_client_credential": "Добавить федеративные учетные данные клиента",
"add_another_federated_client_credential": "Добавить другие федеративные учетные данные клиента", "add_another_federated_client_credential": "Добавить другие федеративные учетные данные клиента",
"oidc_allowed_group_count": "Число разрешенных групп", "oidc_allowed_group_count": "Число разрешенных групп",

View File

@@ -365,6 +365,7 @@
"enter_code_displayed_in_previous_step": "Ange koden som visades i föregående steg.", "enter_code_displayed_in_previous_step": "Ange koden som visades i föregående steg.",
"authorize": "Godkänn", "authorize": "Godkänn",
"federated_client_credentials": "Federerade klientuppgifter", "federated_client_credentials": "Federerade klientuppgifter",
"federated_client_credentials_description": "Federerade klientautentiseringsuppgifter gör det möjligt att autentisera OIDC-klienter utan att hantera långlivade hemligheter. De utnyttjar JWT-tokens som utfärdats av tredjepartsmyndigheter för klientpåståenden, t.ex. identitetstokens för arbetsbelastning.",
"add_federated_client_credential": "Lägg till federerad klientuppgift", "add_federated_client_credential": "Lägg till federerad klientuppgift",
"add_another_federated_client_credential": "Lägg till ytterligare en federerad klientuppgift", "add_another_federated_client_credential": "Lägg till ytterligare en federerad klientuppgift",
"oidc_allowed_group_count": "Tillåtet antal grupper", "oidc_allowed_group_count": "Tillåtet antal grupper",

View File

@@ -365,6 +365,7 @@
"enter_code_displayed_in_previous_step": "Önceki adımda görüntülenen kodu girin.", "enter_code_displayed_in_previous_step": "Önceki adımda görüntülenen kodu girin.",
"authorize": "Yetkilendir", "authorize": "Yetkilendir",
"federated_client_credentials": "Birleştirilmiş İstemci Kimlik Bilgileri", "federated_client_credentials": "Birleştirilmiş İstemci Kimlik Bilgileri",
"federated_client_credentials_description": "Birleştirilmiş istemci kimlik bilgileri, uzun süreli gizli bilgileri yönetmeden OIDC istemcilerinin kimlik doğrulamasını sağlar. Üçüncü taraf yetkililer tarafından istemci beyanları için verilen JWT belirteçlerini (ör. iş yükü kimlik belirteçleri) kullanır.",
"add_federated_client_credential": "Birleştirilmiş İstemci Kimlik Bilgisi Ekle", "add_federated_client_credential": "Birleştirilmiş İstemci Kimlik Bilgisi Ekle",
"add_another_federated_client_credential": "Başka bir birleştirilmiş istemci kimlik bilgisi ekle", "add_another_federated_client_credential": "Başka bir birleştirilmiş istemci kimlik bilgisi ekle",
"oidc_allowed_group_count": "İzin Verilen Grup Sayısı", "oidc_allowed_group_count": "İzin Verilen Grup Sayısı",

View File

@@ -365,6 +365,7 @@
"enter_code_displayed_in_previous_step": "Введіть код, який було показано на попередньому кроці.", "enter_code_displayed_in_previous_step": "Введіть код, який було показано на попередньому кроці.",
"authorize": "Авторизувати", "authorize": "Авторизувати",
"federated_client_credentials": "Федеративні облікові дані клієнта", "federated_client_credentials": "Федеративні облікові дані клієнта",
"federated_client_credentials_description": "Федеративні облікові дані клієнта дозволяють автентифікувати клієнтів OIDC без управління довготривалими секретами. Вони використовують токени JWT, видані сторонніми органами для підтвердження клієнтів, наприклад, токени ідентичності робочого навантаження.",
"add_federated_client_credential": "Додати федеративний обліковий запис клієнта", "add_federated_client_credential": "Додати федеративний обліковий запис клієнта",
"add_another_federated_client_credential": "Додати ще один федеративний обліковий запис клієнта", "add_another_federated_client_credential": "Додати ще один федеративний обліковий запис клієнта",
"oidc_allowed_group_count": "Кількість дозволених груп", "oidc_allowed_group_count": "Кількість дозволених груп",

View File

@@ -365,6 +365,7 @@
"enter_code_displayed_in_previous_step": "Nhập mã đã hiển thị ở bước trước.", "enter_code_displayed_in_previous_step": "Nhập mã đã hiển thị ở bước trước.",
"authorize": "Cho phép", "authorize": "Cho phép",
"federated_client_credentials": "Thông Tin Xác Thực Của Federated Clients", "federated_client_credentials": "Thông Tin Xác Thực Của Federated Clients",
"federated_client_credentials_description": "Thông tin xác thực khách hàng liên kết cho phép xác thực các khách hàng OIDC mà không cần quản lý các khóa bí mật có thời hạn dài. Chúng sử dụng các token JWT do các cơ quan thứ ba cấp để xác thực thông tin của khách hàng, ví dụ như các token danh tính công việc.",
"add_federated_client_credential": "Thêm thông tin xác thực cho federated clients", "add_federated_client_credential": "Thêm thông tin xác thực cho federated clients",
"add_another_federated_client_credential": "Thêm một thông tin xác thực cho federated clients khác", "add_another_federated_client_credential": "Thêm một thông tin xác thực cho federated clients khác",
"oidc_allowed_group_count": "Số lượng nhóm được phép", "oidc_allowed_group_count": "Số lượng nhóm được phép",

View File

@@ -365,6 +365,7 @@
"enter_code_displayed_in_previous_step": "输入在上一步中显示的代码", "enter_code_displayed_in_previous_step": "输入在上一步中显示的代码",
"authorize": "授权", "authorize": "授权",
"federated_client_credentials": "联合身份", "federated_client_credentials": "联合身份",
"federated_client_credentials_description": "联合客户端凭证允许在无需管理长期密钥的情况下验证OIDC客户端。该机制利用第三方机构签发的JWT令牌来验证客户端声明例如工作负载身份令牌。",
"add_federated_client_credential": "添加联合身份", "add_federated_client_credential": "添加联合身份",
"add_another_federated_client_credential": "再添加一个联合身份", "add_another_federated_client_credential": "再添加一个联合身份",
"oidc_allowed_group_count": "允许的群组数量", "oidc_allowed_group_count": "允许的群组数量",

View File

@@ -365,6 +365,7 @@
"enter_code_displayed_in_previous_step": "請輸入上一步顯示的代碼。", "enter_code_displayed_in_previous_step": "請輸入上一步顯示的代碼。",
"authorize": "授權", "authorize": "授權",
"federated_client_credentials": "聯邦身分", "federated_client_credentials": "聯邦身分",
"federated_client_credentials_description": "聯合客戶憑證允許驗證 OIDC 客戶端,無需管理長期存續的機密。此機制利用第三方權威機構發行的 JWT 憑證來驗證客戶端聲明,例如工作負載身分憑證。",
"add_federated_client_credential": "增加聯邦身分", "add_federated_client_credential": "增加聯邦身分",
"add_another_federated_client_credential": "新增另一組聯邦身分", "add_another_federated_client_credential": "新增另一組聯邦身分",
"oidc_allowed_group_count": "允許的群組數量", "oidc_allowed_group_count": "允許的群組數量",

View File

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

View File

@@ -32,6 +32,8 @@
aria-invalid={!!error} aria-invalid={!!error}
data-testid={`callback-url-${i + 1}`} data-testid={`callback-url-${i + 1}`}
type="text" type="text"
inputmode="url"
autocomplete="url"
bind:value={callbackURLs[i]} bind:value={callbackURLs[i]}
/> />
<Button <Button

View File

@@ -53,7 +53,7 @@
"user_id": "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e" "user_id": "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e"
}, },
{ {
"client_id": "7c21a609-96b5-4011-9900-272b8d31a9d1", "client_id": "c48232ff-ff65-45ed-ae96-7afa8a9b443b",
"code": "federated", "code": "federated",
"code_challenge": null, "code_challenge": null,
"code_challenge_method_sha256": null, "code_challenge_method_sha256": null,