@@ -11,6 +11,33 @@ import ( |
| 11 | 11 | "github.com/jackc/pgx/v5/pgtype" |
| 12 | 12 | ) |
| 13 | 13 | |
| 14 | +const bumpUserSessionEpoch = `-- name: BumpUserSessionEpoch :exec |
| 15 | +UPDATE users SET session_epoch = session_epoch + 1 WHERE id = $1 |
| 16 | +` |
| 17 | + |
| 18 | +func (q *Queries) BumpUserSessionEpoch(ctx context.Context, db DBTX, id int64) error { |
| 19 | + _, err := db.Exec(ctx, bumpUserSessionEpoch, id) |
| 20 | + return err |
| 21 | +} |
| 22 | + |
| 23 | +const countRecentUsernameChanges = `-- name: CountRecentUsernameChanges :one |
| 24 | +SELECT count(*) FROM username_redirects |
| 25 | +WHERE user_id = $1 AND changed_at > $2 |
| 26 | +` |
| 27 | + |
| 28 | +type CountRecentUsernameChangesParams struct { |
| 29 | + UserID int64 |
| 30 | + ChangedAt pgtype.Timestamptz |
| 31 | +} |
| 32 | + |
| 33 | +// Drives the 3-changes-per-60d cap. |
| 34 | +func (q *Queries) CountRecentUsernameChanges(ctx context.Context, db DBTX, arg CountRecentUsernameChangesParams) (int64, error) { |
| 35 | + row := db.QueryRow(ctx, countRecentUsernameChanges, arg.UserID, arg.ChangedAt) |
| 36 | + var count int64 |
| 37 | + err := row.Scan(&count) |
| 38 | + return count, err |
| 39 | +} |
| 40 | + |
| 14 | 41 | const countUsers = `-- name: CountUsers :one |
| 15 | 42 | SELECT count(*) FROM users WHERE deleted_at IS NULL |
| 16 | 43 | ` |
@@ -26,7 +53,7 @@ const createUser = `-- name: CreateUser :one |
| 26 | 53 | |
| 27 | 54 | INSERT INTO users (username, display_name, password_hash) |
| 28 | 55 | VALUES ($1, $2, $3) |
| 29 | | -RETURNING id, username, display_name, primary_email_id, password_hash, password_algo, password_updated_at, email_verified, last_login_at, suspended_at, suspended_reason, deleted_at, created_at, updated_at, bio, location, website, company, pronouns, avatar_object_key |
| 56 | +RETURNING id, username, display_name, primary_email_id, password_hash, password_algo, password_updated_at, email_verified, last_login_at, suspended_at, suspended_reason, deleted_at, created_at, updated_at, bio, location, website, company, pronouns, avatar_object_key, theme, session_epoch |
| 30 | 57 | ` |
| 31 | 58 | |
| 32 | 59 | type CreateUserParams struct { |
@@ -60,12 +87,14 @@ func (q *Queries) CreateUser(ctx context.Context, db DBTX, arg CreateUserParams) |
| 60 | 87 | &i.Company, |
| 61 | 88 | &i.Pronouns, |
| 62 | 89 | &i.AvatarObjectKey, |
| 90 | + &i.Theme, |
| 91 | + &i.SessionEpoch, |
| 63 | 92 | ) |
| 64 | 93 | return i, err |
| 65 | 94 | } |
| 66 | 95 | |
| 67 | 96 | const getUserByID = `-- name: GetUserByID :one |
| 68 | | -SELECT id, username, display_name, primary_email_id, password_hash, password_algo, password_updated_at, email_verified, last_login_at, suspended_at, suspended_reason, deleted_at, created_at, updated_at, bio, location, website, company, pronouns, avatar_object_key |
| 97 | +SELECT id, username, display_name, primary_email_id, password_hash, password_algo, password_updated_at, email_verified, last_login_at, suspended_at, suspended_reason, deleted_at, created_at, updated_at, bio, location, website, company, pronouns, avatar_object_key, theme, session_epoch |
| 69 | 98 | FROM users |
| 70 | 99 | WHERE id = $1 AND deleted_at IS NULL |
| 71 | 100 | ` |
@@ -94,12 +123,14 @@ func (q *Queries) GetUserByID(ctx context.Context, db DBTX, id int64) (User, err |
| 94 | 123 | &i.Company, |
| 95 | 124 | &i.Pronouns, |
| 96 | 125 | &i.AvatarObjectKey, |
| 126 | + &i.Theme, |
| 127 | + &i.SessionEpoch, |
| 97 | 128 | ) |
| 98 | 129 | return i, err |
| 99 | 130 | } |
| 100 | 131 | |
| 101 | 132 | const getUserByUsername = `-- name: GetUserByUsername :one |
| 102 | | -SELECT id, username, display_name, primary_email_id, password_hash, password_algo, password_updated_at, email_verified, last_login_at, suspended_at, suspended_reason, deleted_at, created_at, updated_at, bio, location, website, company, pronouns, avatar_object_key |
| 133 | +SELECT id, username, display_name, primary_email_id, password_hash, password_algo, password_updated_at, email_verified, last_login_at, suspended_at, suspended_reason, deleted_at, created_at, updated_at, bio, location, website, company, pronouns, avatar_object_key, theme, session_epoch |
| 103 | 134 | FROM users |
| 104 | 135 | WHERE username = $1 AND deleted_at IS NULL |
| 105 | 136 | ` |
@@ -128,10 +159,92 @@ func (q *Queries) GetUserByUsername(ctx context.Context, db DBTX, username strin |
| 128 | 159 | &i.Company, |
| 129 | 160 | &i.Pronouns, |
| 130 | 161 | &i.AvatarObjectKey, |
| 162 | + &i.Theme, |
| 163 | + &i.SessionEpoch, |
| 164 | + ) |
| 165 | + return i, err |
| 166 | +} |
| 167 | + |
| 168 | +const getUserByUsernameIncludingDeleted = `-- name: GetUserByUsernameIncludingDeleted :one |
| 169 | +SELECT id, username, display_name, primary_email_id, password_hash, password_algo, password_updated_at, email_verified, last_login_at, suspended_at, suspended_reason, deleted_at, created_at, updated_at, bio, location, website, company, pronouns, avatar_object_key, theme, session_epoch FROM users WHERE username = $1 |
| 170 | +` |
| 171 | + |
| 172 | +func (q *Queries) GetUserByUsernameIncludingDeleted(ctx context.Context, db DBTX, username string) (User, error) { |
| 173 | + row := db.QueryRow(ctx, getUserByUsernameIncludingDeleted, username) |
| 174 | + var i User |
| 175 | + err := row.Scan( |
| 176 | + &i.ID, |
| 177 | + &i.Username, |
| 178 | + &i.DisplayName, |
| 179 | + &i.PrimaryEmailID, |
| 180 | + &i.PasswordHash, |
| 181 | + &i.PasswordAlgo, |
| 182 | + &i.PasswordUpdatedAt, |
| 183 | + &i.EmailVerified, |
| 184 | + &i.LastLoginAt, |
| 185 | + &i.SuspendedAt, |
| 186 | + &i.SuspendedReason, |
| 187 | + &i.DeletedAt, |
| 188 | + &i.CreatedAt, |
| 189 | + &i.UpdatedAt, |
| 190 | + &i.Bio, |
| 191 | + &i.Location, |
| 192 | + &i.Website, |
| 193 | + &i.Company, |
| 194 | + &i.Pronouns, |
| 195 | + &i.AvatarObjectKey, |
| 196 | + &i.Theme, |
| 197 | + &i.SessionEpoch, |
| 198 | + ) |
| 199 | + return i, err |
| 200 | +} |
| 201 | + |
| 202 | +const getUserIncludingDeleted = `-- name: GetUserIncludingDeleted :one |
| 203 | +SELECT id, username, display_name, primary_email_id, password_hash, password_algo, password_updated_at, email_verified, last_login_at, suspended_at, suspended_reason, deleted_at, created_at, updated_at, bio, location, website, company, pronouns, avatar_object_key, theme, session_epoch FROM users WHERE id = $1 |
| 204 | +` |
| 205 | + |
| 206 | +// Like GetUserByID but returns the row even when deleted_at IS NOT NULL. |
| 207 | +func (q *Queries) GetUserIncludingDeleted(ctx context.Context, db DBTX, id int64) (User, error) { |
| 208 | + row := db.QueryRow(ctx, getUserIncludingDeleted, id) |
| 209 | + var i User |
| 210 | + err := row.Scan( |
| 211 | + &i.ID, |
| 212 | + &i.Username, |
| 213 | + &i.DisplayName, |
| 214 | + &i.PrimaryEmailID, |
| 215 | + &i.PasswordHash, |
| 216 | + &i.PasswordAlgo, |
| 217 | + &i.PasswordUpdatedAt, |
| 218 | + &i.EmailVerified, |
| 219 | + &i.LastLoginAt, |
| 220 | + &i.SuspendedAt, |
| 221 | + &i.SuspendedReason, |
| 222 | + &i.DeletedAt, |
| 223 | + &i.CreatedAt, |
| 224 | + &i.UpdatedAt, |
| 225 | + &i.Bio, |
| 226 | + &i.Location, |
| 227 | + &i.Website, |
| 228 | + &i.Company, |
| 229 | + &i.Pronouns, |
| 230 | + &i.AvatarObjectKey, |
| 231 | + &i.Theme, |
| 232 | + &i.SessionEpoch, |
| 131 | 233 | ) |
| 132 | 234 | return i, err |
| 133 | 235 | } |
| 134 | 236 | |
| 237 | +const getUserSessionEpoch = `-- name: GetUserSessionEpoch :one |
| 238 | +SELECT session_epoch FROM users WHERE id = $1 |
| 239 | +` |
| 240 | + |
| 241 | +func (q *Queries) GetUserSessionEpoch(ctx context.Context, db DBTX, id int64) (int32, error) { |
| 242 | + row := db.QueryRow(ctx, getUserSessionEpoch, id) |
| 243 | + var session_epoch int32 |
| 244 | + err := row.Scan(&session_epoch) |
| 245 | + return session_epoch, err |
| 246 | +} |
| 247 | + |
| 135 | 248 | const linkUserPrimaryEmail = `-- name: LinkUserPrimaryEmail :exec |
| 136 | 249 | UPDATE users |
| 137 | 250 | SET primary_email_id = $2 |
@@ -163,6 +276,35 @@ func (q *Queries) MarkUserEmailPrimaryVerified(ctx context.Context, db DBTX, id |
| 163 | 276 | return err |
| 164 | 277 | } |
| 165 | 278 | |
| 279 | +const renameUser = `-- name: RenameUser :exec |
| 280 | +UPDATE users |
| 281 | +SET username = $2 |
| 282 | +WHERE id = $1 |
| 283 | +` |
| 284 | + |
| 285 | +type RenameUserParams struct { |
| 286 | + ID int64 |
| 287 | + Username string |
| 288 | +} |
| 289 | + |
| 290 | +// Wrapped by the username-change flow inside a tx that also writes |
| 291 | +// username_redirects, so the old name becomes a redirect target atomically. |
| 292 | +func (q *Queries) RenameUser(ctx context.Context, db DBTX, arg RenameUserParams) error { |
| 293 | + _, err := db.Exec(ctx, renameUser, arg.ID, arg.Username) |
| 294 | + return err |
| 295 | +} |
| 296 | + |
| 297 | +const restoreUserAccount = `-- name: RestoreUserAccount :exec |
| 298 | +UPDATE users SET deleted_at = NULL WHERE id = $1 |
| 299 | +` |
| 300 | + |
| 301 | +// Clears deleted_at; called when a user logs in within the 14-day grace |
| 302 | +// window. The login handler enforces the window check before calling. |
| 303 | +func (q *Queries) RestoreUserAccount(ctx context.Context, db DBTX, id int64) error { |
| 304 | + _, err := db.Exec(ctx, restoreUserAccount, id) |
| 305 | + return err |
| 306 | +} |
| 307 | + |
| 166 | 308 | const softDeleteUser = `-- name: SoftDeleteUser :exec |
| 167 | 309 | UPDATE users |
| 168 | 310 | SET deleted_at = now() |
@@ -202,6 +344,22 @@ func (q *Queries) TouchUserLastLogin(ctx context.Context, db DBTX, id int64) err |
| 202 | 344 | return err |
| 203 | 345 | } |
| 204 | 346 | |
| 347 | +const updateUserAvatarKey = `-- name: UpdateUserAvatarKey :exec |
| 348 | +UPDATE users |
| 349 | +SET avatar_object_key = $2 |
| 350 | +WHERE id = $1 |
| 351 | +` |
| 352 | + |
| 353 | +type UpdateUserAvatarKeyParams struct { |
| 354 | + ID int64 |
| 355 | + AvatarObjectKey pgtype.Text |
| 356 | +} |
| 357 | + |
| 358 | +func (q *Queries) UpdateUserAvatarKey(ctx context.Context, db DBTX, arg UpdateUserAvatarKeyParams) error { |
| 359 | + _, err := db.Exec(ctx, updateUserAvatarKey, arg.ID, arg.AvatarObjectKey) |
| 360 | + return err |
| 361 | +} |
| 362 | + |
| 205 | 363 | const updateUserPassword = `-- name: UpdateUserPassword :exec |
| 206 | 364 | UPDATE users |
| 207 | 365 | SET password_hash = $2, |
@@ -220,3 +378,51 @@ func (q *Queries) UpdateUserPassword(ctx context.Context, db DBTX, arg UpdateUse |
| 220 | 378 | _, err := db.Exec(ctx, updateUserPassword, arg.ID, arg.PasswordHash, arg.PasswordAlgo) |
| 221 | 379 | return err |
| 222 | 380 | } |
| 381 | + |
| 382 | +const updateUserProfile = `-- name: UpdateUserProfile :exec |
| 383 | +UPDATE users |
| 384 | +SET display_name = $2, |
| 385 | + bio = $3, |
| 386 | + location = $4, |
| 387 | + website = $5, |
| 388 | + company = $6, |
| 389 | + pronouns = $7 |
| 390 | +WHERE id = $1 |
| 391 | +` |
| 392 | + |
| 393 | +type UpdateUserProfileParams struct { |
| 394 | + ID int64 |
| 395 | + DisplayName string |
| 396 | + Bio string |
| 397 | + Location string |
| 398 | + Website string |
| 399 | + Company string |
| 400 | + Pronouns string |
| 401 | +} |
| 402 | + |
| 403 | +func (q *Queries) UpdateUserProfile(ctx context.Context, db DBTX, arg UpdateUserProfileParams) error { |
| 404 | + _, err := db.Exec(ctx, updateUserProfile, |
| 405 | + arg.ID, |
| 406 | + arg.DisplayName, |
| 407 | + arg.Bio, |
| 408 | + arg.Location, |
| 409 | + arg.Website, |
| 410 | + arg.Company, |
| 411 | + arg.Pronouns, |
| 412 | + ) |
| 413 | + return err |
| 414 | +} |
| 415 | + |
| 416 | +const updateUserTheme = `-- name: UpdateUserTheme :exec |
| 417 | +UPDATE users SET theme = $2 WHERE id = $1 |
| 418 | +` |
| 419 | + |
| 420 | +type UpdateUserThemeParams struct { |
| 421 | + ID int64 |
| 422 | + Theme string |
| 423 | +} |
| 424 | + |
| 425 | +func (q *Queries) UpdateUserTheme(ctx context.Context, db DBTX, arg UpdateUserThemeParams) error { |
| 426 | + _, err := db.Exec(ctx, updateUserTheme, arg.ID, arg.Theme) |
| 427 | + return err |
| 428 | +} |