mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-02-27 15:33:54 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
097bda349a | ||
|
|
6e24517197 | ||
|
|
a3da943aa6 | ||
|
|
cc34aca2a0 | ||
|
|
fde4e9b38a | ||
|
|
c55143d8c9 | ||
|
|
8973e93cb6 | ||
|
|
8c9cac2655 |
1820
CHANGELOG.md
1820
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -4,11 +4,9 @@ import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"path"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
@@ -16,10 +14,8 @@ import (
|
||||
)
|
||||
|
||||
// initApplicationImages copies the images from the images directory to the application-images directory
|
||||
func initApplicationImages() error {
|
||||
// Images that are built into the Pocket ID binary
|
||||
builtInImageHashes := getBuiltInImageHashes()
|
||||
|
||||
// and returns a map containing the detected file extensions in the application-images directory.
|
||||
func initApplicationImages() (map[string]string, error) {
|
||||
// Previous versions of images
|
||||
// If these are found, they are deleted
|
||||
legacyImageHashes := imageHashMap{
|
||||
@@ -30,21 +26,31 @@ func initApplicationImages() error {
|
||||
|
||||
sourceFiles, err := resources.FS.ReadDir("images")
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("failed to read directory: %w", err)
|
||||
return nil, fmt.Errorf("failed to read directory: %w", err)
|
||||
}
|
||||
|
||||
destinationFiles, err := os.ReadDir(dirPath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("failed to read directory: %w", err)
|
||||
return nil, fmt.Errorf("failed to read directory: %w", err)
|
||||
}
|
||||
destinationFilesMap := make(map[string]bool, len(destinationFiles))
|
||||
dstNameToExt := make(map[string]string, len(destinationFiles))
|
||||
for _, f := range destinationFiles {
|
||||
if f.IsDir() {
|
||||
continue
|
||||
}
|
||||
name := f.Name()
|
||||
destFilePath := filepath.Join(dirPath, name)
|
||||
nameWithoutExt, ext := utils.SplitFileName(name)
|
||||
destFilePath := path.Join(dirPath, name)
|
||||
|
||||
// Skip directories
|
||||
if f.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
h, err := utils.CreateSha256FileHash(destFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get hash for file '%s': %w", name, err)
|
||||
slog.Warn("Failed to get hash for file", slog.String("name", name), slog.Any("error", err))
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if the file is a legacy one - if so, delete it
|
||||
@@ -52,50 +58,43 @@ func initApplicationImages() error {
|
||||
slog.Info("Found legacy application image that will be removed", slog.String("name", name))
|
||||
err = os.Remove(destFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove legacy file '%s': %w", name, err)
|
||||
return nil, fmt.Errorf("failed to remove legacy file '%s': %w", name, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if the file is a built-in one and save it in the map
|
||||
destinationFilesMap[getImageNameWithoutExtension(name)] = builtInImageHashes.Contains(h)
|
||||
// Track existing files
|
||||
dstNameToExt[nameWithoutExt] = ext
|
||||
}
|
||||
|
||||
// Copy images from the images directory to the application-images directory if they don't already exist
|
||||
for _, sourceFile := range sourceFiles {
|
||||
// Skip if it's a directory
|
||||
if sourceFile.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
name := sourceFile.Name()
|
||||
srcFilePath := filepath.Join("images", name)
|
||||
destFilePath := filepath.Join(dirPath, name)
|
||||
nameWithoutExt, ext := utils.SplitFileName(name)
|
||||
srcFilePath := path.Join("images", name)
|
||||
destFilePath := path.Join(dirPath, name)
|
||||
|
||||
// Skip if there's already an image at the path
|
||||
// We do not check the extension because users could have uploaded a different one
|
||||
if imageAlreadyExists(sourceFile, destinationFilesMap) {
|
||||
if _, exists := dstNameToExt[nameWithoutExt]; exists {
|
||||
continue
|
||||
}
|
||||
|
||||
slog.Info("Writing new application image", slog.String("name", name))
|
||||
err := utils.CopyEmbeddedFileToDisk(srcFilePath, destFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to copy file: %w", err)
|
||||
return nil, fmt.Errorf("failed to copy file: %w", err)
|
||||
}
|
||||
|
||||
// Track the newly copied file so it can be included in the extensions map later
|
||||
dstNameToExt[nameWithoutExt] = ext
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getBuiltInImageHashes() imageHashMap {
|
||||
return imageHashMap{
|
||||
"background.webp": mustDecodeHex("3fc436a66d6b872b01d96a4e75046c46b5c3e2daccd51e98ecdf98fd445599ab"),
|
||||
"favicon.ico": mustDecodeHex("70f9c4b6bd4781ade5fc96958b1267511751e91957f83c2354fb880b35ec890a"),
|
||||
"logo.svg": mustDecodeHex("f1e60707df9784152ce0847e3eb59cb68b9015f918ff160376c27ebff1eda796"),
|
||||
"logoDark.svg": mustDecodeHex("0421a8d93714bacf54c78430f1db378fd0d29565f6de59b6a89090d44a82eb16"),
|
||||
"logoLight.svg": mustDecodeHex("6d42c88cf6668f7e57c4f2a505e71ecc8a1e0a27534632aa6adec87b812d0bb0"),
|
||||
}
|
||||
return dstNameToExt, nil
|
||||
}
|
||||
|
||||
type imageHashMap map[string][]byte
|
||||
@@ -112,21 +111,6 @@ func (m imageHashMap) Contains(target []byte) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func imageAlreadyExists(sourceFile fs.DirEntry, destinationFiles map[string]bool) bool {
|
||||
sourceFileWithoutExtension := getImageNameWithoutExtension(sourceFile.Name())
|
||||
_, ok := destinationFiles[sourceFileWithoutExtension]
|
||||
return ok
|
||||
}
|
||||
|
||||
func getImageNameWithoutExtension(fileName string) string {
|
||||
idx := strings.LastIndexByte(fileName, '.')
|
||||
if idx < 1 {
|
||||
// No dot found, or fileName starts with a dot
|
||||
return fileName
|
||||
}
|
||||
return fileName[:idx]
|
||||
}
|
||||
|
||||
func mustDecodeHex(str string) []byte {
|
||||
b, err := hex.DecodeString(str)
|
||||
if err != nil {
|
||||
@@ -1,61 +0,0 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
)
|
||||
|
||||
func TestGetBuiltInImageData(t *testing.T) {
|
||||
// Get the built-in image data map
|
||||
builtInImages := getBuiltInImageHashes()
|
||||
|
||||
// Read the actual images directory from disk
|
||||
imagesDir := filepath.Join("..", "..", "resources", "images")
|
||||
actualFiles, err := os.ReadDir(imagesDir)
|
||||
require.NoError(t, err, "Failed to read images directory")
|
||||
|
||||
// Create a map of actual files for comparison
|
||||
actualFilesMap := make(map[string]struct{})
|
||||
|
||||
// Validate each actual file exists in the built-in data with correct hash
|
||||
for _, file := range actualFiles {
|
||||
fileName := file.Name()
|
||||
if file.IsDir() || strings.HasPrefix(fileName, ".") {
|
||||
continue
|
||||
}
|
||||
|
||||
actualFilesMap[fileName] = struct{}{}
|
||||
|
||||
// Check if the file exists in the built-in data
|
||||
builtInHash, exists := builtInImages[fileName]
|
||||
assert.True(t, exists, "File %s exists in images directory but not in getBuiltInImageData map", fileName)
|
||||
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
filePath := filepath.Join(imagesDir, fileName)
|
||||
|
||||
// Validate SHA256 hash
|
||||
actualHash, err := utils.CreateSha256FileHash(filePath)
|
||||
require.NoError(t, err, "Failed to compute hash for %s", fileName)
|
||||
assert.Equal(t, actualHash, builtInHash, "SHA256 hash mismatch for file %s", fileName)
|
||||
}
|
||||
|
||||
// Ensure the built-in data doesn't have extra files that don't exist in the directory
|
||||
for fileName := range builtInImages {
|
||||
_, exists := actualFilesMap[fileName]
|
||||
assert.True(t, exists, "File %s exists in getBuiltInImageData map but not in images directory", fileName)
|
||||
}
|
||||
|
||||
// Ensure we have at least some files (sanity check)
|
||||
assert.NotEmpty(t, actualFilesMap, "Images directory should contain at least one file")
|
||||
assert.Len(t, actualFilesMap, len(builtInImages), "Number of files in directory should match number in built-in data map")
|
||||
}
|
||||
@@ -21,7 +21,7 @@ func Bootstrap(ctx context.Context) error {
|
||||
}
|
||||
slog.InfoContext(ctx, "Pocket ID is starting")
|
||||
|
||||
err = initApplicationImages()
|
||||
imageExtensions, err := initApplicationImages()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize application images: %w", err)
|
||||
}
|
||||
@@ -33,7 +33,7 @@ func Bootstrap(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// Create all services
|
||||
svc, err := initServices(ctx, db, httpClient)
|
||||
svc, err := initServices(ctx, db, httpClient, imageExtensions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize services: %w", err)
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ func initRouterInternal(db *gorm.DB, svc *services) (utils.Service, error) {
|
||||
controller.NewOidcController(apiGroup, authMiddleware, fileSizeLimitMiddleware, svc.oidcService, svc.jwtService)
|
||||
controller.NewUserController(apiGroup, authMiddleware, middleware.NewRateLimitMiddleware(), svc.userService, svc.appConfigService)
|
||||
controller.NewAppConfigController(apiGroup, authMiddleware, svc.appConfigService, svc.emailService, svc.ldapService)
|
||||
controller.NewAppImagesController(apiGroup, authMiddleware, svc.appImagesService)
|
||||
controller.NewAuditLogController(apiGroup, svc.auditLogService, authMiddleware)
|
||||
controller.NewUserGroupController(apiGroup, authMiddleware, svc.userGroupService)
|
||||
controller.NewCustomClaimController(apiGroup, authMiddleware, svc.customClaimService)
|
||||
@@ -181,9 +182,9 @@ func initRouterInternal(db *gorm.DB, svc *services) (utils.Service, error) {
|
||||
|
||||
func initLogger(r *gin.Engine) {
|
||||
loggerSkipPathsPrefix := []string{
|
||||
"GET /api/application-configuration/logo",
|
||||
"GET /api/application-configuration/background-image",
|
||||
"GET /api/application-configuration/favicon",
|
||||
"GET /api/application-images/logo",
|
||||
"GET /api/application-images/background",
|
||||
"GET /api/application-images/favicon",
|
||||
"GET /_app",
|
||||
"GET /fonts",
|
||||
"GET /healthz",
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
type services struct {
|
||||
appConfigService *service.AppConfigService
|
||||
appImagesService *service.AppImagesService
|
||||
emailService *service.EmailService
|
||||
geoLiteService *service.GeoLiteService
|
||||
auditLogService *service.AuditLogService
|
||||
@@ -27,7 +28,7 @@ type services struct {
|
||||
}
|
||||
|
||||
// Initializes all services
|
||||
func initServices(ctx context.Context, db *gorm.DB, httpClient *http.Client) (svc *services, err error) {
|
||||
func initServices(ctx context.Context, db *gorm.DB, httpClient *http.Client, imageExtensions map[string]string) (svc *services, err error) {
|
||||
svc = &services{}
|
||||
|
||||
svc.appConfigService, err = service.NewAppConfigService(ctx, db)
|
||||
@@ -35,6 +36,8 @@ func initServices(ctx context.Context, db *gorm.DB, httpClient *http.Client) (sv
|
||||
return nil, fmt.Errorf("failed to create app config service: %w", err)
|
||||
}
|
||||
|
||||
svc.appImagesService = service.NewAppImagesService(imageExtensions)
|
||||
|
||||
svc.emailService, err = service.NewEmailService(db, svc.appConfigService)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create email service: %w", err)
|
||||
|
||||
@@ -3,14 +3,12 @@ package controller
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"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/middleware"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
)
|
||||
|
||||
// NewAppConfigController creates a new controller for application configuration endpoints
|
||||
@@ -34,13 +32,6 @@ func NewAppConfigController(
|
||||
group.GET("/application-configuration/all", authMiddleware.Add(), acc.listAllAppConfigHandler)
|
||||
group.PUT("/application-configuration", authMiddleware.Add(), acc.updateAppConfigHandler)
|
||||
|
||||
group.GET("/application-configuration/logo", acc.getLogoHandler)
|
||||
group.GET("/application-configuration/background-image", acc.getBackgroundImageHandler)
|
||||
group.GET("/application-configuration/favicon", acc.getFaviconHandler)
|
||||
group.PUT("/application-configuration/logo", authMiddleware.Add(), acc.updateLogoHandler)
|
||||
group.PUT("/application-configuration/favicon", authMiddleware.Add(), acc.updateFaviconHandler)
|
||||
group.PUT("/application-configuration/background-image", authMiddleware.Add(), acc.updateBackgroundImageHandler)
|
||||
|
||||
group.POST("/application-configuration/test-email", authMiddleware.Add(), acc.testEmailHandler)
|
||||
group.POST("/application-configuration/sync-ldap", authMiddleware.Add(), acc.syncLdapHandler)
|
||||
}
|
||||
@@ -129,147 +120,6 @@ func (acc *AppConfigController) updateAppConfigHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, configVariablesDto)
|
||||
}
|
||||
|
||||
// getLogoHandler godoc
|
||||
// @Summary Get logo image
|
||||
// @Description Get the logo image for the application
|
||||
// @Tags Application Configuration
|
||||
// @Param light query boolean false "Light mode logo (true) or dark mode logo (false)"
|
||||
// @Produce image/png
|
||||
// @Produce image/jpeg
|
||||
// @Produce image/svg+xml
|
||||
// @Success 200 {file} binary "Logo image"
|
||||
// @Router /api/application-configuration/logo [get]
|
||||
func (acc *AppConfigController) getLogoHandler(c *gin.Context) {
|
||||
dbConfig := acc.appConfigService.GetDbConfig()
|
||||
|
||||
lightLogo, _ := strconv.ParseBool(c.DefaultQuery("light", "true"))
|
||||
|
||||
var imageName, imageType string
|
||||
if lightLogo {
|
||||
imageName = "logoLight"
|
||||
imageType = dbConfig.LogoLightImageType.Value
|
||||
} else {
|
||||
imageName = "logoDark"
|
||||
imageType = dbConfig.LogoDarkImageType.Value
|
||||
}
|
||||
|
||||
acc.getImage(c, imageName, imageType)
|
||||
}
|
||||
|
||||
// getFaviconHandler godoc
|
||||
// @Summary Get favicon
|
||||
// @Description Get the favicon for the application
|
||||
// @Tags Application Configuration
|
||||
// @Produce image/x-icon
|
||||
// @Success 200 {file} binary "Favicon image"
|
||||
// @Router /api/application-configuration/favicon [get]
|
||||
func (acc *AppConfigController) getFaviconHandler(c *gin.Context) {
|
||||
acc.getImage(c, "favicon", "ico")
|
||||
}
|
||||
|
||||
// getBackgroundImageHandler godoc
|
||||
// @Summary Get background image
|
||||
// @Description Get the background image for the application
|
||||
// @Tags Application Configuration
|
||||
// @Produce image/png
|
||||
// @Produce image/jpeg
|
||||
// @Success 200 {file} binary "Background image"
|
||||
// @Router /api/application-configuration/background-image [get]
|
||||
func (acc *AppConfigController) getBackgroundImageHandler(c *gin.Context) {
|
||||
imageType := acc.appConfigService.GetDbConfig().BackgroundImageType.Value
|
||||
acc.getImage(c, "background", imageType)
|
||||
}
|
||||
|
||||
// updateLogoHandler godoc
|
||||
// @Summary Update logo
|
||||
// @Description Update the application logo
|
||||
// @Tags Application Configuration
|
||||
// @Accept multipart/form-data
|
||||
// @Param light query boolean false "Light mode logo (true) or dark mode logo (false)"
|
||||
// @Param file formData file true "Logo image file"
|
||||
// @Success 204 "No Content"
|
||||
// @Router /api/application-configuration/logo [put]
|
||||
func (acc *AppConfigController) updateLogoHandler(c *gin.Context) {
|
||||
dbConfig := acc.appConfigService.GetDbConfig()
|
||||
|
||||
lightLogo, _ := strconv.ParseBool(c.DefaultQuery("light", "true"))
|
||||
|
||||
var imageName, imageType string
|
||||
if lightLogo {
|
||||
imageName = "logoLight"
|
||||
imageType = dbConfig.LogoLightImageType.Value
|
||||
} else {
|
||||
imageName = "logoDark"
|
||||
imageType = dbConfig.LogoDarkImageType.Value
|
||||
}
|
||||
|
||||
acc.updateImage(c, imageName, imageType)
|
||||
}
|
||||
|
||||
// updateFaviconHandler godoc
|
||||
// @Summary Update favicon
|
||||
// @Description Update the application favicon
|
||||
// @Tags Application Configuration
|
||||
// @Accept multipart/form-data
|
||||
// @Param file formData file true "Favicon file (.ico)"
|
||||
// @Success 204 "No Content"
|
||||
// @Router /api/application-configuration/favicon [put]
|
||||
func (acc *AppConfigController) updateFaviconHandler(c *gin.Context) {
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
fileType := utils.GetFileExtension(file.Filename)
|
||||
if fileType != "ico" {
|
||||
_ = c.Error(&common.WrongFileTypeError{ExpectedFileType: ".ico"})
|
||||
return
|
||||
}
|
||||
acc.updateImage(c, "favicon", "ico")
|
||||
}
|
||||
|
||||
// updateBackgroundImageHandler godoc
|
||||
// @Summary Update background image
|
||||
// @Description Update the application background image
|
||||
// @Tags Application Configuration
|
||||
// @Accept multipart/form-data
|
||||
// @Param file formData file true "Background image file"
|
||||
// @Success 204 "No Content"
|
||||
// @Router /api/application-configuration/background-image [put]
|
||||
func (acc *AppConfigController) updateBackgroundImageHandler(c *gin.Context) {
|
||||
imageType := acc.appConfigService.GetDbConfig().BackgroundImageType.Value
|
||||
acc.updateImage(c, "background", imageType)
|
||||
}
|
||||
|
||||
// getImage is a helper function to serve image files
|
||||
func (acc *AppConfigController) getImage(c *gin.Context, name string, imageType string) {
|
||||
imagePath := common.EnvConfig.UploadPath + "/application-images/" + name + "." + imageType
|
||||
mimeType := utils.GetImageMimeType(imageType)
|
||||
|
||||
c.Header("Content-Type", mimeType)
|
||||
|
||||
utils.SetCacheControlHeader(c, 15*time.Minute, 24*time.Hour)
|
||||
c.File(imagePath)
|
||||
}
|
||||
|
||||
// updateImage is a helper function to update image files
|
||||
func (acc *AppConfigController) updateImage(c *gin.Context, imageName string, oldImageType string) {
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
err = acc.appConfigService.UpdateImage(c.Request.Context(), file, imageName, oldImageType)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// syncLdapHandler godoc
|
||||
// @Summary Synchronize LDAP
|
||||
// @Description Manually trigger LDAP synchronization
|
||||
|
||||
173
backend/internal/controller/app_images_controller.go
Normal file
173
backend/internal/controller/app_images_controller.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/middleware"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
)
|
||||
|
||||
func NewAppImagesController(
|
||||
group *gin.RouterGroup,
|
||||
authMiddleware *middleware.AuthMiddleware,
|
||||
appImagesService *service.AppImagesService,
|
||||
) {
|
||||
controller := &AppImagesController{
|
||||
appImagesService: appImagesService,
|
||||
}
|
||||
|
||||
group.GET("/application-images/logo", controller.getLogoHandler)
|
||||
group.GET("/application-images/background", controller.getBackgroundImageHandler)
|
||||
group.GET("/application-images/favicon", controller.getFaviconHandler)
|
||||
|
||||
group.PUT("/application-images/logo", authMiddleware.Add(), controller.updateLogoHandler)
|
||||
group.PUT("/application-images/background", authMiddleware.Add(), controller.updateBackgroundImageHandler)
|
||||
group.PUT("/application-images/favicon", authMiddleware.Add(), controller.updateFaviconHandler)
|
||||
}
|
||||
|
||||
type AppImagesController struct {
|
||||
appImagesService *service.AppImagesService
|
||||
}
|
||||
|
||||
// getLogoHandler godoc
|
||||
// @Summary Get logo image
|
||||
// @Description Get the logo image for the application
|
||||
// @Tags Application Images
|
||||
// @Param light query boolean false "Light mode logo (true) or dark mode logo (false)"
|
||||
// @Produce image/png
|
||||
// @Produce image/jpeg
|
||||
// @Produce image/svg+xml
|
||||
// @Success 200 {file} binary "Logo image"
|
||||
// @Router /api/application-images/logo [get]
|
||||
func (c *AppImagesController) getLogoHandler(ctx *gin.Context) {
|
||||
lightLogo, _ := strconv.ParseBool(ctx.DefaultQuery("light", "true"))
|
||||
imageName := "logoLight"
|
||||
if !lightLogo {
|
||||
imageName = "logoDark"
|
||||
}
|
||||
|
||||
c.getImage(ctx, imageName)
|
||||
}
|
||||
|
||||
// getBackgroundImageHandler godoc
|
||||
// @Summary Get background image
|
||||
// @Description Get the background image for the application
|
||||
// @Tags Application Images
|
||||
// @Produce image/png
|
||||
// @Produce image/jpeg
|
||||
// @Success 200 {file} binary "Background image"
|
||||
// @Router /api/application-images/background [get]
|
||||
func (c *AppImagesController) getBackgroundImageHandler(ctx *gin.Context) {
|
||||
c.getImage(ctx, "background")
|
||||
}
|
||||
|
||||
// getFaviconHandler godoc
|
||||
// @Summary Get favicon
|
||||
// @Description Get the favicon for the application
|
||||
// @Tags Application Images
|
||||
// @Produce image/x-icon
|
||||
// @Success 200 {file} binary "Favicon image"
|
||||
// @Router /api/application-images/favicon [get]
|
||||
func (c *AppImagesController) getFaviconHandler(ctx *gin.Context) {
|
||||
c.getImage(ctx, "favicon")
|
||||
}
|
||||
|
||||
// updateLogoHandler godoc
|
||||
// @Summary Update logo
|
||||
// @Description Update the application logo
|
||||
// @Tags Application Images
|
||||
// @Accept multipart/form-data
|
||||
// @Param light query boolean false "Light mode logo (true) or dark mode logo (false)"
|
||||
// @Param file formData file true "Logo image file"
|
||||
// @Success 204 "No Content"
|
||||
// @Router /api/application-images/logo [put]
|
||||
func (c *AppImagesController) updateLogoHandler(ctx *gin.Context) {
|
||||
file, err := ctx.FormFile("file")
|
||||
if err != nil {
|
||||
_ = ctx.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
lightLogo, _ := strconv.ParseBool(ctx.DefaultQuery("light", "true"))
|
||||
imageName := "logoLight"
|
||||
if !lightLogo {
|
||||
imageName = "logoDark"
|
||||
}
|
||||
|
||||
if err := c.appImagesService.UpdateImage(file, imageName); err != nil {
|
||||
_ = ctx.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// updateBackgroundImageHandler godoc
|
||||
// @Summary Update background image
|
||||
// @Description Update the application background image
|
||||
// @Tags Application Images
|
||||
// @Accept multipart/form-data
|
||||
// @Param file formData file true "Background image file"
|
||||
// @Success 204 "No Content"
|
||||
// @Router /api/application-images/background [put]
|
||||
func (c *AppImagesController) updateBackgroundImageHandler(ctx *gin.Context) {
|
||||
file, err := ctx.FormFile("file")
|
||||
if err != nil {
|
||||
_ = ctx.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.appImagesService.UpdateImage(file, "background"); err != nil {
|
||||
_ = ctx.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// updateFaviconHandler godoc
|
||||
// @Summary Update favicon
|
||||
// @Description Update the application favicon
|
||||
// @Tags Application Images
|
||||
// @Accept multipart/form-data
|
||||
// @Param file formData file true "Favicon file (.ico)"
|
||||
// @Success 204 "No Content"
|
||||
// @Router /api/application-images/favicon [put]
|
||||
func (c *AppImagesController) updateFaviconHandler(ctx *gin.Context) {
|
||||
file, err := ctx.FormFile("file")
|
||||
if err != nil {
|
||||
_ = ctx.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
fileType := utils.GetFileExtension(file.Filename)
|
||||
if fileType != "ico" {
|
||||
_ = ctx.Error(&common.WrongFileTypeError{ExpectedFileType: ".ico"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.appImagesService.UpdateImage(file, "favicon"); err != nil {
|
||||
_ = ctx.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (c *AppImagesController) getImage(ctx *gin.Context, name string) {
|
||||
imagePath, mimeType, err := c.appImagesService.GetImage(name)
|
||||
if err != nil {
|
||||
_ = ctx.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Header("Content-Type", mimeType)
|
||||
utils.SetCacheControlHeader(ctx, 15*time.Minute, 24*time.Hour)
|
||||
ctx.File(imagePath)
|
||||
}
|
||||
@@ -44,10 +44,7 @@ type AppConfig struct {
|
||||
SignupDefaultUserGroupIDs AppConfigVariable `key:"signupDefaultUserGroupIDs"`
|
||||
SignupDefaultCustomClaims AppConfigVariable `key:"signupDefaultCustomClaims"`
|
||||
// Internal
|
||||
BackgroundImageType AppConfigVariable `key:"backgroundImageType,internal"` // Internal
|
||||
LogoLightImageType AppConfigVariable `key:"logoLightImageType,internal"` // Internal
|
||||
LogoDarkImageType AppConfigVariable `key:"logoDarkImageType,internal"` // Internal
|
||||
InstanceID AppConfigVariable `key:"instanceId,internal"` // Internal
|
||||
InstanceID AppConfigVariable `key:"instanceId,internal"` // Internal
|
||||
// Email
|
||||
SmtpHost AppConfigVariable `key:"smtpHost"`
|
||||
SmtpPort AppConfigVariable `key:"smtpPort"`
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
@@ -70,10 +69,7 @@ func (s *AppConfigService) getDefaultDbConfig() *model.AppConfig {
|
||||
SignupDefaultCustomClaims: model.AppConfigVariable{Value: "[]"},
|
||||
AccentColor: model.AppConfigVariable{Value: "default"},
|
||||
// Internal
|
||||
BackgroundImageType: model.AppConfigVariable{Value: "webp"},
|
||||
LogoLightImageType: model.AppConfigVariable{Value: "svg"},
|
||||
LogoDarkImageType: model.AppConfigVariable{Value: "svg"},
|
||||
InstanceID: model.AppConfigVariable{Value: ""},
|
||||
InstanceID: model.AppConfigVariable{Value: ""},
|
||||
// Email
|
||||
SmtpHost: model.AppConfigVariable{},
|
||||
SmtpPort: model.AppConfigVariable{},
|
||||
@@ -322,39 +318,6 @@ func (s *AppConfigService) ListAppConfig(showAll bool) []model.AppConfigVariable
|
||||
return s.GetDbConfig().ToAppConfigVariableSlice(showAll, true)
|
||||
}
|
||||
|
||||
func (s *AppConfigService) UpdateImage(ctx context.Context, uploadedFile *multipart.FileHeader, imageName string, oldImageType string) (err error) {
|
||||
fileType := strings.ToLower(utils.GetFileExtension(uploadedFile.Filename))
|
||||
mimeType := utils.GetImageMimeType(fileType)
|
||||
if mimeType == "" {
|
||||
return &common.FileTypeNotSupportedError{}
|
||||
}
|
||||
|
||||
// Save the updated image
|
||||
imagePath := common.EnvConfig.UploadPath + "/application-images/" + imageName + "." + fileType
|
||||
err = utils.SaveFile(uploadedFile, imagePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete the old image if it has a different file type, then update the type in the database
|
||||
if fileType != oldImageType {
|
||||
oldImagePath := common.EnvConfig.UploadPath + "/application-images/" + imageName + "." + oldImageType
|
||||
err = os.Remove(oldImagePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update the file type in the database
|
||||
err = s.UpdateAppConfigValues(ctx, imageName+"ImageType", fileType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadDbConfig loads the configuration values from the database into the DbConfig struct.
|
||||
func (s *AppConfigService) LoadDbConfig(ctx context.Context) (err error) {
|
||||
dest, err := s.loadDbConfigInternal(ctx, s.db)
|
||||
|
||||
82
backend/internal/service/app_images_service.go
Normal file
82
backend/internal/service/app_images_service.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
)
|
||||
|
||||
type AppImagesService struct {
|
||||
mu sync.RWMutex
|
||||
extensions map[string]string
|
||||
}
|
||||
|
||||
func NewAppImagesService(extensions map[string]string) *AppImagesService {
|
||||
return &AppImagesService{extensions: extensions}
|
||||
}
|
||||
|
||||
func (s *AppImagesService) GetImage(name string) (string, string, error) {
|
||||
ext, err := s.getExtension(name)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
mimeType := utils.GetImageMimeType(ext)
|
||||
if mimeType == "" {
|
||||
return "", "", fmt.Errorf("unsupported image type '%s'", ext)
|
||||
}
|
||||
|
||||
imagePath := filepath.Join(common.EnvConfig.UploadPath, "application-images", fmt.Sprintf("%s.%s", name, ext))
|
||||
return imagePath, mimeType, nil
|
||||
}
|
||||
|
||||
func (s *AppImagesService) UpdateImage(file *multipart.FileHeader, imageName string) error {
|
||||
fileType := strings.ToLower(utils.GetFileExtension(file.Filename))
|
||||
mimeType := utils.GetImageMimeType(fileType)
|
||||
if mimeType == "" {
|
||||
return &common.FileTypeNotSupportedError{}
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
currentExt, ok := s.extensions[imageName]
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown application image '%s'", imageName)
|
||||
}
|
||||
|
||||
imagePath := filepath.Join(common.EnvConfig.UploadPath, "application-images", fmt.Sprintf("%s.%s", imageName, fileType))
|
||||
|
||||
if err := utils.SaveFile(file, imagePath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if currentExt != "" && currentExt != fileType {
|
||||
oldImagePath := filepath.Join(common.EnvConfig.UploadPath, "application-images", fmt.Sprintf("%s.%s", imageName, currentExt))
|
||||
if err := os.Remove(oldImagePath); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
s.extensions[imageName] = fileType
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AppImagesService) getExtension(name string) (string, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
ext, ok := s.extensions[name]
|
||||
if !ok || ext == "" {
|
||||
return "", fmt.Errorf("unknown application image '%s'", name)
|
||||
}
|
||||
|
||||
return strings.ToLower(ext), nil
|
||||
}
|
||||
88
backend/internal/service/app_images_service_test.go
Normal file
88
backend/internal/service/app_images_service_test.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/fs"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
)
|
||||
|
||||
func TestAppImagesService_GetImage(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
originalUploadPath := common.EnvConfig.UploadPath
|
||||
common.EnvConfig.UploadPath = tempDir
|
||||
t.Cleanup(func() {
|
||||
common.EnvConfig.UploadPath = originalUploadPath
|
||||
})
|
||||
|
||||
imagesDir := filepath.Join(tempDir, "application-images")
|
||||
require.NoError(t, os.MkdirAll(imagesDir, 0o755))
|
||||
|
||||
filePath := filepath.Join(imagesDir, "background.webp")
|
||||
require.NoError(t, os.WriteFile(filePath, []byte("data"), fs.FileMode(0o644)))
|
||||
|
||||
service := NewAppImagesService(map[string]string{"background": "webp"})
|
||||
|
||||
path, mimeType, err := service.GetImage("background")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, filePath, path)
|
||||
require.Equal(t, "image/webp", mimeType)
|
||||
}
|
||||
|
||||
func TestAppImagesService_UpdateImage(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
originalUploadPath := common.EnvConfig.UploadPath
|
||||
common.EnvConfig.UploadPath = tempDir
|
||||
t.Cleanup(func() {
|
||||
common.EnvConfig.UploadPath = originalUploadPath
|
||||
})
|
||||
|
||||
imagesDir := filepath.Join(tempDir, "application-images")
|
||||
require.NoError(t, os.MkdirAll(imagesDir, 0o755))
|
||||
|
||||
oldPath := filepath.Join(imagesDir, "logoLight.svg")
|
||||
require.NoError(t, os.WriteFile(oldPath, []byte("old"), fs.FileMode(0o644)))
|
||||
|
||||
service := NewAppImagesService(map[string]string{"logoLight": "svg"})
|
||||
|
||||
fileHeader := newFileHeader(t, "logoLight.png", []byte("new"))
|
||||
|
||||
require.NoError(t, service.UpdateImage(fileHeader, "logoLight"))
|
||||
|
||||
_, err := os.Stat(filepath.Join(imagesDir, "logoLight.png"))
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = os.Stat(oldPath)
|
||||
require.ErrorIs(t, err, os.ErrNotExist)
|
||||
}
|
||||
|
||||
func newFileHeader(t *testing.T, filename string, content []byte) *multipart.FileHeader {
|
||||
t.Helper()
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
part, err := writer.CreateFormFile("file", filename)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = part.Write(content)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, writer.Close())
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/", body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
_, fileHeader, err := req.FormFile("file")
|
||||
require.NoError(t, err)
|
||||
|
||||
return fileHeader
|
||||
}
|
||||
@@ -74,7 +74,7 @@ func SendEmail[V any](ctx context.Context, srv *EmailService, toEmail email.Addr
|
||||
|
||||
data := &email.TemplateData[V]{
|
||||
AppName: dbConfig.AppName.Value,
|
||||
LogoURL: common.EnvConfig.AppURL + "/api/application-configuration/logo",
|
||||
LogoURL: common.EnvConfig.AppURL + "/api/application-images/logo",
|
||||
Data: tData,
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ package email
|
||||
import (
|
||||
"fmt"
|
||||
htemplate "html/template"
|
||||
"path/filepath"
|
||||
"path"
|
||||
ttemplate "text/template"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/resources"
|
||||
@@ -30,7 +30,7 @@ func PrepareTextTemplates(templates []string) (map[string]*ttemplate.Template, e
|
||||
textTemplates := make(map[string]*ttemplate.Template, len(templates))
|
||||
for _, tmpl := range templates {
|
||||
filename := tmpl + "_text.tmpl"
|
||||
templatePath := filepath.Join("email-templates", filename)
|
||||
templatePath := path.Join("email-templates", filename)
|
||||
|
||||
parsedTemplate, err := ttemplate.ParseFS(resources.FS, templatePath)
|
||||
if err != nil {
|
||||
@@ -47,7 +47,7 @@ func PrepareHTMLTemplates(templates []string) (map[string]*htemplate.Template, e
|
||||
htmlTemplates := make(map[string]*htemplate.Template, len(templates))
|
||||
for _, tmpl := range templates {
|
||||
filename := tmpl + "_html.tmpl"
|
||||
templatePath := filepath.Join("email-templates", filename)
|
||||
templatePath := path.Join("email-templates", filename)
|
||||
|
||||
parsedTemplate, err := htemplate.ParseFS(resources.FS, templatePath)
|
||||
if err != nil {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -24,6 +25,15 @@ func GetFileExtension(filename string) string {
|
||||
return filename
|
||||
}
|
||||
|
||||
// SplitFileName splits a full file name into name and extension.
|
||||
func SplitFileName(fullName string) (name, ext string) {
|
||||
dot := strings.LastIndex(fullName, ".")
|
||||
if dot == -1 || dot == 0 {
|
||||
return fullName, "" // no extension or hidden file like .gitignore
|
||||
}
|
||||
return fullName[:dot], fullName[dot+1:]
|
||||
}
|
||||
|
||||
func GetImageMimeType(ext string) string {
|
||||
switch ext {
|
||||
case "jpg", "jpeg":
|
||||
|
||||
@@ -2,8 +2,36 @@ package utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSplitFileName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
fullName string
|
||||
wantName string
|
||||
wantExt string
|
||||
}{
|
||||
{"background.jpg", "background", "jpg"},
|
||||
{"archive.tar.gz", "archive.tar", "gz"},
|
||||
{".gitignore", ".gitignore", ""},
|
||||
{"noext", "noext", ""},
|
||||
{"a.b.c", "a.b", "c"},
|
||||
{".hidden.ext", ".hidden", "ext"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.fullName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
name, ext := SplitFileName(tc.fullName)
|
||||
assert.Equal(t, tc.wantName, name)
|
||||
assert.Equal(t, tc.wantExt, ext)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetFileExtension(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
49
cliff.toml
Normal file
49
cliff.toml
Normal file
@@ -0,0 +1,49 @@
|
||||
# git-cliff ~ configuration file
|
||||
# https://git-cliff.org/docs/configuration
|
||||
|
||||
[remote.github]
|
||||
owner = "pocket-id"
|
||||
repo = "pocket-id"
|
||||
|
||||
[git]
|
||||
conventional_commits = true
|
||||
filter_unconventional = true
|
||||
commit_preprocessors = [{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" }]
|
||||
commit_parsers = [
|
||||
{ message = "^feat", group = "Features" },
|
||||
{ message = "^fix", group = "Bug Fixes" },
|
||||
{ message = "^docs", group = "Documentation" },
|
||||
{ message = "^perf", group = "Performance Improvements" },
|
||||
{ message = "^release", skip = true },
|
||||
{ message = "update translations via Crowdin", skip = true },
|
||||
{ message = ".*", group = "Other", default_scope = "other"},
|
||||
]
|
||||
filter_commits = false
|
||||
|
||||
[changelog]
|
||||
trim = true
|
||||
body = """
|
||||
## {{ version | default(value="Unknown Version") }}
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group | title }}
|
||||
{% for commit in commits %}
|
||||
* {{ commit.message }} \
|
||||
{%- if commit.remote.pr_number -%}
|
||||
([#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}) by @{{ commit.remote.username | default(value=commit.author.name) }})
|
||||
{%- else -%}
|
||||
([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}) by @{{ commit.remote.username | default(value=commit.author.name) }})
|
||||
{%- endif -%}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% if version %}
|
||||
{% if previous.version %}
|
||||
**Full Changelog**: {{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }}
|
||||
{% endif %}
|
||||
{% else -%}
|
||||
{% raw %}\n{% endraw %}
|
||||
{% endif %}
|
||||
|
||||
{%- macro remote_url() -%}
|
||||
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
|
||||
{%- endmacro -%}
|
||||
"""
|
||||
@@ -120,6 +120,8 @@
|
||||
"username": "Uživatelské jméno",
|
||||
"save": "Uložit",
|
||||
"username_can_only_contain": "Uživatelské jméno může obsahovat pouze malá písmena, číslice, podtržítka, tečky, pomlčky a symbol '@'",
|
||||
"username_must_start_with": "Uživatelské jméno musí začínat alfanumerickým znakem.",
|
||||
"username_must_end_with": "Uživatelské jméno musí končit alfanumerickým znakem.",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Přihlaste se pomocí následujícího kódu. Platnost kódu vyprší za 15 minut.",
|
||||
"or_visit": "nebo navštívit",
|
||||
"added_on": "Přidáno",
|
||||
@@ -443,7 +445,10 @@
|
||||
"custom_client_id_description": "Nastavte vlastní ID klienta, pokud to vyžaduje vaše aplikace. V opačném případě pole nechte prázdné, aby bylo vygenerováno náhodné ID.",
|
||||
"generated": "Vygenerováno",
|
||||
"administration": "Správa",
|
||||
"group_rdn_attribute_description": "Atribut použitý v rozlišovacím jménu skupiny (DN). Doporučená hodnota: `cn`",
|
||||
"group_rdn_attribute_description": "Atribut použitý v rozlišovacím jménu (DN) skupiny.",
|
||||
"display_name_attribute": "Atribut zobrazovaného jména",
|
||||
"display_name": "Zobrazované jméno",
|
||||
"configure_application_images": "Konfigurace obrazů aplikací",
|
||||
"ui_config_disabled_info_title": "Konfigurace uživatelského rozhraní je deaktivována",
|
||||
"ui_config_disabled_info_description": "Konfigurace uživatelského rozhraní je deaktivována, protože nastavení konfigurace aplikace se spravuje prostřednictvím proměnných prostředí. Některá nastavení nemusí být editovatelná."
|
||||
}
|
||||
|
||||
@@ -120,6 +120,8 @@
|
||||
"username": "Brugernavn",
|
||||
"save": "Gem",
|
||||
"username_can_only_contain": "Brugernavn må kun indeholde små bogstaver, tal, understregninger (_), punktummer (.), bindestreger (-) og @-tegn",
|
||||
"username_must_start_with": "Brugernavnet skal begynde med et alfanumerisk tegn",
|
||||
"username_must_end_with": "Brugernavnet skal slutte med et alfanumerisk tegn",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Log ind med nedenstående kode. Koden udløber om 15 minutter.",
|
||||
"or_visit": "eller besøg",
|
||||
"added_on": "Tilføjet den",
|
||||
@@ -443,7 +445,10 @@
|
||||
"custom_client_id_description": "Indstil et brugerdefineret klient-id, hvis dette kræves af din applikation. Ellers skal du lade feltet være tomt for at generere et tilfældigt id.",
|
||||
"generated": "Genereret",
|
||||
"administration": "Administration",
|
||||
"group_rdn_attribute_description": "Den attribut, der bruges i gruppernes skelnenavn (DN). Anbefalet værdi: `cn`",
|
||||
"group_rdn_attribute_description": "Den attribut, der bruges i gruppernes skelnenavn (DN).",
|
||||
"display_name_attribute": "Visningsnavn-attribut",
|
||||
"display_name": "Visningsnavn",
|
||||
"configure_application_images": "Konfigurer applikationsbilleder",
|
||||
"ui_config_disabled_info_title": "UI-konfiguration deaktiveret",
|
||||
"ui_config_disabled_info_description": "UI-konfigurationen er deaktiveret, fordi applikationskonfigurationsindstillingerne administreres via miljøvariabler. Nogle indstillinger kan muligvis ikke redigeres."
|
||||
}
|
||||
|
||||
@@ -120,6 +120,8 @@
|
||||
"username": "Benutzername",
|
||||
"save": "Speichern",
|
||||
"username_can_only_contain": "Der Benutzername darf nur Kleinbuchstaben, Ziffern, Unterstriche, Punkte, Bindestriche und das Symbol „@“ enthalten",
|
||||
"username_must_start_with": "Der Benutzername muss mit einem Buchstaben oder einer Zahl anfangen.",
|
||||
"username_must_end_with": "Der Benutzername muss mit einem Buchstaben oder einer Zahl enden.",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Melde dich mit dem folgenden Code an. Der Code läuft in 15 Minuten ab.",
|
||||
"or_visit": "oder besuche",
|
||||
"added_on": "Hinzugefügt am",
|
||||
@@ -443,7 +445,10 @@
|
||||
"custom_client_id_description": "Gib eine eigene Client-ID ein, wenn deine App das braucht. Ansonsten lass das Feld leer, damit eine zufällige ID generiert wird.",
|
||||
"generated": "Generiert",
|
||||
"administration": "Verwaltung",
|
||||
"group_rdn_attribute_description": "Das Attribut, das im Distinguished Name (DN) der Gruppen benutzt wird. Empfohlener Wert: `cn`",
|
||||
"group_rdn_attribute_description": "Das Attribut, das im Distinguished Name (DN) der Gruppen benutzt wird.",
|
||||
"display_name_attribute": "Anzeigename-Attribut",
|
||||
"display_name": "Anzeigename",
|
||||
"configure_application_images": "Anwendungsimages einrichten",
|
||||
"ui_config_disabled_info_title": "UI-Konfiguration deaktiviert",
|
||||
"ui_config_disabled_info_description": "Die UI-Konfiguration ist deaktiviert, weil die Anwendungseinstellungen über Umgebungsvariablen verwaltet werden. Manche Einstellungen können vielleicht nicht geändert werden."
|
||||
}
|
||||
|
||||
@@ -120,6 +120,8 @@
|
||||
"username": "Nombre de usuario",
|
||||
"save": "Guardar",
|
||||
"username_can_only_contain": "El nombre de usuario solo puede contener letras minúsculas, números, guiones bajos, puntos, guiones y símbolos '@'",
|
||||
"username_must_start_with": "El nombre de usuario debe comenzar con un carácter alfanumérico.",
|
||||
"username_must_end_with": "El nombre de usuario debe terminar con un carácter alfanumérico.",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Inicia sesión usando el siguiente código. El código caducará en 15 minutos.",
|
||||
"or_visit": "o visita",
|
||||
"added_on": "Añadido el",
|
||||
@@ -443,7 +445,10 @@
|
||||
"custom_client_id_description": "Establece un ID de cliente personalizado si tu aplicación lo requiere. De lo contrario, déjalo en blanco para generar uno aleatorio.",
|
||||
"generated": "Generado",
|
||||
"administration": "Administración",
|
||||
"group_rdn_attribute_description": "El atributo utilizado en el nombre distintivo (DN) de los grupos. Valor recomendado: `cn`",
|
||||
"group_rdn_attribute_description": "El atributo utilizado en el nombre distintivo (DN) de los grupos.",
|
||||
"display_name_attribute": "Atributo de nombre para mostrar",
|
||||
"display_name": "Nombre para mostrar",
|
||||
"configure_application_images": "Configurar imágenes de aplicaciones",
|
||||
"ui_config_disabled_info_title": "Configuración de la interfaz de usuario desactivada",
|
||||
"ui_config_disabled_info_description": "La configuración de la interfaz de usuario está desactivada porque los ajustes de configuración de la aplicación se gestionan a través de variables de entorno. Es posible que algunos ajustes no se puedan editar."
|
||||
}
|
||||
|
||||
@@ -120,6 +120,8 @@
|
||||
"username": "Nom d'utilisateur",
|
||||
"save": "Enregistrer",
|
||||
"username_can_only_contain": "Le nom d'utilisateur ne peut contenir que des lettres minuscules, des chiffres, des tirets, des tirets bas et le symbole '@'",
|
||||
"username_must_start_with": "Le nom d'utilisateur doit commencer par un caractère alphanumérique.",
|
||||
"username_must_end_with": "Le nom d'utilisateur doit finir par un caractère alphanumérique.",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Connectez-vous avec le code suivant. Le code expirera dans 15 minutes.",
|
||||
"or_visit": "ou visiter",
|
||||
"added_on": "Ajoutée le",
|
||||
@@ -443,7 +445,10 @@
|
||||
"custom_client_id_description": "Définissez un identifiant client personnalisé si votre application l'exige. Sinon, laissez ce champ vide pour qu'un identifiant aléatoire soit généré.",
|
||||
"generated": "Généré",
|
||||
"administration": "Administration",
|
||||
"group_rdn_attribute_description": "L'attribut utilisé dans le nom distinctif (DN) des groupes. Valeur recommandée : `cn`",
|
||||
"group_rdn_attribute_description": "L'attribut utilisé dans le nom distinctif (DN) des groupes.",
|
||||
"display_name_attribute": "Attribut du nom d'affichage",
|
||||
"display_name": "Nom d'affichage",
|
||||
"configure_application_images": "Configurer les images d'application",
|
||||
"ui_config_disabled_info_title": "Configuration de l'interface utilisateur désactivée",
|
||||
"ui_config_disabled_info_description": "La configuration de l'interface utilisateur est désactivée parce que les paramètres de configuration de l'application sont gérés par des variables d'environnement. Certains paramètres peuvent ne pas être modifiables."
|
||||
}
|
||||
|
||||
@@ -120,6 +120,8 @@
|
||||
"username": "Nome utente",
|
||||
"save": "Salva",
|
||||
"username_can_only_contain": "Il nome utente può contenere solo lettere minuscole, numeri, underscore, punti, trattini e simboli '@'",
|
||||
"username_must_start_with": "Il nome utente deve iniziare con un carattere alfanumerico.",
|
||||
"username_must_end_with": "Il nome utente deve finire con un carattere alfanumerico",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Accedi utilizzando il seguente codice. Il codice scadrà tra 15 minuti.",
|
||||
"or_visit": "o visita",
|
||||
"added_on": "Aggiunto il",
|
||||
@@ -443,7 +445,10 @@
|
||||
"custom_client_id_description": "Imposta un ID cliente personalizzato se la tua app lo richiede. Altrimenti, lascia vuoto per generarne uno casuale.",
|
||||
"generated": "Generato",
|
||||
"administration": "Amministrazione",
|
||||
"group_rdn_attribute_description": "L'attributo usato nel nome distinto (DN) dei gruppi. Valore consigliato: `cn`",
|
||||
"group_rdn_attribute_description": "L'attributo usato nel nome distinto (DN) dei gruppi.",
|
||||
"display_name_attribute": "Attributo del nome visualizzato",
|
||||
"display_name": "Nome visualizzato",
|
||||
"configure_application_images": "Configurare le immagini dell'applicazione",
|
||||
"ui_config_disabled_info_title": "Configurazione dell'interfaccia utente disattivata",
|
||||
"ui_config_disabled_info_description": "La configurazione dell'interfaccia utente è disattivata perché le impostazioni di configurazione dell'applicazione sono gestite tramite variabili di ambiente. Alcune impostazioni potrebbero non essere modificabili."
|
||||
}
|
||||
|
||||
@@ -120,6 +120,8 @@
|
||||
"username": "사용자 이름",
|
||||
"save": "저장",
|
||||
"username_can_only_contain": "사용자 이름은 영어 소문자, 숫자, 밑줄, 점, 하이픈, '@' 기호만 포함할 수 있습니다",
|
||||
"username_must_start_with": "사용자 이름은 영숫자로 시작해야 합니다",
|
||||
"username_must_end_with": "사용자 이름은 영숫자로 끝나야 합니다",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "다음 코드를 사용하여 로그인하세요. 이 코드는 15분 후에 만료됩니다.",
|
||||
"or_visit": "또는",
|
||||
"added_on": "추가:",
|
||||
@@ -443,7 +445,10 @@
|
||||
"custom_client_id_description": "애플리케이션에서 사용자 정의 클라이언트 ID가 요구되는 경우 설정하세요. 그렇지 않으면 빈 상태로 두어서 무작위로 생성할 수 있습니다.",
|
||||
"generated": "생성됨",
|
||||
"administration": "관리",
|
||||
"group_rdn_attribute_description": "그룹의 고유 식별자(DN)에 사용되는 속성. 권장 값: `cn`",
|
||||
"group_rdn_attribute_description": "그룹의 고유 식별자(DN)에 사용되는 속성.",
|
||||
"display_name_attribute": "표시 이름 속성",
|
||||
"display_name": "표시 이름",
|
||||
"configure_application_images": "애플리케이션 이미지 구성",
|
||||
"ui_config_disabled_info_title": "UI 구성 비활성화됨",
|
||||
"ui_config_disabled_info_description": "UI 구성이 비활성화되었습니다. 애플리케이션 구성 설정은 환경 변수를 통해 관리되기 때문입니다. 일부 설정은 편집할 수 없을 수 있습니다."
|
||||
}
|
||||
|
||||
@@ -120,6 +120,8 @@
|
||||
"username": "Gebruikersnaam",
|
||||
"save": "Opslaan",
|
||||
"username_can_only_contain": "Gebruikersnaam mag alleen kleine letters, cijfers, onderstrepingstekens, punten, koppeltekens en '@'-symbolen bevatten",
|
||||
"username_must_start_with": "Je gebruikersnaam moet beginnen met een letter of cijfer.",
|
||||
"username_must_end_with": "Je gebruikersnaam moet eindigen met een letter of cijfer.",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Meld je aan met de volgende code. De code verloopt over 15 minuten.",
|
||||
"or_visit": "of bezoek",
|
||||
"added_on": "Toegevoegd op",
|
||||
@@ -419,7 +421,7 @@
|
||||
"created": "Gemaakt",
|
||||
"token": "Token",
|
||||
"loading": "Bezig met laden",
|
||||
"delete_signup_token": "Registratietoken verwijderen",
|
||||
"delete_signup_token": "Aanmeldtoken verwijderen",
|
||||
"are_you_sure_you_want_to_delete_this_signup_token": "Weet je zeker dat je dit aanmeldingstoken wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.",
|
||||
"signup_with_token": "Aanmelden met token",
|
||||
"signup_with_token_description": "Je kunt je alleen aanmelden met een geldige aanmeldtoken die door een beheerder is aangemaakt.",
|
||||
@@ -439,11 +441,14 @@
|
||||
"revoke_access_description": "Toegang intrekken tot <b>{clientName}</b>. <b>{clientName}</b> kan je accountgegevens niet meer gebruiken.",
|
||||
"revoke_access_successful": "De toegang tot {clientName} is nu succesvol geblokkeerd.",
|
||||
"last_signed_in_ago": "Laatst ingelogd {time} geleden",
|
||||
"invalid_client_id": "De client-ID mag alleen letters, cijfers, onderstrepingstekens en koppeltekens bevatten",
|
||||
"custom_client_id_description": "Stel een aangepaste client-ID in als je app dit nodig heeft. Anders laat je het gewoon leeg en wordt er een willekeurige ID gegenereerd.",
|
||||
"invalid_client_id": "De Client-ID mag alleen letters, cijfers, onderstrepingstekens en koppeltekens bevatten.",
|
||||
"custom_client_id_description": "Stel een aangepaste Client-ID in als je app dit nodig heeft. Als je het leeg laat wordt er een willekeurige ID gegenereerd.",
|
||||
"generated": "Gemaakt",
|
||||
"administration": "Beheer",
|
||||
"group_rdn_attribute_description": "Het kenmerk dat je gebruikt in de onderscheidende naam (DN) van de groepen. Aanbevolen waarde: `cn`",
|
||||
"group_rdn_attribute_description": "Het kenmerk dat wordt gebruikt in de onderscheidende naam (DN) van de groepen.",
|
||||
"display_name_attribute": "Weergavenaam-attribuut",
|
||||
"display_name": "Weergavenaam",
|
||||
"configure_application_images": "Configureer applicatieafbeeldingen",
|
||||
"ui_config_disabled_info_title": "UI-configuratie uitgeschakeld",
|
||||
"ui_config_disabled_info_description": "De UI-configuratie is uitgeschakeld omdat de configuratie-instellingen van de app via omgevingsvariabelen worden beheerd. Sommige instellingen kun je misschien niet aanpassen."
|
||||
}
|
||||
|
||||
@@ -120,6 +120,8 @@
|
||||
"username": "Nazwa użytkownika",
|
||||
"save": "Zapisz",
|
||||
"username_can_only_contain": "Nazwa użytkownika może zawierać tylko małe litery, cyfry, podkreślenia, kropki, myślniki i symbole '@'",
|
||||
"username_must_start_with": "Nazwa użytkownika musi zaczynać się od znaku alfanumerycznego.",
|
||||
"username_must_end_with": "Nazwa użytkownika musi kończyć się znakiem alfanumerycznym.",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Zaloguj się, używając następującego kodu. Kod wygaśnie za 15 minut.",
|
||||
"or_visit": "lub odwiedź",
|
||||
"added_on": "Dodano",
|
||||
@@ -443,7 +445,10 @@
|
||||
"custom_client_id_description": "Ustaw niestandardowy identyfikator klienta, jeśli jest to wymagane przez twoją aplikację. W przeciwnym razie pozostaw to pole puste, aby wygenerować losowy identyfikator.",
|
||||
"generated": "Wygenerowano",
|
||||
"administration": "Administracja",
|
||||
"group_rdn_attribute_description": "Atrybut używany w nazwie wyróżniającej grupy (DN). Zalecana wartość: `cn`",
|
||||
"group_rdn_attribute_description": "Atrybut używany w nazwie wyróżniającej grupy (DN).",
|
||||
"display_name_attribute": "Atrybut nazwy wyświetlanej",
|
||||
"display_name": "Wyświetlana nazwa",
|
||||
"configure_application_images": "Konfigurowanie obrazów aplikacji",
|
||||
"ui_config_disabled_info_title": "Konfiguracja interfejsu użytkownika wyłączona",
|
||||
"ui_config_disabled_info_description": "Konfiguracja interfejsu użytkownika jest wyłączona, ponieważ ustawienia konfiguracyjne aplikacji są zarządzane za pomocą zmiennych środowiskowych. Niektóre ustawienia mogą nie być edytowalne."
|
||||
}
|
||||
|
||||
@@ -120,6 +120,8 @@
|
||||
"username": "Nome de usuário",
|
||||
"save": "Salvar",
|
||||
"username_can_only_contain": "O nome de usuário só pode conter letras minúsculas, números, underscores, pontos, hífens e símbolos '@'",
|
||||
"username_must_start_with": "O nome de usuário precisa começar com um caractere alfanumérico.",
|
||||
"username_must_end_with": "O nome de usuário precisa terminar com um caractere alfanumérico.",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Faça o login usando o código a seguir. O código irá expirar em 15 minutos.",
|
||||
"or_visit": "ou visite",
|
||||
"added_on": "Adicionado em",
|
||||
@@ -443,7 +445,10 @@
|
||||
"custom_client_id_description": "Defina um ID de cliente personalizado se for necessário para o seu aplicativo. Caso contrário, deixe em branco para gerar um aleatório.",
|
||||
"generated": "Gerado",
|
||||
"administration": "Administração",
|
||||
"group_rdn_attribute_description": "O atributo usado no nome distinto (DN) dos grupos. Valor recomendado: `cn`",
|
||||
"group_rdn_attribute_description": "O atributo usado no nome distinto (DN) dos grupos.",
|
||||
"display_name_attribute": "Atributo Nome de exibição",
|
||||
"display_name": "Nome de exibição",
|
||||
"configure_application_images": "Configurar imagens de aplicativos",
|
||||
"ui_config_disabled_info_title": "Configuração da interface do usuário desativada",
|
||||
"ui_config_disabled_info_description": "A configuração da interface do usuário está desativada porque as configurações do aplicativo são gerenciadas por meio de variáveis de ambiente. Algumas configurações podem não ser editáveis."
|
||||
}
|
||||
|
||||
@@ -120,6 +120,8 @@
|
||||
"username": "Имя пользователя",
|
||||
"save": "Сохранить",
|
||||
"username_can_only_contain": "Имя пользователя может содержать только строчные буквы, цифры, знак подчеркивания, точки, дефиса и символ '@'",
|
||||
"username_must_start_with": "Имя пользователя должно начинаться с буквы или цифры",
|
||||
"username_must_end_with": "Имя пользователя должно заканчиваться буквой или цифрой",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Войдите, используя следующий код. Код истечет через 15 минут.",
|
||||
"or_visit": "или посетите",
|
||||
"added_on": "Добавлен",
|
||||
@@ -443,7 +445,10 @@
|
||||
"custom_client_id_description": "Установите пользовательский ID клиента, если это нужно для вашего приложения. Если нет, оставьте поле пустым, чтобы он был сгенерирован случайным образом.",
|
||||
"generated": "Сгенерированный",
|
||||
"administration": "Администрирование",
|
||||
"group_rdn_attribute_description": "Атрибут, который используется в distinguished name (DN) групп. Рекомендуемое значение: `cn`",
|
||||
"group_rdn_attribute_description": "Атрибут, который используется в различающемся имени группы (DN).",
|
||||
"display_name_attribute": "Атрибут отображаемого имени",
|
||||
"display_name": "Отображаемое имя",
|
||||
"configure_application_images": "Настройка изображений приложения",
|
||||
"ui_config_disabled_info_title": "Конфигурация пользовательского интерфейса отключена",
|
||||
"ui_config_disabled_info_description": "Конфигурация пользовательского интерфейса отключена, потому что настройки приложения управляются через переменные среды. Некоторые настройки могут быть недоступны для редактирования."
|
||||
}
|
||||
|
||||
@@ -120,6 +120,8 @@
|
||||
"username": "Användarnamn",
|
||||
"save": "Spara",
|
||||
"username_can_only_contain": "Användarnamnet får endast innehålla små bokstäver, siffror, understreck, punkter, bindestreck och '@'-tecken",
|
||||
"username_must_start_with": "Användarnamnet måste börja med ett alfanumeriskt tecken",
|
||||
"username_must_end_with": "Användarnamnet måste sluta med ett alfanumeriskt tecken",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Logga in med följande kod. Koden upphör att gälla om 15 minuter.",
|
||||
"or_visit": "eller besök",
|
||||
"added_on": "Tillagd den",
|
||||
@@ -443,7 +445,10 @@
|
||||
"custom_client_id_description": "Ange ett anpassat Client ID om detta krävs av din applikation. Annars lämnar du fältet tomt för att generera ett slumpmässigt ID.",
|
||||
"generated": "Genererad",
|
||||
"administration": "Administration",
|
||||
"group_rdn_attribute_description": "Attributet som används i gruppens distinguished name (DN). Rekommenderat värde: `cn`",
|
||||
"group_rdn_attribute_description": "Attributet som används i gruppernas distinkta namn (DN).",
|
||||
"display_name_attribute": "Visningsnamnattribut",
|
||||
"display_name": "Visningsnamn",
|
||||
"configure_application_images": "Konfigurera applikationsbilder",
|
||||
"ui_config_disabled_info_title": "UI-konfiguration inaktiverad",
|
||||
"ui_config_disabled_info_description": "UI-konfigurationen är inaktiverad eftersom applikationens konfigurationsinställningar hanteras via miljövariabler. Vissa inställningar kan inte redigeras."
|
||||
}
|
||||
|
||||
@@ -120,6 +120,8 @@
|
||||
"username": "Ім’я користувача",
|
||||
"save": "Зберегти",
|
||||
"username_can_only_contain": "Ім’я користувача може містити лише малі літери, цифри, підкреслення, крапки, дефіси та символ '@'",
|
||||
"username_must_start_with": "Ім'я користувача повинно починатися з буквено-цифрового символу",
|
||||
"username_must_end_with": "Ім'я користувача повинно закінчуватися буквено-цифровим символом",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Увійдіть, використовуючи наступний код. Код дійсний протягом 15 хвилин.",
|
||||
"or_visit": "або відвідайте",
|
||||
"added_on": "Додано",
|
||||
@@ -443,7 +445,10 @@
|
||||
"custom_client_id_description": "Встановіть власний ідентифікатор клієнта, якщо це потрібно для вашої програми. В іншому випадку залиште поле порожнім, щоб створити випадковий ідентифікатор.",
|
||||
"generated": "Створено",
|
||||
"administration": "Адміністрація",
|
||||
"group_rdn_attribute_description": "Атрибут, що використовується в розпізнавальному імені групи (DN). Рекомендоване значення: `cn`",
|
||||
"group_rdn_attribute_description": "Атрибут, що використовується в розрізнювальному імені групи (DN).",
|
||||
"display_name_attribute": "Атрибут імені для відображення",
|
||||
"display_name": "Ім'я для відображення",
|
||||
"configure_application_images": "Налаштування зображень додатків",
|
||||
"ui_config_disabled_info_title": "Конфігурація інтерфейсу користувача вимкнена",
|
||||
"ui_config_disabled_info_description": "Конфігурація інтерфейсу користувача вимкнена, оскільки налаштування конфігурації програми керуються через змінні середовища. Деякі налаштування можуть бути недоступними для редагування."
|
||||
}
|
||||
|
||||
@@ -120,6 +120,8 @@
|
||||
"username": "Tên đăng nhập",
|
||||
"save": "Lưu",
|
||||
"username_can_only_contain": "Tên người dùng chỉ có thể chứa các ký tự chữ thường, số, dấu gạch dưới, dấu chấm, dấu gạch ngang và ký hiệu '@'.",
|
||||
"username_must_start_with": "Tên người dùng phải bắt đầu bằng một ký tự alphanumeric.",
|
||||
"username_must_end_with": "Tên người dùng phải kết thúc bằng một ký tự alphanumeric.",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "Đăng nhập bằng mã sau. Mã này sẽ hết hạn trong 15 phút.",
|
||||
"or_visit": "hoặc truy cập",
|
||||
"added_on": "Đã được thêm vào",
|
||||
@@ -443,7 +445,10 @@
|
||||
"custom_client_id_description": "Đặt ID khách hàng tùy chỉnh nếu ứng dụng của bạn yêu cầu. Nếu không, hãy để trống để hệ thống tự động tạo một ID ngẫu nhiên.",
|
||||
"generated": "Được tạo ra",
|
||||
"administration": "Quản lý",
|
||||
"group_rdn_attribute_description": "Thuộc tính được sử dụng trong tên phân biệt (DN) của nhóm. Giá trị được khuyến nghị: `cn`",
|
||||
"group_rdn_attribute_description": "Thuộc tính được sử dụng trong tên phân biệt (DN) của nhóm.",
|
||||
"display_name_attribute": "Thuộc tính Tên hiển thị",
|
||||
"display_name": "Tên hiển thị",
|
||||
"configure_application_images": "Cấu hình hình ảnh ứng dụng",
|
||||
"ui_config_disabled_info_title": "Cấu hình giao diện người dùng đã bị vô hiệu hóa",
|
||||
"ui_config_disabled_info_description": "Cấu hình giao diện người dùng (UI) đã bị vô hiệu hóa vì các thiết lập cấu hình ứng dụng được quản lý thông qua biến môi trường. Một số thiết lập có thể không thể chỉnh sửa."
|
||||
}
|
||||
|
||||
@@ -120,6 +120,8 @@
|
||||
"username": "用户名",
|
||||
"save": "保存",
|
||||
"username_can_only_contain": "用户名只能包含小写字母、数字、下划线、点、连字符和 '@' 符号",
|
||||
"username_must_start_with": "用户名必须以字母数字字符开头",
|
||||
"username_must_end_with": "用户名必须以字母数字字符结尾",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "使用以下代码登录。该代码将在 15 分钟后失效。",
|
||||
"or_visit": "或访问",
|
||||
"added_on": "添加于",
|
||||
@@ -439,11 +441,14 @@
|
||||
"revoke_access_description": "撤销对 <b>{clientName}</b>. <b>{clientName}</b>将无法再访问您的账户信息。",
|
||||
"revoke_access_successful": "对 {clientName} 的访问权限已成功撤销。",
|
||||
"last_signed_in_ago": "最后一次登录 {time} 前",
|
||||
"invalid_client_id": "客户 ID 只能包含字母、数字、下划线和连字符。",
|
||||
"invalid_client_id": "客户端 ID 只能包含字母、数字、下划线和连字符。",
|
||||
"custom_client_id_description": "此处可根据应用需要设置自定义客户端 ID。留空随机生成。",
|
||||
"generated": "已生成",
|
||||
"administration": "管理员选项",
|
||||
"group_rdn_attribute_description": "在组的区分名称(DN)中使用的属性。推荐值:`cn`",
|
||||
"group_rdn_attribute_description": "在组的区分名称(DN)中使用的属性。",
|
||||
"display_name_attribute": "显示名称属性",
|
||||
"display_name": "显示名称",
|
||||
"configure_application_images": "配置应用程序图标",
|
||||
"ui_config_disabled_info_title": "用户界面配置已禁用",
|
||||
"ui_config_disabled_info_description": "用户界面配置已禁用,因为应用程序配置设置通过环境变量进行管理。某些设置可能无法编辑。"
|
||||
}
|
||||
|
||||
@@ -120,6 +120,8 @@
|
||||
"username": "使用者名稱",
|
||||
"save": "儲存",
|
||||
"username_can_only_contain": "使用者名稱僅能包含小寫英文字母、數字、底線(_)、句點(.)、連字號(-)與 @ 符號",
|
||||
"username_must_start_with": "使用者名稱必須以英數字元開頭",
|
||||
"username_must_end_with": "使用者名稱必須以英數字元結尾",
|
||||
"sign_in_using_the_following_code_the_code_will_expire_in_minutes": "使用以下代碼登入。 這個代碼將於 15 分鐘後到期。",
|
||||
"or_visit": "或造訪",
|
||||
"added_on": "新增於",
|
||||
@@ -443,7 +445,10 @@
|
||||
"custom_client_id_description": "如果您的應用程式需要,請設定自訂用戶端 ID。否則,請留空以產生隨機 ID。",
|
||||
"generated": "產生",
|
||||
"administration": "行政管理",
|
||||
"group_rdn_attribute_description": "群組識別名 (DN) 中使用的屬性。建議值: `cn`",
|
||||
"group_rdn_attribute_description": "用於群組區別名稱(DN)的屬性。",
|
||||
"display_name_attribute": "顯示名稱屬性",
|
||||
"display_name": "顯示名稱",
|
||||
"configure_application_images": "設定應用程式映像檔",
|
||||
"ui_config_disabled_info_title": "使用者介面設定已停用",
|
||||
"ui_config_disabled_info_description": "使用者介面設定已停用,因為應用程式的設定參數是透過環境變數進行管理。部分設定可能無法編輯。"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pocket-id-frontend",
|
||||
"version": "1.11.0",
|
||||
"version": "1.11.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="%lang%">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="/api/application-configuration/favicon" />
|
||||
<link rel="icon" href="/api/application-images/favicon" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="robots" content="noindex" />
|
||||
<link rel="manifest" href="/app.webmanifest" />
|
||||
|
||||
@@ -32,14 +32,14 @@ export default class AppConfigService extends APIService {
|
||||
const formData = new FormData();
|
||||
formData.append('file', favicon!);
|
||||
|
||||
await this.api.put(`/application-configuration/favicon`, formData);
|
||||
await this.api.put(`/application-images/favicon`, formData);
|
||||
}
|
||||
|
||||
async updateLogo(logo: File, light = true) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', logo!);
|
||||
|
||||
await this.api.put(`/application-configuration/logo`, formData, {
|
||||
await this.api.put(`/application-images/logo`, formData, {
|
||||
params: { light }
|
||||
});
|
||||
cachedApplicationLogo.bustCache(light);
|
||||
@@ -49,7 +49,7 @@ export default class AppConfigService extends APIService {
|
||||
const formData = new FormData();
|
||||
formData.append('file', backgroundImage!);
|
||||
|
||||
await this.api.put(`/application-configuration/background-image`, formData);
|
||||
await this.api.put(`/application-images/background`, formData);
|
||||
cachedBackgroundImage.bustCache();
|
||||
}
|
||||
|
||||
|
||||
@@ -9,14 +9,14 @@ type CachableImage = {
|
||||
|
||||
export const cachedApplicationLogo: CachableImage = {
|
||||
getUrl: (light = true) => {
|
||||
let url = '/api/application-configuration/logo';
|
||||
let url = '/api/application-images/logo';
|
||||
if (!light) {
|
||||
url += '?light=false';
|
||||
}
|
||||
return getCachedImageUrl(url);
|
||||
},
|
||||
bustCache: (light = true) => {
|
||||
let url = '/api/application-configuration/logo';
|
||||
let url = '/api/application-images/logo';
|
||||
if (!light) {
|
||||
url += '?light=false';
|
||||
}
|
||||
@@ -25,8 +25,8 @@ export const cachedApplicationLogo: CachableImage = {
|
||||
};
|
||||
|
||||
export const cachedBackgroundImage: CachableImage = {
|
||||
getUrl: () => getCachedImageUrl('/api/application-configuration/background-image'),
|
||||
bustCache: () => bustImageCache('/api/application-configuration/background-image')
|
||||
getUrl: () => getCachedImageUrl('/api/application-images/background'),
|
||||
bustCache: () => bustImageCache('/api/application-images/background')
|
||||
};
|
||||
|
||||
export const cachedProfilePicture: CachableImage = {
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
imageClass="size-14 p-2"
|
||||
label={m.favicon()}
|
||||
bind:image={favicon}
|
||||
imageURL="/api/application-configuration/favicon"
|
||||
imageURL="/api/application-images/favicon"
|
||||
accept="image/x-icon"
|
||||
/>
|
||||
<ApplicationImage
|
||||
|
||||
@@ -4,15 +4,10 @@ if [ ! -f .version ] || [ ! -f frontend/package.json ] || [ ! -f CHANGELOG.md ];
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if conventional-changelog is installed, if not install it
|
||||
if ! command -v conventional-changelog &>/dev/null; then
|
||||
echo "conventional-changelog not found, installing..."
|
||||
npm install -g conventional-changelog-cli
|
||||
# Verify installation was successful
|
||||
if ! command -v conventional-changelog &>/dev/null; then
|
||||
echo "Error: Failed to install conventional-changelog-cli."
|
||||
exit 1
|
||||
fi
|
||||
# Check if git cliff is installed
|
||||
if ! command -v git cliff &>/dev/null; then
|
||||
echo "Error: git cliff is not installed. Please install it from https://git-cliff.org/docs/installation."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if GitHub CLI is installed
|
||||
@@ -113,7 +108,7 @@ git add frontend/package.json
|
||||
|
||||
# Generate changelog
|
||||
echo "Generating changelog..."
|
||||
conventional-changelog -p conventionalcommits -i CHANGELOG.md -s --pkg frontend/package.json
|
||||
git cliff --github-token=$(gh auth token) --prepend CHANGELOG.md --tag "v$NEW_VERSION" --unreleased
|
||||
git add CHANGELOG.md
|
||||
|
||||
# Commit the changes with the new version
|
||||
@@ -128,7 +123,7 @@ git push --tags
|
||||
|
||||
# Extract the changelog content for the latest release
|
||||
echo "Extracting changelog content for version $NEW_VERSION..."
|
||||
CHANGELOG=$(awk '/^## / {if (NR > 1) exit} NR > 1 {print}' CHANGELOG.md | awk 'NR > 2 || NF {print}')
|
||||
CHANGELOG=$(awk '/^## v[0-9]/ { if (found) exit; found=1; next } found' CHANGELOG.md)
|
||||
|
||||
if [ -z "$CHANGELOG" ]; then
|
||||
echo "Error: Could not extract changelog for version $NEW_VERSION."
|
||||
|
||||
@@ -128,15 +128,15 @@ test('Update application images', async ({ page }) => {
|
||||
await expect(page.locator('[data-type="success"]')).toHaveText('Images updated successfully');
|
||||
|
||||
await page.request
|
||||
.get('/api/application-configuration/favicon')
|
||||
.get('/api/application-images/favicon')
|
||||
.then((res) => expect.soft(res.status()).toBe(200));
|
||||
await page.request
|
||||
.get('/api/application-configuration/logo?light=true')
|
||||
.get('/api/application-images/logo?light=true')
|
||||
.then((res) => expect.soft(res.status()).toBe(200));
|
||||
await page.request
|
||||
.get('/api/application-configuration/logo?light=false')
|
||||
.get('/api/application-images/logo?light=false')
|
||||
.then((res) => expect.soft(res.status()).toBe(200));
|
||||
await page.request
|
||||
.get('/api/application-configuration/background-image')
|
||||
.get('/api/application-images/background')
|
||||
.then((res) => expect.soft(res.status()).toBe(200));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user