mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-02-23 05:23:52 +00:00
Compare commits
7 Commits
fix/callba
...
chore/node
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
913115fc7e | ||
|
|
4f515a0803 | ||
|
|
b8e42af446 | ||
|
|
a6b1141ada | ||
|
|
6efcf01899 | ||
|
|
ae269371da | ||
|
|
27caaf2cac |
@@ -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": {
|
||||
|
||||
4
.github/workflows/backend-linter.yml
vendored
4
.github/workflows/backend-linter.yml
vendored
@@ -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' }}
|
||||
|
||||
2
.github/workflows/build-next.yml
vendored
2
.github/workflows/build-next.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/e2e-tests.yml
vendored
2
.github/workflows/e2e-tests.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -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"
|
||||
|
||||
2
.github/workflows/svelte-check.yml
vendored
2
.github/workflows/svelte-check.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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, ",")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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("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"),
|
||||
Value: new("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"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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:]
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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 ./
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user