mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-03-29 02:36:35 +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
|
# 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
|
timeout-minutes: 20
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Build and export
|
- name: Build and export
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
tags: pocket-id/pocket-id:test
|
push: false
|
||||||
|
load: false
|
||||||
|
tags: pocket-id:test
|
||||||
outputs: type=docker,dest=/tmp/docker-image.tar
|
outputs: type=docker,dest=/tmp/docker-image.tar
|
||||||
build-args: BUILD_TAGS=e2etest
|
build-args: BUILD_TAGS=e2etest
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
- name: Upload Docker image artifact
|
- name: Upload Docker image artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: docker-image
|
name: docker-image
|
||||||
path: /tmp/docker-image.tar
|
path: /tmp/docker-image.tar
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
test-sqlite:
|
test-sqlite:
|
||||||
if: github.event.pull_request.head.ref != 'i18n_crowdin'
|
if: github.event.pull_request.head.ref != 'i18n_crowdin'
|
||||||
@@ -47,13 +54,22 @@ jobs:
|
|||||||
cache: "npm"
|
cache: "npm"
|
||||||
cache-dependency-path: frontend/package-lock.json
|
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
|
- name: Download Docker image artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: docker-image
|
name: docker-image
|
||||||
path: /tmp
|
path: /tmp
|
||||||
|
|
||||||
- name: Load Docker Image
|
- name: Load Docker image
|
||||||
run: docker load -i /tmp/docker-image.tar
|
run: docker load -i /tmp/docker-image.tar
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
@@ -62,6 +78,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
working-directory: ./frontend
|
working-directory: ./frontend
|
||||||
|
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||||
run: npx playwright install --with-deps chromium
|
run: npx playwright install --with-deps chromium
|
||||||
|
|
||||||
- name: Run Docker Container with Sqlite DB
|
- name: Run Docker Container with Sqlite DB
|
||||||
@@ -69,7 +86,7 @@ jobs:
|
|||||||
docker run -d --name pocket-id-sqlite \
|
docker run -d --name pocket-id-sqlite \
|
||||||
-p 80:80 \
|
-p 80:80 \
|
||||||
-e APP_ENV=test \
|
-e APP_ENV=test \
|
||||||
pocket-id/pocket-id:test
|
pocket-id:test
|
||||||
|
|
||||||
docker logs -f pocket-id-sqlite &> /tmp/backend.log &
|
docker logs -f pocket-id-sqlite &> /tmp/backend.log &
|
||||||
|
|
||||||
@@ -77,16 +94,18 @@ jobs:
|
|||||||
working-directory: ./frontend
|
working-directory: ./frontend
|
||||||
run: npx playwright test
|
run: npx playwright test
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- name: Upload Frontend Test Report
|
||||||
if: always()
|
uses: actions/upload-artifact@v4
|
||||||
|
if: always() && github.event.pull_request.head.ref != 'i18n_crowdin'
|
||||||
with:
|
with:
|
||||||
name: playwright-report-sqlite
|
name: playwright-report-sqlite
|
||||||
path: frontend/tests/.report
|
path: frontend/tests/.report
|
||||||
include-hidden-files: true
|
include-hidden-files: true
|
||||||
retention-days: 15
|
retention-days: 15
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- name: Upload Backend Test Report
|
||||||
if: always()
|
uses: actions/upload-artifact@v4
|
||||||
|
if: always() && github.event.pull_request.head.ref != 'i18n_crowdin'
|
||||||
with:
|
with:
|
||||||
name: backend-sqlite
|
name: backend-sqlite
|
||||||
path: /tmp/backend.log
|
path: /tmp/backend.log
|
||||||
@@ -105,12 +124,39 @@ jobs:
|
|||||||
cache: "npm"
|
cache: "npm"
|
||||||
cache-dependency-path: frontend/package-lock.json
|
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
|
- name: Download Docker image artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: docker-image
|
name: docker-image
|
||||||
path: /tmp
|
path: /tmp
|
||||||
- name: Load Docker Image
|
|
||||||
|
- name: Load Docker image
|
||||||
run: docker load -i /tmp/docker-image.tar
|
run: docker load -i /tmp/docker-image.tar
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
@@ -119,6 +165,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
working-directory: ./frontend
|
working-directory: ./frontend
|
||||||
|
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||||
run: npx playwright install --with-deps chromium
|
run: npx playwright install --with-deps chromium
|
||||||
|
|
||||||
- name: Create Docker network
|
- name: Create Docker network
|
||||||
@@ -153,7 +200,7 @@ jobs:
|
|||||||
-e APP_ENV=test \
|
-e APP_ENV=test \
|
||||||
-e DB_PROVIDER=postgres \
|
-e DB_PROVIDER=postgres \
|
||||||
-e DB_CONNECTION_STRING=postgresql://postgres:postgres@pocket-id-db:5432/pocket-id \
|
-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 &
|
docker logs -f pocket-id-postgres &> /tmp/backend.log &
|
||||||
|
|
||||||
@@ -161,7 +208,8 @@ jobs:
|
|||||||
working-directory: ./frontend
|
working-directory: ./frontend
|
||||||
run: npx playwright test
|
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'
|
if: always() && github.event.pull_request.head.ref != 'i18n_crowdin'
|
||||||
with:
|
with:
|
||||||
name: playwright-report-postgres
|
name: playwright-report-postgres
|
||||||
@@ -169,8 +217,9 @@ jobs:
|
|||||||
include-hidden-files: true
|
include-hidden-files: true
|
||||||
retention-days: 15
|
retention-days: 15
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- name: Upload Backend Test Report
|
||||||
if: always()
|
uses: actions/upload-artifact@v4
|
||||||
|
if: always() && github.event.pull_request.head.ref != 'i18n_crowdin'
|
||||||
with:
|
with:
|
||||||
name: backend-postgres
|
name: backend-postgres
|
||||||
path: /tmp/backend.log
|
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)
|
## [](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
|
module github.com/pocket-id/pocket-id/backend
|
||||||
|
|
||||||
go 1.24
|
go 1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/caarlos0/env/v11 v11.3.1
|
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)
|
oidcService := service.NewOidcService(db, jwtService, appConfigService, auditLogService, customClaimService)
|
||||||
userGroupService := service.NewUserGroupService(db, appConfigService)
|
userGroupService := service.NewUserGroupService(db, appConfigService)
|
||||||
ldapService := service.NewLdapService(db, appConfigService, userService, userGroupService)
|
ldapService := service.NewLdapService(db, appConfigService, userService, userGroupService)
|
||||||
apiKeyService := service.NewApiKeyService(db)
|
apiKeyService := service.NewApiKeyService(db, emailService)
|
||||||
|
|
||||||
rateLimitMiddleware := middleware.NewRateLimitMiddleware()
|
rateLimitMiddleware := middleware.NewRateLimitMiddleware()
|
||||||
|
|
||||||
@@ -61,6 +61,7 @@ func initRouter(ctx context.Context, db *gorm.DB, appConfigService *service.AppC
|
|||||||
job.RegisterLdapJobs(ctx, ldapService, appConfigService)
|
job.RegisterLdapJobs(ctx, ldapService, appConfigService)
|
||||||
job.RegisterDbCleanupJobs(ctx, db)
|
job.RegisterDbCleanupJobs(ctx, db)
|
||||||
job.RegisterFileCleanupJobs(ctx, db)
|
job.RegisterFileCleanupJobs(ctx, db)
|
||||||
|
job.RegisterApiKeyExpiryJob(ctx, apiKeyService, appConfigService)
|
||||||
|
|
||||||
// Initialize middleware for specific routes
|
// Initialize middleware for specific routes
|
||||||
authMiddleware := middleware.NewAuthMiddleware(apiKeyService, userService, jwtService)
|
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/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-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/: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-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/:id/profile-picture", authMiddleware.Add(), uc.resetUserProfilePictureHandler)
|
||||||
group.DELETE("/users/me/profile-picture", authMiddleware.WithAdminNotRequired().Add(), uc.resetCurrentUserProfilePictureHandler)
|
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)
|
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) {
|
func (uc *UserController) createAdminOneTimeAccessTokenHandler(c *gin.Context) {
|
||||||
uc.createOneTimeAccessTokenHandler(c, false)
|
uc.createOneTimeAccessTokenHandler(c, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uc *UserController) requestOneTimeAccessEmailHandler(c *gin.Context) {
|
// RequestOneTimeAccessEmailAsUnauthenticatedUserHandler godoc
|
||||||
var input dto.OneTimeAccessEmailDto
|
// @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 {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
_ = c.Error(err)
|
_ = c.Error(err)
|
||||||
return
|
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 {
|
if err != nil {
|
||||||
_ = c.Error(err)
|
_ = c.Error(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -11,12 +11,13 @@ type ApiKeyCreateDto struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ApiKeyDto struct {
|
type ApiKeyDto struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
ExpiresAt datatype.DateTime `json:"expiresAt"`
|
ExpiresAt datatype.DateTime `json:"expiresAt"`
|
||||||
LastUsedAt *datatype.DateTime `json:"lastUsedAt"`
|
LastUsedAt *datatype.DateTime `json:"lastUsedAt"`
|
||||||
CreatedAt datatype.DateTime `json:"createdAt"`
|
CreatedAt datatype.DateTime `json:"createdAt"`
|
||||||
|
ExpirationEmailSent bool `json:"expirationEmailSent"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ApiKeyResponseDto struct {
|
type ApiKeyResponseDto struct {
|
||||||
|
|||||||
@@ -12,37 +12,39 @@ type AppConfigVariableDto struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AppConfigUpdateDto struct {
|
type AppConfigUpdateDto struct {
|
||||||
AppName string `json:"appName" binding:"required,min=1,max=30"`
|
AppName string `json:"appName" binding:"required,min=1,max=30"`
|
||||||
SessionDuration string `json:"sessionDuration" binding:"required"`
|
SessionDuration string `json:"sessionDuration" binding:"required"`
|
||||||
EmailsVerified string `json:"emailsVerified" binding:"required"`
|
EmailsVerified string `json:"emailsVerified" binding:"required"`
|
||||||
DisableAnimations string `json:"disableAnimations" binding:"required"`
|
DisableAnimations string `json:"disableAnimations" binding:"required"`
|
||||||
AllowOwnAccountEdit string `json:"allowOwnAccountEdit" binding:"required"`
|
AllowOwnAccountEdit string `json:"allowOwnAccountEdit" binding:"required"`
|
||||||
SmtpHost string `json:"smtpHost"`
|
SmtpHost string `json:"smtpHost"`
|
||||||
SmtpPort string `json:"smtpPort"`
|
SmtpPort string `json:"smtpPort"`
|
||||||
SmtpFrom string `json:"smtpFrom" binding:"omitempty,email"`
|
SmtpFrom string `json:"smtpFrom" binding:"omitempty,email"`
|
||||||
SmtpUser string `json:"smtpUser"`
|
SmtpUser string `json:"smtpUser"`
|
||||||
SmtpPassword string `json:"smtpPassword"`
|
SmtpPassword string `json:"smtpPassword"`
|
||||||
SmtpTls string `json:"smtpTls" binding:"required,oneof=none starttls tls"`
|
SmtpTls string `json:"smtpTls" binding:"required,oneof=none starttls tls"`
|
||||||
SmtpSkipCertVerify string `json:"smtpSkipCertVerify"`
|
SmtpSkipCertVerify string `json:"smtpSkipCertVerify"`
|
||||||
LdapEnabled string `json:"ldapEnabled" binding:"required"`
|
LdapEnabled string `json:"ldapEnabled" binding:"required"`
|
||||||
LdapUrl string `json:"ldapUrl"`
|
LdapUrl string `json:"ldapUrl"`
|
||||||
LdapBindDn string `json:"ldapBindDn"`
|
LdapBindDn string `json:"ldapBindDn"`
|
||||||
LdapBindPassword string `json:"ldapBindPassword"`
|
LdapBindPassword string `json:"ldapBindPassword"`
|
||||||
LdapBase string `json:"ldapBase"`
|
LdapBase string `json:"ldapBase"`
|
||||||
LdapUserSearchFilter string `json:"ldapUserSearchFilter"`
|
LdapUserSearchFilter string `json:"ldapUserSearchFilter"`
|
||||||
LdapUserGroupSearchFilter string `json:"ldapUserGroupSearchFilter"`
|
LdapUserGroupSearchFilter string `json:"ldapUserGroupSearchFilter"`
|
||||||
LdapSkipCertVerify string `json:"ldapSkipCertVerify"`
|
LdapSkipCertVerify string `json:"ldapSkipCertVerify"`
|
||||||
LdapAttributeUserUniqueIdentifier string `json:"ldapAttributeUserUniqueIdentifier"`
|
LdapAttributeUserUniqueIdentifier string `json:"ldapAttributeUserUniqueIdentifier"`
|
||||||
LdapAttributeUserUsername string `json:"ldapAttributeUserUsername"`
|
LdapAttributeUserUsername string `json:"ldapAttributeUserUsername"`
|
||||||
LdapAttributeUserEmail string `json:"ldapAttributeUserEmail"`
|
LdapAttributeUserEmail string `json:"ldapAttributeUserEmail"`
|
||||||
LdapAttributeUserFirstName string `json:"ldapAttributeUserFirstName"`
|
LdapAttributeUserFirstName string `json:"ldapAttributeUserFirstName"`
|
||||||
LdapAttributeUserLastName string `json:"ldapAttributeUserLastName"`
|
LdapAttributeUserLastName string `json:"ldapAttributeUserLastName"`
|
||||||
LdapAttributeUserProfilePicture string `json:"ldapAttributeUserProfilePicture"`
|
LdapAttributeUserProfilePicture string `json:"ldapAttributeUserProfilePicture"`
|
||||||
LdapAttributeGroupMember string `json:"ldapAttributeGroupMember"`
|
LdapAttributeGroupMember string `json:"ldapAttributeGroupMember"`
|
||||||
LdapAttributeGroupUniqueIdentifier string `json:"ldapAttributeGroupUniqueIdentifier"`
|
LdapAttributeGroupUniqueIdentifier string `json:"ldapAttributeGroupUniqueIdentifier"`
|
||||||
LdapAttributeGroupName string `json:"ldapAttributeGroupName"`
|
LdapAttributeGroupName string `json:"ldapAttributeGroupName"`
|
||||||
LdapAttributeAdminGroup string `json:"ldapAttributeAdminGroup"`
|
LdapAttributeAdminGroup string `json:"ldapAttributeAdminGroup"`
|
||||||
LdapSoftDeleteUsers string `json:"ldapSoftDeleteUsers"`
|
LdapSoftDeleteUsers string `json:"ldapSoftDeleteUsers"`
|
||||||
EmailOneTimeAccessEnabled string `json:"emailOneTimeAccessEnabled" binding:"required"`
|
EmailOneTimeAccessAsAdminEnabled string `json:"emailOneTimeAccessAsAdminEnabled" binding:"required"`
|
||||||
EmailLoginNotificationEnabled string `json:"emailLoginNotificationEnabled" 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"`
|
ExpiresAt time.Time `json:"expiresAt" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OneTimeAccessEmailDto struct {
|
type OneTimeAccessEmailAsUnauthenticatedUserDto struct {
|
||||||
Email string `json:"email" binding:"required,email"`
|
Email string `json:"email" binding:"required,email"`
|
||||||
RedirectPath string `json:"redirectPath"`
|
RedirectPath string `json:"redirectPath"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OneTimeAccessEmailAsAdminDto struct {
|
||||||
|
ExpiresAt time.Time `json:"expiresAt" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
type UserUpdateUserGroupDto struct {
|
type UserUpdateUserGroupDto struct {
|
||||||
UserGroupIds []string `json:"userGroupIds" binding:"required"`
|
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 {
|
type ApiKey struct {
|
||||||
Base
|
Base
|
||||||
|
|
||||||
Name string `sortable:"true"`
|
Name string `sortable:"true"`
|
||||||
Key string
|
Key string
|
||||||
Description *string
|
Description *string
|
||||||
ExpiresAt datatype.DateTime `sortable:"true"`
|
ExpiresAt datatype.DateTime `sortable:"true"`
|
||||||
LastUsedAt *datatype.DateTime `sortable:"true"`
|
LastUsedAt *datatype.DateTime `sortable:"true"`
|
||||||
|
ExpirationEmailSent bool
|
||||||
|
|
||||||
UserID string
|
UserID string
|
||||||
User User
|
User User
|
||||||
|
|||||||
@@ -41,15 +41,17 @@ type AppConfig struct {
|
|||||||
LogoLightImageType AppConfigVariable `key:"logoLightImageType,internal"` // Internal
|
LogoLightImageType AppConfigVariable `key:"logoLightImageType,internal"` // Internal
|
||||||
LogoDarkImageType AppConfigVariable `key:"logoDarkImageType,internal"` // Internal
|
LogoDarkImageType AppConfigVariable `key:"logoDarkImageType,internal"` // Internal
|
||||||
// Email
|
// Email
|
||||||
SmtpHost AppConfigVariable `key:"smtpHost"`
|
SmtpHost AppConfigVariable `key:"smtpHost"`
|
||||||
SmtpPort AppConfigVariable `key:"smtpPort"`
|
SmtpPort AppConfigVariable `key:"smtpPort"`
|
||||||
SmtpFrom AppConfigVariable `key:"smtpFrom"`
|
SmtpFrom AppConfigVariable `key:"smtpFrom"`
|
||||||
SmtpUser AppConfigVariable `key:"smtpUser"`
|
SmtpUser AppConfigVariable `key:"smtpUser"`
|
||||||
SmtpPassword AppConfigVariable `key:"smtpPassword"`
|
SmtpPassword AppConfigVariable `key:"smtpPassword"`
|
||||||
SmtpTls AppConfigVariable `key:"smtpTls"`
|
SmtpTls AppConfigVariable `key:"smtpTls"`
|
||||||
SmtpSkipCertVerify AppConfigVariable `key:"smtpSkipCertVerify"`
|
SmtpSkipCertVerify AppConfigVariable `key:"smtpSkipCertVerify"`
|
||||||
EmailLoginNotificationEnabled AppConfigVariable `key:"emailLoginNotificationEnabled"`
|
EmailLoginNotificationEnabled AppConfigVariable `key:"emailLoginNotificationEnabled"`
|
||||||
EmailOneTimeAccessEnabled AppConfigVariable `key:"emailOneTimeAccessEnabled,public"` // Public
|
EmailOneTimeAccessAsUnauthenticatedEnabled AppConfigVariable `key:"emailOneTimeAccessAsUnauthenticatedEnabled,public"` // Public
|
||||||
|
EmailOneTimeAccessAsAdminEnabled AppConfigVariable `key:"emailOneTimeAccessAsAdminEnabled,public"` // Public
|
||||||
|
EmailApiKeyExpirationEnabled AppConfigVariable `key:"emailApiKeyExpirationEnabled"`
|
||||||
// LDAP
|
// LDAP
|
||||||
LdapEnabled AppConfigVariable `key:"ldapEnabled,public"` // Public
|
LdapEnabled AppConfigVariable `key:"ldapEnabled,public"` // Public
|
||||||
LdapUrl AppConfigVariable `key:"ldapUrl"`
|
LdapUrl AppConfigVariable `key:"ldapUrl"`
|
||||||
@@ -77,7 +79,7 @@ func (c *AppConfig) ToAppConfigVariableSlice(showAll bool) []AppConfigVariable {
|
|||||||
cfgValue := reflect.ValueOf(c).Elem()
|
cfgValue := reflect.ValueOf(c).Elem()
|
||||||
cfgType := cfgValue.Type()
|
cfgType := cfgValue.Type()
|
||||||
|
|
||||||
res := make([]AppConfigVariable, cfgType.NumField())
|
var res []AppConfigVariable
|
||||||
|
|
||||||
for i := range cfgType.NumField() {
|
for i := range cfgType.NumField() {
|
||||||
field := cfgType.Field(i)
|
field := cfgType.Field(i)
|
||||||
@@ -94,10 +96,12 @@ func (c *AppConfig) ToAppConfigVariableSlice(showAll bool) []AppConfigVariable {
|
|||||||
|
|
||||||
fieldValue := cfgValue.Field(i)
|
fieldValue := cfgValue.Field(i)
|
||||||
|
|
||||||
res[i] = AppConfigVariable{
|
appConfigVariable := AppConfigVariable{
|
||||||
Key: key,
|
Key: key,
|
||||||
Value: fieldValue.FieldByName("Value").String(),
|
Value: fieldValue.FieldByName("Value").String(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
res = append(res, appConfigVariable)
|
||||||
}
|
}
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
||||||
|
"github.com/pocket-id/pocket-id/backend/internal/utils/email"
|
||||||
|
|
||||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||||
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||||
@@ -16,11 +17,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ApiKeyService struct {
|
type ApiKeyService struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
emailService *EmailService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApiKeyService(db *gorm.DB) *ApiKeyService {
|
func NewApiKeyService(db *gorm.DB, emailService *EmailService) *ApiKeyService {
|
||||||
return &ApiKeyService{db: db}
|
return &ApiKeyService{db: db, emailService: emailService}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ApiKeyService) ListApiKeys(ctx context.Context, userID string, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.ApiKey, utils.PaginationResponse, error) {
|
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
|
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"},
|
SmtpTls: model.AppConfigVariable{Value: "none"},
|
||||||
SmtpSkipCertVerify: model.AppConfigVariable{Value: "false"},
|
SmtpSkipCertVerify: model.AppConfigVariable{Value: "false"},
|
||||||
EmailLoginNotificationEnabled: 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
|
// LDAP
|
||||||
LdapEnabled: model.AppConfigVariable{Value: "false"},
|
LdapEnabled: model.AppConfigVariable{Value: "false"},
|
||||||
LdapUrl: model.AppConfigVariable{},
|
LdapUrl: model.AppConfigVariable{},
|
||||||
@@ -151,11 +153,6 @@ func (s *AppConfigService) UpdateAppConfig(ctx context.Context, input dto.AppCon
|
|||||||
return nil, &common.UiConfigDisabledError{}
|
return nil, &common.UiConfigDisabledError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If EmailLoginNotificationEnabled is set to false (explicitly), disable the EmailOneTimeAccessEnabled
|
|
||||||
if input.EmailLoginNotificationEnabled == "false" {
|
|
||||||
input.EmailOneTimeAccessEnabled = "false"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the transaction
|
// Start the transaction
|
||||||
tx, err := s.updateAppConfigStartTransaction(ctx)
|
tx, err := s.updateAppConfigStartTransaction(ctx)
|
||||||
if err != nil {
|
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) {
|
t.Run("cannot update when UiConfigDisabled is true", func(t *testing.T) {
|
||||||
// Save the original state and restore it after the test
|
// Save the original state and restore it after the test
|
||||||
originalUiConfigDisabled := common.EnvConfig.UiConfigDisabled
|
originalUiConfigDisabled := common.EnvConfig.UiConfigDisabled
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ func (s *AuditLogService) CreateNewSignInWithEmail(ctx context.Context, ipAddres
|
|||||||
}
|
}
|
||||||
|
|
||||||
innerErr = SendEmail(innerCtx, s.emailService, email.Address{
|
innerErr = SendEmail(innerCtx, s.emailService, email.Address{
|
||||||
Name: user.Username,
|
Name: user.FullName(),
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
}, NewLoginTemplate, &NewLoginTemplateData{
|
}, NewLoginTemplate, &NewLoginTemplateData{
|
||||||
IPAddress: ipAddress,
|
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)
|
// 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
|
// if the address does not have an @ (which would be unusual), we use hostname
|
||||||
|
|
||||||
from_address := dbConfig.SmtpFrom.Value
|
fromAddress := dbConfig.SmtpFrom.Value
|
||||||
domain := ""
|
domain := ""
|
||||||
if strings.Contains(from_address, "@") {
|
if strings.Contains(fromAddress, "@") {
|
||||||
domain = strings.Split(from_address, "@")[1]
|
domain = strings.Split(fromAddress, "@")[1]
|
||||||
} else {
|
} else {
|
||||||
hostname, err := os.Hostname()
|
hostname, err := os.Hostname()
|
||||||
if err != nil {
|
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 {
|
type NewLoginTemplateData struct {
|
||||||
IPAddress string
|
IPAddress string
|
||||||
Country string
|
Country string
|
||||||
@@ -54,7 +61,14 @@ type OneTimeAccessTemplateData = struct {
|
|||||||
Code string
|
Code string
|
||||||
LoginLink string
|
LoginLink string
|
||||||
LoginLinkWithCode 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
|
// 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
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserService) RequestOneTimeAccessEmail(ctx context.Context, emailAddress, redirectPath string) error {
|
func (s *UserService) RequestOneTimeAccessEmailAsAdmin(ctx context.Context, userID string, expiration time.Time) error {
|
||||||
tx := s.db.Begin()
|
isDisabled := !s.appConfigService.GetDbConfig().EmailOneTimeAccessAsAdminEnabled.IsTrue()
|
||||||
defer func() {
|
|
||||||
tx.Rollback()
|
|
||||||
}()
|
|
||||||
|
|
||||||
isDisabled := !s.appConfigService.GetDbConfig().EmailOneTimeAccessEnabled.IsTrue()
|
|
||||||
if isDisabled {
|
if isDisabled {
|
||||||
return &common.OneTimeAccessDisabledError{}
|
return &common.OneTimeAccessDisabledError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
var user model.User
|
return s.requestOneTimeAccessEmailInternal(ctx, userID, "", expiration)
|
||||||
err := tx.
|
|
||||||
WithContext(ctx).
|
}
|
||||||
Where("email = ?", emailAddress).
|
|
||||||
First(&user).
|
func (s *UserService) RequestOneTimeAccessEmailAsUnauthenticatedUser(ctx context.Context, userID, redirectPath string) error {
|
||||||
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 {
|
if err != nil {
|
||||||
// Do not return error if user not found to prevent email enumeration
|
// Do not return error if user not found to prevent email enumeration
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -399,12 +415,13 @@ func (s *UserService) RequestOneTimeAccessEmail(ctx context.Context, emailAddres
|
|||||||
}
|
}
|
||||||
|
|
||||||
errInternal := SendEmail(innerCtx, s.emailService, email.Address{
|
errInternal := SendEmail(innerCtx, s.emailService, email.Address{
|
||||||
Name: user.Username,
|
Name: user.FullName(),
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
}, OneTimeAccessTemplate, &OneTimeAccessTemplateData{
|
}, OneTimeAccessTemplate, &OneTimeAccessTemplateData{
|
||||||
Code: oneTimeAccessToken,
|
Code: oneTimeAccessToken,
|
||||||
LoginLink: link,
|
LoginLink: link,
|
||||||
LoginLinkWithCode: linkWithCode,
|
LoginLinkWithCode: linkWithCode,
|
||||||
|
ExpirationString: utils.DurationToString(time.Until(expiration).Round(time.Second)),
|
||||||
})
|
})
|
||||||
if errInternal != nil {
|
if errInternal != nil {
|
||||||
log.Printf("Failed to send email to '%s': %v\n", user.Email, errInternal)
|
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">
|
<div class="content">
|
||||||
<h2>Login Code</h2>
|
<h2>Login Code</h2>
|
||||||
<p class="message">
|
<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>
|
</p>
|
||||||
<div class="button-container">
|
<div class="button-container">
|
||||||
<a class="button" href="{{ .Data.LoginLinkWithCode }}" class="button">Sign In</a>
|
<a class="button" href="{{ .Data.LoginLinkWithCode }}" class="button">Sign In</a>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
Login Code
|
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 }}
|
{{ .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",
|
"actions": "Akce",
|
||||||
"images_updated_successfully": "Obrázky úspěšně aktualizovány",
|
"images_updated_successfully": "Obrázky úspěšně aktualizovány",
|
||||||
"general": "Obecné",
|
"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",
|
"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.",
|
"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",
|
"images": "Obrázky",
|
||||||
@@ -180,7 +180,10 @@
|
|||||||
"enabled_emails": "Povolené e-maily",
|
"enabled_emails": "Povolené e-maily",
|
||||||
"email_login_notification": "E-mailovová oznámení o přihlášení",
|
"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í.",
|
"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",
|
"send_test_email": "Odeslat testovací e-mail",
|
||||||
"application_configuration_updated_successfully": "Nastavení aplikace bylo úspěšně aktualizováno",
|
"application_configuration_updated_successfully": "Nastavení aplikace bylo úspěšně aktualizováno",
|
||||||
"application_name": "Název aplikace",
|
"application_name": "Název aplikace",
|
||||||
@@ -333,5 +336,11 @@
|
|||||||
"disable_firstname_lastname": "Disable {firstName} {lastName}",
|
"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.",
|
"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": "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",
|
"actions": "Aktionen",
|
||||||
"images_updated_successfully": "Bild erfolgreich aktualisiert",
|
"images_updated_successfully": "Bild erfolgreich aktualisiert",
|
||||||
"general": "Allgemein",
|
"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",
|
"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.",
|
"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",
|
"images": "Bilder",
|
||||||
@@ -180,7 +180,10 @@
|
|||||||
"enabled_emails": "E-Mails aktivieren",
|
"enabled_emails": "E-Mails aktivieren",
|
||||||
"email_login_notification": "E-Mail Benachrichtigung bei Login",
|
"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.",
|
"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",
|
"send_test_email": "Test-E-Mail senden",
|
||||||
"application_configuration_updated_successfully": "Anwendungskonfiguration erfolgreich aktualisiert",
|
"application_configuration_updated_successfully": "Anwendungskonfiguration erfolgreich aktualisiert",
|
||||||
"application_name": "Anwendungsname",
|
"application_name": "Anwendungsname",
|
||||||
@@ -333,5 +336,11 @@
|
|||||||
"disable_firstname_lastname": "Disable {firstName} {lastName}",
|
"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.",
|
"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": "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",
|
"actions": "Actions",
|
||||||
"images_updated_successfully": "Images updated successfully",
|
"images_updated_successfully": "Images updated successfully",
|
||||||
"general": "General",
|
"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",
|
"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.",
|
"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",
|
"images": "Images",
|
||||||
@@ -180,7 +180,10 @@
|
|||||||
"enabled_emails": "Enabled Emails",
|
"enabled_emails": "Enabled Emails",
|
||||||
"email_login_notification": "Email Login Notification",
|
"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.",
|
"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",
|
"send_test_email": "Send test email",
|
||||||
"application_configuration_updated_successfully": "Application configuration updated successfully",
|
"application_configuration_updated_successfully": "Application configuration updated successfully",
|
||||||
"application_name": "Application Name",
|
"application_name": "Application Name",
|
||||||
@@ -333,5 +336,11 @@
|
|||||||
"disable_firstname_lastname": "Disable {firstName} {lastName}",
|
"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.",
|
"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": "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",
|
"actions": "Actions",
|
||||||
"images_updated_successfully": "Images updated successfully",
|
"images_updated_successfully": "Images updated successfully",
|
||||||
"general": "General",
|
"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",
|
"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.",
|
"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",
|
"images": "Images",
|
||||||
@@ -180,7 +180,10 @@
|
|||||||
"enabled_emails": "Enabled Emails",
|
"enabled_emails": "Enabled Emails",
|
||||||
"email_login_notification": "Email Login Notification",
|
"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.",
|
"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",
|
"send_test_email": "Send test email",
|
||||||
"application_configuration_updated_successfully": "Application configuration updated successfully",
|
"application_configuration_updated_successfully": "Application configuration updated successfully",
|
||||||
"application_name": "Application Name",
|
"application_name": "Application Name",
|
||||||
@@ -333,5 +336,11 @@
|
|||||||
"disable_firstname_lastname": "Disable {firstName} {lastName}",
|
"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.",
|
"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": "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",
|
"actions": "Actions",
|
||||||
"images_updated_successfully": "Image mise à jour avec succès",
|
"images_updated_successfully": "Image mise à jour avec succès",
|
||||||
"general": "Général",
|
"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",
|
"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.",
|
"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",
|
"images": "Images",
|
||||||
@@ -180,7 +180,10 @@
|
|||||||
"enabled_emails": "Emails activés",
|
"enabled_emails": "Emails activés",
|
||||||
"email_login_notification": "Notification de connexion par e-mail",
|
"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.",
|
"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": "",
|
"send_test_email": "",
|
||||||
"application_configuration_updated_successfully": "Mise à jour de l'application avec succès",
|
"application_configuration_updated_successfully": "Mise à jour de l'application avec succès",
|
||||||
"application_name": "Nom de l'application",
|
"application_name": "Nom de l'application",
|
||||||
@@ -333,5 +336,11 @@
|
|||||||
"disable_firstname_lastname": "Disable {firstName} {lastName}",
|
"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.",
|
"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": "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",
|
"actions": "Azioni",
|
||||||
"images_updated_successfully": "Immagini aggiornate con successo",
|
"images_updated_successfully": "Immagini aggiornate con successo",
|
||||||
"general": "Generale",
|
"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",
|
"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.",
|
"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",
|
"images": "Immagini",
|
||||||
@@ -180,7 +180,10 @@
|
|||||||
"enabled_emails": "Email Abilitate",
|
"enabled_emails": "Email Abilitate",
|
||||||
"email_login_notification": "Notifica Accesso Email",
|
"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.",
|
"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",
|
"send_test_email": "Invia email di prova",
|
||||||
"application_configuration_updated_successfully": "Configurazione dell'applicazione aggiornata con successo",
|
"application_configuration_updated_successfully": "Configurazione dell'applicazione aggiornata con successo",
|
||||||
"application_name": "Nome dell'applicazione",
|
"application_name": "Nome dell'applicazione",
|
||||||
@@ -324,14 +327,20 @@
|
|||||||
"client_authorization": "Autorizzazione client",
|
"client_authorization": "Autorizzazione client",
|
||||||
"new_client_authorization": "Nuova autorizzazione client",
|
"new_client_authorization": "Nuova autorizzazione client",
|
||||||
"disable_animations": "Disabilita animazioni",
|
"disable_animations": "Disabilita animazioni",
|
||||||
"turn_off_all_animations_throughout_the_admin_ui": "Turn off all animations throughout the Admin UI.",
|
"turn_off_all_animations_throughout_the_admin_ui": "Disattiva tutte le animazioni nell'interfaccia di amministrazione.",
|
||||||
"user_disabled": "Account Disabled",
|
"user_disabled": "Account disabilitato",
|
||||||
"disabled_users_cannot_log_in_or_use_services": "Disabled users cannot log in or use services.",
|
"disabled_users_cannot_log_in_or_use_services": "Gli utenti disabilitati non possono accedere o utilizzare i servizi.",
|
||||||
"user_disabled_successfully": "User has been disabled successfully.",
|
"user_disabled_successfully": "Utente disabilitato con successo.",
|
||||||
"user_enabled_successfully": "User has been enabled successfully.",
|
"user_enabled_successfully": "Utente abilitato con successo.",
|
||||||
"status": "Status",
|
"status": "Stato",
|
||||||
"disable_firstname_lastname": "Disable {firstName} {lastName}",
|
"disable_firstname_lastname": "Disabilita {firstName} {lastName}",
|
||||||
"are_you_sure_you_want_to_disable_this_user": "Are you sure you want to disable this user? They will not be able to log in or access any services.",
|
"are_you_sure_you_want_to_disable_this_user": "Sei sicuro di voler disabilitare questo utente? Non sarà in grado di accedere o utilizzare qualsiasi servizio.",
|
||||||
"ldap_soft_delete_users": "Keep disabled users from LDAP.",
|
"ldap_soft_delete_users": "Mantieni gli utenti disabilitati da 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": "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",
|
"actions": "Acties",
|
||||||
"images_updated_successfully": "Afbeeldingen succesvol bijgewerkt",
|
"images_updated_successfully": "Afbeeldingen succesvol bijgewerkt",
|
||||||
"general": "Algemeen",
|
"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",
|
"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.",
|
"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",
|
"images": "Afbeeldingen",
|
||||||
@@ -180,7 +180,10 @@
|
|||||||
"enabled_emails": "Ingeschakelde e-mails",
|
"enabled_emails": "Ingeschakelde e-mails",
|
||||||
"email_login_notification": "E-mail-inlogmelding",
|
"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.",
|
"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",
|
"send_test_email": "Test-e-mail verzenden",
|
||||||
"application_configuration_updated_successfully": "Toepassingsconfiguratie succesvol bijgewerkt",
|
"application_configuration_updated_successfully": "Toepassingsconfiguratie succesvol bijgewerkt",
|
||||||
"application_name": "Toepassingsnaam",
|
"application_name": "Toepassingsnaam",
|
||||||
@@ -333,5 +336,11 @@
|
|||||||
"disable_firstname_lastname": "Disable {firstName} {lastName}",
|
"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.",
|
"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": "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",
|
"actions": "Ações",
|
||||||
"images_updated_successfully": "Imagens atualizadas com sucesso",
|
"images_updated_successfully": "Imagens atualizadas com sucesso",
|
||||||
"general": "Geral",
|
"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",
|
"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.",
|
"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",
|
"images": "Imagens",
|
||||||
@@ -180,7 +180,10 @@
|
|||||||
"enabled_emails": "Enabled Emails",
|
"enabled_emails": "Enabled Emails",
|
||||||
"email_login_notification": "Email Login Notification",
|
"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.",
|
"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",
|
"send_test_email": "Send test email",
|
||||||
"application_configuration_updated_successfully": "Application configuration updated successfully",
|
"application_configuration_updated_successfully": "Application configuration updated successfully",
|
||||||
"application_name": "Application Name",
|
"application_name": "Application Name",
|
||||||
@@ -333,5 +336,11 @@
|
|||||||
"disable_firstname_lastname": "Disable {firstName} {lastName}",
|
"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.",
|
"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": "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",
|
"actions": "Actions",
|
||||||
"images_updated_successfully": "Images updated successfully",
|
"images_updated_successfully": "Images updated successfully",
|
||||||
"general": "General",
|
"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",
|
"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.",
|
"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",
|
"images": "Images",
|
||||||
@@ -180,7 +180,10 @@
|
|||||||
"enabled_emails": "Enabled Emails",
|
"enabled_emails": "Enabled Emails",
|
||||||
"email_login_notification": "Email Login Notification",
|
"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.",
|
"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",
|
"send_test_email": "Send test email",
|
||||||
"application_configuration_updated_successfully": "Application configuration updated successfully",
|
"application_configuration_updated_successfully": "Application configuration updated successfully",
|
||||||
"application_name": "Application Name",
|
"application_name": "Application Name",
|
||||||
@@ -333,5 +336,11 @@
|
|||||||
"disable_firstname_lastname": "Disable {firstName} {lastName}",
|
"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.",
|
"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": "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": "Включить уведомления пользователей по электронной почте при обнаружении логина с нового устройства или локации.",
|
"configure_smtp_to_send_emails": "Enable email notifications to alert users when a login is detected from a new device or location.",
|
||||||
"ldap": "LDAP",
|
"ldap": "LDAP",
|
||||||
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Настроить конфигурацию LDAP для синхронизации пользователей и групп с сервером LDAP.",
|
"configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server": "Настроить конфигурацию LDAP для синхронизации пользователей и групп с сервером LDAP.",
|
||||||
"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": "Позволяет пользователям войти с помощью кода входа, отправленного на их электронную почту. Это значительно снижает безопасность так как любой человек, имеющий доступ к электронной почте пользователя, сможет получить доступ.",
|
"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": "Название приложения",
|
||||||
@@ -324,14 +327,20 @@
|
|||||||
"client_authorization": "Авторизация в клиенте",
|
"client_authorization": "Авторизация в клиенте",
|
||||||
"new_client_authorization": "Новая авторизация в клиенте",
|
"new_client_authorization": "Новая авторизация в клиенте",
|
||||||
"disable_animations": "Отключить анимации",
|
"disable_animations": "Отключить анимации",
|
||||||
"turn_off_all_animations_throughout_the_admin_ui": "Turn off all animations throughout the Admin UI.",
|
"turn_off_all_animations_throughout_the_admin_ui": "Выключить все анимации в интерфейсе администратора.",
|
||||||
"user_disabled": "Account Disabled",
|
"user_disabled": "Аккаунт отключен",
|
||||||
"disabled_users_cannot_log_in_or_use_services": "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_disabled_successfully": "Пользователь успешно отключен.",
|
||||||
"user_enabled_successfully": "User has been enabled successfully.",
|
"user_enabled_successfully": "Пользователь успешно включен.",
|
||||||
"status": "Status",
|
"status": "Статус",
|
||||||
"disable_firstname_lastname": "Disable {firstName} {lastName}",
|
"disable_firstname_lastname": "Отключить {firstName} {lastName}",
|
||||||
"are_you_sure_you_want_to_disable_this_user": "Are you sure you want to disable this user? They will not be able to log in or access any services.",
|
"are_you_sure_you_want_to_disable_this_user": "Вы уверены, что хотите отключить этого пользователя? Они не смогут войти в систему или получить доступ к любым сервисам.",
|
||||||
"ldap_soft_delete_users": "Keep disabled users from LDAP.",
|
"ldap_soft_delete_users": "Оставить отключенных пользователей от 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": "Когда включено, пользователи удалённые из 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",
|
"name": "pocket-id-frontend",
|
||||||
"version": "0.48.0",
|
"version": "0.49.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -9,8 +9,10 @@
|
|||||||
import { Separator } from '$lib/components/ui/separator';
|
import { Separator } from '$lib/components/ui/separator';
|
||||||
import { m } from '$lib/paraglide/messages';
|
import { m } from '$lib/paraglide/messages';
|
||||||
import UserService from '$lib/services/user-service';
|
import UserService from '$lib/services/user-service';
|
||||||
|
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||||
import { mode } from 'mode-watcher';
|
import { mode } from 'mode-watcher';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
userId = $bindable()
|
userId = $bindable()
|
||||||
@@ -32,7 +34,7 @@
|
|||||||
[m.one_month()]: 60 * 60 * 24 * 30
|
[m.one_month()]: 60 * 60 * 24 * 30
|
||||||
};
|
};
|
||||||
|
|
||||||
async function createOneTimeAccessToken() {
|
async function createLoginCode() {
|
||||||
try {
|
try {
|
||||||
const expiration = new Date(Date.now() + availableExpirations[selectedExpiration] * 1000);
|
const expiration = new Date(Date.now() + availableExpirations[selectedExpiration] * 1000);
|
||||||
code = await userService.createOneTimeAccessToken(expiration, userId!);
|
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) {
|
function onOpenChange(open: boolean) {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
oneTimeLink = null;
|
oneTimeLink = null;
|
||||||
@@ -81,13 +94,20 @@
|
|||||||
</Select.Content>
|
</Select.Content>
|
||||||
</Select.Root>
|
</Select.Root>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Dialog.Footer class="mt-2">
|
||||||
onclick={() => createOneTimeAccessToken()}
|
{#if $appConfigStore.emailOneTimeAccessAsAdminEnabled}
|
||||||
disabled={!selectedExpiration}
|
<Button
|
||||||
class="mt-2 w-full"
|
onclick={() => sendLoginCodeEmail()}
|
||||||
>
|
variant="secondary"
|
||||||
{m.generate_code()}
|
disabled={!selectedExpiration}
|
||||||
</Button>
|
>
|
||||||
|
{m.send_email()}
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
<Button onclick={() => createLoginCode()} disabled={!selectedExpiration}
|
||||||
|
>{m.show_code()}</Button
|
||||||
|
>
|
||||||
|
</Dialog.Footer>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex flex-col items-center gap-2">
|
<div class="flex flex-col items-center gap-2">
|
||||||
<CopyToClipboard value={code!}>
|
<CopyToClipboard value={code!}>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Command as CommandPrimitive } from "cmdk-sv";
|
import { Command as CommandPrimitive } from "cmdk-sv";
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from "$lib/utils/style.js";
|
||||||
|
import type { ClassValue } from "svelte/elements";
|
||||||
|
|
||||||
type $$Props = CommandPrimitive.EmptyProps;
|
type $$Props = CommandPrimitive.EmptyProps;
|
||||||
let className: string | undefined | null = undefined;
|
let className: ClassValue | undefined | null = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Command as CommandPrimitive } from "cmdk-sv";
|
import { Command as CommandPrimitive } from "cmdk-sv";
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from "$lib/utils/style.js";
|
||||||
|
import type { ClassValue } from "svelte/elements";
|
||||||
type $$Props = CommandPrimitive.GroupProps;
|
type $$Props = CommandPrimitive.GroupProps;
|
||||||
|
|
||||||
let className: string | undefined | null = undefined;
|
let className: ClassValue | undefined | null = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Command as CommandPrimitive } from "cmdk-sv";
|
import { Command as CommandPrimitive } from "cmdk-sv";
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from "$lib/utils/style.js";
|
||||||
|
import type { ClassValue } from "svelte/elements";
|
||||||
|
|
||||||
type $$Props = CommandPrimitive.ItemProps;
|
type $$Props = CommandPrimitive.ItemProps;
|
||||||
|
|
||||||
export let asChild = false;
|
export let asChild = false;
|
||||||
|
|
||||||
let className: string | undefined | null = undefined;
|
let className: ClassValue | undefined | null = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Command as CommandPrimitive } from "cmdk-sv";
|
import { Command as CommandPrimitive } from "cmdk-sv";
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from "$lib/utils/style.js";
|
||||||
|
import type { ClassValue } from "svelte/elements";
|
||||||
|
|
||||||
type $$Props = CommandPrimitive.ListProps;
|
type $$Props = CommandPrimitive.ListProps;
|
||||||
let className: string | undefined | null = undefined;
|
let className: ClassValue | undefined | null = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Command as CommandPrimitive } from "cmdk-sv";
|
import { Command as CommandPrimitive } from "cmdk-sv";
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from "$lib/utils/style.js";
|
||||||
|
import type { ClassValue } from "svelte/elements";
|
||||||
|
|
||||||
type $$Props = CommandPrimitive.SeparatorProps;
|
type $$Props = CommandPrimitive.SeparatorProps;
|
||||||
let className: string | undefined | null = undefined;
|
let className: ClassValue | undefined | null = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { ClassValue, HTMLAttributes } from "svelte/elements";
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from "$lib/utils/style.js";
|
||||||
|
|
||||||
type $$Props = HTMLAttributes<HTMLSpanElement>;
|
type $$Props = HTMLAttributes<HTMLSpanElement>;
|
||||||
|
|
||||||
let className: string | undefined | null = undefined;
|
let className: ClassValue | undefined | null = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Command as CommandPrimitive } from "cmdk-sv";
|
import { Command as CommandPrimitive } from "cmdk-sv";
|
||||||
import { cn } from "$lib/utils/style.js";
|
import { cn } from "$lib/utils/style.js";
|
||||||
|
import type { ClassValue } from "svelte/elements";
|
||||||
|
|
||||||
type $$Props = CommandPrimitive.CommandProps;
|
type $$Props = CommandPrimitive.CommandProps;
|
||||||
|
|
||||||
export let value: $$Props["value"] = undefined;
|
export let value: $$Props["value"] = undefined;
|
||||||
|
|
||||||
let className: string | undefined | null = undefined;
|
let className: ClassValue | undefined | null = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -87,10 +87,14 @@ export default class UserService extends APIService {
|
|||||||
return res.data as User;
|
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 });
|
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[]) {
|
async updateUserGroups(id: string, userGroupIds: string[]) {
|
||||||
const res = await this.api.put(`/users/${id}/user-groups`, { userGroupIds });
|
const res = await this.api.put(`/users/${id}/user-groups`, { userGroupIds });
|
||||||
return res.data as User;
|
return res.data as User;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
export type AppConfig = {
|
export type AppConfig = {
|
||||||
appName: string;
|
appName: string;
|
||||||
allowOwnAccountEdit: boolean;
|
allowOwnAccountEdit: boolean;
|
||||||
emailOneTimeAccessEnabled: boolean;
|
emailOneTimeAccessAsUnauthenticatedEnabled: boolean;
|
||||||
|
emailOneTimeAccessAsAdminEnabled: boolean;
|
||||||
ldapEnabled: boolean;
|
ldapEnabled: boolean;
|
||||||
disableAnimations: boolean;
|
disableAnimations: boolean;
|
||||||
};
|
};
|
||||||
@@ -19,6 +20,7 @@ export type AllAppConfig = AppConfig & {
|
|||||||
smtpTls: 'none' | 'starttls' | 'tls';
|
smtpTls: 'none' | 'starttls' | 'tls';
|
||||||
smtpSkipCertVerify: boolean;
|
smtpSkipCertVerify: boolean;
|
||||||
emailLoginNotificationEnabled: boolean;
|
emailLoginNotificationEnabled: boolean;
|
||||||
|
emailApiKeyExpirationEnabled: boolean;
|
||||||
// LDAP
|
// LDAP
|
||||||
ldapUrl: string;
|
ldapUrl: string;
|
||||||
ldapBindDn: string;
|
ldapBindDn: string;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export type User = {
|
|||||||
customClaims: CustomClaim[];
|
customClaims: CustomClaim[];
|
||||||
locale?: Locale;
|
locale?: Locale;
|
||||||
ldapId?: string;
|
ldapId?: string;
|
||||||
disabled: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserCreate = Omit<User, 'id' | 'customClaims' | 'ldapId' | 'userGroups'>;
|
export type UserCreate = Omit<User, 'id' | 'customClaims' | 'ldapId' | 'userGroups'>;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import SignInWrapper from '$lib/components/login-wrapper.svelte';
|
import SignInWrapper from '$lib/components/login-wrapper.svelte';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
|
import { m } from '$lib/paraglide/messages';
|
||||||
import OidcService from '$lib/services/oidc-service';
|
import OidcService from '$lib/services/oidc-service';
|
||||||
import WebAuthnService from '$lib/services/webauthn-service';
|
import WebAuthnService from '$lib/services/webauthn-service';
|
||||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||||
@@ -14,7 +15,6 @@
|
|||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import ClientProviderImages from './components/client-provider-images.svelte';
|
import ClientProviderImages from './components/client-provider-images.svelte';
|
||||||
import ScopeItem from './components/scope-item.svelte';
|
import ScopeItem from './components/scope-item.svelte';
|
||||||
import { m } from '$lib/paraglide/messages';
|
|
||||||
|
|
||||||
const webauthnService = new WebAuthnService();
|
const webauthnService = new WebAuthnService();
|
||||||
const oidService = new OidcService();
|
const oidService = new OidcService();
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
{#if client == null}
|
{#if client == null}
|
||||||
<p>{m.client_not_found()}</p>
|
<p>{m.client_not_found()}</p>
|
||||||
{:else}
|
{:else}
|
||||||
<SignInWrapper animate showAlternativeSignInMethodButton>
|
<SignInWrapper animate={!$appConfigStore.disableAnimations} showAlternativeSignInMethodButton={$userStore == null}>
|
||||||
<ClientProviderImages {client} {success} error={!!errorMessage} />
|
<ClientProviderImages {client} {success} error={!!errorMessage} />
|
||||||
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">
|
<h1 class="font-playfair mt-5 text-3xl font-bold sm:text-4xl">
|
||||||
{m.sign_in_to({ name: client.name })}
|
{m.sign_in_to({ name: client.name })}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($appConfigStore.emailOneTimeAccessEnabled) {
|
if ($appConfigStore.emailOneTimeAccessAsUnauthenticatedEnabled) {
|
||||||
methods.push({
|
methods.push({
|
||||||
icon: LucideMail,
|
icon: LucideMail,
|
||||||
title: m.email_login(),
|
title: m.email_login(),
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
async function requestEmail() {
|
async function requestEmail() {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
await userService
|
await userService
|
||||||
.requestOneTimeAccessEmail(email, data.redirect)
|
.requestOneTimeAccessEmailAsUnauthenticatedUser(email, data.redirect)
|
||||||
.then(() => (success = true))
|
.then(() => (success = true))
|
||||||
.catch((e) => (error = e.response?.data.error || m.an_unknown_error_occurred()));
|
.catch((e) => (error = e.response?.data.error || m.an_unknown_error_occurred()));
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { m } from '$lib/paraglide/messages';
|
import { m } from '$lib/paraglide/messages';
|
||||||
import WebAuthnService from '$lib/services/webauthn-service';
|
import WebAuthnService from '$lib/services/webauthn-service';
|
||||||
|
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||||
import userStore from '$lib/stores/user-store.js';
|
import userStore from '$lib/stores/user-store.js';
|
||||||
import { axiosErrorToast } from '$lib/utils/error-util.js';
|
import { axiosErrorToast } from '$lib/utils/error-util.js';
|
||||||
|
|
||||||
@@ -26,7 +27,7 @@
|
|||||||
<title>{m.logout()}</title>
|
<title>{m.logout()}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<SignInWrapper animate>
|
<SignInWrapper animate={!$appConfigStore.disableAnimations}>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<div class="bg-muted rounded-2xl p-3">
|
<div class="bg-muted rounded-2xl p-3">
|
||||||
<Logo class="h-10 w-10" />
|
<Logo class="h-10 w-10" />
|
||||||
|
|||||||
@@ -18,12 +18,12 @@
|
|||||||
'it-IT': 'Italiano'
|
'it-IT': 'Italiano'
|
||||||
};
|
};
|
||||||
|
|
||||||
function updateLocale(locale: Locale) {
|
async function updateLocale(locale: Locale) {
|
||||||
setLocale(locale);
|
await userService.updateCurrent({
|
||||||
userService.updateCurrent({
|
|
||||||
...$userStore!,
|
...$userStore!,
|
||||||
locale
|
locale
|
||||||
});
|
});
|
||||||
|
setLocale(locale);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,7 @@
|
|||||||
id="application-configuration-email"
|
id="application-configuration-email"
|
||||||
icon={Mail}
|
icon={Mail}
|
||||||
title={m.email()}
|
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} />
|
<AppConfigEmailForm {appConfig} callback={updateAppConfig} />
|
||||||
</CollapsibleCard>
|
</CollapsibleCard>
|
||||||
|
|||||||
@@ -39,8 +39,10 @@
|
|||||||
smtpFrom: z.string().email(),
|
smtpFrom: z.string().email(),
|
||||||
smtpTls: z.enum(['none', 'starttls', 'tls']),
|
smtpTls: z.enum(['none', 'starttls', 'tls']),
|
||||||
smtpSkipCertVerify: z.boolean(),
|
smtpSkipCertVerify: z.boolean(),
|
||||||
emailOneTimeAccessEnabled: z.boolean(),
|
emailOneTimeAccessAsUnauthenticatedEnabled: z.boolean(),
|
||||||
emailLoginNotificationEnabled: z.boolean()
|
emailOneTimeAccessAsAdminEnabled: z.boolean(),
|
||||||
|
emailLoginNotificationEnabled: z.boolean(),
|
||||||
|
emailApiKeyExpirationEnabled: z.boolean()
|
||||||
});
|
});
|
||||||
|
|
||||||
const { inputs, ...form } = createForm<typeof formSchema>(formSchema, appConfig);
|
const { inputs, ...form } = createForm<typeof formSchema>(formSchema, appConfig);
|
||||||
@@ -88,9 +90,7 @@
|
|||||||
await appConfigService
|
await appConfigService
|
||||||
.sendTestEmail()
|
.sendTestEmail()
|
||||||
.then(() => toast.success(m.test_email_sent_successfully()))
|
.then(() => toast.success(m.test_email_sent_successfully()))
|
||||||
.catch(() =>
|
.catch(() => toast.error(m.failed_to_send_test_email()))
|
||||||
toast.error(m.failed_to_send_test_email())
|
|
||||||
)
|
|
||||||
.finally(() => (isSendingTestEmail = false));
|
.finally(() => (isSendingTestEmail = false));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -135,11 +135,24 @@
|
|||||||
description={m.send_an_email_to_the_user_when_they_log_in_from_a_new_device()}
|
description={m.send_an_email_to_the_user_when_they_log_in_from_a_new_device()}
|
||||||
bind:checked={$inputs.emailLoginNotificationEnabled.value}
|
bind:checked={$inputs.emailLoginNotificationEnabled.value}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CheckboxWithLabel
|
<CheckboxWithLabel
|
||||||
id="email-login"
|
id="email-login-admin"
|
||||||
label={m.email_login()}
|
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()}
|
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>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|||||||
@@ -20,12 +20,10 @@
|
|||||||
allowEmpty?: boolean;
|
allowEmpty?: boolean;
|
||||||
children?: Snippet;
|
children?: Snippet;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
const limit = 20;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div {...restProps}>
|
<div {...restProps}>
|
||||||
<FormInput {label}>
|
<FormInput {label} description={m.callback_url_description()}>
|
||||||
<div class="flex flex-col gap-y-2">
|
<div class="flex flex-col gap-y-2">
|
||||||
{#each callbackURLs as _, i}
|
{#each callbackURLs as _, i}
|
||||||
<div class="flex gap-x-2">
|
<div class="flex gap-x-2">
|
||||||
@@ -46,15 +44,13 @@
|
|||||||
{#if error}
|
{#if error}
|
||||||
<p class="mt-1 text-sm text-red-500">{error}</p>
|
<p class="mt-1 text-sm text-red-500">{error}</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#if callbackURLs.length < limit}
|
<Button
|
||||||
<Button
|
class="mt-2"
|
||||||
class="mt-2"
|
variant="secondary"
|
||||||
variant="secondary"
|
size="sm"
|
||||||
size="sm"
|
on:click={() => (callbackURLs = [...callbackURLs, ''])}
|
||||||
on:click={() => (callbackURLs = [...callbackURLs, ''])}
|
>
|
||||||
>
|
<LucidePlus class="mr-1 h-4 w-4" />
|
||||||
<LucidePlus class="mr-1 h-4 w-4" />
|
{callbackURLs.length === 0 ? m.add() : m.add_another()}
|
||||||
{callbackURLs.length === 0 ? m.add() : m.add_another()}
|
</Button>
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import FormInput from '$lib/components/form/form-input.svelte';
|
import FormInput from '$lib/components/form/form-input.svelte';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import Label from '$lib/components/ui/label/label.svelte';
|
import Label from '$lib/components/ui/label/label.svelte';
|
||||||
|
import { m } from '$lib/paraglide/messages';
|
||||||
import type {
|
import type {
|
||||||
OidcClient,
|
OidcClient,
|
||||||
OidcClientCreate,
|
OidcClientCreate,
|
||||||
@@ -12,7 +13,6 @@
|
|||||||
import { createForm } from '$lib/utils/form-util';
|
import { createForm } from '$lib/utils/form-util';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import OidcCallbackUrlInput from './oidc-callback-url-input.svelte';
|
import OidcCallbackUrlInput from './oidc-callback-url-input.svelte';
|
||||||
import { m } from '$lib/paraglide/messages';
|
|
||||||
|
|
||||||
let {
|
let {
|
||||||
callback,
|
callback,
|
||||||
@@ -38,8 +38,8 @@
|
|||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
name: z.string().min(2).max(50),
|
name: z.string().min(2).max(50),
|
||||||
callbackURLs: z.array(z.string()).nonempty(),
|
callbackURLs: z.array(z.string().nonempty()).nonempty(),
|
||||||
logoutCallbackURLs: z.array(z.string()),
|
logoutCallbackURLs: z.array(z.string().nonempty()),
|
||||||
isPublic: z.boolean(),
|
isPublic: z.boolean(),
|
||||||
pkceEnabled: z.boolean()
|
pkceEnabled: z.boolean()
|
||||||
});
|
});
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form onsubmit={onSubmit}>
|
<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} />
|
<FormInput label={m.name()} class="w-full" bind:input={$inputs.name} />
|
||||||
<div></div>
|
<div></div>
|
||||||
<OidcCallbackUrlInput
|
<OidcCallbackUrlInput
|
||||||
@@ -120,7 +120,7 @@
|
|||||||
<img
|
<img
|
||||||
class="m-auto max-h-full max-w-full object-contain"
|
class="m-auto max-h-full max-w-full object-contain"
|
||||||
src={logoDataURL}
|
src={logoDataURL}
|
||||||
alt={m.name_logo({name: $inputs.name.value})}
|
alt={m.name_logo({ name: $inputs.name.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -161,4 +161,4 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
</AdvancedTable>
|
</AdvancedTable>
|
||||||
|
|
||||||
<OneTimeLinkModal userId={userIdToCreateOneTimeLink} />
|
<OneTimeLinkModal bind:userId={userIdToCreateOneTimeLink} />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ACCESS_TOKEN_COOKIE_NAME } from '$lib/constants';
|
import { ACCESS_TOKEN_COOKIE_NAME } from '$lib/constants';
|
||||||
import AuditLogService from '$lib/services/audit-log-service';
|
import AuditLogService from '$lib/services/audit-log-service';
|
||||||
import type { SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
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 }) => {
|
export const load: PageServerLoad = async ({ cookies }) => {
|
||||||
const auditLogService = new AuditLogService(cookies.get(ACCESS_TOKEN_COOKIE_NAME));
|
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 Password').fill('password');
|
||||||
await page.getByLabel('SMTP From').fill('test@gmail.com');
|
await page.getByLabel('SMTP From').fill('test@gmail.com');
|
||||||
await page.getByLabel('Email Login Notification').click();
|
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();
|
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 Password')).toHaveValue('password');
|
||||||
await expect(page.getByLabel('SMTP From')).toHaveValue('test@gmail.com');
|
await expect(page.getByLabel('SMTP From')).toHaveValue('test@gmail.com');
|
||||||
await expect(page.getByLabel('Email Login Notification')).toBeChecked();
|
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 }) => {
|
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.getByLabel('Login Code').getByRole('combobox').click();
|
||||||
await page.getByRole('option', { name: '12 hours' }).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();
|
const link = await page.getByTestId('login-code-link').textContent();
|
||||||
await context.clearCookies();
|
await context.clearCookies();
|
||||||
|
|||||||
Reference in New Issue
Block a user