536dbcaebe
Users now have an avatar. The account page takes an image, lets the user
drag and zoom it under a circular mask, and uploads exactly what the circle
shows as a 256x256 PNG. The picture replaces the initial-letter circle in
the sidebar, on the account page, on the on-call hero and week strip, on
the schedule's day chips and in the incident assign menu; someone without
one keeps the initial.
The picture lives in Postgres (user_avatars, bytea) rather than on a
volume: the chart runs two replicas on a read-only root filesystem, and
the k8up pg_dump backup then covers it without any chart or operator
change. The server does not trust the client's crop. It decodes the PNG or
JPEG, checks the declared size before decoding, centre-crops to a square,
scales to 256 and re-encodes, which drops EXIF and anything that is not
pixels. Standard library only; WebP is cropped by the browser and arrives
as PNG, a raw WebP sent to the API is refused.
GET/PUT/DELETE /api/users/{id}/avatar: anyone signed in may read, only the
owner or an administrator may change. /api/me and /api/users gain
avatar_version, absent when there is no picture, which is also the ?v= that
busts the cache after a change. Incident timeline entries and the queue are
text-only and are unchanged.
Claude-Session: https://claude.ai/code/session_016mBLURvJoMuUEr9cB2RpUN
69 lines
2.8 KiB
Go
69 lines
2.8 KiB
Go
package models
|
|
|
|
import "time"
|
|
|
|
type User struct {
|
|
ID int64 `json:"id"`
|
|
Username string `json:"username"`
|
|
Email string `json:"email"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
|
|
// Channels are where this user's notifications go. Empty means they get none
|
|
// of their own; incidents assigned to them fall back to the configured
|
|
// fallback target instead. Only filled in for the user themselves and for an
|
|
// administrator, and the address only for the user themselves.
|
|
Channels []NotificationChannel `json:"channels,omitempty"`
|
|
|
|
// DisabledAt is when the account was taken out of use, or nil. A disabled
|
|
// user cannot authenticate by either credential, and keeps their name on
|
|
// every acknowledgement and timeline entry they made.
|
|
DisabledAt *time.Time `json:"disabled_at,omitempty"`
|
|
|
|
// AvatarVersion identifies the user's current profile picture, or is nil
|
|
// when they have none and clients show their initials. It changes on every
|
|
// upload, so it goes in the image URL (?v=) as a cache buster.
|
|
AvatarVersion *int64 `json:"avatar_version,omitempty"`
|
|
|
|
// IsAdmin is the system administrator flag: managing users and API keys.
|
|
// Not omitempty — a client has to be able to tell "false" from "this server
|
|
// is too old to have the field", and the web UI decides what to show from
|
|
// it.
|
|
IsAdmin bool `json:"is_admin"`
|
|
|
|
// AdminSource is who granted the flag: "manual" or "oidc". An "oidc"
|
|
// administrator follows the identity provider's groups, so the UI shows it as
|
|
// managed there and the API refuses to revoke it by hand. Only set on the
|
|
// user endpoints that show it.
|
|
AdminSource string `json:"admin_source,omitempty"`
|
|
}
|
|
|
|
// NotificationChannel is one way of reaching a user: a provider type and an
|
|
// address that provider understands.
|
|
type NotificationChannel struct {
|
|
ID int64 `json:"id"`
|
|
Type string `json:"type"`
|
|
|
|
// Address is an ntfy topic, later an email address. Blank unless the caller is
|
|
// the owner: it can be a secret, and an administrator has no need of it.
|
|
Address string `json:"address,omitempty"`
|
|
Enabled bool `json:"enabled"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
type APIKey struct {
|
|
ID int64 `json:"id"`
|
|
UserID int64 `json:"user_id"`
|
|
Name string `json:"name"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
|
|
|
|
// ExpiresAt is nil for a key that never expires, which is every key
|
|
// created before this field existed and still the default for a new one
|
|
// unless its creator asks otherwise (see handleCreateAPIKey's
|
|
// expires_in_days). AuthMiddleware stops accepting a key once this
|
|
// passes; nothing deletes the row for it.
|
|
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
|
|
|
Key string `json:"key,omitempty"` // populated only on creation, never stored
|
|
}
|