Compare commits

...

7 Commits

Author SHA1 Message Date
ItalyPaleAle
913115fc7e Remove more unused code 2026-02-22 14:08:18 -08:00
Kyle Mendell
4f515a0803 modernize code with go fix 2026-02-22 15:58:39 -06:00
Kyle Mendell
b8e42af446 tests 2026-02-22 15:44:24 -06:00
Kyle Mendell
a6b1141ada golangci-lint bump 2026-02-22 15:02:32 -06:00
Kyle Mendell
6efcf01899 chore(deps): upgrade to node 24 and go 1.26.0 2026-02-22 14:59:57 -06:00
Kyle Mendell
ae269371da feat: current version api endpoint (#1310) 2026-02-22 10:39:19 -08:00
James Ward
27caaf2cac feat: add JWT ID for generated tokens (#1322) 2026-02-22 16:23:14 +00:00
38 changed files with 156 additions and 221 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -42,7 +42,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: 22
node-version: 24
- name: Install dependencies
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:
- **feat** - is a new feature
- **doc** - documentation only changes
- **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:
- [Node.js](https://nodejs.org/en/download/) >= 22
- [Go](https://golang.org/doc/install) >= 1.25
- [Node.js](https://nodejs.org/en/download/) >= 24
- [Go](https://golang.org/doc/install) >= 1.26
- [Git](https://git-scm.com/downloads)
### 2. Setup

View File

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

View File

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

View File

@@ -85,7 +85,7 @@ func initRouter(db *gorm.DB, svc *services) (utils.Service, error) {
controller.NewAuditLogController(apiGroup, svc.auditLogService, authMiddleware)
controller.NewUserGroupController(apiGroup, authMiddleware, svc.userGroupService)
controller.NewCustomClaimController(apiGroup, authMiddleware, svc.customClaimService)
controller.NewVersionController(apiGroup, svc.versionService)
controller.NewVersionController(apiGroup, authMiddleware, svc.versionService)
controller.NewScimController(apiGroup, authMiddleware, svc.scimService)
controller.NewUserSignupController(apiGroup, authMiddleware, middleware.NewRateLimitMiddleware(), svc.userSignUpService, svc.appConfigService)

View File

@@ -106,7 +106,7 @@ func defaultConfig() EnvConfigSchema {
func parseEnvConfig() error {
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
},
}
@@ -184,8 +184,8 @@ func ValidateEnvConfig(config *EnvConfigSchema) error {
}
// Validate LOCAL_IPV6_RANGES
ranges := strings.Split(config.LocalIPv6Ranges, ",")
for _, rangeStr := range ranges {
ranges := strings.SplitSeq(config.LocalIPv6Ranges, ",")
for rangeStr := range ranges {
rangeStr = strings.TrimSpace(rangeStr)
if rangeStr == "" {
continue
@@ -235,9 +235,9 @@ func prepareEnvConfig(config *EnvConfigSchema) error {
fieldType := typ.Field(i)
optionsTag := fieldType.Tag.Get("options")
options := strings.Split(optionsTag, ",")
options := strings.SplitSeq(optionsTag, ",")
for _, option := range options {
for option := range options {
switch option {
case "toLower":
if field.Kind() == reflect.String {

View File

@@ -5,14 +5,17 @@ import (
"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"
)
// NewVersionController registers version-related routes.
func NewVersionController(group *gin.RouterGroup, versionService *service.VersionService) {
func NewVersionController(group *gin.RouterGroup, authMiddleware *middleware.AuthMiddleware, versionService *service.VersionService) {
vc := &VersionController{versionService: versionService}
group.GET("/version/latest", vc.getLatestVersionHandler)
group.GET("/version/current", authMiddleware.WithAdminNotRequired().Add(), vc.getCurrentVersionHandler)
}
type VersionController struct {
@@ -38,3 +41,16 @@ func (vc *VersionController) getLatestVersionHandler(c *gin.Context) {
"latestVersion": tag,
})
}
// getCurrentVersionHandler godoc
// @Summary Get current deployed version of Pocket ID
// @Tags Version
// @Produce json
// @Success 200 {object} map[string]string "Current version information"
// @Router /api/version/current [get]
func (vc *VersionController) getCurrentVersionHandler(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"currentVersion": common.Version,
})
}

View File

@@ -9,7 +9,6 @@ import (
"github.com/pocket-id/pocket-id/backend/internal/model"
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
"github.com/pocket-id/pocket-id/backend/internal/utils"
)
type sourceStruct struct {
@@ -60,11 +59,11 @@ type embeddedStruct struct {
func TestMapStruct(t *testing.T) {
src := sourceStruct{
AString: "abcd",
AStringPtr: utils.Ptr("xyz"),
AStringPtr: new("xyz"),
ABool: true,
ABoolPtr: utils.Ptr(false),
ABoolPtr: new(false),
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,
ASlice: []string{"a", "b", "c"},
AMap: map[string]int{
@@ -80,8 +79,8 @@ func TestMapStruct(t *testing.T) {
Bar: 111,
},
StringPtrToString: utils.Ptr("foobar"),
EmptyStringPtrToString: utils.Ptr(""),
StringPtrToString: new("foobar"),
EmptyStringPtrToString: new(""),
NilStringPtrToString: nil,
IntToInt64: 99,
AuditLogEventToString: model.AuditLogEventAccountCreated,
@@ -118,11 +117,11 @@ func TestMapStructList(t *testing.T) {
sources := []sourceStruct{
{
AString: "first",
AStringPtr: utils.Ptr("one"),
AStringPtr: new("one"),
ABool: true,
ABoolPtr: utils.Ptr(false),
ABoolPtr: new(false),
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"},
AMap: map[string]int{
"a": 1,
@@ -136,11 +135,11 @@ func TestMapStructList(t *testing.T) {
},
{
AString: "second",
AStringPtr: utils.Ptr("two"),
AStringPtr: new("two"),
ABool: false,
ABoolPtr: utils.Ptr(true),
ABoolPtr: new(true),
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"},
AMap: map[string]int{
"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.
func Normalize(obj any) {
v := reflect.ValueOf(obj)
if v.Kind() != reflect.Ptr || v.IsNil() {
if v.Kind() != reflect.Pointer || v.IsNil() {
return
}
v = v.Elem()
@@ -21,7 +21,7 @@ func Normalize(obj any) {
if v.Kind() == reflect.Slice {
for i := 0; i < v.Len(); 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())
} else if elem.Kind() == reflect.Struct && elem.CanAddr() {
Normalize(elem.Addr().Interface())

View File

@@ -61,14 +61,14 @@ type ScimResourceData struct {
ID string `json:"id,omitempty"`
ExternalID string `json:"externalId,omitempty"`
Schemas []string `json:"schemas"`
Meta ScimResourceMeta `json:"meta,omitempty"`
Meta ScimResourceMeta `json:"meta"`
}
type ScimResourceMeta struct {
Location string `json:"location,omitempty"`
ResourceType string `json:"resourceType,omitempty"`
Created time.Time `json:"created,omitempty"`
LastModified time.Time `json:"lastModified,omitempty"`
Created time.Time `json:"created"`
LastModified time.Time `json:"lastModified"`
Version string `json:"version,omitempty"`
}

View File

@@ -3,7 +3,6 @@ package dto
import (
"testing"
"github.com/pocket-id/pocket-id/backend/internal/utils"
"github.com/stretchr/testify/require"
)
@@ -17,7 +16,7 @@ func TestUserCreateDto_Validate(t *testing.T) {
name: "valid input",
input: UserCreateDto{
Username: "testuser",
Email: utils.Ptr("test@example.com"),
Email: new("test@example.com"),
FirstName: "John",
LastName: "Doe",
DisplayName: "John Doe",
@@ -27,7 +26,7 @@ func TestUserCreateDto_Validate(t *testing.T) {
{
name: "missing username",
input: UserCreateDto{
Email: utils.Ptr("test@example.com"),
Email: new("test@example.com"),
FirstName: "John",
LastName: "Doe",
DisplayName: "John Doe",
@@ -37,7 +36,7 @@ func TestUserCreateDto_Validate(t *testing.T) {
{
name: "missing display name",
input: UserCreateDto{
Email: utils.Ptr("test@example.com"),
Email: new("test@example.com"),
FirstName: "John",
LastName: "Doe",
},
@@ -47,7 +46,7 @@ func TestUserCreateDto_Validate(t *testing.T) {
name: "username contains invalid characters",
input: UserCreateDto{
Username: "test/ser",
Email: utils.Ptr("test@example.com"),
Email: new("test@example.com"),
FirstName: "John",
LastName: "Doe",
DisplayName: "John Doe",
@@ -58,7 +57,7 @@ func TestUserCreateDto_Validate(t *testing.T) {
name: "invalid email",
input: UserCreateDto{
Username: "testuser",
Email: utils.Ptr("not-an-email"),
Email: new("not-an-email"),
FirstName: "John",
LastName: "Doe",
DisplayName: "John Doe",
@@ -69,7 +68,7 @@ func TestUserCreateDto_Validate(t *testing.T) {
name: "first name too short",
input: UserCreateDto{
Username: "testuser",
Email: utils.Ptr("test@example.com"),
Email: new("test@example.com"),
FirstName: "",
LastName: "Doe",
DisplayName: "John Doe",
@@ -80,7 +79,7 @@ func TestUserCreateDto_Validate(t *testing.T) {
name: "last name too long",
input: UserCreateDto{
Username: "testuser",
Email: utils.Ptr("test@example.com"),
Email: new("test@example.com"),
FirstName: "John",
LastName: "abcdfghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz",
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
// 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) {
appConfigType := reflect.TypeOf(model.AppConfig{})
updateDtoType := reflect.TypeOf(dto.AppConfigUpdateDto{})
appConfigType := reflect.TypeFor[model.AppConfig]()
updateDtoType := reflect.TypeFor[dto.AppConfigUpdateDto]()
// Process AppConfig fields
appConfigFields := make(map[string]string)
for i := 0; i < appConfigType.NumField(); i++ {
field := appConfigType.Field(i)
for field := range appConfigType.Fields() {
if field.Tag.Get("key") == "" {
// Skip internal fields
continue
@@ -91,9 +90,7 @@ func TestAppConfigStructMatchesUpdateDto(t *testing.T) {
// Process AppConfigUpdateDto fields
dtoFields := make(map[string]string)
for i := 0; i < updateDtoType.NumField(); i++ {
field := updateDtoType.Field(i)
for field := range updateDtoType.Fields() {
// Extract the json name from the tag (takes the part before any binding constraints)
jsonTag := field.Tag.Get("json")
jsonName, _, _ := strings.Cut(jsonTag, ",")

View File

@@ -58,7 +58,7 @@ type ReauthenticationToken struct {
type AuthenticatorTransportList []protocol.AuthenticatorTransport //nolint:recvcheck
// 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)
}
@@ -69,7 +69,7 @@ func (atl AuthenticatorTransportList) Value() (driver.Value, error) {
type CredentialParameters []protocol.CredentialParameter //nolint:recvcheck
// 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)
}

View File

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

View File

@@ -186,8 +186,7 @@ func (s *AppConfigService) UpdateAppConfig(ctx context.Context, input dto.AppCon
rt := reflect.ValueOf(input).Type()
rv := reflect.ValueOf(input)
dbUpdate := make([]model.AppConfigVariable, 0, rt.NumField())
for i := range rt.NumField() {
field := rt.Field(i)
for field := range rt.Fields() {
value := rv.FieldByName(field.Name).String()
// 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",
},
Username: "tim",
Email: utils.Ptr("tim.cook@test.com"),
Email: new("tim.cook@test.com"),
EmailVerified: true,
FirstName: "Tim",
LastName: "Cook",
@@ -93,7 +93,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
ID: "1cd19686-f9a6-43f4-a41f-14a0bf5b4036",
},
Username: "craig",
Email: utils.Ptr("craig.federighi@test.com"),
Email: new("craig.federighi@test.com"),
EmailVerified: false,
FirstName: "Craig",
LastName: "Federighi",
@@ -105,7 +105,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
ID: "d9256384-98ad-49a7-bc58-99ad0b4dc23c",
},
Username: "eddy",
Email: utils.Ptr("eddy.cue@test.com"),
Email: new("eddy.cue@test.com"),
FirstName: "Eddy",
LastName: "Cue",
DisplayName: "Eddy Cue",
@@ -171,12 +171,12 @@ func (s *TestService) SeedDatabase(baseURL string) error {
ID: "3654a746-35d4-4321-ac61-0bdcff2b4055",
},
Name: "Nextcloud",
LaunchURL: utils.Ptr("https://nextcloud.local"),
LaunchURL: new("https://nextcloud.local"),
Secret: "$2a$10$9dypwot8nGuCjT6wQWWpJOckZfRprhe2EkwpKizxS/fpVHrOLEJHC", // w2mUeZISmEvIDMEDvpY0PnxQIpj1m3zY
CallbackURLs: model.UrlList{"http://nextcloud/auth/callback"},
LogoutCallbackURLs: model.UrlList{"http://nextcloud/auth/logout/callback"},
ImageType: utils.StringPointer("png"),
CreatedByID: utils.Ptr(users[0].ID),
ImageType: new("png"),
CreatedByID: new(users[0].ID),
},
{
Base: model.Base{
@@ -185,7 +185,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
Name: "Immich",
Secret: "$2a$10$Ak.FP8riD1ssy2AGGbG.gOpnp/rBpymd74j0nxNMtW0GG1Lb4gzxe", // PYjrE9u4v9GVqXKi52eur0eb2Ci4kc0x
CallbackURLs: model.UrlList{"http://immich/auth/callback"},
CreatedByID: utils.Ptr(users[1].ID),
CreatedByID: new(users[1].ID),
IsGroupRestricted: true,
AllowedUserGroups: []model.UserGroup{
userGroups[1],
@@ -200,7 +200,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
CallbackURLs: model.UrlList{"http://tailscale/auth/callback"},
LogoutCallbackURLs: model.UrlList{"http://tailscale/auth/logout/callback"},
IsGroupRestricted: true,
CreatedByID: utils.Ptr(users[0].ID),
CreatedByID: new(users[0].ID),
},
{
Base: model.Base{
@@ -209,7 +209,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
Name: "Federated",
Secret: "$2a$10$Ak.FP8riD1ssy2AGGbG.gOpnp/rBpymd74j0nxNMtW0GG1Lb4gzxe", // PYjrE9u4v9GVqXKi52eur0eb2Ci4kc0x
CallbackURLs: model.UrlList{"http://federated/auth/callback"},
CreatedByID: utils.Ptr(users[1].ID),
CreatedByID: new(users[1].ID),
AllowedUserGroups: []model.UserGroup{},
Credentials: model.OidcClientCredentials{
FederatedIdentities: []model.OidcClientFederatedIdentity{
@@ -229,7 +229,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
Name: "SCIM Client",
Secret: "$2a$10$h4wfa8gI7zavDAxwzSq1sOwYU4e8DwK1XZ8ZweNnY5KzlJ3Iz.qdK", // nQbiuMRG7FpdK2EnDd5MBivWQeKFXohn
CallbackURLs: model.UrlList{"http://scimclient/auth/callback"},
CreatedByID: utils.Ptr(users[0].ID),
CreatedByID: new(users[0].ID),
IsGroupRestricted: true,
AllowedUserGroups: []model.UserGroup{
userGroups[0],
@@ -458,7 +458,7 @@ func (s *TestService) SeedDatabase(baseURL string) error {
{
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"}
Value: utils.Ptr("7d/5hl7diJ2rnFL14hEAQf9tzpu29aqXQ8jpJ2iqqKUNFZpdOkEpud0CmRv4H3r8yyk2u/Gqqj9klSy58DJkYXGF5PAYgLyoBIb7L3JXWRbxg4cQ3QJCug13l2OTmpAKoVc+rmX8c3j3h1sNqyJ+7Ql5sS0jSeyiYgIsFNCdnK5alBDyvtcpe/QDpklmP4JCeVpvmf2rLGplk3g5UO5ydJ8UiDXxfDmi+gF6NKJvrGnnah8Ar3G/x88z+tTJtp0DIQFwxXwUM2XZqzEVGm8K2r0w5o9/Keh6bBBaiuH2C78ZOaijGV3DovhR+e9J0cYUYGwT42MZMx9fSWQ/lvWGGnf+Uq3MXJfjWSREfhkp8KTQwR9F7+dnVJWswOEk7jPR8I7hCWTMxJyvaFX3wgAXIVmhrgXZQQbYOqTt56IoqUl0xOJku8dA8opg2UcLlmmuOh6+hfkXKsiiS/H/9c1BVIGj1fCOiT6IePh4wKKSTbwJnPD5EKmdJpgTsUpjcDnXQKY4ReO0UpdRdKxwRDDLeQuG6j+ljGxR9GPudCU9Nmci6rFVI6n5LWYkQxBA1O73RpmXRZPDzntDfpXMEonkmSvOoxaCK2Id7CRKMdqvR0kEouwnhk5WSFtsfi3sA0pkXzPFxwZeWM8vFtbffZOZzXaOhxCOfcj1NClZohlZhyc4jvkxmrpY7PSaAzih0AmHI7y0LYFi6fZu/K4EheVa1+KF55nWZ8ARikHMWKAKkyExkTak7xyN884TDmzURRaPlQg4jzQte5WMNjAG/hlHibdMBNvgwiYd49ZxteJ8ABdbiXVRl+2JGbdjl2ubpQZwOn7bJKlqO56bIwsZ+e4+pXsuOGdBahkHrUjtMEmH3DZbGc6CJLbcmdhdpApLQRRcLAazxJhzAwJ47FRYsHsj57LnYNvmcKdIxw8rxCdLUuzz95uw0T3ankEO5J9sjem+HMEuKdwXK1UcuOn2rjR8Sd/BuvQmeso27dFbPXqXYNS90Ml45YyTvcKSiopD181oZR703TFUSpR7dsiqROMr+p/2jN9h6a8WbQ8xpksyclaQByY/M77AssbXnG6wfhRsntNIINCZLbBnjXOyz6ZHIC5K4tSTdcnWaiYPeRPQmnw9UUvHAcNU2yMWsy0eU377yDS0WstTxOdQutTdkczl8kv5Lo26JiEK7mSIuRK19ffF9Zz8FG8+eKv5zdyIPjyQRDYBysUoDv5huKe2eoxJu/MWS2Pql/ZtUGeD6Ozm3mCvh0vQ9ceagBkY6Ocm3du0ziAKP29Ri0mjg4DizVorbLzsh+EQH/s2Pi9MnjUZDlEmuLl2Xfp7/w4j/8u0N0tVR70VDFuGdKpTjFY3vS8EJrPtyMTM51x1D9rb8gIql8aR/rJw4YF+huxg1mv5n6+tGVqg5msbPmF12eJijP4lkmaRwIpLW5pJTtaDkUj7uOeu1mm4k+Dt5nh0/0jPHzrv6bcTCcbV7UjMHDoTXXqEpFAAJ66rHR7zdAJu+YKsnTIZyLmOpcowq7LL8G9qTvV0OSpyQWUIavRSgbDHFqEqRs+JU94jAzkq8nCY5MTd9m5sIv9InfdT3k+pwpsE/FKge8nghFLtbUrafGkzTky8SE2druvVcIvbfXMfLIKRUYjJgnWc0gQzF5J6pzXM7D2r/RG6JDzASqjlbURq6v9bhNerlOVdMujWKEEVcKWIzlbt4RkihRjM8AUqIZQOyicGQ+4yfIjAHw5viuABONYs3OIWULnFqJxdvS9rNKhfxSjIq9cfqyzevq2xrRoMXEonobh6M3bD2Vang8OAeVeD1OXWPERi4pepCYFS9RJ/Xa/UWxptsqSNuGcb3fAzQSmLpXLGdWRoKXvSe7EYgc0bGcLOjSTu5RURKo+EF9i4KT9EJauf6VXw5dTf/CCIJRXE1bWzXhSCFYntohYhX2ldOCDYpi/jFBC6Vtkw0ud3/xq8Nmhd5gUk+SpngByCZH3Pm3H+jvlbMpiqkDkm1v74hDX13Xhrcw2eWyuqKBVoRCCniUvwpYNbGvBfjC6Hcizv0Aybciwj+4nybt5EPoEUm6S6Gs7fG7QpPdvrzpAxX70MlmdkF/gwyuhbEeJhLK+WL7qAsN5CvHPzVbsIf90x+nGTtMJPgpxVr0tJMj+vprXV4WxutfARBiOnqe58MhA857sd+MzKBgKnoLOBRTiC3qc/0/ULwbG2HCCD7nmwzz7M4nUuMvo8rgS7z0BF68OClT8X3JwSXbL5Wg=="),
Value: new("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"),
},
}

View File

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

View File

@@ -7,6 +7,7 @@ import (
"fmt"
"time"
"github.com/google/uuid"
"github.com/lestrrat-go/jwx/v3/jwa"
"github.com/lestrrat-go/jwx/v3/jwk"
"github.com/lestrrat-go/jwx/v3/jwt"
@@ -193,6 +194,7 @@ func (s *JwtService) GenerateAccessToken(user model.User) (string, error) {
Expiration(now.Add(s.appConfigService.GetDbConfig().SessionDuration.AsDurationMinutes())).
IssuedAt(now).
Issuer(s.envConfig.AppURL).
JwtID(uuid.New().String()).
Build()
if err != nil {
return "", fmt.Errorf("failed to build token: %w", err)
@@ -247,6 +249,7 @@ func (s *JwtService) BuildIDToken(userClaims map[string]any, clientID string, no
Expiration(now.Add(1 * time.Hour)).
IssuedAt(now).
Issuer(s.envConfig.AppURL).
JwtID(uuid.New().String()).
Build()
if err != nil {
return nil, fmt.Errorf("failed to build token: %w", err)
@@ -336,6 +339,7 @@ func (s *JwtService) BuildOAuthAccessToken(user model.User, clientID string) (jw
Expiration(now.Add(1 * time.Hour)).
IssuedAt(now).
Issuer(s.envConfig.AppURL).
JwtID(uuid.New().String()).
Build()
if err != nil {
return nil, fmt.Errorf("failed to build token: %w", err)

View File

@@ -20,13 +20,14 @@ import (
"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/utils"
jwkutils "github.com/pocket-id/pocket-id/backend/internal/utils/jwk"
testutils "github.com/pocket-id/pocket-id/backend/internal/utils/testing"
)
const testEncryptionKey = "0123456789abcdef0123456789abcdef"
const uuidRegexPattern = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
func newTestEnvConfig() *common.EnvConfigSchema {
return &common.EnvConfigSchema{
AppURL: "https://test.example.com",
@@ -303,7 +304,7 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
user := model.User{
Base: model.Base{ID: "user123"},
Email: utils.Ptr("user@example.com"),
Email: new("user@example.com"),
IsAdmin: false,
}
@@ -323,6 +324,9 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
audience, ok := claims.Audience()
_ = assert.True(t, ok, "Audience not found in token") &&
assert.Equal(t, []string{service.envConfig.AppURL}, audience, "Audience should contain the app URL")
jwtID, ok := claims.JwtID()
_ = assert.True(t, ok, "JWT ID not found in token") &&
assert.Regexp(t, uuidRegexPattern, jwtID, "JWT ID is not a UUID")
expectedExp := time.Now().Add(1 * time.Hour)
expiration, ok := claims.Expiration()
@@ -336,7 +340,7 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
adminUser := model.User{
Base: model.Base{ID: "admin123"},
Email: utils.Ptr("admin@example.com"),
Email: new("admin@example.com"),
IsAdmin: true,
}
@@ -388,7 +392,7 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
user := model.User{
Base: model.Base{ID: "eddsauser123"},
Email: utils.Ptr("eddsauser@example.com"),
Email: new("eddsauser@example.com"),
IsAdmin: true,
}
@@ -425,7 +429,7 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
user := model.User{
Base: model.Base{ID: "ecdsauser123"},
Email: utils.Ptr("ecdsauser@example.com"),
Email: new("ecdsauser@example.com"),
IsAdmin: true,
}
@@ -462,7 +466,7 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
user := model.User{
Base: model.Base{ID: "rsauser123"},
Email: utils.Ptr("rsauser@example.com"),
Email: new("rsauser@example.com"),
IsAdmin: true,
}
@@ -497,7 +501,7 @@ func TestGenerateVerifyIdToken(t *testing.T) {
t.Run("generates and verifies ID token with standard claims", func(t *testing.T) {
service, _, _ := setupJwtService(t, mockConfig)
userClaims := map[string]interface{}{
userClaims := map[string]any{
"sub": "user123",
"name": "Test User",
"email": "user@example.com",
@@ -520,6 +524,9 @@ func TestGenerateVerifyIdToken(t *testing.T) {
issuer, ok := claims.Issuer()
_ = assert.True(t, ok, "Issuer not found in token") &&
assert.Equal(t, service.envConfig.AppURL, issuer, "Issuer should match app URL")
jwtID, ok := claims.JwtID()
_ = assert.True(t, ok, "JWT ID not found in token") &&
assert.Regexp(t, uuidRegexPattern, jwtID, "JWT ID is not a UUID")
expectedExp := time.Now().Add(1 * time.Hour)
expiration, ok := claims.Expiration()
@@ -531,7 +538,7 @@ func TestGenerateVerifyIdToken(t *testing.T) {
t.Run("can accept expired tokens if told so", func(t *testing.T) {
service, _, _ := setupJwtService(t, mockConfig)
userClaims := map[string]interface{}{
userClaims := map[string]any{
"sub": "user123",
"name": "Test User",
"email": "user@example.com",
@@ -579,7 +586,7 @@ func TestGenerateVerifyIdToken(t *testing.T) {
t.Run("generates and verifies ID token with nonce", func(t *testing.T) {
service, _, _ := setupJwtService(t, mockConfig)
userClaims := map[string]interface{}{
userClaims := map[string]any{
"sub": "user456",
"name": "Another User",
}
@@ -604,7 +611,7 @@ func TestGenerateVerifyIdToken(t *testing.T) {
t.Run("fails verification with incorrect issuer", func(t *testing.T) {
service, _, _ := setupJwtService(t, mockConfig)
userClaims := map[string]interface{}{
userClaims := map[string]any{
"sub": "user789",
}
tokenString, err := service.GenerateIDToken(userClaims, "client-789", "")
@@ -626,7 +633,7 @@ func TestGenerateVerifyIdToken(t *testing.T) {
require.True(t, ok)
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",
"name": "EdDSA User",
"email": "eddsauser@example.com",
@@ -664,7 +671,7 @@ func TestGenerateVerifyIdToken(t *testing.T) {
require.True(t, ok)
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",
"email": "ecdsauser@example.com",
}
@@ -701,7 +708,7 @@ func TestGenerateVerifyIdToken(t *testing.T) {
require.True(t, ok)
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",
"name": "RSA User",
"email": "rsauser@example.com",
@@ -734,7 +741,7 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) {
user := model.User{
Base: model.Base{ID: "user123"},
Email: utils.Ptr("user@example.com"),
Email: new("user@example.com"),
}
const clientID = "test-client-123"
@@ -754,6 +761,9 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) {
issuer, ok := claims.Issuer()
_ = assert.True(t, ok, "Issuer not found in token") &&
assert.Equal(t, service.envConfig.AppURL, issuer, "Issuer should match app URL")
jwtID, ok := claims.JwtID()
_ = assert.True(t, ok, "JWT ID not found in token") &&
assert.Regexp(t, uuidRegexPattern, jwtID, "JWT ID is not a UUID")
expectedExp := time.Now().Add(1 * time.Hour)
expiration, ok := claims.Expiration()
@@ -814,7 +824,7 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) {
user := model.User{
Base: model.Base{ID: "eddsauser789"},
Email: utils.Ptr("eddsaoauth@example.com"),
Email: new("eddsaoauth@example.com"),
}
const clientID = "eddsa-oauth-client"
@@ -851,7 +861,7 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) {
user := model.User{
Base: model.Base{ID: "ecdsauser789"},
Email: utils.Ptr("ecdsaoauth@example.com"),
Email: new("ecdsaoauth@example.com"),
}
const clientID = "ecdsa-oauth-client"
@@ -888,7 +898,7 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) {
user := model.User{
Base: model.Base{ID: "rsauser789"},
Email: utils.Ptr("rsaoauth@example.com"),
Email: new("rsaoauth@example.com"),
}
const clientID = "rsa-oauth-client"

View File

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

View File

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

View File

@@ -11,6 +11,7 @@ import (
"net/http"
"net/url"
"path"
"slices"
"strconv"
"strings"
"time"
@@ -243,7 +244,7 @@ func (s *ScimService) SyncServiceProvider(ctx context.Context, serviceProviderID
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 {
return err
}
@@ -788,10 +789,8 @@ func ensureScimStatus(
resp *http.Response,
provider model.ScimServiceProvider,
allowedStatuses ...int) error {
for _, status := range allowedStatuses {
if resp.StatusCode == status {
return nil
}
if slices.Contains(allowedStatuses, resp.StatusCode) {
return nil
}
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.FriendlyName = input.FriendlyName
group.UpdatedAt = utils.Ptr(datatype.DateTime(time.Now()))
group.UpdatedAt = new(datatype.DateTime(time.Now()))
err = tx.
WithContext(ctx).
@@ -228,7 +228,7 @@ func (s *UserGroupService) updateUsersInternal(ctx context.Context, id string, u
}
// Save the updated group
group.UpdatedAt = utils.Ptr(datatype.DateTime(time.Now()))
group.UpdatedAt = new(datatype.DateTime(time.Now()))
err = tx.
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.
WithContext(ctx).
@@ -501,9 +501,9 @@ func (s *UserService) UpdateUserGroups(ctx context.Context, id string, userGroup
}
// Update the UpdatedAt field for all affected groups
now := time.Now()
now := datatype.DateTime(time.Now())
for _, group := range groups {
group.UpdatedAt = utils.Ptr(datatype.DateTime(now))
group.UpdatedAt = &now
err = tx.WithContext(ctx).Save(&group).Error
if err != nil {
return model.User{}, err
@@ -636,7 +636,7 @@ func (s *UserService) VerifyEmail(ctx context.Context, userID string, token stri
}
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
if err != nil {
return err

View File

@@ -414,10 +414,10 @@ func TestGetCallbackURLFromList_LoopbackSpecialHandling(t *testing.T) {
expectMatch: true,
},
{
name: "IPv6 loopback without brackets in input",
urls: []string{"http://[::1]/callback"},
inputCallbackURL: "http://::1:8080/callback",
expectedURL: "http://::1:8080/callback",
name: "IPv6 loopback with wildcard path",
urls: []string{"http://[::1]/auth/*"},
inputCallbackURL: "http://[::1]:8080/auth/callback",
expectedURL: "http://[::1]:8080/auth/callback",
expectMatch: true,
},
{
@@ -462,6 +462,13 @@ func TestGetCallbackURLFromList_LoopbackSpecialHandling(t *testing.T) {
expectedURL: "http://127.0.0.1:8080/callback",
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 {

View File

@@ -87,9 +87,9 @@ func listContainsIP(ipNets []*net.IPNet, ip net.IP) bool {
func loadLocalIPv6Ranges() {
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)
if rangeStr == "" {
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) {
case []byte:
return json.Unmarshal(v, data)

View File

@@ -43,7 +43,7 @@ func ParseListRequestOptions(ctx *gin.Context) (listRequestOptions ListRequestOp
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)
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)
}
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 {
page = 1
}
@@ -117,8 +117,8 @@ func parseNestedFilters(ctx *gin.Context) map[string][]any {
// Keys can be "filters[field]" or "filters[field][0]"
raw := strings.TrimPrefix(key, "filters[")
// Take everything up to the first closing bracket
if idx := strings.IndexByte(raw, ']'); idx != -1 {
field := raw[:idx]
if before, _, ok := strings.Cut(raw, "]"); ok {
field := before
for _, v := range values {
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
func extractModelMetadata(model interface{}) map[string]FieldMeta {
func extractModelMetadata(model any) map[string]FieldMeta {
meta := make(map[string]FieldMeta)
// Unwrap pointers and slices to get the element struct type
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()
if t == nil {
return meta
@@ -180,8 +180,7 @@ func extractModelMetadata(model interface{}) map[string]FieldMeta {
// recursive parser that merges fields from embedded structs
var parseStruct func(reflect.Type)
parseStruct = func(st reflect.Type) {
for i := 0; i < st.NumField(); i++ {
field := st.Field(i)
for field := range st.Fields() {
ft := field.Type
// If the field is an embedded/anonymous struct, recurse into it

View File

@@ -1,10 +1,5 @@
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,
// otherwise it returns nil.
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()
}
// StringPointer creates a string pointer from a string value
func StringPointer(s string) *string {
return &s
}
func CapitalizeFirstLetter(str string) string {
if str == "" {
return ""

View File

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

View File

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