aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXe Iaso <me@xeiaso.net>2025-04-03 19:58:52 -0400
committerXe Iaso <me@xeiaso.net>2025-04-03 19:58:52 -0400
commit5b1a06499de6bbc1a11601796b3d507e3cded6da (patch)
tree8d00dd6b322d8a9018a0886fbb75dc272795a79b
parent196e188328e3f9c8e711a862b3029535e0cdbf20 (diff)
downloadx-5b1a06499de6bbc1a11601796b3d507e3cded6da.tar.xz
x-5b1a06499de6bbc1a11601796b3d507e3cded6da.zip
oops
Signed-off-by: Xe Iaso <me@xeiaso.net>
-rw-r--r--.github/workflows/package-builds.yml2
-rw-r--r--web/ollama/hallucinate.go119
-rw-r--r--web/pocketid/pocketid.go1929
3 files changed, 2049 insertions, 1 deletions
diff --git a/.github/workflows/package-builds.yml b/.github/workflows/package-builds.yml
index 7a71696..4d55f39 100644
--- a/.github/workflows/package-builds.yml
+++ b/.github/workflows/package-builds.yml
@@ -5,7 +5,7 @@ on:
types: [published]
permissions:
- contents: read
+ contents: write
actions: write
jobs:
diff --git a/web/ollama/hallucinate.go b/web/ollama/hallucinate.go
new file mode 100644
index 0000000..ac5a7f3
--- /dev/null
+++ b/web/ollama/hallucinate.go
@@ -0,0 +1,119 @@
+//go:build ignore
+
+package ollama
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "log/slog"
+ "net/http"
+ "time"
+
+ "github.com/swaggest/jsonschema-go"
+ "within.website/x/valid"
+ "within.website/x/web"
+)
+
+// Hallucinate prompts the model to hallucinate a "valid" JSON response to the given input.
+func Hallucinate[T valid.Interface](ctx context.Context, c *Client, opts HallucinateOpts) (*T, error) {
+ reflector := jsonschema.Reflector{}
+
+ schema, err := reflector.Reflect(new(T))
+ if err != nil {
+ return nil, err
+ }
+
+ inp := &CompleteRequest{
+ Model: opts.Model,
+ Messages: opts.Messages,
+ Format: p("json"),
+ Stream: true,
+ KeepAlive: (9999 * time.Minute).String(),
+ }
+ tries := 0
+ for tries <= 5 {
+ tries++
+
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+ buf := &bytes.Buffer{}
+ if err := json.NewEncoder(buf).Encode(inp); err != nil {
+ return nil, fmt.Errorf("ollama: error encoding request: %w", err)
+ }
+
+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/api/chat", buf)
+ if err != nil {
+ return nil, fmt.Errorf("ollama: error creating request: %w", err)
+ }
+
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Accept", "application/json")
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("ollama: error making request: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, web.NewError(http.StatusOK, resp)
+ }
+
+ whitespaceCount := 0
+
+ dec := json.NewDecoder(resp.Body)
+ buf = bytes.NewBuffer(nil)
+
+ for {
+ var cr CompleteResponse
+ err := dec.Decode(&cr)
+ if err != nil {
+ if !errors.Is(err, io.EOF) {
+ return nil, fmt.Errorf("ollama: error decoding response: %w", err)
+ } else {
+ break
+ }
+ }
+
+ //slog.Debug("got response", "response", cr.Message.Content)
+
+ if _, err := fmt.Fprint(buf, cr.Message.Content); err != nil {
+ return nil, fmt.Errorf("ollama: error writing response to buffer: %w", err)
+ }
+
+ for _, r := range cr.Message.Content {
+ if r == '\n' {
+ whitespaceCount++
+ }
+ }
+
+ if whitespaceCount > 10 {
+ cancel()
+ }
+
+ //slog.Debug("buffer is now", "buffer", buf.String())
+
+ var result T
+ if err := json.NewDecoder(bytes.NewBuffer(buf.Bytes())).Decode(&result); err != nil {
+ //slog.Debug("error decoding response", "err", err)
+ continue
+ }
+
+ if err := result.Valid(); err != nil {
+ slog.Debug("error validating response", "err", err)
+ continue
+ }
+
+ //slog.Debug("got valid response", "response", result)
+ cancel()
+
+ return &result, nil
+ }
+ }
+
+ return nil, fmt.Errorf("ollama: failed to hallucinate a valid response after 5 tries")
+}
diff --git a/web/pocketid/pocketid.go b/web/pocketid/pocketid.go
new file mode 100644
index 0000000..60066d6
--- /dev/null
+++ b/web/pocketid/pocketid.go
@@ -0,0 +1,1929 @@
+// Package pocketid provides a Go client for interacting with the PocketID API.
+//
+// PocketID is an identity provider for your homelab. This client allows you to
+// manage users, groups, OIDC clients, API keys, and more.
+//
+// The client uses a standard HTTP client for all API interactions, allowing for
+// customization of timeouts, transport, and other settings.
+//
+// This was generated from the Swagger docs with Google Gemini.
+package pocketid
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "mime/multipart"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// Client is the main client for interacting with the PocketID API.
+//
+// Use NewClient to create a new instance.
+type Client struct {
+ // BaseURL is the base URL for the PocketID API.
+ //
+ // For example: "https://pocket-id.example.com".
+ BaseURL string
+
+ // HTTPClient is the underlying HTTP client used for API requests.
+ //
+ // This allows for customization of timeouts, transport, and other settings.
+ HTTPClient *http.Client
+
+ // apiKey is the API key used for authentication.
+ apiKey string
+}
+
+// NewClient creates a new PocketID client.
+//
+// baseURL is the base URL for the PocketID API (e.g., "https://pocket-id.example.com").
+// apiKey is the API key to use for authentication.
+// httpClient is an optional *http.Client to use for requests. If nil, http.DefaultClient will be used.
+func NewClient(baseURL string, apiKey string, httpClient *http.Client) *Client {
+ if httpClient == nil {
+ httpClient = http.DefaultClient
+ }
+
+ return &Client{
+ BaseURL: baseURL,
+ HTTPClient: httpClient,
+ apiKey: apiKey,
+ }
+}
+
+// ErrorResponse represents a generic error response from the PocketID API.
+type ErrorResponse struct {
+ Error string `json:"error"`
+}
+
+// setAPIKeyHeader sets the X-API-KEY header on the given request.
+func (c *Client) setAPIKeyHeader(req *http.Request) {
+ if c.apiKey != "" {
+ req.Header.Set("X-API-KEY", c.apiKey)
+ }
+}
+
+// Helper to decode to a Paginated type
+func decodePaginated[T any](resp *http.Response, paginated *Paginated[T]) error {
+ return json.NewDecoder(resp.Body).Decode(paginated)
+}
+
+// request performs an HTTP request to the PocketID API.
+func (c *Client) request(method, path string, body io.Reader, contentType string) (*http.Response, error) {
+ req, err := http.NewRequest(method, fmt.Sprintf("%s%s", c.BaseURL, path), body)
+ if err != nil {
+ return nil, err
+ }
+ if contentType != "" {
+ req.Header.Set("Content-Type", contentType)
+ }
+ c.setAPIKeyHeader(req)
+
+ resp, err := c.HTTPClient.Do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ return resp, nil
+}
+
+// --- Well Known ---
+
+// GetJWKS returns the JSON Web Key Set used for token verification.
+//
+// See https://pocket-id.example.com/.well-known/jwks.json
+func (c *Client) GetJWKS() (map[string]interface{}, error) {
+ resp, err := c.request("GET", "/.well-known/jwks.json", nil, "")
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
+ }
+
+ var jwks map[string]interface{}
+ err = json.NewDecoder(resp.Body).Decode(&jwks)
+ if err != nil {
+ return nil, err
+ }
+
+ return jwks, nil
+}
+
+// GetOpenIDConfiguration returns the OpenID Connect discovery document.
+//
+// See https://pocket-id.example.com/.well-known/openid-configuration
+func (c *Client) GetOpenIDConfiguration() (map[string]interface{}, error) {
+ resp, err := c.request("GET", "/.well-known/openid-configuration", nil, "")
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
+ }
+
+ var config map[string]interface{}
+ err = json.NewDecoder(resp.Body).Decode(&config)
+ if err != nil {
+ return nil, err
+ }
+
+ return config, nil
+}
+
+// --- API Keys ---
+
+// ApiKeyCreateDto represents the request body for creating a new API key.
+type ApiKeyCreateDto struct {
+ Name string `json:"name" validate:"required,min=3,max=50"`
+ Description string `json:"description"`
+ ExpiresAt time.Time `json:"expiresAt" validate:"required"`
+}
+
+// Valid validates the ApiKeyCreateDto fields.
+func (t ApiKeyCreateDto) Valid() error {
+ var errs []error
+
+ if len(t.Name) < 3 || len(t.Name) > 50 {
+ errs = append(errs, fmt.Errorf("name must be between 3 and 50 characters"))
+ }
+
+ if t.ExpiresAt.IsZero() {
+ errs = append(errs, fmt.Errorf("expiresAt is required"))
+ }
+
+ if len(errs) != 0 {
+ return fmt.Errorf("validation errors: %v", errs)
+ }
+
+ return nil
+}
+
+// ApiKeyDto represents an API key.
+type ApiKeyDto struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ CreatedAt string `json:"createdAt"`
+ ExpiresAt string `json:"expiresAt"`
+ LastUsedAt string `json:"lastUsedAt"`
+}
+
+// ApiKeyResponseDto represents the response body when creating a new API key.
+type ApiKeyResponseDto struct {
+ ApiKey *ApiKeyDto `json:"apiKey"`
+ Token string `json:"token"`
+}
+
+// Paginated represents a paginated response.
+type Paginated[T any] struct {
+ Data []T `json:"data"`
+ Pagination Pagination `json:"pagination"`
+}
+
+// Pagination represents pagination metadata.
+type Pagination struct {
+ CurrentPage int `json:"currentPage"`
+ ItemsPerPage int `json:"itemsPerPage"`
+ TotalItems int `json:"totalItems"`
+ TotalPages int `json:"totalPages"`
+}
+
+// ListAPIKeys gets a paginated list of API keys belonging to the current user.
+//
+// See https://pocket-id.example.com/api-keys
+func (c *Client) ListAPIKeys(page, limit int, sortColumn, sortDirection string) (*Paginated[ApiKeyDto], error) {
+ params := url.Values{}
+ params.Set("page", strconv.Itoa(page))
+ params.Set("limit", strconv.Itoa(limit))
+ params.Set("sort_column", sortColumn)
+ params.Set("sort_direction", sortDirection)
+
+ resp, err := c.request("GET", fmt.Sprintf("/api-keys?%s", params.Encode()), nil, "")
+
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
+ }
+
+ var apiKeys Paginated[ApiKeyDto]
+ if err := decodePaginated(resp, &apiKeys); err != nil {
+ return nil, err
+ }
+
+ return &apiKeys, nil
+}
+
+// CreateAPIKey creates a new API key for the current user.
+//
+// See https://pocket-id.example.com/api-keys
+func (c *Client) CreateAPIKey(apiKeyCreateDto ApiKeyCreateDto) (*ApiKeyResponseDto, error) {
+ if err := apiKeyCreateDto.Valid(); err != nil {
+ return nil, err
+ }
+
+ body, err := json.Marshal(apiKeyCreateDto)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := c.request("POST", "/api-keys", bytes.NewBuffer(body), "application/json")
+
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusCreated {
+ return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
+ }
+
+ var apiKeyResponseDto ApiKeyResponseDto
+ err = json.NewDecoder(resp.Body).Decode(&apiKeyResponseDto)
+ if err != nil {
+ return nil, err
+ }
+
+ return &apiKeyResponseDto, nil
+}
+
+// RevokeAPIKey revokes (deletes) an existing API key by ID.
+//
+// See https://pocket-id.example.com/api-keys/{id}
+func (c *Client) RevokeAPIKey(id string) error {
+ resp, err := c.request("DELETE", fmt.Sprintf("/api-keys/%s", id), nil, "")
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusNoContent {
+ return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
+ }
+
+ return nil
+}
+
+// --- Application Configuration ---
+
+// AppConfigUpdateDto represents the request body for updating application configuration.
+type AppConfigUpdateDto struct {
+ AppName string `json:"appName" validate:"required,min=1,max=30"`
+ AllowOwnAccountEdit string `json:"allowOwnAccountEdit" validate:"required"`
+ EmailLoginNotificationEnabled string `json:"emailLoginNotificationEnabled" validate:"required"`
+ EmailOneTimeAccessEnabled string `json:"emailOneTimeAccessEnabled" validate:"required"`
+ EmailsVerified string `json:"emailsVerified" validate:"required"`
+ LdapEnabled string `json:"ldapEnabled" validate:"required"`
+ SessionDuration string `json:"sessionDuration" validate:"required"`
+ SmtpTls string `json:"smtpTls" validate:"required,oneof=none starttls tls"`
+ LdapAttributeAdminGroup string `json:"ldapAttributeAdminGroup"`
+ LdapAttributeGroupMember string `json:"ldapAttributeGroupMember"`
+ LdapAttributeGroupName string `json:"ldapAttributeGroupName"`
+ LdapAttributeGroupUniqueIdentifier string `json:"ldapAttributeGroupUniqueIdentifier"`
+ LdapAttributeUserEmail string `json:"ldapAttributeUserEmail"`
+ LdapAttributeUserFirstName string `json:"ldapAttributeUserFirstName"`
+ LdapAttributeUserLastName string `json:"ldapAttributeUserLastName"`
+ LdapAttributeUserProfilePicture string `json:"ldapAttributeUserProfilePicture"`
+ LdapAttributeUserUniqueIdentifier string `json:"ldapAttributeUserUniqueIdentifier"`
+ LdapAttributeUserUsername string `json:"ldapAttributeUserUsername"`
+ LdapBase string `json:"ldapBase"`
+ LdapBindDn string `json:"ldapBindDn"`
+ LdapBindPassword string `json:"ldapBindPassword"`
+ LdapSkipCertVerify string `json:"ldapSkipCertVerify"`
+ LdapUrl string `json:"ldapUrl"`
+ LdapUserGroupSearchFilter string `json:"ldapUserGroupSearchFilter"`
+ LdapUserSearchFilter string `json:"ldapUserSearchFilter"`
+ SmtpFrom string `json:"smtpFrom"`
+ SmtpHost string `json:"smtpHost"`
+ SmtpPassword string `json:"smtpPassword"`
+ SmtpPort string `json:"smtpPort"`
+ SmtpSkipCertVerify string `json:"smtpSkipCertVerify"`
+ SmtpUser string `json:"smtpUser"`
+}
+
+// AppConfigVariableDto represents an application configuration variable.
+type AppConfigVariableDto struct {
+ IsPublic bool `json:"isPublic"`
+ Key string `json:"key"`
+ Type string `json:"type"`
+ Value string `json:"value"`
+}
+
+// PublicAppConfigVariableDto represents a public application configuration variable.
+type PublicAppConfigVariableDto struct {
+ Key string `json:"key"`
+ Type string `json:"type"`
+ Value string `json:"value"`
+}
+
+// ListPublicAppConfig gets all public application configurations.
+//
+// See https://pocket-id.example.com/application-configuration
+func (c *Client) ListPublicAppConfig() ([]PublicAppConfigVariableDto, error) {
+ resp, err := c.request("GET", "/application-configuration", nil, "")
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
+ }
+
+ var config []PublicAppConfigVariableDto
+ err = json.NewDecoder(resp.Body).Decode(&config)
+ if err != nil {
+ return nil, err
+ }
+
+ return config, nil
+}
+
+// UpdateAppConfig updates application configuration settings.
+//
+// See https://pocket-id.example.com/application-configuration
+func (c *Client) UpdateAppConfig(config AppConfigUpdateDto) ([]AppConfigVariableDto, error) {
+ body, err := json.Marshal(config)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := c.request("PUT", "/application-configuration", bytes.NewBuffer(body), "application/json")
+
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
+ }
+
+ var updatedConfig []AppConfigVariableDto
+ err = json.NewDecoder(resp.Body).Decode(&updatedConfig)
+ if err != nil {
+ return nil, err
+ }
+
+ return updatedConfig, nil
+}
+
+// ListAllAppConfig gets all application configurations, including private ones.
+//
+// See https://pocket-id.example.com/application-configuration/all
+func (c *Client) ListAllAppConfig() ([]AppConfigVariableDto, error) {
+ resp, err := c.request("GET", "/application-configuration/all", nil, "")
+
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
+ }
+
+ var config []AppConfigVariableDto
+ err = json.NewDecoder(resp.Body).Decode(&config)
+ if err != nil {
+ return nil, err
+ }
+
+ return config, nil
+}
+
+// GetBackgroundImage gets the background image for the application.
+//
+// See https://pocket-id.example.com/application-configuration/background-image
+func (c *Client) GetBackgroundImage() ([]byte, error) {
+ resp, err := c.request("GET", "/application-configuration/background-image", nil, "")
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("unexpected status code: %d - %w", resp.StatusCode, err)
+ }
+
+ return io.ReadAll(resp.Body)
+}
+
+// UpdateBackgroundImage updates the application background image.
+//
+// See https://pocket-id.example.com/application-configuration/background-image
+func (c *Client) UpdateBackgroundImage(file io.Reader) error {
+ var b bytes.Buffer
+ w := multipart.NewWriter(&b)
+ fw, err := w.CreateFormFile("file", "background.png") // filename doesn't matter for this endpoint
+ if err != nil {
+ return err
+ }
+ if _, err = io.Copy(fw, file); err != nil {
+ return err
+ }
+ w.Close()
+
+ resp, err := c.request("PUT", "/application-configuration/background-image", &b, w.FormDataContentType())
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusNoContent {
+ return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
+ }
+ return nil
+}
+
+// GetFavicon gets the favicon for the application.
+//
+// See https://pocket-id.example.com/application-configuration/favicon
+func (c *Client) GetFavicon() ([]byte, error) {
+ resp, err := c.request("GET", "/application-configuration/favicon", nil, "")
+
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
+ }
+
+ return io.ReadAll(resp.Body)
+}
+
+// UpdateFavicon updates the application favicon.
+//
+// See https://pocket-id.example.com/application-configuration/favicon
+func (c *Client) UpdateFavicon(file io.Reader) error {
+ var b bytes.Buffer
+ w := multipart.NewWriter(&b)
+ fw, err := w.CreateFormFile("file", "favicon.ico")
+ if err != nil {
+ return err
+ }
+ if _, err = io.Copy(fw, file); err != nil {
+ return err
+ }
+ w.Close()
+
+ resp, err := c.request("PUT", "/application-configuration/favicon", &b, w.FormDataContentType())
+
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusNoContent {
+ return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
+ }
+ return nil
+}
+
+// GetLogo gets the logo image for the application. If isLight is true, the light mode logo is returned.
+//
+// See https://pocket-id.example.com/application-configuration/logo
+func (c *Client) GetLogo(isLight bool) ([]byte, error) {
+ params := url.Values{}
+ params.Set("light", strconv.FormatBool(isLight))
+ resp, err := c.request("GET", fmt.Sprintf("/application-configuration/logo?%s", params.Encode()), nil, "")
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
+ }
+ return io.ReadAll(resp.Body)
+}
+
+// UpdateLogo updates the application logo. If isLight is true, the light mode logo is updated.
+//
+// See https://pocket-id.example.com/application-configuration/logo
+func (c *Client) UpdateLogo(file io.Reader, isLight bool) error {
+ var b bytes.Buffer
+ w := multipart.NewWriter(&b)
+
+ // Add the 'light' parameter as a form field
+ if err := w.WriteField("light", strconv.FormatBool(isLight)); err != nil {
+ return err
+ }
+
+ fw, err := w.CreateFormFile("file", "logo.png") // filename doesn't matter for this endpoint
+ if err != nil {
+ return err
+ }
+ if _, err = io.Copy(fw, file); err != nil {
+ return err
+ }
+ w.Close()
+
+ resp, err := c.request("PUT", "/application-configuration/logo", &b, w.FormDataContentType())
+
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusNoContent {
+ return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
+ }
+ return nil
+}
+
+// SyncLDAP manually triggers LDAP synchronization.
+//
+// See https://pocket-id.example.com/application-configuration/sync-ldap
+func (c *Client) SyncLDAP() error {
+ resp, err := c.request("POST", "/application-configuration/sync-ldap", nil, "")
+
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusNoContent {
+ return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
+ }
+ return nil
+}
+
+// SendTestEmail sends a test email to verify email configuration.
+//
+// See https://pocket-id.example.com/application-configuration/test-email
+func (c *Client) SendTestEmail() error {
+ resp, err := c.request("POST", "/application-configuration/test-email", nil, "")
+
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusNoContent {
+ return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
+ }
+ return nil
+}
+
+// --- Audit Logs ---
+
+// AuditLogDto represents an audit log entry.
+type AuditLogDto struct {
+ ID string `json:"id"`
+ UserID string `json:"userID"`
+ Event AuditLogEvent `json:"event"`
+ IPAddress string `json:"ipAddress"`
+ Device string `json:"device"`
+ City string `json:"city"`
+ Country string `json:"country"`
+ CreatedAt string `json:"createdAt"`
+ Data AuditLogData `json:"data"` // Custom type for nested JSON object
+}
+
+// AuditLogEvent represents the type of event in an audit log.
+type AuditLogEvent string
+
+// Constants for AuditLogEvent.
+const (
+ AuditLogEventSignIn AuditLogEvent = "SIGN_IN"
+ AuditLogEventOneTimeAccessTokenSignIn AuditLogEvent = "TOKEN_SIGN_IN"
+ AuditLogEventClientAuthorization AuditLogEvent = "CLIENT_AUTHORIZATION"
+ AuditLogEventNewClientAuthorization AuditLogEvent = "NEW_CLIENT_AUTHORIZATION"
+)
+
+// AuditLogData is a custom type representing additional data in the AuditLogDto.
+type AuditLogData map[string]string
+
+// ListAuditLogs gets a paginated list of audit logs for the current user.
+//
+// See https://pocket-id.example.com/audit-logs
+func (c *Client) ListAuditLogs(page, limit int, sortColumn, sortDirection string) (*Paginated[AuditLogDto], error) {
+ params := url.Values{}
+ params.Set("page", strconv.Itoa(page))
+ params.Set("limit", strconv.Itoa(limit))
+ params.Set("sort_column", sortColumn)
+ params.Set("sort_direction", sortDirection)
+
+ resp, err := c.request("GET", fmt.Sprintf("/audit-logs?%s", params.Encode()), nil, "")
+
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
+ }
+
+ var auditLogs Paginated[AuditLogDto]
+ err = decodePaginated(resp, &auditLogs)
+ if err != nil {
+ return nil, err
+ }
+
+ return &auditLogs, nil
+}
+
+// --- Custom Claims ---
+
+// CustomClaimCreateDto represents the request body for creating or updating custom claims.
+type CustomClaimDto struct {
+ Key string `json:"key" validate:"required"`
+ Value string `json:"value" validate:"required"`
+}
+
+// Valid validates the CustomClaimCreateDto fields.
+func (t CustomClaimDto) Valid() error {
+ var errs []error
+
+ if t.Key == "" {
+ errs = append(errs, fmt.Errorf("key is required"))
+ }
+
+ if t.Value == "" {
+ errs = append(errs, fmt.Errorf("value is required"))
+ }
+
+ if len(errs) != 0 {
+ return fmt.Errorf("validation errors: %v", errs)
+ }
+
+ return nil
+}
+
+// GetCustomClaimSuggestions gets a list of suggested custom claim names.
+//
+// See https://pocket-id.example.com/custom-claims/suggestions
+func (c *Client) GetCustomClaimSuggestions() ([]string, error) {
+ resp, err := c.request("GET", "/custom-claims/suggestions", nil, "")
+
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
+ }
+
+ var suggestions []string
+ err = json.NewDecoder(resp.Body).Decode(&suggestions)
+ if err != nil {
+ return nil, err
+ }
+
+ return suggestions, nil
+}
+
+// UpdateCustomClaimsForUserGroup updates or creates custom claims for a specific user group.
+//
+// See https://pocket-id.example.com/custom-claims/user-group/{userGroupId}
+func (c *Client) UpdateCustomClaimsForUserGroup(userGroupID string, claims []CustomClaimDto) ([]CustomClaimDto, error) {
+ body, err := json.Marshal(claims)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := c.request("PUT", fmt.Sprintf("/custom-claims/user-group/%s", userGroupID), bytes.NewBuffer(body), "application/json")
+
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
+ }
+
+ var updatedClaims []CustomClaimDto
+ err = json.NewDecoder(resp.Body).Decode(&updatedClaims)
+ if err != nil {
+ return nil, err
+ }
+
+ return updatedClaims, nil
+}
+
+// UpdateCustomClaimsForUser updates or creates custom claims for a specific user.
+//
+// See https://pocket-id.example.com/custom-claims/user/{userId}
+func (c *Client) UpdateCustomClaimsForUser(userID string, claims []CustomClaimDto) ([]CustomClaimDto, error) {
+ body, err := json.Marshal(claims)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := c.request("PUT", fmt.Sprintf("/custom-claims/user/%s", userID), bytes.NewBuffer(body), "application/json")
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
+ }
+
+ var updatedClaims []CustomClaimDto
+ err = json.NewDecoder(resp.Body).Decode(&updatedClaims)
+ if err != nil {
+ return nil, err
+ }
+
+ return updatedClaims, nil
+}
+
+// --- OIDC ---
+
+// AuthorizationRequiredDto is used to check if authorization is required.
+type AuthorizationRequiredDto struct {
+ ClientID string `json:"clientID" validate:"required"`
+ Scope string `json:"scope" validate:"required"`
+}
+
+// AuthorizeOidcClientRequestDto represents the request body for authorizing an OIDC client.
+type AuthorizeOidcClientRequestDto struct {
+ ClientID string `json:"clientID" validate:"required"`
+ Scope string `json:"scope" validate:"required"`
+ CallbackURL string `json:"callbackURL"`
+ CodeChallenge string `json:"codeChallenge"`
+ CodeChallengeMethod string `json:"codeChallengeMethod"`
+ Nonce string `json:"nonce"`
+}
+
+// AuthorizeOidcClientResponseDto represents the response body for authorizing an OIDC client.
+type AuthorizeOidcClientResponseDto struct {
+ CallbackURL string `json:"callbackURL"`
+ Code string `json:"code"`
+}
+
+// OidcClientCreateDto represents the request body for creating an OIDC client.
+type OidcClientCreateDto struct {
+ Name string `json:"name" validate:"required,max=50"`
+ CallbackURLs []string `json:"callbackURLs" validate:"required"`
+ IsPublic bool `json:"isPublic"`
+ LogoutCallbackURLs []string `json:"logoutCallbackURLs"`
+ PkceEnabled bool `json:"pkceEnabled"`
+}
+
+// OidcClientDto represents an OIDC client.
+type OidcClientDto struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ CallbackURLs []string `json:"callbackURLs"`
+ IsPublic bool `json:"isPublic"`
+ LogoutCallbackURLs []string `json:"logoutCallbackURLs"`
+ PkceEnabled bool `json:"pkceEnabled"`
+ HasLogo bool `json:"hasLogo"`
+}
+
+// OidcClientMetaDataDto represents OIDC client metadata.
+type OidcClientMetaDataDto struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ HasLogo bool `json:"hasLogo"`
+}
+
+// OidcClientWithAllowedUserGroupsDto represents an OIDC client with allowed user groups.
+type OidcClientWithAllowedUserGroupsDto struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ CallbackURLs []string `json:"callbackURLs"`
+ IsPublic bool `json:"isPublic"`
+ LogoutCallbackURLs []string `json:"logoutCallbackURLs"`
+ PkceEnabled bool `json:"pkceEnabled"`
+ HasLogo bool `json:"hasLogo"`
+ AllowedUserGroups []UserGroupDtoWithUserCount `json:"allowedUserGroups"`
+}
+
+// UserGroupDtoWithUserCount represents a user group with a user count.
+type UserGroupDtoWithUserCount struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ FriendlyName string `json:"friendlyName"`
+ CreatedAt string `json:"createdAt"`
+ LdapId string `json:"ldapId"`
+ CustomClaims []CustomClaimDto `json:"customClaims"`
+ UserCount int `json:"userCount"`
+}
+
+// OidcUpdateAllowedUserGroupsDto represents the request body for updating allowed user groups.
+type OidcUpdateAllowedUserGroupsDto struct {
+ UserGroupIds []string `json:"userGroupIds" validate:"required"`
+}
+
+// CheckAuthorizationRequired checks if the user needs to confirm authorization for the client.
+//
+// See https://pocket-id.example.com/oidc/authorization-required
+func (c *Client) CheckAuthorizationRequired(request AuthorizationRequiredDto) (bool, error) {
+ body, err := json.Marshal(request)
+ if err != nil {
+ return false, err
+ }
+
+ resp, err := c.request("POST", "/oidc/authorization-required", bytes.NewBuffer(body), "application/json")
+ if err != nil {
+ return false, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
+ }
+
+ var response struct {
+ AuthorizationRequired bool `json:"authorizationRequired"`
+ }
+ err = json.NewDecoder(resp.Body).Decode(&response)
+ if err != nil {
+ return false, err
+ }
+
+ return response.AuthorizationRequired, nil
+}
+
+// AuthorizeOIDCClient starts the OIDC authorization process for a client.
+//
+// See https://pocket-id.example.com/oidc/authorize
+func (c *Client) AuthorizeOIDCClient(request AuthorizeOidcClientRequestDto) (*AuthorizeOidcClientResponseDto, error) {
+ body, err := json.Marshal(request)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := c.request("POST", "/oidc/authorize", bytes.NewBuffer(body), "application/json")
+
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
+ }
+
+ var responseDto AuthorizeOidcClientResponseDto
+ err = json.NewDecoder(resp.Body).Decode(&responseDto)
+ if err != nil {
+ return nil, err
+ }
+
+ return &responseDto, nil
+}
+
+// ListOIDCClients gets a paginated list of OIDC clients.
+//
+// See https://pocket-id.example.com/oidc/clients
+func (c *Client) ListOIDCClients(search string, page, limit int, sortColumn, sortDirection string) (*Paginated[OidcClientDto], error) {
+ params := url.Values{}
+ params.Set("search", search)
+ params.Set("page", strconv.Itoa(page))
+ params.Set("limit", strconv.Itoa(limit))
+ params.Set("sort_column", sortColumn)
+ params.Set("sort_direction", sortDirection)
+
+ resp, err := c.request("GET", fmt.Sprintf("/oidc/clients?%s", params.Encode()), nil, "")
+
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
+ }
+
+ var clients Paginated[OidcClientDto]
+ err = decodePaginated(resp, &clients)
+ if err != nil {
+ return nil, err
+ }
+
+ return &clients, nil
+}
+
+// CreateOIDCClient creates a new OIDC client.
+//
+// See https://pocket-id.example.com/oidc/clients
+func (c *Client) CreateOIDCClient(clientCreateDto OidcClientCreateDto) (*OidcClientWithAllowedUserGroupsDto, error) {
+ body, err := json.Marshal(clientCreateDto)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := c.request("POST", "/oidc/clients", bytes.NewBuffer(body), "application/json")
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusCreated {
+ return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
+ }
+
+ var clientDto OidcClientWithAllowedUserGroupsDto
+ err = json.NewDecoder(resp.Body).Decode(&clientDto)
+ if err != nil {
+ return nil, err
+ }
+
+ return &clientDto, nil
+}
+
+// DeleteOIDCClient deletes an OIDC client by ID.
+//
+// See https://pocket-id.example.com/oidc/clients/{id}
+func (c *Client) DeleteOIDCClient(id string) error {
+ resp, err := c.request("DELETE", fmt.Sprintf("/oidc/clients/%s", id), nil, "")
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusNoContent {
+ return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
+ }
+
+ return nil
+}
+
+// GetOIDCClient gets deta