mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-10 15:05:40 +00:00
chore: bump Go toolchain to 1.26 (TASK-763) (#247)
* chore: bump Go toolchain to 1.26 (TASK-763) Bump Go from 1.25 to 1.26 across all toolchain pins: - go.mod — go 1.25.0 → go 1.26.0 - Dockerfile — golang:1.25-alpine → golang:1.26-alpine - .github/workflows/ci.yml — three setup-go steps (Go, Go-Postgres, E2E jobs) - .github/workflows/release.yml — release pipeline No `toolchain` directive: the repo is pre-launch with no external contributors yet, so we set the floor where we want it (hard requirement). Verified locally before commit: - golangci-lint v2.11.4 builds and runs under Go 1.26.2 (pinned in CI) - golang:1.26-alpine and 1.26.2-alpine images present on Docker Hub - go build ./... clean - go vet ./... clean - go test ./... all pass Parent: PLAN-644 (OSS Repo Hygiene and Launch Polish). * chore: gofmt -w under Go 1.26 (TASK-763) Apply Go 1.26's gofmt to the codebase. ~41 files reformatted, all struct-tag whitespace realignment — no semantic changes. Verified: - gofmt -l ./cmd ./internal returns empty after - go build ./... still clean - go test ./... still passes (run before commit) Bundling the gofmt diff with the toolchain bump in the same PR because the formatting drift is a direct consequence of moving from 1.25 to 1.26; splitting them creates a mandatory two-PR ordering for no value. Parent: PLAN-644. * docs: bump documented Go floor to 1.26 (TASK-763) Match go.mod's hard 1.26.0 requirement in the source-build instructions. Caught by Codex review round 1 on PR #247. - README.md:158 — "Go 1.25+" → "Go 1.26+" - CONTRIBUTING.md:9 — "Go 1.25+" → "Go 1.26+"
This commit is contained in:
@@ -30,7 +30,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
|
||||
with:
|
||||
go-version: "1.25"
|
||||
go-version: "1.26"
|
||||
|
||||
- name: Create web build placeholder for embed
|
||||
run: mkdir -p web/build && echo "placeholder" > web/build/.gitkeep
|
||||
@@ -99,7 +99,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
|
||||
with:
|
||||
go-version: "1.25"
|
||||
go-version: "1.26"
|
||||
|
||||
- name: Create web build placeholder for embed
|
||||
run: mkdir -p web/build && echo "placeholder" > web/build/.gitkeep
|
||||
@@ -157,7 +157,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
|
||||
with:
|
||||
go-version: "1.25"
|
||||
go-version: "1.26"
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
|
||||
with:
|
||||
go-version: "1.25"
|
||||
go-version: "1.26"
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ Thanks for your interest in contributing to Pad! This guide will help you get se
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Go 1.25+](https://go.dev/dl/)
|
||||
- [Go 1.26+](https://go.dev/dl/)
|
||||
- [Node.js 22+](https://nodejs.org/)
|
||||
- Make
|
||||
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ COPY web/ ./
|
||||
RUN npm run build
|
||||
|
||||
# Stage 2: Build Go binary
|
||||
FROM golang:1.25-alpine AS go-builder
|
||||
FROM golang:1.26-alpine AS go-builder
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
@@ -155,7 +155,7 @@ make build
|
||||
cp pad ~/.local/bin/ # or /usr/local/bin/
|
||||
```
|
||||
|
||||
Requires Go 1.25+ and Node.js 22+.
|
||||
Requires Go 1.26+ and Node.js 22+.
|
||||
|
||||
The `go install github.com/xarmian/pad/cmd/pad@latest` path is not supported for the full Pad binary, because the web UI must be built and embedded during the source build.
|
||||
|
||||
|
||||
+1
-1
@@ -30,9 +30,9 @@ import (
|
||||
"github.com/xarmian/pad/internal/config"
|
||||
"regexp"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/xarmian/pad/internal/billing"
|
||||
"github.com/xarmian/pad/internal/email"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/xarmian/pad/internal/events"
|
||||
"github.com/xarmian/pad/internal/logging"
|
||||
"github.com/xarmian/pad/internal/metrics"
|
||||
|
||||
+12
-12
@@ -57,14 +57,14 @@ type serverInfoWorkspace struct {
|
||||
}
|
||||
|
||||
type serverInfoLocal struct {
|
||||
BindAddr string `json:"bind_addr"`
|
||||
ServerRunning bool `json:"server_running"`
|
||||
PID *int `json:"pid,omitempty"`
|
||||
PIDFile string `json:"pid_file"`
|
||||
LogFile string `json:"log_file"`
|
||||
DBPath string `json:"db_path"`
|
||||
DBExists bool `json:"db_exists"`
|
||||
DBSizeBytes int64 `json:"db_size_bytes"`
|
||||
BindAddr string `json:"bind_addr"`
|
||||
ServerRunning bool `json:"server_running"`
|
||||
PID *int `json:"pid,omitempty"`
|
||||
PIDFile string `json:"pid_file"`
|
||||
LogFile string `json:"log_file"`
|
||||
DBPath string `json:"db_path"`
|
||||
DBExists bool `json:"db_exists"`
|
||||
DBSizeBytes int64 `json:"db_size_bytes"`
|
||||
}
|
||||
|
||||
func infoCmd() *cobra.Command {
|
||||
@@ -171,11 +171,11 @@ func includeLocalRuntime(cfg *config.Config) bool {
|
||||
|
||||
func collectLocalInfo(cfg *config.Config) *serverInfoLocal {
|
||||
info := &serverInfoLocal{
|
||||
BindAddr: cfg.Addr(),
|
||||
BindAddr: cfg.Addr(),
|
||||
ServerRunning: cli.IsServerRunning(cfg),
|
||||
PIDFile: cfg.PIDFile(),
|
||||
LogFile: cfg.LogFile(),
|
||||
DBPath: cfg.DBPath,
|
||||
PIDFile: cfg.PIDFile(),
|
||||
LogFile: cfg.LogFile(),
|
||||
DBPath: cfg.DBPath,
|
||||
}
|
||||
|
||||
if pid, ok := readPID(cfg.PIDFile()); ok {
|
||||
|
||||
@@ -39,9 +39,9 @@ func TestCollectServerInfoRemoteAuthenticated(t *testing.T) {
|
||||
t.Fatalf("unexpected auth header %q", got)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"authenticated": true,
|
||||
"authenticated": true,
|
||||
"setup_required": false,
|
||||
"auth_method": "password",
|
||||
"auth_method": "password",
|
||||
"user": map[string]any{
|
||||
"id": "user-1",
|
||||
"email": "dave@example.com",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/xarmian/pad
|
||||
|
||||
go 1.25.0
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.6.0
|
||||
|
||||
@@ -64,6 +64,7 @@ func TestCancelCustomer_HappyPath_SendsCorrectRequest(t *testing.T) {
|
||||
// - 403 on cloud_secret mismatch
|
||||
// - 500 on internal / Stripe failure
|
||||
// - 503 when Stripe is not configured
|
||||
//
|
||||
// All of them must produce a SidecarError — the handler treats every one
|
||||
// as "abort the delete", regardless of bucket.
|
||||
func TestCancelCustomer_NonOK_ReturnsSidecarError(t *testing.T) {
|
||||
@@ -159,11 +160,11 @@ func TestCancelCustomer_UnconfiguredClient_ReturnsError(t *testing.T) {
|
||||
|
||||
func TestResolveOutboundSecret(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
explicit string
|
||||
inboundList string
|
||||
want string
|
||||
wantEmpty bool
|
||||
name string
|
||||
explicit string
|
||||
inboundList string
|
||||
want string
|
||||
wantEmpty bool
|
||||
}{
|
||||
{
|
||||
name: "explicit wins over inbound",
|
||||
|
||||
@@ -15,8 +15,8 @@ import (
|
||||
|
||||
// Color definitions for reuse across the CLI.
|
||||
var (
|
||||
Bold = color.New(color.Bold)
|
||||
Dim = color.New(color.Faint)
|
||||
Bold = color.New(color.Bold)
|
||||
Dim = color.New(color.Faint)
|
||||
BoldCyan = color.New(color.Bold, color.FgCyan)
|
||||
)
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ func Defaults() []DefaultCollection {
|
||||
Type: "select",
|
||||
Options: []string{"xs", "s", "m", "l", "xl"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Settings: models.CollectionSettings{
|
||||
Layout: "fields-primary",
|
||||
|
||||
@@ -41,9 +41,9 @@ type Config struct {
|
||||
EmailFromName string `toml:"email_from_name"` // Sender display name (e.g. Pad)
|
||||
|
||||
// Cloud mode
|
||||
CloudSecret string `toml:"cloud_secret"` // Inbound shared secret(s) accepted from pad-cloud. Comma-separated list supports rotation.
|
||||
CloudSidecarURL string `toml:"cloud_sidecar_url"` // Base URL pad uses to call the pad-cloud sidecar (reverse direction, e.g. Stripe cancel-customer on account delete)
|
||||
CloudOutboundSecret string `toml:"cloud_outbound_secret"` // Optional: exact secret to send when calling pad-cloud. Falls back to the LAST entry of CloudSecret (the older rotation value, which is what pad-cloud is usually running). See DEPLOY.md "Cloud secret rotation".
|
||||
CloudSecret string `toml:"cloud_secret"` // Inbound shared secret(s) accepted from pad-cloud. Comma-separated list supports rotation.
|
||||
CloudSidecarURL string `toml:"cloud_sidecar_url"` // Base URL pad uses to call the pad-cloud sidecar (reverse direction, e.g. Stripe cancel-customer on account delete)
|
||||
CloudOutboundSecret string `toml:"cloud_outbound_secret"` // Optional: exact secret to send when calling pad-cloud. Falls back to the LAST entry of CloudSecret (the older rotation value, which is what pad-cloud is usually running). See DEPLOY.md "Cloud secret rotation".
|
||||
|
||||
// Encryption
|
||||
EncryptionKey string `toml:"encryption_key"` // 32-byte hex-encoded AES-256 key for encrypting sensitive fields
|
||||
|
||||
@@ -39,7 +39,7 @@ const (
|
||||
|
||||
// Default replay buffer settings.
|
||||
const (
|
||||
DefaultReplayBufferSize = 1024 // max events to retain per workspace
|
||||
DefaultReplayBufferSize = 1024 // max events to retain per workspace
|
||||
DefaultReplayMaxAge = 5 * time.Minute // discard events older than this
|
||||
)
|
||||
|
||||
|
||||
@@ -35,8 +35,8 @@ type RedisBus struct {
|
||||
|
||||
// Track which workspace channels we're subscribed to in Redis,
|
||||
// so we subscribe/unsubscribe as local SSE clients come and go.
|
||||
wsCounts map[string]int // workspace → local subscriber count
|
||||
wsSubs map[string]*redisSub // workspace → active Redis subscription
|
||||
wsCounts map[string]int // workspace → local subscriber count
|
||||
wsSubs map[string]*redisSub // workspace → active Redis subscription
|
||||
|
||||
// Monotonic sequence counter for event IDs (local to this instance).
|
||||
seq atomic.Int64
|
||||
|
||||
@@ -14,8 +14,8 @@ type InstrumentedBus struct {
|
||||
inner events.EventBus
|
||||
metrics *Metrics
|
||||
|
||||
mu sync.Mutex
|
||||
workspaces map[chan events.Event]string // channel → workspaceID for gauge decrement
|
||||
mu sync.Mutex
|
||||
workspaces map[chan events.Event]string // channel → workspaceID for gauge decrement
|
||||
}
|
||||
|
||||
// NewInstrumentedBus wraps an EventBus with Prometheus instrumentation.
|
||||
|
||||
@@ -72,11 +72,11 @@ func TestRegisterDBCollector(t *testing.T) {
|
||||
}
|
||||
|
||||
expected := map[string]bool{
|
||||
"pad_db_open_connections": false,
|
||||
"pad_db_idle_connections": false,
|
||||
"pad_db_in_use_connections": false,
|
||||
"pad_db_wait_count_total": false,
|
||||
"pad_db_wait_duration_seconds_total": false,
|
||||
"pad_db_open_connections": false,
|
||||
"pad_db_idle_connections": false,
|
||||
"pad_db_in_use_connections": false,
|
||||
"pad_db_wait_count_total": false,
|
||||
"pad_db_wait_duration_seconds_total": false,
|
||||
}
|
||||
|
||||
for _, f := range families {
|
||||
|
||||
+19
-19
@@ -9,25 +9,25 @@ var ValidActions = []string{
|
||||
|
||||
// Audit action constants for auth/admin events
|
||||
const (
|
||||
ActionLogin = "login"
|
||||
ActionLoginFailed = "login_failed"
|
||||
ActionLogout = "logout"
|
||||
ActionBootstrap = "bootstrap"
|
||||
ActionRegister = "register"
|
||||
ActionPasswordChanged = "password_changed"
|
||||
ActionPasswordReset = "password_reset"
|
||||
ActionTokenCreated = "token_created"
|
||||
ActionTokenRevoked = "token_revoked"
|
||||
ActionTokenRotated = "token_rotated"
|
||||
ActionTOTPEnabled = "totp_enabled"
|
||||
ActionTOTPDisabled = "totp_disabled"
|
||||
ActionMemberInvited = "member_invited"
|
||||
ActionMemberRemoved = "member_removed"
|
||||
ActionRoleChanged = "role_changed"
|
||||
ActionSettingsChanged = "settings_changed"
|
||||
ActionOAuthLogin = "oauth_login"
|
||||
ActionOAuthLoginFailed = "oauth_login_failed"
|
||||
ActionPlanChanged = "plan_changed"
|
||||
ActionLogin = "login"
|
||||
ActionLoginFailed = "login_failed"
|
||||
ActionLogout = "logout"
|
||||
ActionBootstrap = "bootstrap"
|
||||
ActionRegister = "register"
|
||||
ActionPasswordChanged = "password_changed"
|
||||
ActionPasswordReset = "password_reset"
|
||||
ActionTokenCreated = "token_created"
|
||||
ActionTokenRevoked = "token_revoked"
|
||||
ActionTokenRotated = "token_rotated"
|
||||
ActionTOTPEnabled = "totp_enabled"
|
||||
ActionTOTPDisabled = "totp_disabled"
|
||||
ActionMemberInvited = "member_invited"
|
||||
ActionMemberRemoved = "member_removed"
|
||||
ActionRoleChanged = "role_changed"
|
||||
ActionSettingsChanged = "settings_changed"
|
||||
ActionOAuthLogin = "oauth_login"
|
||||
ActionOAuthLoginFailed = "oauth_login_failed"
|
||||
ActionPlanChanged = "plan_changed"
|
||||
ActionPasswordResetByAdmin = "password_reset_by_admin"
|
||||
ActionUserDisabled = "user_disabled"
|
||||
ActionUserEnabled = "user_enabled"
|
||||
|
||||
@@ -24,7 +24,7 @@ type AgentRole struct {
|
||||
// AgentRoleCreate is the input for creating a new agent role.
|
||||
type AgentRoleCreate struct {
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug,omitempty"` // auto-generated from name if empty
|
||||
Slug string `json:"slug,omitempty"` // auto-generated from name if empty
|
||||
Description string `json:"description,omitempty"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Tools string `json:"tools,omitempty"`
|
||||
|
||||
@@ -5,14 +5,14 @@ import "time"
|
||||
type FieldDef struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
Type string `json:"type"` // text, number, select, multi_select, date, checkbox, url, relation
|
||||
Type string `json:"type"` // text, number, select, multi_select, date, checkbox, url, relation
|
||||
Options []string `json:"options,omitempty"`
|
||||
TerminalOptions []string `json:"terminal_options,omitempty"` // for select fields: which options represent a terminal/finalized state
|
||||
Default any `json:"default,omitempty"`
|
||||
Required bool `json:"required,omitempty"`
|
||||
Computed bool `json:"computed,omitempty"`
|
||||
Collection string `json:"collection,omitempty"` // for relation type
|
||||
Suffix string `json:"suffix,omitempty"` // for number type display
|
||||
Collection string `json:"collection,omitempty"` // for relation type
|
||||
Suffix string `json:"suffix,omitempty"` // for number type display
|
||||
}
|
||||
|
||||
type CollectionSchema struct {
|
||||
@@ -21,15 +21,15 @@ type CollectionSchema struct {
|
||||
|
||||
// QuickAction defines a prompt template that can be triggered from the UI.
|
||||
type QuickAction struct {
|
||||
Label string `json:"label"` // display label for the button
|
||||
Prompt string `json:"prompt"` // prompt template with {ref}, {title}, {status}, etc.
|
||||
Scope string `json:"scope"` // "item" or "collection"
|
||||
Icon string `json:"icon,omitempty"` // optional emoji/icon
|
||||
Label string `json:"label"` // display label for the button
|
||||
Prompt string `json:"prompt"` // prompt template with {ref}, {title}, {status}, etc.
|
||||
Scope string `json:"scope"` // "item" or "collection"
|
||||
Icon string `json:"icon,omitempty"` // optional emoji/icon
|
||||
}
|
||||
|
||||
type CollectionSettings struct {
|
||||
Layout string `json:"layout,omitempty"` // fields-primary, content-primary, balanced
|
||||
DefaultView string `json:"default_view,omitempty"` // list, board, table
|
||||
Layout string `json:"layout,omitempty"` // fields-primary, content-primary, balanced
|
||||
DefaultView string `json:"default_view,omitempty"` // list, board, table
|
||||
BoardGroupBy string `json:"board_group_by,omitempty"`
|
||||
ListSortBy string `json:"list_sort_by,omitempty"`
|
||||
ListGroupBy string `json:"list_group_by,omitempty"`
|
||||
@@ -45,7 +45,7 @@ type Collection struct {
|
||||
Icon string `json:"icon"`
|
||||
Description string `json:"description"`
|
||||
Schema string `json:"schema"` // JSON string in DB, parsed via methods
|
||||
Settings string `json:"settings"` // JSON string in DB
|
||||
Settings string `json:"settings"` // JSON string in DB
|
||||
Prefix string `json:"prefix"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
|
||||
+16
-16
@@ -39,14 +39,14 @@ type Document struct {
|
||||
}
|
||||
|
||||
type DocumentCreate struct {
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content,omitempty"`
|
||||
DocType string `json:"doc_type,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Tags string `json:"tags,omitempty"`
|
||||
Pinned bool `json:"pinned,omitempty"`
|
||||
CreatedBy string `json:"created_by,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content,omitempty"`
|
||||
DocType string `json:"doc_type,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Tags string `json:"tags,omitempty"`
|
||||
Pinned bool `json:"pinned,omitempty"`
|
||||
CreatedBy string `json:"created_by,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
}
|
||||
|
||||
type DocumentUpdate struct {
|
||||
@@ -63,14 +63,14 @@ type DocumentUpdate struct {
|
||||
}
|
||||
|
||||
type QuickSave struct {
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
DocType string `json:"doc_type,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Tags string `json:"tags,omitempty"`
|
||||
CreatedBy string `json:"created_by,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
ChangeSummary string `json:"change_summary,omitempty"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
DocType string `json:"doc_type,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Tags string `json:"tags,omitempty"`
|
||||
CreatedBy string `json:"created_by,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
ChangeSummary string `json:"change_summary,omitempty"`
|
||||
}
|
||||
|
||||
type DocumentListParams struct {
|
||||
|
||||
@@ -2,7 +2,7 @@ package models
|
||||
|
||||
// WorkspaceExport is the complete portable representation of a workspace.
|
||||
type WorkspaceExport struct {
|
||||
Version int `json:"version"` // Export format version (1)
|
||||
Version int `json:"version"` // Export format version (1)
|
||||
ExportedAt string `json:"exported_at"`
|
||||
Workspace WorkspaceExportMeta `json:"workspace"`
|
||||
Collections []CollectionExport `json:"collections"`
|
||||
|
||||
@@ -47,17 +47,17 @@ type Item struct {
|
||||
AgentRoleName string `json:"agent_role_name,omitempty"`
|
||||
AgentRoleSlug string `json:"agent_role_slug,omitempty"`
|
||||
AgentRoleIcon string `json:"agent_role_icon,omitempty"`
|
||||
CollectionSlug string `json:"collection_slug,omitempty"`
|
||||
CollectionName string `json:"collection_name,omitempty"`
|
||||
CollectionIcon string `json:"collection_icon,omitempty"`
|
||||
CollectionPrefix string `json:"collection_prefix,omitempty"`
|
||||
CollectionSlug string `json:"collection_slug,omitempty"`
|
||||
CollectionName string `json:"collection_name,omitempty"`
|
||||
CollectionIcon string `json:"collection_icon,omitempty"`
|
||||
CollectionPrefix string `json:"collection_prefix,omitempty"`
|
||||
|
||||
// Parent link (populated by enrichItemForResponse / enrichItemsWithParent)
|
||||
ParentLinkID string `json:"parent_link_id,omitempty"`
|
||||
ParentRef string `json:"parent_ref,omitempty"`
|
||||
ParentTitle string `json:"parent_title,omitempty"`
|
||||
ParentSlug string `json:"parent_slug,omitempty"`
|
||||
ParentCollectionSlug string `json:"parent_collection_slug,omitempty"`
|
||||
ParentLinkID string `json:"parent_link_id,omitempty"`
|
||||
ParentRef string `json:"parent_ref,omitempty"`
|
||||
ParentTitle string `json:"parent_title,omitempty"`
|
||||
ParentSlug string `json:"parent_slug,omitempty"`
|
||||
ParentCollectionSlug string `json:"parent_collection_slug,omitempty"`
|
||||
|
||||
// HasChildren is true if this item has child items linked to it.
|
||||
// Populated by enrichment, not stored in the DB.
|
||||
|
||||
@@ -12,7 +12,7 @@ const (
|
||||
ItemLinkTypeSplitFrom = "split_from"
|
||||
ItemLinkTypeSupersedes = "supersedes"
|
||||
ItemLinkTypeImplements = "implements"
|
||||
ItemLinkTypeParent = "parent"
|
||||
ItemLinkTypeParent = "parent"
|
||||
)
|
||||
|
||||
var itemLinkTypeAliases = map[string]string{
|
||||
|
||||
@@ -6,15 +6,15 @@ import "time"
|
||||
// Tokens are hashed at rest; the raw token is returned only once on creation.
|
||||
type ShareLink struct {
|
||||
ID string `json:"id"`
|
||||
TokenHash string `json:"-"` // Never serialized
|
||||
Token string `json:"token,omitempty"` // Only set on creation response
|
||||
TargetType string `json:"target_type"` // "item" or "collection"
|
||||
TokenHash string `json:"-"` // Never serialized
|
||||
Token string `json:"token,omitempty"` // Only set on creation response
|
||||
TargetType string `json:"target_type"` // "item" or "collection"
|
||||
TargetID string `json:"target_id"`
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
Permission string `json:"permission"` // "view" or "edit"
|
||||
Permission string `json:"permission"` // "view" or "edit"
|
||||
CreatedBy string `json:"created_by"`
|
||||
PasswordHash *string `json:"-"` // Never serialized
|
||||
HasPassword bool `json:"has_password"` // Derived: password_hash IS NOT NULL
|
||||
PasswordHash *string `json:"-"` // Never serialized
|
||||
HasPassword bool `json:"has_password"` // Derived: password_hash IS NOT NULL
|
||||
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
||||
MaxViews *int `json:"max_views,omitempty"`
|
||||
RequireAuth bool `json:"require_auth"`
|
||||
|
||||
+12
-12
@@ -9,21 +9,21 @@ import (
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Username string `json:"username"` // Unique handle; empty until set
|
||||
Username string `json:"username"` // Unique handle; empty until set
|
||||
Name string `json:"name"`
|
||||
PasswordHash string `json:"-"` // Never serialized
|
||||
Role string `json:"role"` // "admin" or "member"
|
||||
PasswordHash string `json:"-"` // Never serialized
|
||||
Role string `json:"role"` // "admin" or "member"
|
||||
AvatarURL string `json:"avatar_url,omitempty"`
|
||||
TOTPSecret string `json:"-"` // Never serialized
|
||||
TOTPSecret string `json:"-"` // Never serialized
|
||||
TOTPEnabled bool `json:"totp_enabled"`
|
||||
RecoveryCodes string `json:"-"` // Never serialized
|
||||
Plan string `json:"plan"` // "free", "pro", or "self-hosted"
|
||||
RecoveryCodes string `json:"-"` // Never serialized
|
||||
Plan string `json:"plan"` // "free", "pro", or "self-hosted"
|
||||
PlanExpiresAt string `json:"plan_expires_at,omitempty"`
|
||||
StripeCustomerID string `json:"-"` // Never serialized
|
||||
StripeCustomerID string `json:"-"` // Never serialized
|
||||
PlanOverrides string `json:"plan_overrides,omitempty"` // JSON overrides for per-user limits
|
||||
OAuthProviders string `json:"-"` // JSON array of linked providers, e.g. ["github","google"]
|
||||
PasswordSet bool `json:"password_set"` // True if the user explicitly set a password (vs. OAuth placeholder hash)
|
||||
DisabledAt string `json:"disabled_at,omitempty"` // Non-empty = account disabled
|
||||
OAuthProviders string `json:"-"` // JSON array of linked providers, e.g. ["github","google"]
|
||||
PasswordSet bool `json:"password_set"` // True if the user explicitly set a password (vs. OAuth placeholder hash)
|
||||
DisabledAt string `json:"disabled_at,omitempty"` // Non-empty = account disabled
|
||||
LastActiveAt string `json:"last_active_at,omitempty"` // Last authenticated API request
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
@@ -69,8 +69,8 @@ type UserCreate struct {
|
||||
Email string `json:"email"`
|
||||
Username string `json:"username,omitempty"` // Optional; auto-generated if empty
|
||||
Name string `json:"name"`
|
||||
Password string `json:"password"` // Plaintext, will be hashed
|
||||
Role string `json:"role,omitempty"` // Defaults to "member"
|
||||
Password string `json:"password"` // Plaintext, will be hashed
|
||||
Role string `json:"role,omitempty"` // Defaults to "member"
|
||||
}
|
||||
|
||||
// UserUpdate is the input for updating user profile fields.
|
||||
|
||||
@@ -3,19 +3,19 @@ package models
|
||||
import "time"
|
||||
|
||||
type Workspace struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
OwnerID string `json:"owner_id,omitempty"` // User ID of workspace owner
|
||||
OwnerUsername string `json:"owner_username,omitempty"` // Populated by JOIN (not stored)
|
||||
IsGuest bool `json:"is_guest,omitempty"` // True when user has grants but no membership
|
||||
Description string `json:"description"`
|
||||
Settings string `json:"settings"` // JSON
|
||||
SortOrder int `json:"sort_order"`
|
||||
Context *WorkspaceContext `json:"context,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt *time.Time `json:"deleted_at,omitempty"`
|
||||
Description string `json:"description"`
|
||||
Settings string `json:"settings"` // JSON
|
||||
SortOrder int `json:"sort_order"`
|
||||
Context *WorkspaceContext `json:"context,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt *time.Time `json:"deleted_at,omitempty"`
|
||||
}
|
||||
|
||||
type WorkspaceCreate struct {
|
||||
|
||||
@@ -60,4 +60,3 @@ func TestDecodeJSONWithLimit_CustomCap(t *testing.T) {
|
||||
t.Fatal("expected 256 KiB cap to reject 1 MiB body, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,11 +9,11 @@ func TestNormalizeRecoveryCode(t *testing.T) {
|
||||
tests := []struct {
|
||||
in, want string
|
||||
}{
|
||||
{"ABCDEFGHIJKLMNOP", "ABCDEFGHIJKLMNOP"}, // already normalized
|
||||
{"abcdefghijklmnop", "ABCDEFGHIJKLMNOP"}, // lowercase → upper
|
||||
{"ABCD-EFGH-IJKL-MNOP", "ABCDEFGHIJKLMNOP"}, // dashes stripped
|
||||
{"ABCDEFGHIJKLMNOP", "ABCDEFGHIJKLMNOP"}, // already normalized
|
||||
{"abcdefghijklmnop", "ABCDEFGHIJKLMNOP"}, // lowercase → upper
|
||||
{"ABCD-EFGH-IJKL-MNOP", "ABCDEFGHIJKLMNOP"}, // dashes stripped
|
||||
{" abcd efgh ijkl mnop ", "ABCDEFGHIJKLMNOP"}, // whitespace stripped
|
||||
{"ABcd-EFgh\nIJkl", "ABCDEFGHIJKL"}, // mixed + newline
|
||||
{"ABcd-EFgh\nIJkl", "ABCDEFGHIJKL"}, // mixed + newline
|
||||
{"", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -10,11 +10,11 @@ import (
|
||||
|
||||
// Known platform setting keys. Values are stored in the platform_settings table.
|
||||
const (
|
||||
settingEmailProvider = "email_provider" // "maileroo" or empty
|
||||
settingEmailProvider = "email_provider" // "maileroo" or empty
|
||||
settingMailerooAPIKey = "maileroo_api_key"
|
||||
settingEmailFrom = "email_from" // Sender address
|
||||
settingEmailFromName = "email_from_name" // Sender display name
|
||||
settingPlatformName = "platform_name" // Instance name (default: "Pad")
|
||||
settingEmailFrom = "email_from" // Sender address
|
||||
settingEmailFromName = "email_from_name" // Sender display name
|
||||
settingPlatformName = "platform_name" // Instance name (default: "Pad")
|
||||
|
||||
// Token policy settings
|
||||
settingTokenDefaultExpiryDays = "token_default_expiry_days" // Default: 90
|
||||
|
||||
@@ -343,10 +343,10 @@ func (s *Server) handleAdminResetPassword(w http.ResponseWriter, r *http.Request
|
||||
}))
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"ok": true,
|
||||
"method": "temporary_password",
|
||||
"temp_password": tempPassword,
|
||||
"message": "Temporary password generated. The user's existing sessions have been invalidated.",
|
||||
"ok": true,
|
||||
"method": "temporary_password",
|
||||
"temp_password": tempPassword,
|
||||
"message": "Temporary password generated. The user's existing sessions have been invalidated.",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -550,10 +550,10 @@ func (s *Server) handleAdminStats(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"users": userCount,
|
||||
"users_by_plan": planCounts,
|
||||
"workspaces": len(workspaces),
|
||||
"cloud_mode": s.cloudMode,
|
||||
"users": userCount,
|
||||
"users_by_plan": planCounts,
|
||||
"workspaces": len(workspaces),
|
||||
"cloud_mode": s.cloudMode,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -15,14 +15,14 @@ import (
|
||||
// Dashboard response types
|
||||
|
||||
type DashboardResponse struct {
|
||||
Summary DashboardSummary `json:"summary"`
|
||||
ActiveItems []DashboardActiveItem `json:"active_items"`
|
||||
ActivePlans []DashboardPlan `json:"active_plans"`
|
||||
StarredItems []DashboardActiveItem `json:"starred_items,omitempty"`
|
||||
ByRole []store.RoleBreakdown `json:"by_role,omitempty"`
|
||||
Attention []DashboardAttention `json:"attention"`
|
||||
RecentActivity []DashboardActivity `json:"recent_activity"`
|
||||
SuggestedNext []DashboardSuggestion `json:"suggested_next"`
|
||||
Summary DashboardSummary `json:"summary"`
|
||||
ActiveItems []DashboardActiveItem `json:"active_items"`
|
||||
ActivePlans []DashboardPlan `json:"active_plans"`
|
||||
StarredItems []DashboardActiveItem `json:"starred_items,omitempty"`
|
||||
ByRole []store.RoleBreakdown `json:"by_role,omitempty"`
|
||||
Attention []DashboardAttention `json:"attention"`
|
||||
RecentActivity []DashboardActivity `json:"recent_activity"`
|
||||
SuggestedNext []DashboardSuggestion `json:"suggested_next"`
|
||||
}
|
||||
|
||||
type DashboardActiveItem struct {
|
||||
|
||||
@@ -14,9 +14,9 @@ import (
|
||||
// rbacTestEnv holds everything needed for RBAC tests:
|
||||
// a server with an admin, workspace, and users with different roles.
|
||||
type rbacTestEnv struct {
|
||||
srv *Server
|
||||
wsSlug string
|
||||
ownerToken string
|
||||
srv *Server
|
||||
wsSlug string
|
||||
ownerToken string
|
||||
editorToken string
|
||||
viewerToken string
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ func TestTrustedProxyRealIP_TrustedPeer_XRealIPUsed(t *testing.T) {
|
||||
mw := TrustedProxyRealIP(cidrs)(next)
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req.RemoteAddr = "10.0.0.5:12345" // in trusted CIDR
|
||||
req.RemoteAddr = "10.0.0.5:12345" // in trusted CIDR
|
||||
req.Header.Set("X-Real-IP", "198.51.100.7")
|
||||
mw.ServeHTTP(httptest.NewRecorder(), req)
|
||||
|
||||
|
||||
@@ -16,9 +16,9 @@ func TestSecurityHeaders(t *testing.T) {
|
||||
|
||||
headers := map[string]string{
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"X-Frame-Options": "DENY",
|
||||
"Referrer-Policy": "strict-origin-when-cross-origin",
|
||||
"Permissions-Policy": "camera=(), microphone=(), geolocation=()",
|
||||
"X-Frame-Options": "DENY",
|
||||
"Referrer-Policy": "strict-origin-when-cross-origin",
|
||||
"Permissions-Policy": "camera=(), microphone=(), geolocation=()",
|
||||
}
|
||||
|
||||
for name, expected := range headers {
|
||||
|
||||
@@ -463,4 +463,3 @@ func (s *Store) ResolveAgentRoleID(workspaceID, idOrSlug string) (string, error)
|
||||
}
|
||||
return role.ID, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -197,9 +197,9 @@ func TestConcurrentMixedReadWrite(t *testing.T) {
|
||||
)
|
||||
|
||||
var (
|
||||
readOps, writeOps atomic.Int64
|
||||
readErrs, writeErrs atomic.Int64
|
||||
wg sync.WaitGroup
|
||||
readOps, writeOps atomic.Int64
|
||||
readErrs, writeErrs atomic.Int64
|
||||
wg sync.WaitGroup
|
||||
)
|
||||
|
||||
deadline := time.Now().Add(duration)
|
||||
|
||||
@@ -593,4 +593,3 @@ func scanDocuments(rows *sql.Rows) ([]models.Document, error) {
|
||||
}
|
||||
return docs, rows.Err()
|
||||
}
|
||||
|
||||
|
||||
@@ -235,4 +235,3 @@ func (s *Store) DeleteStarsForItem(itemID string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1451,8 +1451,9 @@ func scanCollectionDoneFilters(rows *sql.Rows) []collectionDoneFilter {
|
||||
// for GetAllItemProgress).
|
||||
//
|
||||
// Expression shape:
|
||||
// ((<alias>.collection_id = ? AND LOWER(COALESCE(<field_A>, '')) IN (?,?)) OR
|
||||
// (<alias>.collection_id = ? AND LOWER(COALESCE(<field_B>, '')) IN (?,?)))
|
||||
//
|
||||
// ((<alias>.collection_id = ? AND LOWER(COALESCE(<field_A>, '')) IN (?,?)) OR
|
||||
// (<alias>.collection_id = ? AND LOWER(COALESCE(<field_B>, '')) IN (?,?)))
|
||||
//
|
||||
// The `<field_X>` JSON extract uses scalar text extraction; this works
|
||||
// because DoneFieldKey in the models package only resolves done fields to
|
||||
|
||||
@@ -50,7 +50,7 @@ var DefaultProLimits = PlanLimits{
|
||||
type LimitResult struct {
|
||||
Allowed bool `json:"allowed"`
|
||||
Feature string `json:"feature"`
|
||||
Limit int `json:"limit"` // -1 means unlimited
|
||||
Limit int `json:"limit"` // -1 means unlimited
|
||||
Current int `json:"current"`
|
||||
Plan string `json:"plan"`
|
||||
}
|
||||
|
||||
@@ -355,7 +355,7 @@ func (s *Store) Search(params SearchParams) (*SearchResponse, error) {
|
||||
// Direct ref hits (e.g. searching "TASK-5") are prepended to results
|
||||
// and always appear first. They occupy slots on page 0; on subsequent
|
||||
// pages we exclude them and adjust the FTS offset accordingly.
|
||||
refHits := results // save ref results before FTS
|
||||
refHits := results // save ref results before FTS
|
||||
refCount := len(refHits)
|
||||
refIDs := make(map[string]bool, refCount)
|
||||
for _, r := range refHits {
|
||||
|
||||
@@ -70,7 +70,7 @@ func TestSearchQualityComparison(t *testing.T) {
|
||||
wsIDs[name] = ws.ID
|
||||
|
||||
coll, err := s.CreateCollection(ws.ID, models.CollectionCreate{
|
||||
Name: "Tasks",
|
||||
Name: "Tasks",
|
||||
Schema: `{"fields":[{"key":"status","label":"Status","type":"select","options":["open","done"],"default":"open","required":true},{"key":"priority","label":"Priority","type":"select","options":["low","medium","high"]}]}`,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -193,7 +193,7 @@ func TestSearchEdgeCases(t *testing.T) {
|
||||
|
||||
ws := createTestWorkspace(t, s, "search-edge")
|
||||
coll, err := s.CreateCollection(ws.ID, models.CollectionCreate{
|
||||
Name: "Items",
|
||||
Name: "Items",
|
||||
Schema: `{"fields":[{"key":"status","label":"Status","type":"select","options":["open","done"],"default":"open","required":true}]}`,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -41,9 +41,11 @@ func (s *Store) DB() *sql.DB { return s.db }
|
||||
// locked, retry for up to 5 seconds before returning SQLITE_BUSY. The
|
||||
// pragma is applied per-connection by the driver, so every pool member
|
||||
// inherits it.
|
||||
//
|
||||
// - `_pragma=foreign_keys(on)`: foreign-key enforcement is per-connection
|
||||
// in SQLite. Setting it via the DSN guarantees ALL pool members enforce
|
||||
// them, not just the one that received a `db.Exec("PRAGMA ...")` call.
|
||||
//
|
||||
// - `_txlock=immediate`: makes every `db.Begin()` issue `BEGIN IMMEDIATE`
|
||||
// instead of the default `BEGIN DEFERRED`. With deferred mode, a
|
||||
// transaction starts holding only a SHARED lock and tries to upgrade
|
||||
@@ -79,6 +81,7 @@ func (s *Store) DB() *sql.DB { return s.db }
|
||||
// model the realistic fallout is small (link tables already cascade),
|
||||
// but the integrity check `PRAGMA foreign_key_check` can surface
|
||||
// any pre-existing offenders.
|
||||
//
|
||||
// - `journal_mode=WAL` is set via Exec below; WAL is a database-level
|
||||
// setting (stored in the file header), so it persists across
|
||||
// connections after the first one applies it.
|
||||
|
||||
@@ -238,8 +238,8 @@ func TestSQLiteConcurrentWritersNoBusy(t *testing.T) {
|
||||
done.Add(1)
|
||||
go func() {
|
||||
defer done.Done()
|
||||
ready.Done() // signal "I'm at the gate"
|
||||
release.Wait() // park here until main thread releases
|
||||
ready.Done() // signal "I'm at the gate"
|
||||
release.Wait() // park here until main thread releases
|
||||
for op := 0; op < opsPerWorker; op++ {
|
||||
_, err := s.CreateItem(ws.ID, coll.ID, models.ItemCreate{
|
||||
Title: fmt.Sprintf("concurrent-%d-%d", idx, op),
|
||||
@@ -251,8 +251,8 @@ func TestSQLiteConcurrentWritersNoBusy(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
}
|
||||
ready.Wait() // every worker has called ready.Done() (best-effort gate)
|
||||
release.Done() // release them all at once
|
||||
ready.Wait() // every worker has called ready.Done() (best-effort gate)
|
||||
release.Done() // release them all at once
|
||||
done.Wait()
|
||||
close(errCh)
|
||||
|
||||
|
||||
@@ -30,9 +30,9 @@ type WebhookPayload struct {
|
||||
|
||||
// Dispatcher sends webhook HTTP POST notifications for workspace events.
|
||||
type Dispatcher struct {
|
||||
store WebhookStore
|
||||
client *http.Client
|
||||
SkipSSRF bool // Skip SSRF validation (for tests only)
|
||||
store WebhookStore
|
||||
client *http.Client
|
||||
SkipSSRF bool // Skip SSRF validation (for tests only)
|
||||
}
|
||||
|
||||
// NewDispatcher creates a Dispatcher with the given store.
|
||||
|
||||
@@ -16,7 +16,7 @@ type mockStore struct {
|
||||
mu sync.Mutex
|
||||
hooks []models.Webhook
|
||||
failures map[string]bool // id -> last failed state
|
||||
updated chan string // signals when UpdateWebhookFailure is called
|
||||
updated chan string // signals when UpdateWebhookFailure is called
|
||||
}
|
||||
|
||||
func newMockStore(hooks []models.Webhook) *mockStore {
|
||||
|
||||
Reference in New Issue
Block a user