mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-02-23 21:44:07 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
453a765107 | ||
|
|
f03645d545 | ||
|
|
55273d68c9 | ||
|
|
4e05b82f02 | ||
|
|
2597907578 | ||
|
|
debef9a66b | ||
|
|
9122e75101 | ||
|
|
fe1c4b18cd | ||
|
|
e571996cb5 | ||
|
|
fb862d3ec3 | ||
|
|
26f01f205b | ||
|
|
c37a3e0ed1 | ||
|
|
eb689eb56e | ||
|
|
60bad9e985 | ||
|
|
e21ee8a871 |
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,2 +1,2 @@
|
||||
# These are supported funding model platforms
|
||||
github: stonith404
|
||||
github: [stonith404, kmendell]
|
||||
|
||||
73
.github/workflows/e2e-tests.yml
vendored
73
.github/workflows/e2e-tests.yml
vendored
@@ -19,21 +19,28 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and export
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
tags: pocket-id/pocket-id:test
|
||||
push: false
|
||||
load: false
|
||||
tags: pocket-id:test
|
||||
outputs: type=docker,dest=/tmp/docker-image.tar
|
||||
build-args: BUILD_TAGS=e2etest
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Upload Docker image artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: docker-image
|
||||
path: /tmp/docker-image.tar
|
||||
retention-days: 1
|
||||
|
||||
test-sqlite:
|
||||
if: github.event.pull_request.head.ref != 'i18n_crowdin'
|
||||
@@ -47,13 +54,22 @@ jobs:
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Cache Playwright Browsers
|
||||
uses: actions/cache@v3
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('frontend/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-playwright-
|
||||
|
||||
- name: Download Docker image artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: docker-image
|
||||
path: /tmp
|
||||
|
||||
- name: Load Docker Image
|
||||
- name: Load Docker image
|
||||
run: docker load -i /tmp/docker-image.tar
|
||||
|
||||
- name: Install frontend dependencies
|
||||
@@ -62,6 +78,7 @@ jobs:
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
working-directory: ./frontend
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
run: npx playwright install --with-deps chromium
|
||||
|
||||
- name: Run Docker Container with Sqlite DB
|
||||
@@ -69,7 +86,7 @@ jobs:
|
||||
docker run -d --name pocket-id-sqlite \
|
||||
-p 80:80 \
|
||||
-e APP_ENV=test \
|
||||
pocket-id/pocket-id:test
|
||||
pocket-id:test
|
||||
|
||||
docker logs -f pocket-id-sqlite &> /tmp/backend.log &
|
||||
|
||||
@@ -77,16 +94,18 @@ jobs:
|
||||
working-directory: ./frontend
|
||||
run: npx playwright test
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
- name: Upload Frontend Test Report
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always() && github.event.pull_request.head.ref != 'i18n_crowdin'
|
||||
with:
|
||||
name: playwright-report-sqlite
|
||||
path: frontend/tests/.report
|
||||
include-hidden-files: true
|
||||
retention-days: 15
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
- name: Upload Backend Test Report
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always() && github.event.pull_request.head.ref != 'i18n_crowdin'
|
||||
with:
|
||||
name: backend-sqlite
|
||||
path: /tmp/backend.log
|
||||
@@ -105,12 +124,39 @@ jobs:
|
||||
cache: "npm"
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Cache Playwright Browsers
|
||||
uses: actions/cache@v3
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('frontend/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-playwright-
|
||||
|
||||
- name: Cache PostgreSQL Docker image
|
||||
uses: actions/cache@v3
|
||||
id: postgres-cache
|
||||
with:
|
||||
path: /tmp/postgres-image.tar
|
||||
key: postgres-17-${{ runner.os }}
|
||||
|
||||
- name: Pull and save PostgreSQL image
|
||||
if: steps.postgres-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
docker pull postgres:17
|
||||
docker save postgres:17 > /tmp/postgres-image.tar
|
||||
|
||||
- name: Load PostgreSQL image from cache
|
||||
if: steps.postgres-cache.outputs.cache-hit == 'true'
|
||||
run: docker load < /tmp/postgres-image.tar
|
||||
|
||||
- name: Download Docker image artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: docker-image
|
||||
path: /tmp
|
||||
- name: Load Docker Image
|
||||
|
||||
- name: Load Docker image
|
||||
run: docker load -i /tmp/docker-image.tar
|
||||
|
||||
- name: Install frontend dependencies
|
||||
@@ -119,6 +165,7 @@ jobs:
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
working-directory: ./frontend
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
run: npx playwright install --with-deps chromium
|
||||
|
||||
- name: Create Docker network
|
||||
@@ -153,7 +200,7 @@ jobs:
|
||||
-e APP_ENV=test \
|
||||
-e DB_PROVIDER=postgres \
|
||||
-e DB_CONNECTION_STRING=postgresql://postgres:postgres@pocket-id-db:5432/pocket-id \
|
||||
pocket-id/pocket-id:test
|
||||
pocket-id:test
|
||||
|
||||
docker logs -f pocket-id-postgres &> /tmp/backend.log &
|
||||
|
||||
@@ -161,7 +208,8 @@ jobs:
|
||||
working-directory: ./frontend
|
||||
run: npx playwright test
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
- name: Upload Frontend Test Report
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always() && github.event.pull_request.head.ref != 'i18n_crowdin'
|
||||
with:
|
||||
name: playwright-report-postgres
|
||||
@@ -169,8 +217,9 @@ jobs:
|
||||
include-hidden-files: true
|
||||
retention-days: 15
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
- name: Upload Backend Test Report
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always() && github.event.pull_request.head.ref != 'i18n_crowdin'
|
||||
with:
|
||||
name: backend-postgres
|
||||
path: /tmp/backend.log
|
||||
|
||||
18
CHANGELOG.md
18
CHANGELOG.md
@@ -1,3 +1,21 @@
|
||||
## [](https://github.com/pocket-id/pocket-id/compare/v0.48.0...v) (2025-04-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add ability to disable API key expiration email ([9122e75](https://github.com/pocket-id/pocket-id/commit/9122e75101ad39a40135ccf931eb2bfd351b5db6))
|
||||
* add ability to send login code via email ([#457](https://github.com/pocket-id/pocket-id/issues/457)) ([fe1c4b1](https://github.com/pocket-id/pocket-id/commit/fe1c4b18cdcc46a4256e0c111b34f1ce00f8e0e1))
|
||||
* add description to callback URL inputs ([eb689eb](https://github.com/pocket-id/pocket-id/commit/eb689eb56ec9eaf8b0fb1485040e26f841b9225d))
|
||||
* send email to user when api key expires within 7 days ([#451](https://github.com/pocket-id/pocket-id/issues/451)) ([26f01f2](https://github.com/pocket-id/pocket-id/commit/26f01f205be01fb8abd8c2e564c90c0fc4480ea5))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* disable animations not respected on authorize and logout page ([e571996](https://github.com/pocket-id/pocket-id/commit/e571996cb57d04232c1f47ab337ad656f48bb3cb))
|
||||
* hide alternative sign in button if user is already authenticated ([4e05b82](https://github.com/pocket-id/pocket-id/commit/4e05b82f02740a4bae07cec6c6a64acd34ca0fc3))
|
||||
* locale change in dropdown doesn't work on first try ([60bad9e](https://github.com/pocket-id/pocket-id/commit/60bad9e9859d81c9967e6939e1ed10a65145a936))
|
||||
* remove limit of 20 callback URLs ([c37a3e0](https://github.com/pocket-id/pocket-id/commit/c37a3e0ed177c3bd2b9a618d1f4b0709004478b0))
|
||||
|
||||
## [](https://github.com/pocket-id/pocket-id/compare/v0.47.0...v) (2025-04-18)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/pocket-id/pocket-id/backend
|
||||
|
||||
go 1.24
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/caarlos0/env/v11 v11.3.1
|
||||
|
||||
@@ -49,7 +49,7 @@ func initRouter(ctx context.Context, db *gorm.DB, appConfigService *service.AppC
|
||||
oidcService := service.NewOidcService(db, jwtService, appConfigService, auditLogService, customClaimService)
|
||||
userGroupService := service.NewUserGroupService(db, appConfigService)
|
||||
ldapService := service.NewLdapService(db, appConfigService, userService, userGroupService)
|
||||
apiKeyService := service.NewApiKeyService(db)
|
||||
apiKeyService := service.NewApiKeyService(db, emailService)
|
||||
|
||||
rateLimitMiddleware := middleware.NewRateLimitMiddleware()
|
||||
|
||||
@@ -61,6 +61,7 @@ func initRouter(ctx context.Context, db *gorm.DB, appConfigService *service.AppC
|
||||
job.RegisterLdapJobs(ctx, ldapService, appConfigService)
|
||||
job.RegisterDbCleanupJobs(ctx, db)
|
||||
job.RegisterFileCleanupJobs(ctx, db)
|
||||
job.RegisterApiKeyExpiryJob(ctx, apiKeyService, appConfigService)
|
||||
|
||||
// Initialize middleware for specific routes
|
||||
authMiddleware := middleware.NewAuthMiddleware(apiKeyService, userService, jwtService)
|
||||
|
||||
@@ -43,9 +43,10 @@ func NewUserController(group *gin.RouterGroup, authMiddleware *middleware.AuthMi
|
||||
|
||||
group.POST("/users/me/one-time-access-token", authMiddleware.WithAdminNotRequired().Add(), uc.createOwnOneTimeAccessTokenHandler)
|
||||
group.POST("/users/:id/one-time-access-token", authMiddleware.Add(), uc.createAdminOneTimeAccessTokenHandler)
|
||||
group.POST("/users/:id/one-time-access-email", authMiddleware.Add(), uc.RequestOneTimeAccessEmailAsAdminHandler)
|
||||
group.POST("/one-time-access-token/:token", rateLimitMiddleware.Add(rate.Every(10*time.Second), 5), uc.exchangeOneTimeAccessTokenHandler)
|
||||
group.POST("/one-time-access-token/setup", uc.getSetupAccessTokenHandler)
|
||||
group.POST("/one-time-access-email", rateLimitMiddleware.Add(rate.Every(10*time.Minute), 3), uc.requestOneTimeAccessEmailHandler)
|
||||
group.POST("/one-time-access-email", rateLimitMiddleware.Add(rate.Every(10*time.Minute), 3), uc.RequestOneTimeAccessEmailAsUnauthenticatedUserHandler)
|
||||
|
||||
group.DELETE("/users/:id/profile-picture", authMiddleware.Add(), uc.resetUserProfilePictureHandler)
|
||||
group.DELETE("/users/me/profile-picture", authMiddleware.WithAdminNotRequired().Add(), uc.resetCurrentUserProfilePictureHandler)
|
||||
@@ -356,18 +357,63 @@ func (uc *UserController) createOwnOneTimeAccessTokenHandler(c *gin.Context) {
|
||||
uc.createOneTimeAccessTokenHandler(c, true)
|
||||
}
|
||||
|
||||
// createAdminOneTimeAccessTokenHandler godoc
|
||||
// @Summary Create one-time access token for user (admin)
|
||||
// @Description Generate a one-time access token for a specific user (admin only)
|
||||
// @Tags Users
|
||||
// @Param id path string true "User ID"
|
||||
// @Param body body dto.OneTimeAccessTokenCreateDto true "Token options"
|
||||
// @Success 201 {object} object "{ \"token\": \"string\" }"
|
||||
// @Router /api/users/{id}/one-time-access-token [post]
|
||||
func (uc *UserController) createAdminOneTimeAccessTokenHandler(c *gin.Context) {
|
||||
uc.createOneTimeAccessTokenHandler(c, false)
|
||||
}
|
||||
|
||||
func (uc *UserController) requestOneTimeAccessEmailHandler(c *gin.Context) {
|
||||
var input dto.OneTimeAccessEmailDto
|
||||
// RequestOneTimeAccessEmailAsUnauthenticatedUserHandler godoc
|
||||
// @Summary Request one-time access email
|
||||
// @Description Request a one-time access email for unauthenticated users
|
||||
// @Tags Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body dto.OneTimeAccessEmailAsUnauthenticatedUserDto true "Email request information"
|
||||
// @Success 204 "No Content"
|
||||
// @Router /api/one-time-access-email [post]
|
||||
func (uc *UserController) RequestOneTimeAccessEmailAsUnauthenticatedUserHandler(c *gin.Context) {
|
||||
var input dto.OneTimeAccessEmailAsUnauthenticatedUserDto
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
err := uc.userService.RequestOneTimeAccessEmail(c.Request.Context(), input.Email, input.RedirectPath)
|
||||
err := uc.userService.RequestOneTimeAccessEmailAsUnauthenticatedUser(c.Request.Context(), input.Email, input.RedirectPath)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// RequestOneTimeAccessEmailAsAdminHandler godoc
|
||||
// @Summary Request one-time access email (admin)
|
||||
// @Description Request a one-time access email for a specific user (admin only)
|
||||
// @Tags Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "User ID"
|
||||
// @Param body body dto.OneTimeAccessEmailAsAdminDto true "Email request options"
|
||||
// @Success 204 "No Content"
|
||||
// @Router /api/users/{id}/one-time-access-email [post]
|
||||
func (uc *UserController) RequestOneTimeAccessEmailAsAdminHandler(c *gin.Context) {
|
||||
var input dto.OneTimeAccessEmailAsAdminDto
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
userID := c.Param("id")
|
||||
|
||||
err := uc.userService.RequestOneTimeAccessEmailAsAdmin(c.Request.Context(), userID, input.ExpiresAt)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
|
||||
@@ -11,12 +11,13 @@ type ApiKeyCreateDto struct {
|
||||
}
|
||||
|
||||
type ApiKeyDto struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
ExpiresAt datatype.DateTime `json:"expiresAt"`
|
||||
LastUsedAt *datatype.DateTime `json:"lastUsedAt"`
|
||||
CreatedAt datatype.DateTime `json:"createdAt"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
ExpiresAt datatype.DateTime `json:"expiresAt"`
|
||||
LastUsedAt *datatype.DateTime `json:"lastUsedAt"`
|
||||
CreatedAt datatype.DateTime `json:"createdAt"`
|
||||
ExpirationEmailSent bool `json:"expirationEmailSent"`
|
||||
}
|
||||
|
||||
type ApiKeyResponseDto struct {
|
||||
|
||||
@@ -12,37 +12,39 @@ type AppConfigVariableDto struct {
|
||||
}
|
||||
|
||||
type AppConfigUpdateDto struct {
|
||||
AppName string `json:"appName" binding:"required,min=1,max=30"`
|
||||
SessionDuration string `json:"sessionDuration" binding:"required"`
|
||||
EmailsVerified string `json:"emailsVerified" binding:"required"`
|
||||
DisableAnimations string `json:"disableAnimations" binding:"required"`
|
||||
AllowOwnAccountEdit string `json:"allowOwnAccountEdit" binding:"required"`
|
||||
SmtpHost string `json:"smtpHost"`
|
||||
SmtpPort string `json:"smtpPort"`
|
||||
SmtpFrom string `json:"smtpFrom" binding:"omitempty,email"`
|
||||
SmtpUser string `json:"smtpUser"`
|
||||
SmtpPassword string `json:"smtpPassword"`
|
||||
SmtpTls string `json:"smtpTls" binding:"required,oneof=none starttls tls"`
|
||||
SmtpSkipCertVerify string `json:"smtpSkipCertVerify"`
|
||||
LdapEnabled string `json:"ldapEnabled" binding:"required"`
|
||||
LdapUrl string `json:"ldapUrl"`
|
||||
LdapBindDn string `json:"ldapBindDn"`
|
||||
LdapBindPassword string `json:"ldapBindPassword"`
|
||||
LdapBase string `json:"ldapBase"`
|
||||
LdapUserSearchFilter string `json:"ldapUserSearchFilter"`
|
||||
LdapUserGroupSearchFilter string `json:"ldapUserGroupSearchFilter"`
|
||||
LdapSkipCertVerify string `json:"ldapSkipCertVerify"`
|
||||
LdapAttributeUserUniqueIdentifier string `json:"ldapAttributeUserUniqueIdentifier"`
|
||||
LdapAttributeUserUsername string `json:"ldapAttributeUserUsername"`
|
||||
LdapAttributeUserEmail string `json:"ldapAttributeUserEmail"`
|
||||
LdapAttributeUserFirstName string `json:"ldapAttributeUserFirstName"`
|
||||
LdapAttributeUserLastName string `json:"ldapAttributeUserLastName"`
|
||||
LdapAttributeUserProfilePicture string `json:"ldapAttributeUserProfilePicture"`
|
||||
LdapAttributeGroupMember string `json:"ldapAttributeGroupMember"`
|
||||
LdapAttributeGroupUniqueIdentifier string `json:"ldapAttributeGroupUniqueIdentifier"`
|
||||
LdapAttributeGroupName string `json:"ldapAttributeGroupName"`
|
||||
LdapAttributeAdminGroup string `json:"ldapAttributeAdminGroup"`
|
||||
LdapSoftDeleteUsers string `json:"ldapSoftDeleteUsers"`
|
||||
EmailOneTimeAccessEnabled string `json:"emailOneTimeAccessEnabled" binding:"required"`
|
||||
EmailLoginNotificationEnabled string `json:"emailLoginNotificationEnabled" binding:"required"`
|
||||
AppName string `json:"appName" binding:"required,min=1,max=30"`
|
||||
SessionDuration string `json:"sessionDuration" binding:"required"`
|
||||
EmailsVerified string `json:"emailsVerified" binding:"required"`
|
||||
DisableAnimations string `json:"disableAnimations" binding:"required"`
|
||||
AllowOwnAccountEdit string `json:"allowOwnAccountEdit" binding:"required"`
|
||||
SmtpHost string `json:"smtpHost"`
|
||||
SmtpPort string `json:"smtpPort"`
|
||||
SmtpFrom string `json:"smtpFrom" binding:"omitempty,email"`
|
||||
SmtpUser string `json:"smtpUser"`
|
||||
SmtpPassword string `json:"smtpPassword"`
|
||||
SmtpTls string `json:"smtpTls" binding:"required,oneof=none starttls tls"`
|
||||
SmtpSkipCertVerify string `json:"smtpSkipCertVerify"`
|
||||
LdapEnabled string `json:"ldapEnabled" binding:"required"`
|
||||
LdapUrl string `json:"ldapUrl"`
|
||||
LdapBindDn string `json:"ldapBindDn"`
|
||||
LdapBindPassword string `json:"ldapBindPassword"`
|
||||
LdapBase string `json:"ldapBase"`
|
||||
LdapUserSearchFilter string `json:"ldapUserSearchFilter"`
|
||||
LdapUserGroupSearchFilter string `json:"ldapUserGroupSearchFilter"`
|
||||
LdapSkipCertVerify string `json:"ldapSkipCertVerify"`
|
||||
LdapAttributeUserUniqueIdentifier string `json:"ldapAttributeUserUniqueIdentifier"`
|
||||
LdapAttributeUserUsername string `json:"ldapAttributeUserUsername"`
|
||||
LdapAttributeUserEmail string `json:"ldapAttributeUserEmail"`
|
||||
LdapAttributeUserFirstName string `json:"ldapAttributeUserFirstName"`
|
||||
LdapAttributeUserLastName string `json:"ldapAttributeUserLastName"`
|
||||
LdapAttributeUserProfilePicture string `json:"ldapAttributeUserProfilePicture"`
|
||||
LdapAttributeGroupMember string `json:"ldapAttributeGroupMember"`
|
||||
LdapAttributeGroupUniqueIdentifier string `json:"ldapAttributeGroupUniqueIdentifier"`
|
||||
LdapAttributeGroupName string `json:"ldapAttributeGroupName"`
|
||||
LdapAttributeAdminGroup string `json:"ldapAttributeAdminGroup"`
|
||||
LdapSoftDeleteUsers string `json:"ldapSoftDeleteUsers"`
|
||||
EmailOneTimeAccessAsAdminEnabled string `json:"emailOneTimeAccessAsAdminEnabled" binding:"required"`
|
||||
EmailOneTimeAccessAsUnauthenticatedEnabled string `json:"emailOneTimeAccessAsUnauthenticatedEnabled" binding:"required"`
|
||||
EmailLoginNotificationEnabled string `json:"emailLoginNotificationEnabled" binding:"required"`
|
||||
EmailApiKeyExpirationEnabled string `json:"emailApiKeyExpirationEnabled" binding:"required"`
|
||||
}
|
||||
|
||||
@@ -32,11 +32,15 @@ type OneTimeAccessTokenCreateDto struct {
|
||||
ExpiresAt time.Time `json:"expiresAt" binding:"required"`
|
||||
}
|
||||
|
||||
type OneTimeAccessEmailDto struct {
|
||||
type OneTimeAccessEmailAsUnauthenticatedUserDto struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
RedirectPath string `json:"redirectPath"`
|
||||
}
|
||||
|
||||
type OneTimeAccessEmailAsAdminDto struct {
|
||||
ExpiresAt time.Time `json:"expiresAt" binding:"required"`
|
||||
}
|
||||
|
||||
type UserUpdateUserGroupDto struct {
|
||||
UserGroupIds []string `json:"userGroupIds" binding:"required"`
|
||||
}
|
||||
|
||||
53
backend/internal/job/api_key_expiry_job.go
Normal file
53
backend/internal/job/api_key_expiry_job.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"github.com/go-co-op/gocron/v2"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||
)
|
||||
|
||||
type ApiKeyEmailJobs struct {
|
||||
apiKeyService *service.ApiKeyService
|
||||
appConfigService *service.AppConfigService
|
||||
}
|
||||
|
||||
func RegisterApiKeyExpiryJob(ctx context.Context, apiKeyService *service.ApiKeyService, appConfigService *service.AppConfigService) {
|
||||
jobs := &ApiKeyEmailJobs{
|
||||
apiKeyService: apiKeyService,
|
||||
appConfigService: appConfigService,
|
||||
}
|
||||
|
||||
scheduler, err := gocron.NewScheduler()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create a new scheduler: %v", err)
|
||||
}
|
||||
|
||||
registerJob(ctx, scheduler, "ExpiredApiKeyEmailJob", "0 0 * * *", jobs.checkAndNotifyExpiringApiKeys)
|
||||
|
||||
scheduler.Start()
|
||||
}
|
||||
|
||||
func (j *ApiKeyEmailJobs) checkAndNotifyExpiringApiKeys(ctx context.Context) error {
|
||||
// Skip if the feature is disabled
|
||||
if !j.appConfigService.GetDbConfig().EmailApiKeyExpirationEnabled.IsTrue() {
|
||||
return nil
|
||||
}
|
||||
|
||||
apiKeys, err := j.apiKeyService.ListExpiringApiKeys(ctx, 7)
|
||||
if err != nil {
|
||||
log.Printf("Failed to list expiring API keys: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
for _, key := range apiKeys {
|
||||
if key.User.Email == "" {
|
||||
continue
|
||||
}
|
||||
if err := j.apiKeyService.SendApiKeyExpiringSoonEmail(ctx, key); err != nil {
|
||||
log.Printf("Failed to send email for key %s: %v", key.ID, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -5,11 +5,12 @@ import datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
||||
type ApiKey struct {
|
||||
Base
|
||||
|
||||
Name string `sortable:"true"`
|
||||
Key string
|
||||
Description *string
|
||||
ExpiresAt datatype.DateTime `sortable:"true"`
|
||||
LastUsedAt *datatype.DateTime `sortable:"true"`
|
||||
Name string `sortable:"true"`
|
||||
Key string
|
||||
Description *string
|
||||
ExpiresAt datatype.DateTime `sortable:"true"`
|
||||
LastUsedAt *datatype.DateTime `sortable:"true"`
|
||||
ExpirationEmailSent bool
|
||||
|
||||
UserID string
|
||||
User User
|
||||
|
||||
@@ -41,15 +41,17 @@ type AppConfig struct {
|
||||
LogoLightImageType AppConfigVariable `key:"logoLightImageType,internal"` // Internal
|
||||
LogoDarkImageType AppConfigVariable `key:"logoDarkImageType,internal"` // Internal
|
||||
// Email
|
||||
SmtpHost AppConfigVariable `key:"smtpHost"`
|
||||
SmtpPort AppConfigVariable `key:"smtpPort"`
|
||||
SmtpFrom AppConfigVariable `key:"smtpFrom"`
|
||||
SmtpUser AppConfigVariable `key:"smtpUser"`
|
||||
SmtpPassword AppConfigVariable `key:"smtpPassword"`
|
||||
SmtpTls AppConfigVariable `key:"smtpTls"`
|
||||
SmtpSkipCertVerify AppConfigVariable `key:"smtpSkipCertVerify"`
|
||||
EmailLoginNotificationEnabled AppConfigVariable `key:"emailLoginNotificationEnabled"`
|
||||
EmailOneTimeAccessEnabled AppConfigVariable `key:"emailOneTimeAccessEnabled,public"` // Public
|
||||
SmtpHost AppConfigVariable `key:"smtpHost"`
|
||||
SmtpPort AppConfigVariable `key:"smtpPort"`
|
||||
SmtpFrom AppConfigVariable `key:"smtpFrom"`
|
||||
SmtpUser AppConfigVariable `key:"smtpUser"`
|
||||
SmtpPassword AppConfigVariable `key:"smtpPassword"`
|
||||
SmtpTls AppConfigVariable `key:"smtpTls"`
|
||||
SmtpSkipCertVerify AppConfigVariable `key:"smtpSkipCertVerify"`
|
||||
EmailLoginNotificationEnabled AppConfigVariable `key:"emailLoginNotificationEnabled"`
|
||||
EmailOneTimeAccessAsUnauthenticatedEnabled AppConfigVariable `key:"emailOneTimeAccessAsUnauthenticatedEnabled,public"` // Public
|
||||
EmailOneTimeAccessAsAdminEnabled AppConfigVariable `key:"emailOneTimeAccessAsAdminEnabled,public"` // Public
|
||||
EmailApiKeyExpirationEnabled AppConfigVariable `key:"emailApiKeyExpirationEnabled"`
|
||||
// LDAP
|
||||
LdapEnabled AppConfigVariable `key:"ldapEnabled,public"` // Public
|
||||
LdapUrl AppConfigVariable `key:"ldapUrl"`
|
||||
@@ -77,7 +79,7 @@ func (c *AppConfig) ToAppConfigVariableSlice(showAll bool) []AppConfigVariable {
|
||||
cfgValue := reflect.ValueOf(c).Elem()
|
||||
cfgType := cfgValue.Type()
|
||||
|
||||
res := make([]AppConfigVariable, cfgType.NumField())
|
||||
var res []AppConfigVariable
|
||||
|
||||
for i := range cfgType.NumField() {
|
||||
field := cfgType.Field(i)
|
||||
@@ -94,10 +96,12 @@ func (c *AppConfig) ToAppConfigVariableSlice(showAll bool) []AppConfigVariable {
|
||||
|
||||
fieldValue := cfgValue.Field(i)
|
||||
|
||||
res[i] = AppConfigVariable{
|
||||
appConfigVariable := AppConfigVariable{
|
||||
Key: key,
|
||||
Value: fieldValue.FieldByName("Value").String(),
|
||||
}
|
||||
|
||||
res = append(res, appConfigVariable)
|
||||
}
|
||||
|
||||
return res
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils/email"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||
@@ -16,11 +17,12 @@ import (
|
||||
)
|
||||
|
||||
type ApiKeyService struct {
|
||||
db *gorm.DB
|
||||
db *gorm.DB
|
||||
emailService *EmailService
|
||||
}
|
||||
|
||||
func NewApiKeyService(db *gorm.DB) *ApiKeyService {
|
||||
return &ApiKeyService{db: db}
|
||||
func NewApiKeyService(db *gorm.DB, emailService *EmailService) *ApiKeyService {
|
||||
return &ApiKeyService{db: db, emailService: emailService}
|
||||
}
|
||||
|
||||
func (s *ApiKeyService) ListApiKeys(ctx context.Context, userID string, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.ApiKey, utils.PaginationResponse, error) {
|
||||
@@ -117,3 +119,47 @@ func (s *ApiKeyService) ValidateApiKey(ctx context.Context, apiKey string) (mode
|
||||
|
||||
return key.User, nil
|
||||
}
|
||||
|
||||
func (s *ApiKeyService) ListExpiringApiKeys(ctx context.Context, daysAhead int) ([]model.ApiKey, error) {
|
||||
var keys []model.ApiKey
|
||||
now := time.Now()
|
||||
cutoff := now.AddDate(0, 0, daysAhead)
|
||||
|
||||
err := s.db.
|
||||
WithContext(ctx).
|
||||
Preload("User").
|
||||
Where("expires_at > ? AND expires_at <= ? AND expiration_email_sent = ?", datatype.DateTime(now), datatype.DateTime(cutoff), false).
|
||||
Find(&keys).
|
||||
Error
|
||||
|
||||
return keys, err
|
||||
}
|
||||
|
||||
func (s *ApiKeyService) SendApiKeyExpiringSoonEmail(ctx context.Context, apiKey model.ApiKey) error {
|
||||
user := apiKey.User
|
||||
|
||||
if user.ID == "" {
|
||||
if err := s.db.WithContext(ctx).First(&user, "id = ?", apiKey.UserID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err := SendEmail(ctx, s.emailService, email.Address{
|
||||
Name: user.FullName(),
|
||||
Email: user.Email,
|
||||
}, ApiKeyExpiringSoonTemplate, &ApiKeyExpiringSoonTemplateData{
|
||||
ApiKeyName: apiKey.Name,
|
||||
ExpiresAt: apiKey.ExpiresAt.ToTime(),
|
||||
Name: user.FirstName,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Mark the API key as having had an expiration email sent
|
||||
return s.db.WithContext(ctx).
|
||||
Model(&model.ApiKey{}).
|
||||
Where("id = ?", apiKey.ID).
|
||||
Update("expiration_email_sent", true).
|
||||
Error
|
||||
}
|
||||
|
||||
@@ -73,7 +73,9 @@ func (s *AppConfigService) getDefaultDbConfig() *model.AppConfig {
|
||||
SmtpTls: model.AppConfigVariable{Value: "none"},
|
||||
SmtpSkipCertVerify: model.AppConfigVariable{Value: "false"},
|
||||
EmailLoginNotificationEnabled: model.AppConfigVariable{Value: "false"},
|
||||
EmailOneTimeAccessEnabled: model.AppConfigVariable{Value: "false"},
|
||||
EmailOneTimeAccessAsUnauthenticatedEnabled: model.AppConfigVariable{Value: "false"},
|
||||
EmailOneTimeAccessAsAdminEnabled: model.AppConfigVariable{Value: "false"},
|
||||
EmailApiKeyExpirationEnabled: model.AppConfigVariable{Value: "false"},
|
||||
// LDAP
|
||||
LdapEnabled: model.AppConfigVariable{Value: "false"},
|
||||
LdapUrl: model.AppConfigVariable{},
|
||||
@@ -151,11 +153,6 @@ func (s *AppConfigService) UpdateAppConfig(ctx context.Context, input dto.AppCon
|
||||
return nil, &common.UiConfigDisabledError{}
|
||||
}
|
||||
|
||||
// If EmailLoginNotificationEnabled is set to false (explicitly), disable the EmailOneTimeAccessEnabled
|
||||
if input.EmailLoginNotificationEnabled == "false" {
|
||||
input.EmailOneTimeAccessEnabled = "false"
|
||||
}
|
||||
|
||||
// Start the transaction
|
||||
tx, err := s.updateAppConfigStartTransaction(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -447,44 +447,6 @@ func TestUpdateAppConfig(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("auto disables EmailOneTimeAccessEnabled when EmailLoginNotificationEnabled is false", func(t *testing.T) {
|
||||
db := newAppConfigTestDatabaseForTest(t)
|
||||
|
||||
// Create a service with default config
|
||||
service := &AppConfigService{
|
||||
db: db,
|
||||
}
|
||||
err := service.LoadDbConfig(t.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
// First enable both settings
|
||||
err = service.UpdateAppConfigValues(t.Context(),
|
||||
"emailLoginNotificationEnabled", "true",
|
||||
"emailOneTimeAccessEnabled", "true",
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify both are enabled
|
||||
config := service.GetDbConfig()
|
||||
require.True(t, config.EmailLoginNotificationEnabled.IsTrue())
|
||||
require.True(t, config.EmailOneTimeAccessEnabled.IsTrue())
|
||||
|
||||
// Now disable EmailLoginNotificationEnabled
|
||||
input := dto.AppConfigUpdateDto{
|
||||
EmailLoginNotificationEnabled: "false",
|
||||
// Don't set EmailOneTimeAccessEnabled, it should be auto-disabled
|
||||
}
|
||||
|
||||
// Update config
|
||||
_, err = service.UpdateAppConfig(t.Context(), input)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify EmailOneTimeAccessEnabled was automatically disabled
|
||||
config = service.GetDbConfig()
|
||||
require.False(t, config.EmailLoginNotificationEnabled.IsTrue())
|
||||
require.False(t, config.EmailOneTimeAccessEnabled.IsTrue())
|
||||
})
|
||||
|
||||
t.Run("cannot update when UiConfigDisabled is true", func(t *testing.T) {
|
||||
// Save the original state and restore it after the test
|
||||
originalUiConfigDisabled := common.EnvConfig.UiConfigDisabled
|
||||
|
||||
@@ -90,7 +90,7 @@ func (s *AuditLogService) CreateNewSignInWithEmail(ctx context.Context, ipAddres
|
||||
}
|
||||
|
||||
innerErr = SendEmail(innerCtx, s.emailService, email.Address{
|
||||
Name: user.Username,
|
||||
Name: user.FullName(),
|
||||
Email: user.Email,
|
||||
}, NewLoginTemplate, &NewLoginTemplateData{
|
||||
IPAddress: ipAddress,
|
||||
|
||||
@@ -104,10 +104,10 @@ func SendEmail[V any](ctx context.Context, srv *EmailService, toEmail email.Addr
|
||||
// so we use the domain of the from address instead (the same as Thunderbird does)
|
||||
// if the address does not have an @ (which would be unusual), we use hostname
|
||||
|
||||
from_address := dbConfig.SmtpFrom.Value
|
||||
fromAddress := dbConfig.SmtpFrom.Value
|
||||
domain := ""
|
||||
if strings.Contains(from_address, "@") {
|
||||
domain = strings.Split(from_address, "@")[1]
|
||||
if strings.Contains(fromAddress, "@") {
|
||||
domain = strings.Split(fromAddress, "@")[1]
|
||||
} else {
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
|
||||
@@ -42,6 +42,13 @@ var TestTemplate = email.Template[struct{}]{
|
||||
},
|
||||
}
|
||||
|
||||
var ApiKeyExpiringSoonTemplate = email.Template[ApiKeyExpiringSoonTemplateData]{
|
||||
Path: "api-key-expiring-soon",
|
||||
Title: func(data *email.TemplateData[ApiKeyExpiringSoonTemplateData]) string {
|
||||
return fmt.Sprintf("API Key \"%s\" Expiring Soon", data.Data.ApiKeyName)
|
||||
},
|
||||
}
|
||||
|
||||
type NewLoginTemplateData struct {
|
||||
IPAddress string
|
||||
Country string
|
||||
@@ -54,7 +61,14 @@ type OneTimeAccessTemplateData = struct {
|
||||
Code string
|
||||
LoginLink string
|
||||
LoginLinkWithCode string
|
||||
ExpirationString string
|
||||
}
|
||||
|
||||
type ApiKeyExpiringSoonTemplateData struct {
|
||||
Name string
|
||||
ApiKeyName string
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
// this is list of all template paths used for preloading templates
|
||||
var emailTemplatesPaths = []string{NewLoginTemplate.Path, OneTimeAccessTemplate.Path, TestTemplate.Path}
|
||||
var emailTemplatesPaths = []string{NewLoginTemplate.Path, OneTimeAccessTemplate.Path, TestTemplate.Path, ApiKeyExpiringSoonTemplate.Path}
|
||||
|
||||
@@ -348,23 +348,24 @@ func (s *UserService) updateUserInternal(ctx context.Context, userID string, upd
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *UserService) RequestOneTimeAccessEmail(ctx context.Context, emailAddress, redirectPath string) error {
|
||||
tx := s.db.Begin()
|
||||
defer func() {
|
||||
tx.Rollback()
|
||||
}()
|
||||
|
||||
isDisabled := !s.appConfigService.GetDbConfig().EmailOneTimeAccessEnabled.IsTrue()
|
||||
func (s *UserService) RequestOneTimeAccessEmailAsAdmin(ctx context.Context, userID string, expiration time.Time) error {
|
||||
isDisabled := !s.appConfigService.GetDbConfig().EmailOneTimeAccessAsAdminEnabled.IsTrue()
|
||||
if isDisabled {
|
||||
return &common.OneTimeAccessDisabledError{}
|
||||
}
|
||||
|
||||
var user model.User
|
||||
err := tx.
|
||||
WithContext(ctx).
|
||||
Where("email = ?", emailAddress).
|
||||
First(&user).
|
||||
Error
|
||||
return s.requestOneTimeAccessEmailInternal(ctx, userID, "", expiration)
|
||||
|
||||
}
|
||||
|
||||
func (s *UserService) RequestOneTimeAccessEmailAsUnauthenticatedUser(ctx context.Context, userID, redirectPath string) error {
|
||||
isDisabled := !s.appConfigService.GetDbConfig().EmailOneTimeAccessAsUnauthenticatedEnabled.IsTrue()
|
||||
if isDisabled {
|
||||
return &common.OneTimeAccessDisabledError{}
|
||||
}
|
||||
|
||||
var userId string
|
||||
err := s.db.Model(&model.User{}).Select("id").Where("email = ?", userID).First(&userId).Error
|
||||
if err != nil {
|
||||
// Do not return error if user not found to prevent email enumeration
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@@ -374,7 +375,22 @@ func (s *UserService) RequestOneTimeAccessEmail(ctx context.Context, emailAddres
|
||||
}
|
||||
}
|
||||
|
||||
oneTimeAccessToken, err := s.createOneTimeAccessTokenInternal(ctx, user.ID, time.Now().Add(15*time.Minute), tx)
|
||||
expiration := time.Now().Add(15 * time.Minute)
|
||||
return s.requestOneTimeAccessEmailInternal(ctx, userId, redirectPath, expiration)
|
||||
}
|
||||
|
||||
func (s *UserService) requestOneTimeAccessEmailInternal(ctx context.Context, userID, redirectPath string, expiration time.Time) error {
|
||||
tx := s.db.Begin()
|
||||
defer func() {
|
||||
tx.Rollback()
|
||||
}()
|
||||
|
||||
user, err := s.GetUser(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
oneTimeAccessToken, err := s.createOneTimeAccessTokenInternal(ctx, user.ID, expiration, tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -399,12 +415,13 @@ func (s *UserService) RequestOneTimeAccessEmail(ctx context.Context, emailAddres
|
||||
}
|
||||
|
||||
errInternal := SendEmail(innerCtx, s.emailService, email.Address{
|
||||
Name: user.Username,
|
||||
Name: user.FullName(),
|
||||
Email: user.Email,
|
||||
}, OneTimeAccessTemplate, &OneTimeAccessTemplateData{
|
||||
Code: oneTimeAccessToken,
|
||||
LoginLink: link,
|
||||
LoginLinkWithCode: linkWithCode,
|
||||
ExpirationString: utils.DurationToString(time.Until(expiration).Round(time.Second)),
|
||||
})
|
||||
if errInternal != nil {
|
||||
log.Printf("Failed to send email to '%s': %v\n", user.Email, errInternal)
|
||||
|
||||
52
backend/internal/utils/date_time_util.go
Normal file
52
backend/internal/utils/date_time_util.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DurationToString converts a time.Duration to a human-readable string. Respects minutes, hours and days.
|
||||
func DurationToString(duration time.Duration) string {
|
||||
// For a duration less than a day
|
||||
if duration < 24*time.Hour {
|
||||
hours := int(duration.Hours())
|
||||
mins := int(duration.Minutes()) % 60
|
||||
|
||||
switch hours {
|
||||
case 0:
|
||||
return fmt.Sprintf("%d minutes", mins)
|
||||
case 1:
|
||||
if mins == 0 {
|
||||
return "1 hour"
|
||||
}
|
||||
return fmt.Sprintf("1 hour and %d minutes", mins)
|
||||
default:
|
||||
if mins == 0 {
|
||||
return fmt.Sprintf("%d hours", hours)
|
||||
}
|
||||
return fmt.Sprintf("%d hours and %d minutes", hours, mins)
|
||||
}
|
||||
} else {
|
||||
// For durations of a day or more
|
||||
days := int(duration.Hours() / 24)
|
||||
hours := int(duration.Hours()) % 24
|
||||
|
||||
switch hours {
|
||||
case 0:
|
||||
if days == 1 {
|
||||
return "1 day"
|
||||
}
|
||||
return fmt.Sprintf("%d days", days)
|
||||
case 1:
|
||||
if days == 1 {
|
||||
return "1 day and 1 hour"
|
||||
}
|
||||
return fmt.Sprintf("%d days and 1 hour", days)
|
||||
default:
|
||||
if days == 1 {
|
||||
return fmt.Sprintf("1 day and %d hours", hours)
|
||||
}
|
||||
return fmt.Sprintf("%d days and %d hours", days, hours)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{{ define "base" }}
|
||||
<div class="header">
|
||||
<div class="logo">
|
||||
<img src="{{ .LogoURL }}" alt="{{ .AppName }}" width="32" height="32" style="width: 32px; height: 32px; max-width: 32px;"/>
|
||||
<h1>{{ .AppName }}</h1>
|
||||
</div>
|
||||
<div class="warning">Warning</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h2>API Key Expiring Soon</h2>
|
||||
<p>
|
||||
Hello {{ .Data.Name }},<br/><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>.<br/><br/>
|
||||
Please generate a new API key if you need continued access.
|
||||
</p>
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -0,0 +1,10 @@
|
||||
{{ define "base" -}}
|
||||
API Key Expiring Soon
|
||||
====================
|
||||
|
||||
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" }}.
|
||||
|
||||
Please generate a new API key if you need continued access.
|
||||
{{ end -}}
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="content">
|
||||
<h2>Login Code</h2>
|
||||
<p class="message">
|
||||
Click the button below to sign in to {{ .AppName }} with a login code.</br>Or visit <a href="{{ .Data.LoginLink }}">{{ .Data.LoginLink }}</a> and enter the code <strong>{{ .Data.Code }}</strong>.</br></br>This code expires in 15 minutes.
|
||||
Click the button below to sign in to {{ .AppName }} with a login code.</br>Or visit <a href="{{ .Data.LoginLink }}">{{ .Data.LoginLink }}</a> and enter the code <strong>{{ .Data.Code }}</strong>.</br></br>This code expires in {{.Data.ExpirationString}}.
|
||||
</p>
|
||||
<div class="button-container">
|
||||
<a class="button" href="{{ .Data.LoginLinkWithCode }}" class="button">Sign In</a>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Login Code
|
||||
====================
|
||||
|
||||
Click the link below to sign in to {{ .AppName }} with a login code. This code expires in 15 minutes.
|
||||
Click the link below to sign in to {{ .AppName }} with a login code. This code expires in {{.Data.ExpirationString}}.
|
||||
|
||||
{{ .Data.LoginLinkWithCode }}
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE api_keys
|
||||
DROP COLUMN IF EXISTS expiration_email_sent;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE api_keys
|
||||
ADD COLUMN expiration_email_sent BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE api_keys
|
||||
DROP COLUMN expiration_email_sent;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE api_keys
|
||||
ADD COLUMN expiration_email_sent BOOLEAN NOT NULL DEFAULT 0;
|
||||
@@ -156,7 +156,7 @@
|
||||
"actions": "Akce",
|
||||
"images_updated_successfully": "Obrázky úspěšně aktualizovány",
|
||||
"general": "Obecné",
|
||||
"enable_email_notifications_to_alert_users_when_a_login_is_detected_from_a_new_device_or_location": "Povolte e-mailová oznámení pro upozornění uživatelů, pokud je zjištěno přihlášení z nového zařízení nebo umístění.",
|
||||
"configure_smtp_to_send_emails": "Enable email notifications to alert users when a login is detected from a new device or location.",
|
||||
"ldap": "LDAP",
|
||||
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Nastavte LDAP pro synchronizaci uživatelů a skupin z LDAP serveru.",
|
||||
"images": "Obrázky",
|
||||
@@ -180,7 +180,10 @@
|
||||
"enabled_emails": "Povolené e-maily",
|
||||
"email_login_notification": "E-mailovová oznámení o přihlášení",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Poslat uživateli e-mail, když se přihlásí z nového zařízení.",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Umožňuje uživatelům přihlásit se pomocí přihlašovacího kódu, který je odeslán na jejich e-mail. To výrazně snižuje bezpečnost, protože každý, kdo má přístup k e-mailu uživatele, může získat vstup.",
|
||||
"emai_login_code_requested_by_user": "Email Login Code Requested by User",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to bypass passkeys by requesting a login code sent to their email. This reduces the security significantly as anyone with access to the user's email can gain entry.",
|
||||
"email_login_code_from_admin": "Email Login Code from Admin",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "Allows an admin to send a login code to the user via email.",
|
||||
"send_test_email": "Odeslat testovací e-mail",
|
||||
"application_configuration_updated_successfully": "Nastavení aplikace bylo úspěšně aktualizováno",
|
||||
"application_name": "Název aplikace",
|
||||
@@ -333,5 +336,11 @@
|
||||
"disable_firstname_lastname": "Disable {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_disable_this_user": "Are you sure you want to disable this user? They will not be able to log in or access any services.",
|
||||
"ldap_soft_delete_users": "Keep disabled users from LDAP.",
|
||||
"ldap_soft_delete_users_description": "When enabled, users removed from LDAP will be disabled rather than deleted from the system."
|
||||
"ldap_soft_delete_users_description": "When enabled, users removed from LDAP will be disabled rather than deleted from the system.",
|
||||
"login_code_email_success": "The login code has been sent to the user.",
|
||||
"send_email": "Send Email",
|
||||
"show_code": "Show Code",
|
||||
"callback_url_description": "URL(s) provided by your client. Wildcards (*) are supported, but best avoided for better security.",
|
||||
"api_key_expiration": "API Key Expiration",
|
||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Send an email to the user when their API key is about to expire."
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
"actions": "Aktionen",
|
||||
"images_updated_successfully": "Bild erfolgreich aktualisiert",
|
||||
"general": "Allgemein",
|
||||
"enable_email_notifications_to_alert_users_when_a_login_is_detected_from_a_new_device_or_location": "Aktiviere E-Mail Benachrichtigungen, um Benutzer zu informieren, wenn ein Login von einem neuen Gerät oder Standort erkannt wird.",
|
||||
"configure_smtp_to_send_emails": "Enable email notifications to alert users when a login is detected from a new device or location.",
|
||||
"ldap": "LDAP",
|
||||
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Konfiguriere LDAP-Einstellungen, um Benutzer und Gruppen von einem LDAP-Server zu synchronisieren.",
|
||||
"images": "Bilder",
|
||||
@@ -180,7 +180,10 @@
|
||||
"enabled_emails": "E-Mails aktivieren",
|
||||
"email_login_notification": "E-Mail Benachrichtigung bei Login",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Sende dem Benutzer eine E-Mail, wenn er sich von einem neuen Gerät aus anmeldet.",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Ermöglicht Benutzer, sich mit einem Login-Code anzumelden, der an ihre E-Mail gesendet wurde. Dies reduziert die Sicherheit erheblich, da jeder, der Zugriff auf die E-Mail des Benutzers hat, Zugang bekommen kann.",
|
||||
"emai_login_code_requested_by_user": "Email Login Code Requested by User",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to bypass passkeys by requesting a login code sent to their email. This reduces the security significantly as anyone with access to the user's email can gain entry.",
|
||||
"email_login_code_from_admin": "Email Login Code from Admin",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "Allows an admin to send a login code to the user via email.",
|
||||
"send_test_email": "Test-E-Mail senden",
|
||||
"application_configuration_updated_successfully": "Anwendungskonfiguration erfolgreich aktualisiert",
|
||||
"application_name": "Anwendungsname",
|
||||
@@ -333,5 +336,11 @@
|
||||
"disable_firstname_lastname": "Disable {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_disable_this_user": "Are you sure you want to disable this user? They will not be able to log in or access any services.",
|
||||
"ldap_soft_delete_users": "Keep disabled users from LDAP.",
|
||||
"ldap_soft_delete_users_description": "When enabled, users removed from LDAP will be disabled rather than deleted from the system."
|
||||
"ldap_soft_delete_users_description": "When enabled, users removed from LDAP will be disabled rather than deleted from the system.",
|
||||
"login_code_email_success": "The login code has been sent to the user.",
|
||||
"send_email": "Send Email",
|
||||
"show_code": "Show Code",
|
||||
"callback_url_description": "URL(s) provided by your client. Wildcards (*) are supported, but best avoided for better security.",
|
||||
"api_key_expiration": "API Key Expiration",
|
||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Send an email to the user when their API key is about to expire."
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
"actions": "Actions",
|
||||
"images_updated_successfully": "Images updated successfully",
|
||||
"general": "General",
|
||||
"enable_email_notifications_to_alert_users_when_a_login_is_detected_from_a_new_device_or_location": "Enable email notifications to alert users when a login is detected from a new device or location.",
|
||||
"configure_smtp_to_send_emails": "Enable email notifications to alert users when a login is detected from a new device or location.",
|
||||
"ldap": "LDAP",
|
||||
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Configure LDAP settings to sync users and groups from an LDAP server.",
|
||||
"images": "Images",
|
||||
@@ -180,7 +180,10 @@
|
||||
"enabled_emails": "Enabled Emails",
|
||||
"email_login_notification": "Email Login Notification",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Send an email to the user when they log in from a new device.",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to sign in with a login code sent to their email. This reduces the security significantly as anyone with access to the user's email can gain entry.",
|
||||
"emai_login_code_requested_by_user": "Email Login Code Requested by User",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to bypass passkeys by requesting a login code sent to their email. This reduces the security significantly as anyone with access to the user's email can gain entry.",
|
||||
"email_login_code_from_admin": "Email Login Code from Admin",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "Allows an admin to send a login code to the user via email.",
|
||||
"send_test_email": "Send test email",
|
||||
"application_configuration_updated_successfully": "Application configuration updated successfully",
|
||||
"application_name": "Application Name",
|
||||
@@ -333,5 +336,11 @@
|
||||
"disable_firstname_lastname": "Disable {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_disable_this_user": "Are you sure you want to disable this user? They will not be able to log in or access any services.",
|
||||
"ldap_soft_delete_users": "Keep disabled users from LDAP.",
|
||||
"ldap_soft_delete_users_description": "When enabled, users removed from LDAP will be disabled rather than deleted from the system."
|
||||
"ldap_soft_delete_users_description": "When enabled, users removed from LDAP will be disabled rather than deleted from the system.",
|
||||
"login_code_email_success": "The login code has been sent to the user.",
|
||||
"send_email": "Send Email",
|
||||
"show_code": "Show Code",
|
||||
"callback_url_description": "URL(s) provided by your client. Wildcards (*) are supported, but best avoided for better security.",
|
||||
"api_key_expiration": "API Key Expiration",
|
||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Send an email to the user when their API key is about to expire."
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
"actions": "Actions",
|
||||
"images_updated_successfully": "Images updated successfully",
|
||||
"general": "General",
|
||||
"enable_email_notifications_to_alert_users_when_a_login_is_detected_from_a_new_device_or_location": "Enable email notifications to alert users when a login is detected from a new device or location.",
|
||||
"configure_smtp_to_send_emails": "Enable email notifications to alert users when a login is detected from a new device or location.",
|
||||
"ldap": "LDAP",
|
||||
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Configure LDAP settings to sync users and groups from an LDAP server.",
|
||||
"images": "Images",
|
||||
@@ -180,7 +180,10 @@
|
||||
"enabled_emails": "Enabled Emails",
|
||||
"email_login_notification": "Email Login Notification",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Send an email to the user when they log in from a new device.",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to sign in with a login code sent to their email. This reduces the security significantly as anyone with access to the user's email can gain entry.",
|
||||
"emai_login_code_requested_by_user": "Email Login Code Requested by User",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to bypass passkeys by requesting a login code sent to their email. This reduces the security significantly as anyone with access to the user's email can gain entry.",
|
||||
"email_login_code_from_admin": "Email Login Code from Admin",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "Allows an admin to send a login code to the user via email.",
|
||||
"send_test_email": "Send test email",
|
||||
"application_configuration_updated_successfully": "Application configuration updated successfully",
|
||||
"application_name": "Application Name",
|
||||
@@ -333,5 +336,11 @@
|
||||
"disable_firstname_lastname": "Disable {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_disable_this_user": "Are you sure you want to disable this user? They will not be able to log in or access any services.",
|
||||
"ldap_soft_delete_users": "Keep disabled users from LDAP.",
|
||||
"ldap_soft_delete_users_description": "When enabled, users removed from LDAP will be disabled rather than deleted from the system."
|
||||
"ldap_soft_delete_users_description": "When enabled, users removed from LDAP will be disabled rather than deleted from the system.",
|
||||
"login_code_email_success": "The login code has been sent to the user.",
|
||||
"send_email": "Send Email",
|
||||
"show_code": "Show Code",
|
||||
"callback_url_description": "URL(s) provided by your client. Wildcards (*) are supported, but best avoided for better security.",
|
||||
"api_key_expiration": "API Key Expiration",
|
||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Send an email to the user when their API key is about to expire."
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
"actions": "Actions",
|
||||
"images_updated_successfully": "Image mise à jour avec succès",
|
||||
"general": "Général",
|
||||
"enable_email_notifications_to_alert_users_when_a_login_is_detected_from_a_new_device_or_location": "Activer les notifications par e-mail pour alerter les utilisateurs lorsqu'une connexion est détecté à partir d'un nouvel appareil ou d'un nouvel emplacement.",
|
||||
"configure_smtp_to_send_emails": "Enable email notifications to alert users when a login is detected from a new device or location.",
|
||||
"ldap": "LDAP",
|
||||
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Configurer les paramètres LDAP pour synchroniser les utilisateurs et les groupes à partir d'un serveur LDAP.",
|
||||
"images": "Images",
|
||||
@@ -180,7 +180,10 @@
|
||||
"enabled_emails": "Emails activés",
|
||||
"email_login_notification": "Notification de connexion par e-mail",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Envoyer un email à l'utilisateur lorsqu'il se connecte à partir d'un nouvel appareil.",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Permet aux utilisateurs de se connecter avec un code de connexion envoyé à leur adresse e-mail. Cela réduit considérablement la sécurité car toute personne ayant accès à l'e-mail de l'utilisateur peuvent se connecter.",
|
||||
"emai_login_code_requested_by_user": "Email Login Code Requested by User",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to bypass passkeys by requesting a login code sent to their email. This reduces the security significantly as anyone with access to the user's email can gain entry.",
|
||||
"email_login_code_from_admin": "Email Login Code from Admin",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "Allows an admin to send a login code to the user via email.",
|
||||
"send_test_email": "",
|
||||
"application_configuration_updated_successfully": "Mise à jour de l'application avec succès",
|
||||
"application_name": "Nom de l'application",
|
||||
@@ -333,5 +336,11 @@
|
||||
"disable_firstname_lastname": "Disable {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_disable_this_user": "Are you sure you want to disable this user? They will not be able to log in or access any services.",
|
||||
"ldap_soft_delete_users": "Keep disabled users from LDAP.",
|
||||
"ldap_soft_delete_users_description": "When enabled, users removed from LDAP will be disabled rather than deleted from the system."
|
||||
"ldap_soft_delete_users_description": "When enabled, users removed from LDAP will be disabled rather than deleted from the system.",
|
||||
"login_code_email_success": "The login code has been sent to the user.",
|
||||
"send_email": "Send Email",
|
||||
"show_code": "Show Code",
|
||||
"callback_url_description": "URL(s) provided by your client. Wildcards (*) are supported, but best avoided for better security.",
|
||||
"api_key_expiration": "API Key Expiration",
|
||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Send an email to the user when their API key is about to expire."
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
"actions": "Azioni",
|
||||
"images_updated_successfully": "Immagini aggiornate con successo",
|
||||
"general": "Generale",
|
||||
"enable_email_notifications_to_alert_users_when_a_login_is_detected_from_a_new_device_or_location": "Abilita le notifiche email per avvisare gli utenti quando viene rilevato un accesso da un nuovo dispositivo o posizione.",
|
||||
"configure_smtp_to_send_emails": "Enable email notifications to alert users when a login is detected from a new device or location.",
|
||||
"ldap": "LDAP",
|
||||
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Configura le impostazioni LDAP per sincronizzare utenti e gruppi da un server LDAP.",
|
||||
"images": "Immagini",
|
||||
@@ -180,7 +180,10 @@
|
||||
"enabled_emails": "Email Abilitate",
|
||||
"email_login_notification": "Notifica Accesso Email",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Invia un'email all'utente quando accede da un nuovo dispositivo.",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Consente agli utenti di accedere con un codice di accesso inviato alla loro email. Questo riduce significativamente la sicurezza poiché chiunque abbia accesso all'email dell'utente può ottenere l'accesso.",
|
||||
"emai_login_code_requested_by_user": "Email Login Code Requested by User",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to bypass passkeys by requesting a login code sent to their email. This reduces the security significantly as anyone with access to the user's email can gain entry.",
|
||||
"email_login_code_from_admin": "Email Login Code from Admin",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "Allows an admin to send a login code to the user via email.",
|
||||
"send_test_email": "Invia email di prova",
|
||||
"application_configuration_updated_successfully": "Configurazione dell'applicazione aggiornata con successo",
|
||||
"application_name": "Nome dell'applicazione",
|
||||
@@ -324,14 +327,20 @@
|
||||
"client_authorization": "Autorizzazione client",
|
||||
"new_client_authorization": "Nuova autorizzazione client",
|
||||
"disable_animations": "Disabilita animazioni",
|
||||
"turn_off_all_animations_throughout_the_admin_ui": "Turn off all animations throughout the Admin UI.",
|
||||
"user_disabled": "Account Disabled",
|
||||
"disabled_users_cannot_log_in_or_use_services": "Disabled users cannot log in or use services.",
|
||||
"user_disabled_successfully": "User has been disabled successfully.",
|
||||
"user_enabled_successfully": "User has been enabled successfully.",
|
||||
"status": "Status",
|
||||
"disable_firstname_lastname": "Disable {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_disable_this_user": "Are you sure you want to disable this user? They will not be able to log in or access any services.",
|
||||
"ldap_soft_delete_users": "Keep disabled users from LDAP.",
|
||||
"ldap_soft_delete_users_description": "When enabled, users removed from LDAP will be disabled rather than deleted from the system."
|
||||
"turn_off_all_animations_throughout_the_admin_ui": "Disattiva tutte le animazioni nell'interfaccia di amministrazione.",
|
||||
"user_disabled": "Account disabilitato",
|
||||
"disabled_users_cannot_log_in_or_use_services": "Gli utenti disabilitati non possono accedere o utilizzare i servizi.",
|
||||
"user_disabled_successfully": "Utente disabilitato con successo.",
|
||||
"user_enabled_successfully": "Utente abilitato con successo.",
|
||||
"status": "Stato",
|
||||
"disable_firstname_lastname": "Disabilita {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_disable_this_user": "Sei sicuro di voler disabilitare questo utente? Non sarà in grado di accedere o utilizzare qualsiasi servizio.",
|
||||
"ldap_soft_delete_users": "Mantieni gli utenti disabilitati da LDAP.",
|
||||
"ldap_soft_delete_users_description": "Se abilitato, gli utenti rimossi da LDAP saranno disabilitati invece che cancellati dal sistema.",
|
||||
"login_code_email_success": "The login code has been sent to the user.",
|
||||
"send_email": "Send Email",
|
||||
"show_code": "Show Code",
|
||||
"callback_url_description": "URL(s) provided by your client. Wildcards (*) are supported, but best avoided for better security.",
|
||||
"api_key_expiration": "API Key Expiration",
|
||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Send an email to the user when their API key is about to expire."
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
"actions": "Acties",
|
||||
"images_updated_successfully": "Afbeeldingen succesvol bijgewerkt",
|
||||
"general": "Algemeen",
|
||||
"enable_email_notifications_to_alert_users_when_a_login_is_detected_from_a_new_device_or_location": "Schakel e-mailmeldingen in om gebruikers te waarschuwen wanneer er wordt ingelogd vanaf een nieuw apparaat of een nieuwe locatie.",
|
||||
"configure_smtp_to_send_emails": "Enable email notifications to alert users when a login is detected from a new device or location.",
|
||||
"ldap": "LDAP",
|
||||
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Configureer LDAP-instellingen om gebruikers en groepen vanaf een LDAP-server te synchroniseren.",
|
||||
"images": "Afbeeldingen",
|
||||
@@ -180,7 +180,10 @@
|
||||
"enabled_emails": "Ingeschakelde e-mails",
|
||||
"email_login_notification": "E-mail-inlogmelding",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Stuur een e-mail naar de gebruiker wanneer deze zich aanmeldt vanaf een nieuw apparaat.",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Hiermee kunnen gebruikers inloggen met een inlogcode die naar hun e-mail is gestuurd. Dit vermindert de beveiliging aanzienlijk, omdat iedereen met toegang tot de e-mail van de gebruiker toegang kan krijgen.",
|
||||
"emai_login_code_requested_by_user": "Email Login Code Requested by User",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to bypass passkeys by requesting a login code sent to their email. This reduces the security significantly as anyone with access to the user's email can gain entry.",
|
||||
"email_login_code_from_admin": "Email Login Code from Admin",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "Allows an admin to send a login code to the user via email.",
|
||||
"send_test_email": "Test-e-mail verzenden",
|
||||
"application_configuration_updated_successfully": "Toepassingsconfiguratie succesvol bijgewerkt",
|
||||
"application_name": "Toepassingsnaam",
|
||||
@@ -333,5 +336,11 @@
|
||||
"disable_firstname_lastname": "Disable {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_disable_this_user": "Are you sure you want to disable this user? They will not be able to log in or access any services.",
|
||||
"ldap_soft_delete_users": "Keep disabled users from LDAP.",
|
||||
"ldap_soft_delete_users_description": "When enabled, users removed from LDAP will be disabled rather than deleted from the system."
|
||||
"ldap_soft_delete_users_description": "When enabled, users removed from LDAP will be disabled rather than deleted from the system.",
|
||||
"login_code_email_success": "The login code has been sent to the user.",
|
||||
"send_email": "Send Email",
|
||||
"show_code": "Show Code",
|
||||
"callback_url_description": "URL(s) provided by your client. Wildcards (*) are supported, but best avoided for better security.",
|
||||
"api_key_expiration": "API Key Expiration",
|
||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Send an email to the user when their API key is about to expire."
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
"actions": "Ações",
|
||||
"images_updated_successfully": "Imagens atualizadas com sucesso",
|
||||
"general": "Geral",
|
||||
"enable_email_notifications_to_alert_users_when_a_login_is_detected_from_a_new_device_or_location": "Enable email notifications to alert users when a login is detected from a new device or location.",
|
||||
"configure_smtp_to_send_emails": "Enable email notifications to alert users when a login is detected from a new device or location.",
|
||||
"ldap": "LDAP",
|
||||
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Configure LDAP settings to sync users and groups from an LDAP server.",
|
||||
"images": "Imagens",
|
||||
@@ -180,7 +180,10 @@
|
||||
"enabled_emails": "Enabled Emails",
|
||||
"email_login_notification": "Email Login Notification",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Send an email to the user when they log in from a new device.",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to sign in with a login code sent to their email. This reduces the security significantly as anyone with access to the user's email can gain entry.",
|
||||
"emai_login_code_requested_by_user": "Email Login Code Requested by User",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to bypass passkeys by requesting a login code sent to their email. This reduces the security significantly as anyone with access to the user's email can gain entry.",
|
||||
"email_login_code_from_admin": "Email Login Code from Admin",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "Allows an admin to send a login code to the user via email.",
|
||||
"send_test_email": "Send test email",
|
||||
"application_configuration_updated_successfully": "Application configuration updated successfully",
|
||||
"application_name": "Application Name",
|
||||
@@ -333,5 +336,11 @@
|
||||
"disable_firstname_lastname": "Disable {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_disable_this_user": "Are you sure you want to disable this user? They will not be able to log in or access any services.",
|
||||
"ldap_soft_delete_users": "Keep disabled users from LDAP.",
|
||||
"ldap_soft_delete_users_description": "When enabled, users removed from LDAP will be disabled rather than deleted from the system."
|
||||
"ldap_soft_delete_users_description": "When enabled, users removed from LDAP will be disabled rather than deleted from the system.",
|
||||
"login_code_email_success": "The login code has been sent to the user.",
|
||||
"send_email": "Send Email",
|
||||
"show_code": "Show Code",
|
||||
"callback_url_description": "URL(s) provided by your client. Wildcards (*) are supported, but best avoided for better security.",
|
||||
"api_key_expiration": "API Key Expiration",
|
||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Send an email to the user when their API key is about to expire."
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
"actions": "Actions",
|
||||
"images_updated_successfully": "Images updated successfully",
|
||||
"general": "General",
|
||||
"enable_email_notifications_to_alert_users_when_a_login_is_detected_from_a_new_device_or_location": "Enable email notifications to alert users when a login is detected from a new device or location.",
|
||||
"configure_smtp_to_send_emails": "Enable email notifications to alert users when a login is detected from a new device or location.",
|
||||
"ldap": "LDAP",
|
||||
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Configure LDAP settings to sync users and groups from an LDAP server.",
|
||||
"images": "Images",
|
||||
@@ -180,7 +180,10 @@
|
||||
"enabled_emails": "Enabled Emails",
|
||||
"email_login_notification": "Email Login Notification",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Send an email to the user when they log in from a new device.",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to sign in with a login code sent to their email. This reduces the security significantly as anyone with access to the user's email can gain entry.",
|
||||
"emai_login_code_requested_by_user": "Email Login Code Requested by User",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to bypass passkeys by requesting a login code sent to their email. This reduces the security significantly as anyone with access to the user's email can gain entry.",
|
||||
"email_login_code_from_admin": "Email Login Code from Admin",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "Allows an admin to send a login code to the user via email.",
|
||||
"send_test_email": "Send test email",
|
||||
"application_configuration_updated_successfully": "Application configuration updated successfully",
|
||||
"application_name": "Application Name",
|
||||
@@ -333,5 +336,11 @@
|
||||
"disable_firstname_lastname": "Disable {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_disable_this_user": "Are you sure you want to disable this user? They will not be able to log in or access any services.",
|
||||
"ldap_soft_delete_users": "Keep disabled users from LDAP.",
|
||||
"ldap_soft_delete_users_description": "When enabled, users removed from LDAP will be disabled rather than deleted from the system."
|
||||
"ldap_soft_delete_users_description": "When enabled, users removed from LDAP will be disabled rather than deleted from the system.",
|
||||
"login_code_email_success": "The login code has been sent to the user.",
|
||||
"send_email": "Send Email",
|
||||
"show_code": "Show Code",
|
||||
"callback_url_description": "URL(s) provided by your client. Wildcards (*) are supported, but best avoided for better security.",
|
||||
"api_key_expiration": "API Key Expiration",
|
||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Send an email to the user when their API key is about to expire."
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
"actions": "Действия",
|
||||
"images_updated_successfully": "Изображения успешно обновлены",
|
||||
"general": "Основное",
|
||||
"enable_email_notifications_to_alert_users_when_a_login_is_detected_from_a_new_device_or_location": "Включить уведомления пользователей по электронной почте при обнаружении логина с нового устройства или локации.",
|
||||
"configure_smtp_to_send_emails": "Enable email notifications to alert users when a login is detected from a new device or location.",
|
||||
"ldap": "LDAP",
|
||||
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Настроить конфигурацию LDAP для синхронизации пользователей и групп с сервером LDAP.",
|
||||
"images": "Изображения",
|
||||
@@ -180,7 +180,10 @@
|
||||
"enabled_emails": "Отправляемые письма",
|
||||
"email_login_notification": "Уведомление о логине по электронной почте",
|
||||
"send_an_email_to_the_user_when_they_log_in_from_a_new_device": "Отправлять пользователю письмо при входе с нового устройства.",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Позволяет пользователям войти с помощью кода входа, отправленного на их электронную почту. Это значительно снижает безопасность так как любой человек, имеющий доступ к электронной почте пользователя, сможет получить доступ.",
|
||||
"emai_login_code_requested_by_user": "Email Login Code Requested by User",
|
||||
"allow_users_to_sign_in_with_a_login_code_sent_to_their_email": "Allows users to bypass passkeys by requesting a login code sent to their email. This reduces the security significantly as anyone with access to the user's email can gain entry.",
|
||||
"email_login_code_from_admin": "Email Login Code from Admin",
|
||||
"allows_an_admin_to_send_a_login_code_to_the_user": "Allows an admin to send a login code to the user via email.",
|
||||
"send_test_email": "Отправить тестовое письмо",
|
||||
"application_configuration_updated_successfully": "Конфигурация приложения успешно обновлена",
|
||||
"application_name": "Название приложения",
|
||||
@@ -324,14 +327,20 @@
|
||||
"client_authorization": "Авторизация в клиенте",
|
||||
"new_client_authorization": "Новая авторизация в клиенте",
|
||||
"disable_animations": "Отключить анимации",
|
||||
"turn_off_all_animations_throughout_the_admin_ui": "Turn off all animations throughout the Admin UI.",
|
||||
"user_disabled": "Account Disabled",
|
||||
"disabled_users_cannot_log_in_or_use_services": "Disabled users cannot log in or use services.",
|
||||
"user_disabled_successfully": "User has been disabled successfully.",
|
||||
"user_enabled_successfully": "User has been enabled successfully.",
|
||||
"status": "Status",
|
||||
"disable_firstname_lastname": "Disable {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_disable_this_user": "Are you sure you want to disable this user? They will not be able to log in or access any services.",
|
||||
"ldap_soft_delete_users": "Keep disabled users from LDAP.",
|
||||
"ldap_soft_delete_users_description": "When enabled, users removed from LDAP will be disabled rather than deleted from the system."
|
||||
"turn_off_all_animations_throughout_the_admin_ui": "Выключить все анимации в интерфейсе администратора.",
|
||||
"user_disabled": "Аккаунт отключен",
|
||||
"disabled_users_cannot_log_in_or_use_services": "Отключенные пользователи не могут войти или использовать сервисы.",
|
||||
"user_disabled_successfully": "Пользователь успешно отключен.",
|
||||
"user_enabled_successfully": "Пользователь успешно включен.",
|
||||
"status": "Статус",
|
||||
"disable_firstname_lastname": "Отключить {firstName} {lastName}",
|
||||
"are_you_sure_you_want_to_disable_this_user": "Вы уверены, что хотите отключить этого пользователя? Они не смогут войти в систему или получить доступ к любым сервисам.",
|
||||
"ldap_soft_delete_users": "Оставить отключенных пользователей от LDAP.",
|
||||
"ldap_soft_delete_users_description": "Когда включено, пользователи удалённые из LDAP будут отключены вместо удаления из системы.",
|
||||
"login_code_email_success": "The login code has been sent to the user.",
|
||||
"send_email": "Send Email",
|
||||
"show_code": "Show Code",
|
||||
"callback_url_description": "URL(s) provided by your client. Wildcards (*) are supported, but best avoided for better security.",
|
||||
"api_key_expiration": "API Key Expiration",
|
||||
"send_an_email_to_the_user_when_their_api_key_is_about_to_expire": "Send an email to the user when their API key is about to expire."
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pocket-id-frontend",
|
||||
"version": "0.48.0",
|
||||
"version": "0.49.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -9,8 +9,10 @@
|
||||
import { Separator } from '$lib/components/ui/separator';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import UserService from '$lib/services/user-service';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
import { mode } from 'mode-watcher';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
let {
|
||||
userId = $bindable()
|
||||
@@ -32,7 +34,7 @@
|
||||
[m.one_month()]: 60 * 60 * 24 * 30
|
||||
};
|
||||
|
||||
async function createOneTimeAccessToken() {
|
||||
async function createLoginCode() {
|
||||
try {
|
||||
const expiration = new Date(Date.now() + availableExpirations[selectedExpiration] * 1000);
|
||||
code = await userService.createOneTimeAccessToken(expiration, userId!);
|
||||
@@ -42,6 +44,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function sendLoginCodeEmail() {
|
||||
try {
|
||||
const expiration = new Date(Date.now() + availableExpirations[selectedExpiration] * 1000);
|
||||
await userService.requestOneTimeAccessEmailAsAdmin(userId!, expiration);
|
||||
toast.success(m.login_code_email_success());
|
||||
onOpenChange(false);
|
||||
} catch (e) {
|
||||
axiosErrorToast(e);
|
||||
}
|
||||
}
|
||||
|
||||
function onOpenChange(open: boolean) {
|
||||
if (!open) {
|
||||
oneTimeLink = null;
|
||||
@@ -81,13 +94,20 @@
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
<Button
|
||||
onclick={() => createOneTimeAccessToken()}
|
||||
disabled={!selectedExpiration}
|
||||
class="mt-2 w-full"
|
||||
>
|
||||
{m.generate_code()}
|
||||
</Button>
|
||||
<Dialog.Footer class="mt-2">
|
||||
{#if $appConfigStore.emailOneTimeAccessAsAdminEnabled}
|
||||
<Button
|
||||
onclick={() => sendLoginCodeEmail()}
|
||||
variant="secondary"
|
||||
disabled={!selectedExpiration}
|
||||
>
|
||||
{m.send_email()}
|
||||
</Button>
|
||||
{/if}
|
||||
<Button onclick={() => createLoginCode()} disabled={!selectedExpiration}
|
||||
>{m.show_code()}</Button
|
||||
>
|
||||
</Dialog.Footer>
|
||||
{:else}
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<CopyToClipboard value={code!}>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from "cmdk-sv";
|
||||
import { cn } from "$lib/utils/style.js";
|
||||
import type { ClassValue } from "svelte/elements";
|
||||
|
||||
type $$Props = CommandPrimitive.EmptyProps;
|
||||
let className: string | undefined | null = undefined;
|
||||
let className: ClassValue | undefined | null = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from "cmdk-sv";
|
||||
import { cn } from "$lib/utils/style.js";
|
||||
import type { ClassValue } from "svelte/elements";
|
||||
type $$Props = CommandPrimitive.GroupProps;
|
||||
|
||||
let className: string | undefined | null = undefined;
|
||||
let className: ClassValue | undefined | null = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from "cmdk-sv";
|
||||
import { cn } from "$lib/utils/style.js";
|
||||
import type { ClassValue } from "svelte/elements";
|
||||
|
||||
type $$Props = CommandPrimitive.ItemProps;
|
||||
|
||||
export let asChild = false;
|
||||
|
||||
let className: string | undefined | null = undefined;
|
||||
let className: ClassValue | undefined | null = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from "cmdk-sv";
|
||||
import { cn } from "$lib/utils/style.js";
|
||||
import type { ClassValue } from "svelte/elements";
|
||||
|
||||
type $$Props = CommandPrimitive.ListProps;
|
||||
let className: string | undefined | null = undefined;
|
||||
let className: ClassValue | undefined | null = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from "cmdk-sv";
|
||||
import { cn } from "$lib/utils/style.js";
|
||||
import type { ClassValue } from "svelte/elements";
|
||||
|
||||
type $$Props = CommandPrimitive.SeparatorProps;
|
||||
let className: string | undefined | null = undefined;
|
||||
let className: ClassValue | undefined | null = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import type { ClassValue, HTMLAttributes } from "svelte/elements";
|
||||
import { cn } from "$lib/utils/style.js";
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLSpanElement>;
|
||||
|
||||
let className: string | undefined | null = undefined;
|
||||
let className: ClassValue | undefined | null = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from "cmdk-sv";
|
||||
import { cn } from "$lib/utils/style.js";
|
||||
import type { ClassValue } from "svelte/elements";
|
||||
|
||||
type $$Props = CommandPrimitive.CommandProps;
|
||||
|
||||
export let value: $$Props["value"] = undefined;
|
||||
|
||||
let className: string | undefined | null = undefined;
|
||||
let className: ClassValue | undefined | null = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
|
||||
@@ -87,10 +87,14 @@ export default class UserService extends APIService {
|
||||
return res.data as User;
|
||||
}
|
||||
|
||||
async requestOneTimeAccessEmail(email: string, redirectPath?: string) {
|
||||
async requestOneTimeAccessEmailAsUnauthenticatedUser(email: string, redirectPath?: string) {
|
||||
await this.api.post('/one-time-access-email', { email, redirectPath });
|
||||
}
|
||||
|
||||
async requestOneTimeAccessEmailAsAdmin(userId: string, expiresAt: Date) {
|
||||
await this.api.post(`/users/${userId}/one-time-access-email`, { expiresAt });
|
||||
}
|
||||
|
||||
async updateUserGroups(id: string, userGroupIds: string[]) {
|
||||
const res = await this.api.put(`/users/${id}/user-groups`, { userGroupIds });
|
||||
return res.data as User;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
export type AppConfig = {
|
||||
appName: string;
|
||||
allowOwnAccountEdit: boolean;
|
||||
emailOneTimeAccessEnabled: boolean;
|
||||
emailOneTimeAccessAsUnauthenticatedEnabled: boolean;
|
||||
emailOneTimeAccessAsAdminEnabled: boolean;
|
||||
ldapEnabled: boolean;
|
||||
disableAnimations: boolean;
|
||||
};
|
||||
@@ -19,6 +20,7 @@ export type AllAppConfig = AppConfig & {
|
||||
smtpTls: 'none' | 'starttls' | 'tls';
|
||||
smtpSkipCertVerify: boolean;
|
||||
emailLoginNotificationEnabled: boolean;
|
||||
emailApiKeyExpirationEnabled: boolean;
|
||||
// LDAP
|
||||
ldapUrl: string;
|
||||
ldapBindDn: string;
|
||||
|
||||
@@ -13,7 +13,7 @@ export type User = {
|
||||
customClaims: CustomClaim[];
|
||||
locale?: Locale;
|
||||
ldapId?: string;
|
||||
disabled: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export type UserCreate = Omit<User, 'id' | 'customClaims' | 'ldapId' | 'userGroups'>;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import SignInWrapper from '$lib/components/login-wrapper.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import OidcService from '$lib/services/oidc-service';
|
||||
import WebAuthnService from '$lib/services/webauthn-service';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
@@ -14,7 +15,6 @@
|
||||
import type { PageData } from './$types';
|
||||
import ClientProviderImages from './components/client-provider-images.svelte';
|
||||
import ScopeItem from './components/scope-item.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
const webauthnService = new WebAuthnService();
|
||||
const oidService = new OidcService();
|
||||
@@ -84,7 +84,7 @@
|
||||
{#if client == null}
|
||||
<p>{m.client_not_found()}</p>
|
||||
{:else}
|
||||
<SignInWrapper animate showAlternativeSignInMethodButton>
|
||||
<SignInWrapper animate={!$appConfigStore.disableAnimations} showAlternativeSignInMethodButton={$userStore == null}>
|
||||
<ClientProviderImages {client} {success} error={!!errorMessage} />
|
||||
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">
|
||||
{m.sign_in_to({ name: client.name })}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
}
|
||||
];
|
||||
|
||||
if ($appConfigStore.emailOneTimeAccessEnabled) {
|
||||
if ($appConfigStore.emailOneTimeAccessAsUnauthenticatedEnabled) {
|
||||
methods.push({
|
||||
icon: LucideMail,
|
||||
title: m.email_login(),
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
async function requestEmail() {
|
||||
isLoading = true;
|
||||
await userService
|
||||
.requestOneTimeAccessEmail(email, data.redirect)
|
||||
.requestOneTimeAccessEmailAsUnauthenticatedUser(email, data.redirect)
|
||||
.then(() => (success = true))
|
||||
.catch((e) => (error = e.response?.data.error || m.an_unknown_error_occurred()));
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import WebAuthnService from '$lib/services/webauthn-service';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
import userStore from '$lib/stores/user-store.js';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util.js';
|
||||
|
||||
@@ -26,7 +27,7 @@
|
||||
<title>{m.logout()}</title>
|
||||
</svelte:head>
|
||||
|
||||
<SignInWrapper animate>
|
||||
<SignInWrapper animate={!$appConfigStore.disableAnimations}>
|
||||
<div class="flex justify-center">
|
||||
<div class="bg-muted rounded-2xl p-3">
|
||||
<Logo class="h-10 w-10" />
|
||||
|
||||
@@ -18,12 +18,12 @@
|
||||
'it-IT': 'Italiano'
|
||||
};
|
||||
|
||||
function updateLocale(locale: Locale) {
|
||||
setLocale(locale);
|
||||
userService.updateCurrent({
|
||||
async function updateLocale(locale: Locale) {
|
||||
await userService.updateCurrent({
|
||||
...$userStore!,
|
||||
locale
|
||||
});
|
||||
setLocale(locale);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
id="application-configuration-email"
|
||||
icon={Mail}
|
||||
title={m.email()}
|
||||
description={m.enable_email_notifications_to_alert_users_when_a_login_is_detected_from_a_new_device_or_location()}
|
||||
description={m.configure_smtp_to_send_emails()}
|
||||
>
|
||||
<AppConfigEmailForm {appConfig} callback={updateAppConfig} />
|
||||
</CollapsibleCard>
|
||||
|
||||
@@ -39,8 +39,10 @@
|
||||
smtpFrom: z.string().email(),
|
||||
smtpTls: z.enum(['none', 'starttls', 'tls']),
|
||||
smtpSkipCertVerify: z.boolean(),
|
||||
emailOneTimeAccessEnabled: z.boolean(),
|
||||
emailLoginNotificationEnabled: z.boolean()
|
||||
emailOneTimeAccessAsUnauthenticatedEnabled: z.boolean(),
|
||||
emailOneTimeAccessAsAdminEnabled: z.boolean(),
|
||||
emailLoginNotificationEnabled: z.boolean(),
|
||||
emailApiKeyExpirationEnabled: z.boolean()
|
||||
});
|
||||
|
||||
const { inputs, ...form } = createForm<typeof formSchema>(formSchema, appConfig);
|
||||
@@ -88,9 +90,7 @@
|
||||
await appConfigService
|
||||
.sendTestEmail()
|
||||
.then(() => toast.success(m.test_email_sent_successfully()))
|
||||
.catch(() =>
|
||||
toast.error(m.failed_to_send_test_email())
|
||||
)
|
||||
.catch(() => toast.error(m.failed_to_send_test_email()))
|
||||
.finally(() => (isSendingTestEmail = false));
|
||||
}
|
||||
</script>
|
||||
@@ -135,11 +135,24 @@
|
||||
description={m.send_an_email_to_the_user_when_they_log_in_from_a_new_device()}
|
||||
bind:checked={$inputs.emailLoginNotificationEnabled.value}
|
||||
/>
|
||||
|
||||
<CheckboxWithLabel
|
||||
id="email-login"
|
||||
label={m.email_login()}
|
||||
id="email-login-admin"
|
||||
label={m.email_login_code_from_admin()}
|
||||
description={m.allows_an_admin_to_send_a_login_code_to_the_user()}
|
||||
bind:checked={$inputs.emailOneTimeAccessAsAdminEnabled.value}
|
||||
/>
|
||||
<CheckboxWithLabel
|
||||
id="api-key-expiration"
|
||||
label={m.api_key_expiration()}
|
||||
description={m.send_an_email_to_the_user_when_their_api_key_is_about_to_expire()}
|
||||
bind:checked={$inputs.emailApiKeyExpirationEnabled.value}
|
||||
/>
|
||||
<CheckboxWithLabel
|
||||
id="email-login-user"
|
||||
label={m.emai_login_code_requested_by_user()}
|
||||
description={m.allow_users_to_sign_in_with_a_login_code_sent_to_their_email()}
|
||||
bind:checked={$inputs.emailOneTimeAccessEnabled.value}
|
||||
bind:checked={$inputs.emailOneTimeAccessAsUnauthenticatedEnabled.value}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
@@ -20,12 +20,10 @@
|
||||
allowEmpty?: boolean;
|
||||
children?: Snippet;
|
||||
} = $props();
|
||||
|
||||
const limit = 20;
|
||||
</script>
|
||||
|
||||
<div {...restProps}>
|
||||
<FormInput {label}>
|
||||
<FormInput {label} description={m.callback_url_description()}>
|
||||
<div class="flex flex-col gap-y-2">
|
||||
{#each callbackURLs as _, i}
|
||||
<div class="flex gap-x-2">
|
||||
@@ -46,15 +44,13 @@
|
||||
{#if error}
|
||||
<p class="mt-1 text-sm text-red-500">{error}</p>
|
||||
{/if}
|
||||
{#if callbackURLs.length < limit}
|
||||
<Button
|
||||
class="mt-2"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
on:click={() => (callbackURLs = [...callbackURLs, ''])}
|
||||
>
|
||||
<LucidePlus class="mr-1 h-4 w-4" />
|
||||
{callbackURLs.length === 0 ? m.add() : m.add_another()}
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
class="mt-2"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
on:click={() => (callbackURLs = [...callbackURLs, ''])}
|
||||
>
|
||||
<LucidePlus class="mr-1 h-4 w-4" />
|
||||
{callbackURLs.length === 0 ? m.add() : m.add_another()}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import FormInput from '$lib/components/form/form-input.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import Label from '$lib/components/ui/label/label.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import type {
|
||||
OidcClient,
|
||||
OidcClientCreate,
|
||||
@@ -12,7 +13,6 @@
|
||||
import { createForm } from '$lib/utils/form-util';
|
||||
import { z } from 'zod';
|
||||
import OidcCallbackUrlInput from './oidc-callback-url-input.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let {
|
||||
callback,
|
||||
@@ -38,8 +38,8 @@
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(2).max(50),
|
||||
callbackURLs: z.array(z.string()).nonempty(),
|
||||
logoutCallbackURLs: z.array(z.string()),
|
||||
callbackURLs: z.array(z.string().nonempty()).nonempty(),
|
||||
logoutCallbackURLs: z.array(z.string().nonempty()),
|
||||
isPublic: z.boolean(),
|
||||
pkceEnabled: z.boolean()
|
||||
});
|
||||
@@ -79,7 +79,7 @@
|
||||
</script>
|
||||
|
||||
<form onsubmit={onSubmit}>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-3 gap-y-7 sm:flex-row">
|
||||
<div class="grid grid-cols-1 gap-x-3 gap-y-7 sm:flex-row md:grid-cols-2">
|
||||
<FormInput label={m.name()} class="w-full" bind:input={$inputs.name} />
|
||||
<div></div>
|
||||
<OidcCallbackUrlInput
|
||||
@@ -120,7 +120,7 @@
|
||||
<img
|
||||
class="m-auto max-h-full max-w-full object-contain"
|
||||
src={logoDataURL}
|
||||
alt={m.name_logo({name: $inputs.name.value})}
|
||||
alt={m.name_logo({ name: $inputs.name.value })}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -161,4 +161,4 @@
|
||||
{/snippet}
|
||||
</AdvancedTable>
|
||||
|
||||
<OneTimeLinkModal userId={userIdToCreateOneTimeLink} />
|
||||
<OneTimeLinkModal bind:userId={userIdToCreateOneTimeLink} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ACCESS_TOKEN_COOKIE_NAME } from '$lib/constants';
|
||||
import AuditLogService from '$lib/services/audit-log-service';
|
||||
import type { SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
import type { PageServerLoad } from '../../global-audit-log/$types';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ cookies }) => {
|
||||
const auditLogService = new AuditLogService(cookies.get(ACCESS_TOKEN_COOKIE_NAME));
|
||||
|
||||
@@ -32,7 +32,9 @@ test('Update email configuration', async ({ page }) => {
|
||||
await page.getByLabel('SMTP Password').fill('password');
|
||||
await page.getByLabel('SMTP From').fill('test@gmail.com');
|
||||
await page.getByLabel('Email Login Notification').click();
|
||||
await page.getByLabel('Email Login', { exact: true }).click();
|
||||
await page.getByLabel('Email Login Code Requested by User').click();
|
||||
await page.getByLabel('Email Login Code from Admin').click();
|
||||
await page.getByLabel('API Key Expiration').click();
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).nth(1).click();
|
||||
|
||||
@@ -46,7 +48,9 @@ test('Update email configuration', async ({ page }) => {
|
||||
await expect(page.getByLabel('SMTP Password')).toHaveValue('password');
|
||||
await expect(page.getByLabel('SMTP From')).toHaveValue('test@gmail.com');
|
||||
await expect(page.getByLabel('Email Login Notification')).toBeChecked();
|
||||
await expect(page.getByLabel('Email Login', { exact: true })).toBeChecked();
|
||||
await expect(page.getByLabel('Email Login Code Requested by User')).toBeChecked();
|
||||
await expect(page.getByLabel('Email Login Code from Admin')).toBeChecked();
|
||||
await expect(page.getByLabel('API Key Expiration')).toBeChecked();
|
||||
});
|
||||
|
||||
test('Update LDAP configuration', async ({ page }) => {
|
||||
|
||||
@@ -64,7 +64,7 @@ test('Create one time access token', async ({ page, context }) => {
|
||||
|
||||
await page.getByLabel('Login Code').getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: '12 hours' }).click();
|
||||
await page.getByRole('button', { name: 'Generate Code' }).click();
|
||||
await page.getByRole('button', { name: 'Show Code' }).click();
|
||||
|
||||
const link = await page.getByTestId('login-code-link').textContent();
|
||||
await context.clearCookies();
|
||||
|
||||
Reference in New Issue
Block a user