mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-03-31 11:46:37 +00:00
Merge branch 'main' into chore/depot
This commit is contained in:
@@ -164,7 +164,7 @@ require (
|
|||||||
golang.org/x/sys v0.41.0 // indirect
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
|
||||||
google.golang.org/grpc v1.79.1 // indirect
|
google.golang.org/grpc v1.79.3 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
modernc.org/libc v1.68.0 // indirect
|
modernc.org/libc v1.68.0 // indirect
|
||||||
|
|||||||
@@ -470,8 +470,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d h1:
|
|||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:48U2I+QQUYhsFrg2SY6r+nJzeOtjey7j//WBESw+qyQ=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:48U2I+QQUYhsFrg2SY6r+nJzeOtjey7j//WBESw+qyQ=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
|
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||||
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/pocket-id/pocket-id/backend/internal/storage"
|
"github.com/pocket-id/pocket-id/backend/internal/storage"
|
||||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||||
@@ -76,6 +77,18 @@ func initApplicationImages(ctx context.Context, fileStorage storage.FileStorage)
|
|||||||
dstNameToExt[nameWithoutExt] = ext
|
dstNameToExt[nameWithoutExt] = ext
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initedPath := path.Join("application-images", ".inited")
|
||||||
|
if _, _, err := fileStorage.Open(ctx, initedPath); err == nil {
|
||||||
|
return dstNameToExt, nil
|
||||||
|
} else if !os.IsNotExist(err) {
|
||||||
|
return nil, fmt.Errorf("failed to read .inited: %w", err)
|
||||||
|
} else {
|
||||||
|
err := fileStorage.Save(ctx, initedPath, strings.NewReader(""))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to store .inited: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Copy images from the images directory to the application-images directory if they don't already exist
|
// Copy images from the images directory to the application-images directory if they don't already exist
|
||||||
for _, sourceFile := range sourceFiles {
|
for _, sourceFile := range sourceFiles {
|
||||||
if sourceFile.IsDir() {
|
if sourceFile.IsDir() {
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ func NewDatabase() (db *gorm.DB, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run migrations
|
// Run migrations
|
||||||
if err := utils.MigrateDatabase(sqlDb); err != nil {
|
err = utils.MigrateDatabase(sqlDb)
|
||||||
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to run migrations: %w", err)
|
return nil, fmt.Errorf("failed to run migrations: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +43,10 @@ func NewDatabase() (db *gorm.DB, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ConnectDatabase() (db *gorm.DB, err error) {
|
func ConnectDatabase() (db *gorm.DB, err error) {
|
||||||
var dialector gorm.Dialector
|
var (
|
||||||
|
dialector gorm.Dialector
|
||||||
|
sqliteNetworkFilesystem bool
|
||||||
|
)
|
||||||
|
|
||||||
// Choose the correct database provider
|
// Choose the correct database provider
|
||||||
var onConnFn func(conn *sql.DB)
|
var onConnFn func(conn *sql.DB)
|
||||||
@@ -63,6 +67,14 @@ func ConnectDatabase() (db *gorm.DB, err error) {
|
|||||||
if err := ensureSqliteDatabaseDir(dbPath); err != nil {
|
if err := ensureSqliteDatabaseDir(dbPath); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sqliteNetworkFilesystem, err = utils.IsNetworkedFileSystem(filepath.Dir(dbPath))
|
||||||
|
if err != nil {
|
||||||
|
// Log the error only
|
||||||
|
slog.Warn("Failed to detect filesystem type for the SQLite database directory", slog.String("path", filepath.Dir(dbPath)), slog.Any("error", err))
|
||||||
|
} else if sqliteNetworkFilesystem {
|
||||||
|
slog.Warn("⚠️⚠️⚠️ SQLite databases should not be stored on a networked file system like NFS, SMB, or FUSE, as there's a risk of crashes and even database corruption", slog.String("path", filepath.Dir(dbPath)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Before we connect, also make sure that there's a temporary folder for SQLite to write its data
|
// Before we connect, also make sure that there's a temporary folder for SQLite to write its data
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ func (wkc *WellKnownController) computeOIDCConfiguration() ([]byte, error) {
|
|||||||
"id_token_signing_alg_values_supported": []string{alg.String()},
|
"id_token_signing_alg_values_supported": []string{alg.String()},
|
||||||
"authorization_response_iss_parameter_supported": true,
|
"authorization_response_iss_parameter_supported": true,
|
||||||
"code_challenge_methods_supported": []string{"plain", "S256"},
|
"code_challenge_methods_supported": []string{"plain", "S256"},
|
||||||
|
"token_endpoint_auth_methods_supported": []string{"client_secret_basic", "client_secret_post", "none"},
|
||||||
}
|
}
|
||||||
return json.Marshal(config)
|
return json.Marshal(config)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
type SignUpDto struct {
|
type SignUpDto struct {
|
||||||
Username string `json:"username" binding:"required,username,min=2,max=50" unorm:"nfc"`
|
Username string `json:"username" binding:"required,username,min=1,max=50" unorm:"nfc"`
|
||||||
Email *string `json:"email" binding:"omitempty,email" unorm:"nfc"`
|
Email *string `json:"email" binding:"omitempty,email" unorm:"nfc"`
|
||||||
FirstName string `json:"firstName" binding:"max=50" unorm:"nfc"`
|
FirstName string `json:"firstName" binding:"max=50" unorm:"nfc"`
|
||||||
LastName string `json:"lastName" binding:"max=50" unorm:"nfc"`
|
LastName string `json:"lastName" binding:"max=50" unorm:"nfc"`
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ type UserDto struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UserCreateDto struct {
|
type UserCreateDto struct {
|
||||||
Username string `json:"username" binding:"required,username,min=2,max=50" unorm:"nfc"`
|
Username string `json:"username" binding:"required,username,min=1,max=50" unorm:"nfc"`
|
||||||
Email *string `json:"email" binding:"omitempty,email" unorm:"nfc"`
|
Email *string `json:"email" binding:"omitempty,email" unorm:"nfc"`
|
||||||
EmailVerified bool `json:"emailVerified"`
|
EmailVerified bool `json:"emailVerified"`
|
||||||
FirstName string `json:"firstName" binding:"max=50" unorm:"nfc"`
|
FirstName string `json:"firstName" binding:"max=50" unorm:"nfc"`
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ import (
|
|||||||
// [a-zA-Z0-9] : The username must start with an alphanumeric character
|
// [a-zA-Z0-9] : The username must start with an alphanumeric character
|
||||||
// [a-zA-Z0-9_.@-]* : The rest of the username can contain alphanumeric characters, dots, underscores, hyphens, and "@" symbols
|
// [a-zA-Z0-9_.@-]* : The rest of the username can contain alphanumeric characters, dots, underscores, hyphens, and "@" symbols
|
||||||
// [a-zA-Z0-9]$ : The username must end with an alphanumeric character
|
// [a-zA-Z0-9]$ : The username must end with an alphanumeric character
|
||||||
var validateUsernameRegex = regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9_.@-]*[a-zA-Z0-9]$")
|
// (...)? : This allows single-character usernames (just one alphanumeric character)
|
||||||
|
var validateUsernameRegex = regexp.MustCompile("^[a-zA-Z0-9]([a-zA-Z0-9_.@-]*[a-zA-Z0-9])?$")
|
||||||
|
|
||||||
var validateClientIDRegex = regexp.MustCompile("^[a-zA-Z0-9._-]+$")
|
var validateClientIDRegex = regexp.MustCompile("^[a-zA-Z0-9._-]+$")
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ func TestValidateUsername(t *testing.T) {
|
|||||||
{"starts with symbol", ".username", false},
|
{"starts with symbol", ".username", false},
|
||||||
{"ends with non-alphanumeric", "username-", false},
|
{"ends with non-alphanumeric", "username-", false},
|
||||||
{"contains space", "user name", false},
|
{"contains space", "user name", false},
|
||||||
|
{"valid single char", "a", true},
|
||||||
{"empty", "", false},
|
{"empty", "", false},
|
||||||
{"only special chars", "-._@", false},
|
{"only special chars", "-._@", false},
|
||||||
{"valid long", "a1234567890_b.c-d@e", true},
|
{"valid long", "a1234567890_b.c-d@e", true},
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -112,7 +113,11 @@ func (s *GeoLiteService) UpdateDatabase(parentCtx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("Updating GeoLite2 City database")
|
slog.Info("Updating GeoLite2 City database")
|
||||||
downloadUrl := fmt.Sprintf(common.EnvConfig.GeoLiteDBUrl, common.EnvConfig.MaxMindLicenseKey)
|
|
||||||
|
downloadUrl := common.EnvConfig.GeoLiteDBUrl
|
||||||
|
if strings.Contains(downloadUrl, "%s") {
|
||||||
|
downloadUrl = fmt.Sprintf(downloadUrl, common.EnvConfig.MaxMindLicenseKey)
|
||||||
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(parentCtx, 10*time.Minute)
|
ctx, cancel := context.WithTimeout(parentCtx, 10*time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|||||||
35
backend/internal/utils/networked_filesystem_linux.go
Normal file
35
backend/internal/utils/networked_filesystem_linux.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Filesystem magic values from Linux's include/uapi/linux/magic.h, used by statfs(2).
|
||||||
|
const (
|
||||||
|
nfsSuperMagic = 0x6969
|
||||||
|
smbSuperMagic = 0x517b
|
||||||
|
cifsSuperMagic = 0xff534d42
|
||||||
|
fuseSuperMagic = 0x65735546
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsNetworkedFileSystem reports whether path is on a filesystem that is known to be unsafe for SQLite, specifically NFS, SMB/CIFS, or FUSE mounts.
|
||||||
|
func IsNetworkedFileSystem(path string) (bool, error) {
|
||||||
|
var statfs syscall.Statfs_t
|
||||||
|
err := syscall.Statfs(path, &statfs)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("error executing statfs syscall: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Statfs_t.Type is arch-dependent (for example, int32 on some systems and int64 on others).
|
||||||
|
// Normalize through uint32 first so signed values still preserve the Linux bit pattern for magic numbers such as CIFS (0xff534d42), then compare in a wide unsigned form.
|
||||||
|
//nolint:gosec
|
||||||
|
switch uint64(uint32(statfs.Type)) {
|
||||||
|
case nfsSuperMagic, smbSuperMagic, cifsSuperMagic, fuseSuperMagic:
|
||||||
|
return true, nil
|
||||||
|
default:
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
8
backend/internal/utils/networked_filesystem_nonlinux.go
Normal file
8
backend/internal/utils/networked_filesystem_nonlinux.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
//go:build !linux
|
||||||
|
|
||||||
|
package utils
|
||||||
|
|
||||||
|
// IsNetworkedFileSystem returns false on non-Linux systems because this detection is only used for Linux-specific statfs(2) filesystem magic values.
|
||||||
|
func IsNetworkedFileSystem(string) (bool, error) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
"see_your_recent_account_activities": "See your account activities within the configured retention period.",
|
"see_your_recent_account_activities": "See your account activities within the configured retention period.",
|
||||||
"time": "Time",
|
"time": "Time",
|
||||||
"event": "Event",
|
"event": "Event",
|
||||||
"approximate_location": "Approximate Location",
|
"approximate_location": "Omtrentlig plassering",
|
||||||
"ip_address": "IP adresse",
|
"ip_address": "IP adresse",
|
||||||
"device": "Enhet",
|
"device": "Enhet",
|
||||||
"client": "Klient",
|
"client": "Klient",
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export const callbackUrlSchema = z
|
|||||||
|
|
||||||
export const usernameSchema = z
|
export const usernameSchema = z
|
||||||
.string()
|
.string()
|
||||||
.min(2)
|
.min(1)
|
||||||
.max(30)
|
.max(30)
|
||||||
.regex(/^[a-zA-Z0-9]/, m.username_must_start_with())
|
.regex(/^[a-zA-Z0-9]/, m.username_must_start_with())
|
||||||
.regex(/[a-zA-Z0-9]$/, m.username_must_end_with())
|
.regex(/[a-zA-Z0-9]$/, m.username_must_end_with())
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ test('Change Locale', async ({ page }) => {
|
|||||||
// Check if the validation messages are translated because they are provided by Zod
|
// Check if the validation messages are translated because they are provided by Zod
|
||||||
await page.getByRole('textbox', { name: 'Gebruikersnaam' }).fill('');
|
await page.getByRole('textbox', { name: 'Gebruikersnaam' }).fill('');
|
||||||
await page.getByRole('button', { name: 'Opslaan' }).click();
|
await page.getByRole('button', { name: 'Opslaan' }).click();
|
||||||
await expect(page.getByText('Te kort: verwacht dat string >=2 tekens heeft')).toBeVisible();
|
await expect(page.getByText('Te kort: verwacht dat string >=1 tekens heeft')).toBeVisible();
|
||||||
|
|
||||||
// Clear all cookies and sign in again to check if the language is still set to Dutch
|
// Clear all cookies and sign in again to check if the language is still set to Dutch
|
||||||
await page.context().clearCookies();
|
await page.context().clearCookies();
|
||||||
@@ -76,7 +76,7 @@ test('Change Locale', async ({ page }) => {
|
|||||||
|
|
||||||
await page.getByRole('textbox', { name: 'Gebruikersnaam' }).fill('');
|
await page.getByRole('textbox', { name: 'Gebruikersnaam' }).fill('');
|
||||||
await page.getByRole('button', { name: 'Opslaan' }).click();
|
await page.getByRole('button', { name: 'Opslaan' }).click();
|
||||||
await expect(page.getByText('Te kort: verwacht dat string >=2 tekens heeft')).toBeVisible();
|
await expect(page.getByText('Te kort: verwacht dat string >=1 tekens heeft')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Add passkey to an account', async ({ page }) => {
|
test('Add passkey to an account', async ({ page }) => {
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ function compareExports(dir1: string, dir2: string): void {
|
|||||||
const hashes2 = hashAllFiles(dir2);
|
const hashes2 = hashAllFiles(dir2);
|
||||||
|
|
||||||
const files1 = Object.keys(hashes1).sort();
|
const files1 = Object.keys(hashes1).sort();
|
||||||
const files2 = Object.keys(hashes2).sort();
|
const files2 = Object.keys(hashes2).sort().filter(p => !p.includes('.inited'));
|
||||||
expect(files2).toEqual(files1);
|
expect(files2).toEqual(files1);
|
||||||
|
|
||||||
for (const file of files1) {
|
for (const file of files1) {
|
||||||
|
|||||||
Reference in New Issue
Block a user