1
0
mirror of https://github.com/pocket-id/pocket-id.git synced 2026-03-22 18:30:09 +00:00

Merge branch 'main' into chore/depot

This commit is contained in:
Kyle Mendell
2026-02-23 13:06:44 -06:00
committed by GitHub
36 changed files with 122 additions and 219 deletions

View File

@@ -2,7 +2,9 @@
"name": "pocket-id", "name": "pocket-id",
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm", "image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm",
"features": { "features": {
"ghcr.io/devcontainers/features/go:1": {} "ghcr.io/devcontainers/features/go:1": {
"version": "1.26"
}
}, },
"customizations": { "customizations": {
"vscode": { "vscode": {

View File

@@ -33,9 +33,9 @@ jobs:
go-version-file: backend/go.mod go-version-file: backend/go.mod
- name: Run Golangci-lint - name: Run Golangci-lint
uses: golangci/golangci-lint-action@v8.0.0 uses: golangci/golangci-lint-action@v9.0.0
with: with:
version: v2.4.0 version: v2.9.0
args: --build-tags=exclude_frontend args: --build-tags=exclude_frontend
working-directory: backend working-directory: backend
only-new-issues: ${{ github.event_name == 'pull_request' }} only-new-issues: ${{ github.event_name == 'pull_request' }}

View File

@@ -28,7 +28,7 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v5 uses: actions/setup-node@v5
with: with:
node-version: 22 node-version: 24
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v6 uses: actions/setup-go@v6

View File

@@ -76,7 +76,7 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v5 uses: actions/setup-node@v5
with: with:
node-version: 22 node-version: 24
- name: Cache Playwright Browsers - name: Cache Playwright Browsers
uses: actions/cache@v4 uses: actions/cache@v4

View File

@@ -22,7 +22,7 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v5 uses: actions/setup-node@v5
with: with:
node-version: 22 node-version: 24
- uses: actions/setup-go@v6 - uses: actions/setup-go@v6
with: with:
go-version-file: "backend/go.mod" go-version-file: "backend/go.mod"

View File

@@ -44,7 +44,7 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v5 uses: actions/setup-node@v5
with: with:
node-version: 22 node-version: 24
- name: Install dependencies - name: Install dependencies
run: pnpm --filter pocket-id-frontend install --frozen-lockfile run: pnpm --filter pocket-id-frontend install --frozen-lockfile

View File

@@ -21,7 +21,6 @@ Before you submit the pull request for review please ensure that
``` ```
Where `TYPE` can be: Where `TYPE` can be:
- **feat** - is a new feature - **feat** - is a new feature
- **doc** - documentation only changes - **doc** - documentation only changes
- **fix** - a bug fix - **fix** - a bug fix
@@ -51,8 +50,8 @@ If you use [Dev Containers](https://code.visualstudio.com/docs/remote/containers
If you don't use Dev Containers, you need to install the following tools manually: If you don't use Dev Containers, you need to install the following tools manually:
- [Node.js](https://nodejs.org/en/download/) >= 22 - [Node.js](https://nodejs.org/en/download/) >= 24
- [Go](https://golang.org/doc/install) >= 1.25 - [Go](https://golang.org/doc/install) >= 1.26
- [Git](https://git-scm.com/downloads) - [Git](https://git-scm.com/downloads)
### 2. Setup ### 2. Setup

View File

@@ -1,6 +1,6 @@
module github.com/pocket-id/pocket-id/backend module github.com/pocket-id/pocket-id/backend
go 1.25.0 go 1.26.0
require ( require (
github.com/aws/aws-sdk-go-v2 v1.41.1 github.com/aws/aws-sdk-go-v2 v1.41.1

View File

@@ -118,11 +118,10 @@ func initOtelLogging(ctx context.Context, resource *resource.Resource) error {
// Set the logger provider globally // Set the logger provider globally
globallog.SetLoggerProvider(provider) globallog.SetLoggerProvider(provider)
// Wrap the handler in a "fanout" one handler = slog.NewMultiHandler(
handler = utils.LogFanoutHandler{
handler, handler,
otelslog.NewHandler(common.Name, otelslog.WithLoggerProvider(provider)), otelslog.NewHandler(common.Name, otelslog.WithLoggerProvider(provider)),
} )
// Set the default slog to send logs to OTel and add the app name // Set the default slog to send logs to OTel and add the app name
log := slog.New(handler). log := slog.New(handler).

View File

@@ -106,7 +106,7 @@ func defaultConfig() EnvConfigSchema {
func parseEnvConfig() error { func parseEnvConfig() error {
parsers := map[reflect.Type]env.ParserFunc{ parsers := map[reflect.Type]env.ParserFunc{
reflect.TypeOf([]byte{}): func(value string) (interface{}, error) { reflect.TypeFor[[]byte](): func(value string) (any, error) {
return []byte(value), nil return []byte(value), nil
}, },
} }
@@ -184,8 +184,8 @@ func ValidateEnvConfig(config *EnvConfigSchema) error {
} }
// Validate LOCAL_IPV6_RANGES // Validate LOCAL_IPV6_RANGES
ranges := strings.Split(config.LocalIPv6Ranges, ",") ranges := strings.SplitSeq(config.LocalIPv6Ranges, ",")
for _, rangeStr := range ranges { for rangeStr := range ranges {
rangeStr = strings.TrimSpace(rangeStr) rangeStr = strings.TrimSpace(rangeStr)
if rangeStr == "" { if rangeStr == "" {
continue continue
@@ -235,9 +235,9 @@ func prepareEnvConfig(config *EnvConfigSchema) error {
fieldType := typ.Field(i) fieldType := typ.Field(i)
optionsTag := fieldType.Tag.Get("options") optionsTag := fieldType.Tag.Get("options")
options := strings.Split(optionsTag, ",") options := strings.SplitSeq(optionsTag, ",")
for _, option := range options { for option := range options {
switch option { switch option {
case "toLower": case "toLower":
if field.Kind() == reflect.String { if field.Kind() == reflect.String {

View File

@@ -9,7 +9,6 @@ import (
"github.com/pocket-id/pocket-id/backend/internal/model" "github.com/pocket-id/pocket-id/backend/internal/model"
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types" datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
"github.com/pocket-id/pocket-id/backend/internal/utils"
) )
type sourceStruct struct { type sourceStruct struct {
@@ -60,11 +59,11 @@ type embeddedStruct struct {
func TestMapStruct(t *testing.T) { func TestMapStruct(t *testing.T) {
src := sourceStruct{ src := sourceStruct{
AString: "abcd", AString: "abcd",
AStringPtr: utils.Ptr("xyz"), AStringPtr: new("xyz"),
ABool: true, ABool: true,
ABoolPtr: utils.Ptr(false), ABoolPtr: new(false),
ACustomDateTime: datatype.DateTime(time.Date(2025, 1, 2, 3, 4, 5, 0, time.UTC)), ACustomDateTime: datatype.DateTime(time.Date(2025, 1, 2, 3, 4, 5, 0, time.UTC)),
ACustomDateTimePtr: utils.Ptr(datatype.DateTime(time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC))), ACustomDateTimePtr: new(datatype.DateTime(time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC))),
ANilStringPtr: nil, ANilStringPtr: nil,
ASlice: []string{"a", "b", "c"}, ASlice: []string{"a", "b", "c"},
AMap: map[string]int{ AMap: map[string]int{
@@ -80,8 +79,8 @@ func TestMapStruct(t *testing.T) {
Bar: 111, Bar: 111,
}, },
StringPtrToString: utils.Ptr("foobar"), StringPtrToString: new("foobar"),
EmptyStringPtrToString: utils.Ptr(""), EmptyStringPtrToString: new(""),
NilStringPtrToString: nil, NilStringPtrToString: nil,
IntToInt64: 99, IntToInt64: 99,
AuditLogEventToString: model.AuditLogEventAccountCreated, AuditLogEventToString: model.AuditLogEventAccountCreated,
@@ -118,11 +117,11 @@ func TestMapStructList(t *testing.T) {
sources := []sourceStruct{ sources := []sourceStruct{
{ {
AString: "first", AString: "first",
AStringPtr: utils.Ptr("one"), AStringPtr: new("one"),
ABool: true, ABool: true,
ABoolPtr: utils.Ptr(false), ABoolPtr: new(false),
ACustomDateTime: datatype.DateTime(time.Date(2025, 1, 2, 3, 4, 5, 0, time.UTC)), ACustomDateTime: datatype.DateTime(time.Date(2025, 1, 2, 3, 4, 5, 0, time.UTC)),
ACustomDateTimePtr: utils.Ptr(datatype.DateTime(time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC))), ACustomDateTimePtr: new(datatype.DateTime(time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC))),
ASlice: []string{"a", "b"}, ASlice: []string{"a", "b"},
AMap: map[string]int{ AMap: map[string]int{
"a": 1, "a": 1,
@@ -136,11 +135,11 @@ func TestMapStructList(t *testing.T) {
}, },
{ {
AString: "second", AString: "second",
AStringPtr: utils.Ptr("two"), AStringPtr: new("two"),
ABool: false, ABool: false,
ABoolPtr: utils.Ptr(true), ABoolPtr: new(true),
ACustomDateTime: datatype.DateTime(time.Date(2026, 6, 7, 8, 9, 10, 0, time.UTC)), ACustomDateTime: datatype.DateTime(time.Date(2026, 6, 7, 8, 9, 10, 0, time.UTC)),
ACustomDateTimePtr: utils.Ptr(datatype.DateTime(time.Date(2023, 6, 7, 8, 9, 10, 0, time.UTC))), ACustomDateTimePtr: new(datatype.DateTime(time.Date(2023, 6, 7, 8, 9, 10, 0, time.UTC))),
ASlice: []string{"c", "d", "e"}, ASlice: []string{"c", "d", "e"},
AMap: map[string]int{ AMap: map[string]int{
"c": 3, "c": 3,

View File

@@ -12,7 +12,7 @@ import (
// Normalize iterates through an object and performs Unicode normalization on all string fields with the `unorm` tag. // Normalize iterates through an object and performs Unicode normalization on all string fields with the `unorm` tag.
func Normalize(obj any) { func Normalize(obj any) {
v := reflect.ValueOf(obj) v := reflect.ValueOf(obj)
if v.Kind() != reflect.Ptr || v.IsNil() { if v.Kind() != reflect.Pointer || v.IsNil() {
return return
} }
v = v.Elem() v = v.Elem()
@@ -21,7 +21,7 @@ func Normalize(obj any) {
if v.Kind() == reflect.Slice { if v.Kind() == reflect.Slice {
for i := 0; i < v.Len(); i++ { for i := 0; i < v.Len(); i++ {
elem := v.Index(i) elem := v.Index(i)
if elem.Kind() == reflect.Ptr && !elem.IsNil() && elem.Elem().Kind() == reflect.Struct { if elem.Kind() == reflect.Pointer && !elem.IsNil() && elem.Elem().Kind() == reflect.Struct {
Normalize(elem.Interface()) Normalize(elem.Interface())
} else if elem.Kind() == reflect.Struct && elem.CanAddr() { } else if elem.Kind() == reflect.Struct && elem.CanAddr() {
Normalize(elem.Addr().Interface()) Normalize(elem.Addr().Interface())

View File

@@ -67,7 +67,7 @@ type ScimResourceData struct {
type ScimResourceMeta struct { type ScimResourceMeta struct {
Location string `json:"location,omitempty"` Location string `json:"location,omitempty"`
ResourceType string `json:"resourceType,omitempty"` ResourceType string `json:"resourceType,omitempty"`
Created time.Time `json:"created,omitempty"` Created time.Time `json:"created"`
LastModified time.Time `json:"lastModified,omitempty"` LastModified time.Time `json:"lastModified,omitempty"`
Version string `json:"version,omitempty"` Version string `json:"version,omitempty"`
} }

View File

@@ -3,7 +3,6 @@ package dto
import ( import (
"testing" "testing"
"github.com/pocket-id/pocket-id/backend/internal/utils"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -17,7 +16,7 @@ func TestUserCreateDto_Validate(t *testing.T) {
name: "valid input", name: "valid input",
input: UserCreateDto{ input: UserCreateDto{
Username: "testuser", Username: "testuser",
Email: utils.Ptr("test@example.com"), Email: new("test@example.com"),
FirstName: "John", FirstName: "John",
LastName: "Doe", LastName: "Doe",
DisplayName: "John Doe", DisplayName: "John Doe",
@@ -27,7 +26,7 @@ func TestUserCreateDto_Validate(t *testing.T) {
{ {
name: "missing username", name: "missing username",
input: UserCreateDto{ input: UserCreateDto{
Email: utils.Ptr("test@example.com"), Email: new("test@example.com"),
FirstName: "John", FirstName: "John",
LastName: "Doe", LastName: "Doe",
DisplayName: "John Doe", DisplayName: "John Doe",
@@ -37,7 +36,7 @@ func TestUserCreateDto_Validate(t *testing.T) {
{ {
name: "missing display name", name: "missing display name",
input: UserCreateDto{ input: UserCreateDto{
Email: utils.Ptr("test@example.com"), Email: new("test@example.com"),
FirstName: "John", FirstName: "John",
LastName: "Doe", LastName: "Doe",
}, },
@@ -47,7 +46,7 @@ func TestUserCreateDto_Validate(t *testing.T) {
name: "username contains invalid characters", name: "username contains invalid characters",
input: UserCreateDto{ input: UserCreateDto{
Username: "test/ser", Username: "test/ser",
Email: utils.Ptr("test@example.com"), Email: new("test@example.com"),
FirstName: "John", FirstName: "John",
LastName: "Doe", LastName: "Doe",
DisplayName: "John Doe", DisplayName: "John Doe",
@@ -58,7 +57,7 @@ func TestUserCreateDto_Validate(t *testing.T) {
name: "invalid email", name: "invalid email",
input: UserCreateDto{ input: UserCreateDto{
Username: "testuser", Username: "testuser",
Email: utils.Ptr("not-an-email"), Email: new("not-an-email"),
FirstName: "John", FirstName: "John",
LastName: "Doe", LastName: "Doe",
DisplayName: "John Doe", DisplayName: "John Doe",
@@ -69,7 +68,7 @@ func TestUserCreateDto_Validate(t *testing.T) {
name: "first name too short", name: "first name too short",
input: UserCreateDto{ input: UserCreateDto{
Username: "testuser", Username: "testuser",
Email: utils.Ptr("test@example.com"), Email: new("test@example.com"),
FirstName: "", FirstName: "",
LastName: "Doe", LastName: "Doe",
DisplayName: "John Doe", DisplayName: "John Doe",
@@ -80,7 +79,7 @@ func TestUserCreateDto_Validate(t *testing.T) {
name: "last name too long", name: "last name too long",
input: UserCreateDto{ input: UserCreateDto{
Username: "testuser", Username: "testuser",
Email: utils.Ptr("test@example.com"), Email: new("test@example.com"),
FirstName: "John", FirstName: "John",
LastName: "abcdfghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz", LastName: "abcdfghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz",
DisplayName: "John Doe", DisplayName: "John Doe",

View File

@@ -70,13 +70,12 @@ func TestAppConfigVariable_AsMinutesDuration(t *testing.T) {
// - dto.AppConfigDto should not include "internal" fields from model.AppConfig // - dto.AppConfigDto should not include "internal" fields from model.AppConfig
// This test is primarily meant to catch discrepancies between the two structs as fields are added or removed over time // This test is primarily meant to catch discrepancies between the two structs as fields are added or removed over time
func TestAppConfigStructMatchesUpdateDto(t *testing.T) { func TestAppConfigStructMatchesUpdateDto(t *testing.T) {
appConfigType := reflect.TypeOf(model.AppConfig{}) appConfigType := reflect.TypeFor[model.AppConfig]()
updateDtoType := reflect.TypeOf(dto.AppConfigUpdateDto{}) updateDtoType := reflect.TypeFor[dto.AppConfigUpdateDto]()
// Process AppConfig fields // Process AppConfig fields
appConfigFields := make(map[string]string) appConfigFields := make(map[string]string)
for i := 0; i < appConfigType.NumField(); i++ { for field := range appConfigType.Fields() {
field := appConfigType.Field(i)
if field.Tag.Get("key") == "" { if field.Tag.Get("key") == "" {
// Skip internal fields // Skip internal fields
continue continue
@@ -91,9 +90,7 @@ func TestAppConfigStructMatchesUpdateDto(t *testing.T) {
// Process AppConfigUpdateDto fields // Process AppConfigUpdateDto fields
dtoFields := make(map[string]string) dtoFields := make(map[string]string)
for i := 0; i < updateDtoType.NumField(); i++ { for field := range updateDtoType.Fields() {
field := updateDtoType.Field(i)
// Extract the json name from the tag (takes the part before any binding constraints) // Extract the json name from the tag (takes the part before any binding constraints)
jsonTag := field.Tag.Get("json") jsonTag := field.Tag.Get("json")
jsonName, _, _ := strings.Cut(jsonTag, ",") jsonName, _, _ := strings.Cut(jsonTag, ",")

View File

@@ -58,7 +58,7 @@ type ReauthenticationToken struct {
type AuthenticatorTransportList []protocol.AuthenticatorTransport //nolint:recvcheck type AuthenticatorTransportList []protocol.AuthenticatorTransport //nolint:recvcheck
// Scan and Value methods for GORM to handle the custom type // Scan and Value methods for GORM to handle the custom type
func (atl *AuthenticatorTransportList) Scan(value interface{}) error { func (atl *AuthenticatorTransportList) Scan(value any) error {
return utils.UnmarshalJSONFromDatabase(atl, value) return utils.UnmarshalJSONFromDatabase(atl, value)
} }
@@ -69,7 +69,7 @@ func (atl AuthenticatorTransportList) Value() (driver.Value, error) {
type CredentialParameters []protocol.CredentialParameter //nolint:recvcheck type CredentialParameters []protocol.CredentialParameter //nolint:recvcheck
// Scan and Value methods for GORM to handle the custom type // Scan and Value methods for GORM to handle the custom type
func (cp *CredentialParameters) Scan(value interface{}) error { func (cp *CredentialParameters) Scan(value any) error {
return utils.UnmarshalJSONFromDatabase(cp, value) return utils.UnmarshalJSONFromDatabase(cp, value)
} }

View File

@@ -170,7 +170,7 @@ func (s *ApiKeyService) ValidateApiKey(ctx context.Context, apiKey string) (mode
Clauses(clause.Returning{}). Clauses(clause.Returning{}).
Where("key = ? AND expires_at > ?", hashedKey, datatype.DateTime(now)). Where("key = ? AND expires_at > ?", hashedKey, datatype.DateTime(now)).
Updates(&model.ApiKey{ Updates(&model.ApiKey{
LastUsedAt: utils.Ptr(datatype.DateTime(now)), LastUsedAt: new(datatype.DateTime(now)),
}). }).
Preload("User"). Preload("User").
First(&key). First(&key).

View File

@@ -186,8 +186,7 @@ func (s *AppConfigService) UpdateAppConfig(ctx context.Context, input dto.AppCon
rt := reflect.ValueOf(input).Type() rt := reflect.ValueOf(input).Type()
rv := reflect.ValueOf(input) rv := reflect.ValueOf(input)
dbUpdate := make([]model.AppConfigVariable, 0, rt.NumField()) dbUpdate := make([]model.AppConfigVariable, 0, rt.NumField())
for i := range rt.NumField() { for field := range rt.Fields() {
field := rt.Field(i)
value := rv.FieldByName(field.Name).String() value := rv.FieldByName(field.Name).String()
// Get the value of the json tag, taking only what's before the comma // Get the value of the json tag, taking only what's before the comma

View File

@@ -81,7 +81,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
ID: "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e", ID: "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e",
}, },
Username: "tim", Username: "tim",
Email: utils.Ptr("tim.cook@test.com"), Email: new("tim.cook@test.com"),
EmailVerified: true, EmailVerified: true,
FirstName: "Tim", FirstName: "Tim",
LastName: "Cook", LastName: "Cook",
@@ -93,7 +93,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
ID: "1cd19686-f9a6-43f4-a41f-14a0bf5b4036", ID: "1cd19686-f9a6-43f4-a41f-14a0bf5b4036",
}, },
Username: "craig", Username: "craig",
Email: utils.Ptr("craig.federighi@test.com"), Email: new("craig.federighi@test.com"),
EmailVerified: false, EmailVerified: false,
FirstName: "Craig", FirstName: "Craig",
LastName: "Federighi", LastName: "Federighi",
@@ -105,7 +105,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
ID: "d9256384-98ad-49a7-bc58-99ad0b4dc23c", ID: "d9256384-98ad-49a7-bc58-99ad0b4dc23c",
}, },
Username: "eddy", Username: "eddy",
Email: utils.Ptr("eddy.cue@test.com"), Email: new("eddy.cue@test.com"),
FirstName: "Eddy", FirstName: "Eddy",
LastName: "Cue", LastName: "Cue",
DisplayName: "Eddy Cue", DisplayName: "Eddy Cue",
@@ -171,12 +171,12 @@ func (s *TestService) SeedDatabase(baseURL string) error {
ID: "3654a746-35d4-4321-ac61-0bdcff2b4055", ID: "3654a746-35d4-4321-ac61-0bdcff2b4055",
}, },
Name: "Nextcloud", Name: "Nextcloud",
LaunchURL: utils.Ptr("https://nextcloud.local"), LaunchURL: new("https://nextcloud.local"),
Secret: "$2a$10$9dypwot8nGuCjT6wQWWpJOckZfRprhe2EkwpKizxS/fpVHrOLEJHC", // w2mUeZISmEvIDMEDvpY0PnxQIpj1m3zY Secret: "$2a$10$9dypwot8nGuCjT6wQWWpJOckZfRprhe2EkwpKizxS/fpVHrOLEJHC", // w2mUeZISmEvIDMEDvpY0PnxQIpj1m3zY
CallbackURLs: model.UrlList{"http://nextcloud/auth/callback"}, CallbackURLs: model.UrlList{"http://nextcloud/auth/callback"},
LogoutCallbackURLs: model.UrlList{"http://nextcloud/auth/logout/callback"}, LogoutCallbackURLs: model.UrlList{"http://nextcloud/auth/logout/callback"},
ImageType: utils.StringPointer("png"), ImageType: new("png"),
CreatedByID: utils.Ptr(users[0].ID), CreatedByID: new(users[0].ID),
}, },
{ {
Base: model.Base{ Base: model.Base{
@@ -185,7 +185,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
Name: "Immich", Name: "Immich",
Secret: "$2a$10$Ak.FP8riD1ssy2AGGbG.gOpnp/rBpymd74j0nxNMtW0GG1Lb4gzxe", // PYjrE9u4v9GVqXKi52eur0eb2Ci4kc0x Secret: "$2a$10$Ak.FP8riD1ssy2AGGbG.gOpnp/rBpymd74j0nxNMtW0GG1Lb4gzxe", // PYjrE9u4v9GVqXKi52eur0eb2Ci4kc0x
CallbackURLs: model.UrlList{"http://immich/auth/callback"}, CallbackURLs: model.UrlList{"http://immich/auth/callback"},
CreatedByID: utils.Ptr(users[1].ID), CreatedByID: new(users[1].ID),
IsGroupRestricted: true, IsGroupRestricted: true,
AllowedUserGroups: []model.UserGroup{ AllowedUserGroups: []model.UserGroup{
userGroups[1], userGroups[1],
@@ -200,7 +200,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
CallbackURLs: model.UrlList{"http://tailscale/auth/callback"}, CallbackURLs: model.UrlList{"http://tailscale/auth/callback"},
LogoutCallbackURLs: model.UrlList{"http://tailscale/auth/logout/callback"}, LogoutCallbackURLs: model.UrlList{"http://tailscale/auth/logout/callback"},
IsGroupRestricted: true, IsGroupRestricted: true,
CreatedByID: utils.Ptr(users[0].ID), CreatedByID: new(users[0].ID),
}, },
{ {
Base: model.Base{ Base: model.Base{
@@ -209,7 +209,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
Name: "Federated", Name: "Federated",
Secret: "$2a$10$Ak.FP8riD1ssy2AGGbG.gOpnp/rBpymd74j0nxNMtW0GG1Lb4gzxe", // PYjrE9u4v9GVqXKi52eur0eb2Ci4kc0x Secret: "$2a$10$Ak.FP8riD1ssy2AGGbG.gOpnp/rBpymd74j0nxNMtW0GG1Lb4gzxe", // PYjrE9u4v9GVqXKi52eur0eb2Ci4kc0x
CallbackURLs: model.UrlList{"http://federated/auth/callback"}, CallbackURLs: model.UrlList{"http://federated/auth/callback"},
CreatedByID: utils.Ptr(users[1].ID), CreatedByID: new(users[1].ID),
AllowedUserGroups: []model.UserGroup{}, AllowedUserGroups: []model.UserGroup{},
Credentials: model.OidcClientCredentials{ Credentials: model.OidcClientCredentials{
FederatedIdentities: []model.OidcClientFederatedIdentity{ FederatedIdentities: []model.OidcClientFederatedIdentity{
@@ -229,7 +229,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
Name: "SCIM Client", Name: "SCIM Client",
Secret: "$2a$10$h4wfa8gI7zavDAxwzSq1sOwYU4e8DwK1XZ8ZweNnY5KzlJ3Iz.qdK", // nQbiuMRG7FpdK2EnDd5MBivWQeKFXohn Secret: "$2a$10$h4wfa8gI7zavDAxwzSq1sOwYU4e8DwK1XZ8ZweNnY5KzlJ3Iz.qdK", // nQbiuMRG7FpdK2EnDd5MBivWQeKFXohn
CallbackURLs: model.UrlList{"http://scimclient/auth/callback"}, CallbackURLs: model.UrlList{"http://scimclient/auth/callback"},
CreatedByID: utils.Ptr(users[0].ID), CreatedByID: new(users[0].ID),
IsGroupRestricted: true, IsGroupRestricted: true,
AllowedUserGroups: []model.UserGroup{ AllowedUserGroups: []model.UserGroup{
userGroups[0], userGroups[0],
@@ -458,7 +458,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
{ {
Key: jwkutils.PrivateKeyDBKey, Key: jwkutils.PrivateKeyDBKey,
// {"alg":"RS256","d":"mvMDWSdPPvcum0c0iEHE2gbqtV2NKMmLwrl9E6K7g8lTV95SePLnW_bwyMPV7EGp7PQk3l17I5XRhFjze7GqTnFIOgKzMianPs7jv2ELtBMGK0xOPATgu1iGb70xZ6vcvuEfRyY3dJ0zr4jpUdVuXwKmx9rK4IdZn2dFCKfvSuspqIpz11RhF1ALrqDLkxGVv7ZwNh0_VhJZU9hcjG5l6xc7rQEKpPRkZp0IdjkGS8Z0FskoVaiRIWAbZuiVFB9WCW8k1czC4HQTPLpII01bUQx2ludbm0UlXRgVU9ptUUbU7GAImQqTOW8LfPGklEvcgzlIlR_oqw4P9yBxLi-yMQ","dp":"pvNCSnnhbo8Igw9psPR-DicxFnkXlu_ix4gpy6efTrxA-z1VDFDioJ814vKQNioYDzpyAP1gfMPhRkvG_q0hRZsJah3Sb9dfA-WkhSWY7lURQP4yIBTMU0PF_rEATuS7lRciYk1SOx5fqXZd3m_LP0vpBC4Ujlq6NAq6CIjCnms","dq":"TtUVGCCkPNgfOLmkYXu7dxxUCV5kB01-xAEK2OY0n0pG8vfDophH4_D_ZC7nvJ8J9uDhs_3JStexq1lIvaWtG99RNTChIEDzpdn6GH9yaVcb_eB4uJjrNm64FhF8PGCCwxA-xMCZMaARKwhMB2_IOMkxUbWboL3gnhJ2rDO_QO0","e":"AQAB","kid":"8uHDw3M6rf8","kty":"RSA","n":"yaeEL0VKoPBXIAaWXsUgmu05lAvEIIdJn0FX9lHh4JE5UY9B83C5sCNdhs9iSWzpeP11EVjWp8i3Yv2CF7c7u50BXnVBGtxpZpFC-585UXacoJ0chUmarL9GRFJcM1nPHBTFu68aRrn1rIKNHUkNaaxFo0NFGl_4EDDTO8HwawTjwkPoQlRzeByhlvGPVvwgB3Fn93B8QJ_cZhXKxJvjjrC_8Pk76heC_ntEMru71Ix77BoC3j2TuyiN7m9RNBW8BU5q6lKoIdvIeZfTFLzi37iufyfvMrJTixp9zhNB1NxlLCeOZl2MXegtiGqd2H3cbAyqoOiv9ihUWTfXj7SxJw","p":"_Yylc9e07CKdqNRD2EosMC2mrhrEa9j5oY_l00Qyy4-jmCA59Q9viyqvveRo0U7cRvFA5BWgWN6GGLh1DG3X-QBqVr0dnk3uzbobb55RYUXyPLuBZI2q6w2oasbiDwPdY7KpkVv_H-bpITQlyDvO8hhucA6rUV7F6KTQVz8M3Ms","q":"y5p3hch-7jJ21TkAhp_Vk1fLCAuD4tbErwQs2of9ja8sB4iJOs5Wn6HD3P7Mc8Plye7qaLHvzc8I5g0tPKWvC0DPd_FLPXiWwMVAzee3NUX_oGeJNOQp11y1w_KqdO9qZqHSEPZ3NcFL_SZMFgggxhM1uzRiPzsVN0lnD_6prZU","qi":"2Grt6uXHm61ji3xSdkBWNtUnj19vS1-7rFJp5SoYztVQVThf_W52BAiXKBdYZDRVoItC_VS2NvAOjeJjhYO_xQ_q3hK7MdtuXfEPpLnyXKkmWo3lrJ26wbeF6l05LexCkI7ShsOuSt-dsyaTJTszuKDIA6YOfWvfo3aVZmlWRaI","use":"sig"} // {"alg":"RS256","d":"mvMDWSdPPvcum0c0iEHE2gbqtV2NKMmLwrl9E6K7g8lTV95SePLnW_bwyMPV7EGp7PQk3l17I5XRhFjze7GqTnFIOgKzMianPs7jv2ELtBMGK0xOPATgu1iGb70xZ6vcvuEfRyY3dJ0zr4jpUdVuXwKmx9rK4IdZn2dFCKfvSuspqIpz11RhF1ALrqDLkxGVv7ZwNh0_VhJZU9hcjG5l6xc7rQEKpPRkZp0IdjkGS8Z0FskoVaiRIWAbZuiVFB9WCW8k1czC4HQTPLpII01bUQx2ludbm0UlXRgVU9ptUUbU7GAImQqTOW8LfPGklEvcgzlIlR_oqw4P9yBxLi-yMQ","dp":"pvNCSnnhbo8Igw9psPR-DicxFnkXlu_ix4gpy6efTrxA-z1VDFDioJ814vKQNioYDzpyAP1gfMPhRkvG_q0hRZsJah3Sb9dfA-WkhSWY7lURQP4yIBTMU0PF_rEATuS7lRciYk1SOx5fqXZd3m_LP0vpBC4Ujlq6NAq6CIjCnms","dq":"TtUVGCCkPNgfOLmkYXu7dxxUCV5kB01-xAEK2OY0n0pG8vfDophH4_D_ZC7nvJ8J9uDhs_3JStexq1lIvaWtG99RNTChIEDzpdn6GH9yaVcb_eB4uJjrNm64FhF8PGCCwxA-xMCZMaARKwhMB2_IOMkxUbWboL3gnhJ2rDO_QO0","e":"AQAB","kid":"8uHDw3M6rf8","kty":"RSA","n":"yaeEL0VKoPBXIAaWXsUgmu05lAvEIIdJn0FX9lHh4JE5UY9B83C5sCNdhs9iSWzpeP11EVjWp8i3Yv2CF7c7u50BXnVBGtxpZpFC-585UXacoJ0chUmarL9GRFJcM1nPHBTFu68aRrn1rIKNHUkNaaxFo0NFGl_4EDDTO8HwawTjwkPoQlRzeByhlvGPVvwgB3Fn93B8QJ_cZhXKxJvjjrC_8Pk76heC_ntEMru71Ix77BoC3j2TuyiN7m9RNBW8BU5q6lKoIdvIeZfTFLzi37iufyfvMrJTixp9zhNB1NxlLCeOZl2MXegtiGqd2H3cbAyqoOiv9ihUWTfXj7SxJw","p":"_Yylc9e07CKdqNRD2EosMC2mrhrEa9j5oY_l00Qyy4-jmCA59Q9viyqvveRo0U7cRvFA5BWgWN6GGLh1DG3X-QBqVr0dnk3uzbobb55RYUXyPLuBZI2q6w2oasbiDwPdY7KpkVv_H-bpITQlyDvO8hhucA6rUV7F6KTQVz8M3Ms","q":"y5p3hch-7jJ21TkAhp_Vk1fLCAuD4tbErwQs2of9ja8sB4iJOs5Wn6HD3P7Mc8Plye7qaLHvzc8I5g0tPKWvC0DPd_FLPXiWwMVAzee3NUX_oGeJNOQp11y1w_KqdO9qZqHSEPZ3NcFL_SZMFgggxhM1uzRiPzsVN0lnD_6prZU","qi":"2Grt6uXHm61ji3xSdkBWNtUnj19vS1-7rFJp5SoYztVQVThf_W52BAiXKBdYZDRVoItC_VS2NvAOjeJjhYO_xQ_q3hK7MdtuXfEPpLnyXKkmWo3lrJ26wbeF6l05LexCkI7ShsOuSt-dsyaTJTszuKDIA6YOfWvfo3aVZmlWRaI","use":"sig"}
Value: utils.Ptr("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"), Value: new("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"),
}, },
} }

View File

@@ -129,39 +129,39 @@ func (s *ExportService) getScanValuesForTable(cols []string, types utils.DBSchem
case "boolean", "bool": case "boolean", "bool":
var x bool var x bool
if types[col].Nullable { if types[col].Nullable {
res[i] = utils.Ptr(utils.Ptr(x)) res[i] = new(new(x))
} else { } else {
res[i] = utils.Ptr(x) res[i] = new(x)
} }
case "blob", "bytea", "jsonb": case "blob", "bytea", "jsonb":
// Treat jsonb columns as binary too // Treat jsonb columns as binary too
var x []byte var x []byte
if types[col].Nullable { if types[col].Nullable {
res[i] = utils.Ptr(utils.Ptr(x)) res[i] = new(new(x))
} else { } else {
res[i] = utils.Ptr(x) res[i] = new(x)
} }
case "timestamp", "timestamptz", "timestamp with time zone", "datetime": case "timestamp", "timestamptz", "timestamp with time zone", "datetime":
var x datatype.DateTime var x datatype.DateTime
if types[col].Nullable { if types[col].Nullable {
res[i] = utils.Ptr(utils.Ptr(x)) res[i] = new(new(x))
} else { } else {
res[i] = utils.Ptr(x) res[i] = new(x)
} }
case "integer", "int", "bigint": case "integer", "int", "bigint":
var x int64 var x int64
if types[col].Nullable { if types[col].Nullable {
res[i] = utils.Ptr(utils.Ptr(x)) res[i] = new(new(x))
} else { } else {
res[i] = utils.Ptr(x) res[i] = new(x)
} }
default: default:
// Treat everything else as a string (including the "numeric" type) // Treat everything else as a string (including the "numeric" type)
var x string var x string
if types[col].Nullable { if types[col].Nullable {
res[i] = utils.Ptr(utils.Ptr(x)) res[i] = new(new(x))
} else { } else {
res[i] = utils.Ptr(x) res[i] = new(x)
} }
} }
} }

View File

@@ -20,7 +20,6 @@ import (
"github.com/pocket-id/pocket-id/backend/internal/common" "github.com/pocket-id/pocket-id/backend/internal/common"
"github.com/pocket-id/pocket-id/backend/internal/model" "github.com/pocket-id/pocket-id/backend/internal/model"
"github.com/pocket-id/pocket-id/backend/internal/utils"
jwkutils "github.com/pocket-id/pocket-id/backend/internal/utils/jwk" jwkutils "github.com/pocket-id/pocket-id/backend/internal/utils/jwk"
testutils "github.com/pocket-id/pocket-id/backend/internal/utils/testing" testutils "github.com/pocket-id/pocket-id/backend/internal/utils/testing"
) )
@@ -305,7 +304,7 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
user := model.User{ user := model.User{
Base: model.Base{ID: "user123"}, Base: model.Base{ID: "user123"},
Email: utils.Ptr("user@example.com"), Email: new("user@example.com"),
IsAdmin: false, IsAdmin: false,
} }
@@ -341,7 +340,7 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
adminUser := model.User{ adminUser := model.User{
Base: model.Base{ID: "admin123"}, Base: model.Base{ID: "admin123"},
Email: utils.Ptr("admin@example.com"), Email: new("admin@example.com"),
IsAdmin: true, IsAdmin: true,
} }
@@ -393,7 +392,7 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
user := model.User{ user := model.User{
Base: model.Base{ID: "eddsauser123"}, Base: model.Base{ID: "eddsauser123"},
Email: utils.Ptr("eddsauser@example.com"), Email: new("eddsauser@example.com"),
IsAdmin: true, IsAdmin: true,
} }
@@ -430,7 +429,7 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
user := model.User{ user := model.User{
Base: model.Base{ID: "ecdsauser123"}, Base: model.Base{ID: "ecdsauser123"},
Email: utils.Ptr("ecdsauser@example.com"), Email: new("ecdsauser@example.com"),
IsAdmin: true, IsAdmin: true,
} }
@@ -467,7 +466,7 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
user := model.User{ user := model.User{
Base: model.Base{ID: "rsauser123"}, Base: model.Base{ID: "rsauser123"},
Email: utils.Ptr("rsauser@example.com"), Email: new("rsauser@example.com"),
IsAdmin: true, IsAdmin: true,
} }
@@ -502,7 +501,7 @@ func TestGenerateVerifyIdToken(t *testing.T) {
t.Run("generates and verifies ID token with standard claims", func(t *testing.T) { t.Run("generates and verifies ID token with standard claims", func(t *testing.T) {
service, _, _ := setupJwtService(t, mockConfig) service, _, _ := setupJwtService(t, mockConfig)
userClaims := map[string]interface{}{ userClaims := map[string]any{
"sub": "user123", "sub": "user123",
"name": "Test User", "name": "Test User",
"email": "user@example.com", "email": "user@example.com",
@@ -539,7 +538,7 @@ func TestGenerateVerifyIdToken(t *testing.T) {
t.Run("can accept expired tokens if told so", func(t *testing.T) { t.Run("can accept expired tokens if told so", func(t *testing.T) {
service, _, _ := setupJwtService(t, mockConfig) service, _, _ := setupJwtService(t, mockConfig)
userClaims := map[string]interface{}{ userClaims := map[string]any{
"sub": "user123", "sub": "user123",
"name": "Test User", "name": "Test User",
"email": "user@example.com", "email": "user@example.com",
@@ -587,7 +586,7 @@ func TestGenerateVerifyIdToken(t *testing.T) {
t.Run("generates and verifies ID token with nonce", func(t *testing.T) { t.Run("generates and verifies ID token with nonce", func(t *testing.T) {
service, _, _ := setupJwtService(t, mockConfig) service, _, _ := setupJwtService(t, mockConfig)
userClaims := map[string]interface{}{ userClaims := map[string]any{
"sub": "user456", "sub": "user456",
"name": "Another User", "name": "Another User",
} }
@@ -612,7 +611,7 @@ func TestGenerateVerifyIdToken(t *testing.T) {
t.Run("fails verification with incorrect issuer", func(t *testing.T) { t.Run("fails verification with incorrect issuer", func(t *testing.T) {
service, _, _ := setupJwtService(t, mockConfig) service, _, _ := setupJwtService(t, mockConfig)
userClaims := map[string]interface{}{ userClaims := map[string]any{
"sub": "user789", "sub": "user789",
} }
tokenString, err := service.GenerateIDToken(userClaims, "client-789", "") tokenString, err := service.GenerateIDToken(userClaims, "client-789", "")
@@ -634,7 +633,7 @@ func TestGenerateVerifyIdToken(t *testing.T) {
require.True(t, ok) require.True(t, ok)
assert.Equal(t, origKeyID, loadedKeyID, "Loaded key should have the same ID as the original") assert.Equal(t, origKeyID, loadedKeyID, "Loaded key should have the same ID as the original")
userClaims := map[string]interface{}{ userClaims := map[string]any{
"sub": "eddsauser456", "sub": "eddsauser456",
"name": "EdDSA User", "name": "EdDSA User",
"email": "eddsauser@example.com", "email": "eddsauser@example.com",
@@ -672,7 +671,7 @@ func TestGenerateVerifyIdToken(t *testing.T) {
require.True(t, ok) require.True(t, ok)
assert.Equal(t, origKeyID, loadedKeyID, "Loaded key should have the same ID as the original") assert.Equal(t, origKeyID, loadedKeyID, "Loaded key should have the same ID as the original")
userClaims := map[string]interface{}{ userClaims := map[string]any{
"sub": "ecdsauser456", "sub": "ecdsauser456",
"email": "ecdsauser@example.com", "email": "ecdsauser@example.com",
} }
@@ -709,7 +708,7 @@ func TestGenerateVerifyIdToken(t *testing.T) {
require.True(t, ok) require.True(t, ok)
assert.Equal(t, origKeyID, loadedKeyID, "Loaded key should have the same ID as the original") assert.Equal(t, origKeyID, loadedKeyID, "Loaded key should have the same ID as the original")
userClaims := map[string]interface{}{ userClaims := map[string]any{
"sub": "rsauser456", "sub": "rsauser456",
"name": "RSA User", "name": "RSA User",
"email": "rsauser@example.com", "email": "rsauser@example.com",
@@ -742,7 +741,7 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) {
user := model.User{ user := model.User{
Base: model.Base{ID: "user123"}, Base: model.Base{ID: "user123"},
Email: utils.Ptr("user@example.com"), Email: new("user@example.com"),
} }
const clientID = "test-client-123" const clientID = "test-client-123"
@@ -825,7 +824,7 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) {
user := model.User{ user := model.User{
Base: model.Base{ID: "eddsauser789"}, Base: model.Base{ID: "eddsauser789"},
Email: utils.Ptr("eddsaoauth@example.com"), Email: new("eddsaoauth@example.com"),
} }
const clientID = "eddsa-oauth-client" const clientID = "eddsa-oauth-client"
@@ -862,7 +861,7 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) {
user := model.User{ user := model.User{
Base: model.Base{ID: "ecdsauser789"}, Base: model.Base{ID: "ecdsauser789"},
Email: utils.Ptr("ecdsaoauth@example.com"), Email: new("ecdsaoauth@example.com"),
} }
const clientID = "ecdsa-oauth-client" const clientID = "ecdsa-oauth-client"
@@ -899,7 +898,7 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) {
user := model.User{ user := model.User{
Base: model.Base{ID: "rsauser789"}, Base: model.Base{ID: "rsauser789"},
Email: utils.Ptr("rsaoauth@example.com"), Email: new("rsaoauth@example.com"),
} }
const clientID = "rsa-oauth-client" const clientID = "rsa-oauth-client"

View File

@@ -529,7 +529,7 @@ func getDNProperty(property string, str string) string {
// First we split at the comma // First we split at the comma
property = strings.ToLower(property) property = strings.ToLower(property)
l := len(property) + 1 l := len(property) + 1
for _, v := range strings.Split(str, ",") { for v := range strings.SplitSeq(str, ",") {
v = strings.TrimSpace(v) v = strings.TrimSpace(v)
if len(v) > l && strings.ToLower(v)[0:l] == property+"=" { if len(v) > l && strings.ToLower(v)[0:l] == property+"=" {
return v[l:] return v[l:]

View File

@@ -731,7 +731,7 @@ func (s *OidcService) CreateClient(ctx context.Context, input dto.OidcClientCrea
Base: model.Base{ Base: model.Base{
ID: input.ID, ID: input.ID,
}, },
CreatedByID: utils.Ptr(userID), CreatedByID: new(userID),
} }
updateOIDCClientModelFromDto(&client, &input.OidcClientUpdateDto) updateOIDCClientModelFromDto(&client, &input.OidcClientUpdateDto)

View File

@@ -11,6 +11,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"path" "path"
"slices"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -243,7 +244,7 @@ func (s *ScimService) SyncServiceProvider(ctx context.Context, serviceProviderID
return errors.Join(errs...) return errors.Join(errs...)
} }
provider.LastSyncedAt = utils.Ptr(datatype.DateTime(time.Now())) provider.LastSyncedAt = new(datatype.DateTime(time.Now()))
if err := s.db.WithContext(ctx).Save(&provider).Error; err != nil { if err := s.db.WithContext(ctx).Save(&provider).Error; err != nil {
return err return err
} }
@@ -788,10 +789,8 @@ func ensureScimStatus(
resp *http.Response, resp *http.Response,
provider model.ScimServiceProvider, provider model.ScimServiceProvider,
allowedStatuses ...int) error { allowedStatuses ...int) error {
for _, status := range allowedStatuses { if slices.Contains(allowedStatuses, resp.StatusCode) {
if resp.StatusCode == status { return nil
return nil
}
} }
body := readScimErrorBody(resp.Body) body := readScimErrorBody(resp.Body)

View File

@@ -162,7 +162,7 @@ func (s *UserGroupService) updateInternal(ctx context.Context, id string, input
group.Name = input.Name group.Name = input.Name
group.FriendlyName = input.FriendlyName group.FriendlyName = input.FriendlyName
group.UpdatedAt = utils.Ptr(datatype.DateTime(time.Now())) group.UpdatedAt = new(datatype.DateTime(time.Now()))
err = tx. err = tx.
WithContext(ctx). WithContext(ctx).
@@ -228,7 +228,7 @@ func (s *UserGroupService) updateUsersInternal(ctx context.Context, id string, u
} }
// Save the updated group // Save the updated group
group.UpdatedAt = utils.Ptr(datatype.DateTime(time.Now())) group.UpdatedAt = new(datatype.DateTime(time.Now()))
err = tx. err = tx.
WithContext(ctx). WithContext(ctx).

View File

@@ -435,7 +435,7 @@ func (s *UserService) updateUserInternal(ctx context.Context, userID string, upd
} }
} }
user.UpdatedAt = utils.Ptr(datatype.DateTime(time.Now())) user.UpdatedAt = new(datatype.DateTime(time.Now()))
err = tx. err = tx.
WithContext(ctx). WithContext(ctx).
@@ -501,9 +501,9 @@ func (s *UserService) UpdateUserGroups(ctx context.Context, id string, userGroup
} }
// Update the UpdatedAt field for all affected groups // Update the UpdatedAt field for all affected groups
now := time.Now() now := datatype.DateTime(time.Now())
for _, group := range groups { for _, group := range groups {
group.UpdatedAt = utils.Ptr(datatype.DateTime(now)) group.UpdatedAt = &now
err = tx.WithContext(ctx).Save(&group).Error err = tx.WithContext(ctx).Save(&group).Error
if err != nil { if err != nil {
return model.User{}, err return model.User{}, err
@@ -636,7 +636,7 @@ func (s *UserService) VerifyEmail(ctx context.Context, userID string, token stri
} }
user.EmailVerified = true user.EmailVerified = true
user.UpdatedAt = utils.Ptr(datatype.DateTime(time.Now())) user.UpdatedAt = new(datatype.DateTime(time.Now()))
err = tx.WithContext(ctx).Save(&user).Error err = tx.WithContext(ctx).Save(&user).Error
if err != nil { if err != nil {
return err return err

View File

@@ -414,10 +414,10 @@ func TestGetCallbackURLFromList_LoopbackSpecialHandling(t *testing.T) {
expectMatch: true, expectMatch: true,
}, },
{ {
name: "IPv6 loopback without brackets in input", name: "IPv6 loopback with wildcard path",
urls: []string{"http://[::1]/callback"}, urls: []string{"http://[::1]/auth/*"},
inputCallbackURL: "http://::1:8080/callback", inputCallbackURL: "http://[::1]:8080/auth/callback",
expectedURL: "http://::1:8080/callback", expectedURL: "http://[::1]:8080/auth/callback",
expectMatch: true, expectMatch: true,
}, },
{ {
@@ -462,6 +462,13 @@ func TestGetCallbackURLFromList_LoopbackSpecialHandling(t *testing.T) {
expectedURL: "http://127.0.0.1:8080/callback", expectedURL: "http://127.0.0.1:8080/callback",
expectMatch: true, expectMatch: true,
}, },
{
name: "wildcard matches IPv6 loopback",
urls: []string{"*"},
inputCallbackURL: "http://[::1]:8080/callback",
expectedURL: "http://[::1]:8080/callback",
expectMatch: true,
},
} }
for _, tt := range tests { for _, tt := range tests {

View File

@@ -87,9 +87,9 @@ func listContainsIP(ipNets []*net.IPNet, ip net.IP) bool {
func loadLocalIPv6Ranges() { func loadLocalIPv6Ranges() {
localIPv6Ranges = nil localIPv6Ranges = nil
ranges := strings.Split(common.EnvConfig.LocalIPv6Ranges, ",") ranges := strings.SplitSeq(common.EnvConfig.LocalIPv6Ranges, ",")
for _, rangeStr := range ranges { for rangeStr := range ranges {
rangeStr = strings.TrimSpace(rangeStr) rangeStr = strings.TrimSpace(rangeStr)
if rangeStr == "" { if rangeStr == "" {
continue continue

View File

@@ -42,7 +42,7 @@ func (d *JSONDuration) UnmarshalJSON(b []byte) error {
} }
} }
func UnmarshalJSONFromDatabase(data interface{}, value any) error { func UnmarshalJSONFromDatabase(data any, value any) error {
switch v := value.(type) { switch v := value.(type) {
case []byte: case []byte:
return json.Unmarshal(v, data) return json.Unmarshal(v, data)

View File

@@ -43,7 +43,7 @@ func ParseListRequestOptions(ctx *gin.Context) (listRequestOptions ListRequestOp
return listRequestOptions return listRequestOptions
} }
func PaginateFilterAndSort(params ListRequestOptions, query *gorm.DB, result interface{}) (PaginationResponse, error) { func PaginateFilterAndSort(params ListRequestOptions, query *gorm.DB, result any) (PaginationResponse, error) {
meta := extractModelMetadata(result) meta := extractModelMetadata(result)
query = applyFilters(params.Filters, query, meta) query = applyFilters(params.Filters, query, meta)
@@ -52,7 +52,7 @@ func PaginateFilterAndSort(params ListRequestOptions, query *gorm.DB, result int
return Paginate(params.Pagination.Page, params.Pagination.Limit, query, result) return Paginate(params.Pagination.Page, params.Pagination.Limit, query, result)
} }
func Paginate(page int, pageSize int, query *gorm.DB, result interface{}) (PaginationResponse, error) { func Paginate(page int, pageSize int, query *gorm.DB, result any) (PaginationResponse, error) {
if page < 1 { if page < 1 {
page = 1 page = 1
} }
@@ -117,8 +117,8 @@ func parseNestedFilters(ctx *gin.Context) map[string][]any {
// Keys can be "filters[field]" or "filters[field][0]" // Keys can be "filters[field]" or "filters[field][0]"
raw := strings.TrimPrefix(key, "filters[") raw := strings.TrimPrefix(key, "filters[")
// Take everything up to the first closing bracket // Take everything up to the first closing bracket
if idx := strings.IndexByte(raw, ']'); idx != -1 { if before, _, ok := strings.Cut(raw, "]"); ok {
field := raw[:idx] field := before
for _, v := range values { for _, v := range values {
result[field] = append(result[field], ConvertStringToType(v)) result[field] = append(result[field], ConvertStringToType(v))
} }
@@ -165,12 +165,12 @@ func applySorting(sortColumn string, sortDirection string, query *gorm.DB, meta
} }
// extractModelMetadata extracts FieldMeta from the model struct using reflection // extractModelMetadata extracts FieldMeta from the model struct using reflection
func extractModelMetadata(model interface{}) map[string]FieldMeta { func extractModelMetadata(model any) map[string]FieldMeta {
meta := make(map[string]FieldMeta) meta := make(map[string]FieldMeta)
// Unwrap pointers and slices to get the element struct type // Unwrap pointers and slices to get the element struct type
t := reflect.TypeOf(model) t := reflect.TypeOf(model)
for t.Kind() == reflect.Ptr || t.Kind() == reflect.Slice { for t.Kind() == reflect.Pointer || t.Kind() == reflect.Slice {
t = t.Elem() t = t.Elem()
if t == nil { if t == nil {
return meta return meta
@@ -180,8 +180,7 @@ func extractModelMetadata(model interface{}) map[string]FieldMeta {
// recursive parser that merges fields from embedded structs // recursive parser that merges fields from embedded structs
var parseStruct func(reflect.Type) var parseStruct func(reflect.Type)
parseStruct = func(st reflect.Type) { parseStruct = func(st reflect.Type) {
for i := 0; i < st.NumField(); i++ { for field := range st.Fields() {
field := st.Field(i)
ft := field.Type ft := field.Type
// If the field is an embedded/anonymous struct, recurse into it // If the field is an embedded/anonymous struct, recurse into it

View File

@@ -1,10 +1,5 @@
package utils package utils
// Ptr returns a pointer to the given value.
func Ptr[T any](v T) *T {
return &v
}
// PtrOrNil returns a pointer to v if v is not the zero value of its type, // PtrOrNil returns a pointer to v if v is not the zero value of its type,
// otherwise it returns nil. // otherwise it returns nil.
func PtrOrNil[T comparable](v T) *T { func PtrOrNil[T comparable](v T) *T {

View File

@@ -1,85 +0,0 @@
package utils
import (
"context"
"errors"
"fmt"
"log/slog"
"slices"
)
// This file contains code adapted from https://github.com/samber/slog-multi
// Source: https://github.com/samber/slog-multi/blob/ced84707f45ec9848138349ed58de178eedaa6f2/pipe.go
// Copyright (C) 2023 Samuel Berthe
// License: MIT (https://github.com/samber/slog-multi/blob/ced84707f45ec9848138349ed58de178eedaa6f2/LICENSE)
// LogFanoutHandler is a slog.Handler that sends logs to multiple destinations
type LogFanoutHandler []slog.Handler
// Implements slog.Handler
func (h LogFanoutHandler) Enabled(ctx context.Context, l slog.Level) bool {
for i := range h {
if h[i].Enabled(ctx, l) {
return true
}
}
return false
}
// Implements slog.Handler
func (h LogFanoutHandler) Handle(ctx context.Context, r slog.Record) error {
errs := make([]error, 0)
for i := range h {
if h[i].Enabled(ctx, r.Level) {
err := try(func() error {
return h[i].Handle(ctx, r.Clone())
})
if err != nil {
errs = append(errs, err)
}
}
}
return errors.Join(errs...)
}
// Implements slog.Handler
func (h LogFanoutHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
res := make(LogFanoutHandler, len(h))
for i, v := range h {
res[i] = v.WithAttrs(slices.Clone(attrs))
}
return res
}
// Implements slog.Handler
func (h LogFanoutHandler) WithGroup(name string) slog.Handler {
// https://cs.opensource.google/go/x/exp/+/46b07846:slog/handler.go;l=247
if name == "" {
return h
}
res := make(LogFanoutHandler, len(h))
for i, v := range h {
res[i] = v.WithGroup(name)
}
return res
}
func try(callback func() error) (err error) {
defer func() {
r := recover()
if r != nil {
if e, ok := r.(error); ok {
err = e
} else {
err = fmt.Errorf("unexpected error: %+v", r)
}
}
}()
err = callback()
return
}

View File

@@ -70,11 +70,6 @@ func GetHostnameFromURL(rawURL string) string {
return parsedURL.Hostname() return parsedURL.Hostname()
} }
// StringPointer creates a string pointer from a string value
func StringPointer(s string) *string {
return &s
}
func CapitalizeFirstLetter(str string) string { func CapitalizeFirstLetter(str string) string {
if str == "" { if str == "" {
return "" return ""

View File

@@ -4,7 +4,7 @@
ARG BUILD_TAGS="" ARG BUILD_TAGS=""
# Stage 1: Build Frontend # Stage 1: Build Frontend
FROM node:22-alpine AS frontend-builder FROM node:24-alpine AS frontend-builder
RUN corepack enable RUN corepack enable
WORKDIR /build WORKDIR /build
@@ -18,7 +18,7 @@ COPY ./frontend ./frontend/
RUN BUILD_OUTPUT_PATH=dist pnpm --filter pocket-id-frontend run build RUN BUILD_OUTPUT_PATH=dist pnpm --filter pocket-id-frontend run build
# Stage 2: Build Backend # Stage 2: Build Backend
FROM golang:1.25-alpine AS backend-builder FROM golang:1.26-alpine AS backend-builder
ARG BUILD_TAGS ARG BUILD_TAGS
WORKDIR /build WORKDIR /build
COPY ./backend/go.mod ./backend/go.sum ./ COPY ./backend/go.mod ./backend/go.sum ./

View File

@@ -86,6 +86,6 @@
{/if} {/if}
{/if} {/if}
{#if input?.error} {#if input?.error}
<Field.Error>{input.error}</Field.Error> <Field.Error class="text-start">{input.error}</Field.Error>
{/if} {/if}
</Field.Field> </Field.Field>

View File

@@ -12,5 +12,5 @@
"test": "pnpm --filter pocket-id-tests test", "test": "pnpm --filter pocket-id-tests test",
"format": "pnpm --filter pocket-id-frontend format" "format": "pnpm --filter pocket-id-frontend format"
}, },
"packageManager": "pnpm@10.27.0+sha512.72d699da16b1179c14ba9e64dc71c9a40988cbdc65c264cb0e489db7de917f20dcf4d64d8723625f2969ba52d4b7e2a1170682d9ac2a5dcaeaab732b7e16f04a" "packageManager": "pnpm@10.30.1+sha512.3590e550d5384caa39bd5c7c739f72270234b2f6059e13018f975c313b1eb9fefcc09714048765d4d9efe961382c312e624572c0420762bdc5d5940cdf9be73a"
} }