package repository import ( "context" "crypto/sha256" "database/sql" "encoding/hex" "encoding/json" "fmt" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "gitlab.com/patrickbritton3/sojorn/go-backend/internal/models" ) type UserRepository struct { pool *pgxpool.Pool } func NewUserRepository(pool *pgxpool.Pool) *UserRepository { return &UserRepository{pool: pool} } func (r *UserRepository) Pool() *pgxpool.Pool { return r.pool } func (r *UserRepository) CreateProfile(ctx context.Context, profile *models.Profile) error { query := ` INSERT INTO public.profiles (id, handle, display_name, bio, origin_country, birth_month, birth_year) VALUES ($1, $2, $3, $4, $5, $6, $7) ` _, err := r.pool.Exec(ctx, query, profile.ID, profile.Handle, profile.DisplayName, profile.Bio, profile.OriginCountry, profile.BirthMonth, profile.BirthYear) if err != nil { return fmt.Errorf("failed to create profile: %w", err) } // Initialize trust state (mimicking the trigger if we want to do it in Go) trustQuery := ` INSERT INTO public.trust_state (user_id, harmony_score, tier) VALUES ($1, 50, 'new') ` _, err = r.pool.Exec(ctx, trustQuery, profile.ID) if err != nil { return fmt.Errorf("failed to initialize trust state: %w", err) } return nil } func (r *UserRepository) GetProfileByID(ctx context.Context, id string) (*models.Profile, error) { query := `SELECT id, handle, display_name, bio, avatar_url, origin_country, has_completed_onboarding, is_official, is_private, role, created_at, COALESCE(birth_month, 0), COALESCE(birth_year, 0) FROM public.profiles WHERE id = $1::uuid` var p models.Profile err := r.pool.QueryRow(ctx, query, id).Scan( &p.ID, &p.Handle, &p.DisplayName, &p.Bio, &p.AvatarURL, &p.OriginCountry, &p.HasCompletedOnboarding, &p.IsOfficial, &p.IsPrivate, &p.Role, &p.CreatedAt, &p.BirthMonth, &p.BirthYear, ) if err != nil { return nil, err } return &p, nil } func (r *UserRepository) GetProfileByHandle(ctx context.Context, handle string) (*models.Profile, error) { query := ` SELECT id, handle, display_name, bio, avatar_url, origin_country, has_completed_onboarding, is_official, is_private, role, created_at, COALESCE(birth_month, 0), COALESCE(birth_year, 0) FROM public.profiles WHERE handle = $1 ` var p models.Profile err := r.pool.QueryRow(ctx, query, handle).Scan( &p.ID, &p.Handle, &p.DisplayName, &p.Bio, &p.AvatarURL, &p.OriginCountry, &p.HasCompletedOnboarding, &p.IsOfficial, &p.IsPrivate, &p.Role, &p.CreatedAt, &p.BirthMonth, &p.BirthYear, ) if err != nil { return nil, err } return &p, nil } func (r *UserRepository) UpdateProfile(ctx context.Context, profile *models.Profile) error { query := ` UPDATE public.profiles SET handle = COALESCE($1, handle), display_name = COALESCE($2, display_name), bio = COALESCE($3, bio), avatar_url = COALESCE($4, avatar_url), cover_url = COALESCE($5, cover_url), location = COALESCE($6, location), website = COALESCE($7, website), interests = COALESCE($8, interests), identity_key = COALESCE($9, identity_key), registration_id = COALESCE($10, registration_id), encrypted_private_key = COALESCE($11, encrypted_private_key), is_private = COALESCE($12, is_private), is_official = COALESCE($13, is_official), updated_at = NOW() WHERE id = $14::uuid ` _, err := r.pool.Exec(ctx, query, profile.Handle, profile.DisplayName, profile.Bio, profile.AvatarURL, profile.CoverURL, profile.Location, profile.Website, profile.Interests, profile.IdentityKey, profile.RegistrationID, profile.EncryptedPrivateKey, profile.IsPrivate, profile.IsOfficial, profile.ID, ) return err } func (r *UserRepository) MarkOnboardingComplete(ctx context.Context, userID string) error { query := `UPDATE public.profiles SET has_completed_onboarding = TRUE, updated_at = NOW() WHERE id = $1::uuid` _, err := r.pool.Exec(ctx, query, userID) return err } func (r *UserRepository) SearchUsers(ctx context.Context, query string, viewerID string, limit int) ([]models.Profile, error) { // The % operator uses pg_trgm for fuzzy matching sql := ` SELECT p.id, p.handle, p.display_name, p.bio, p.avatar_url, p.origin_country, p.has_completed_onboarding, p.created_at FROM public.profiles p LEFT JOIN public.trust_state t ON p.id = t.user_id WHERE ( p.handle % $1 OR p.handle ILIKE '%' || $1 || '%' OR p.display_name % $1 OR p.display_name ILIKE '%' || $1 || '%' ) AND ( p.is_private = FALSE OR ($2 != '' AND EXISTS ( SELECT 1 FROM public.follows f WHERE f.follower_id = $2::uuid AND f.following_id = p.id AND f.status = 'accepted' )) OR ($2 != '' AND p.id = $2::uuid) ) AND NOT public.has_block_between(p.id, CASE WHEN $2 != '' THEN $2::uuid ELSE NULL END) ORDER BY (similarity(p.handle, $1) + CASE WHEN p.handle ILIKE $1 || '%' THEN 0.5 ELSE 0 END + CASE WHEN COALESCE(t.harmony_score, 0) > 80 THEN 0.3 ELSE 0 END) DESC, p.created_at DESC LIMIT $3 ` rows, err := r.pool.Query(ctx, sql, query, viewerID, limit) if err != nil { return nil, err } defer rows.Close() var profiles []models.Profile for rows.Next() { var p models.Profile err := rows.Scan( &p.ID, &p.Handle, &p.DisplayName, &p.Bio, &p.AvatarURL, &p.OriginCountry, &p.HasCompletedOnboarding, &p.CreatedAt, ) if err != nil { return nil, err } profiles = append(profiles, p) } return profiles, nil } func (r *UserRepository) GetTrustState(ctx context.Context, userID string) (*models.TrustState, error) { query := `SELECT user_id, harmony_score, tier, posts_today FROM public.trust_state WHERE user_id = $1::uuid` var ts models.TrustState err := r.pool.QueryRow(ctx, query, userID).Scan( &ts.UserID, &ts.HarmonyScore, &ts.Tier, &ts.PostsToday, ) if err != nil { return nil, err } return &ts, nil } func (r *UserRepository) CreateUser(ctx context.Context, user *models.User) error { query := ` INSERT INTO public.users (id, email, encrypted_password, status, mfa_enabled, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7) ` _, err := r.pool.Exec(ctx, query, user.ID, user.Email, user.PasswordHash, user.Status, user.MFAEnabled, user.CreatedAt, user.UpdatedAt) if err != nil { return fmt.Errorf("failed to create user: %w", err) } return nil } func (r *UserRepository) GetUserByEmail(ctx context.Context, email string) (*models.User, error) { query := `SELECT id, email, encrypted_password, status, mfa_enabled, last_login, created_at FROM public.users WHERE email = $1` var u models.User err := r.pool.QueryRow(ctx, query, email).Scan(&u.ID, &u.Email, &u.PasswordHash, &u.Status, &u.MFAEnabled, &u.LastLogin, &u.CreatedAt) if err != nil { return nil, err } return &u, nil } func (r *UserRepository) GetUserByID(ctx context.Context, id string) (*models.User, error) { query := `SELECT id, email, encrypted_password, status, mfa_enabled, last_login, created_at FROM public.users WHERE id = $1::uuid` var u models.User err := r.pool.QueryRow(ctx, query, id).Scan(&u.ID, &u.Email, &u.PasswordHash, &u.Status, &u.MFAEnabled, &u.LastLogin, &u.CreatedAt) if err != nil { return nil, err } return &u, nil } func (r *UserRepository) FollowUser(ctx context.Context, followerID, followingID string) (string, error) { if followerID == followingID { return "", fmt.Errorf("cannot follow self") } // 1. Check if follow record already exists var currentStatus string err := r.pool.QueryRow(ctx, "SELECT status FROM public.follows WHERE follower_id = $1::uuid AND following_id = $2::uuid", followerID, followingID).Scan(¤tStatus) if err == nil { return currentStatus, nil // Return existing status if found } // 2. Check target user's privacy settings var isPrivate, isOfficial bool err = r.pool.QueryRow(ctx, "SELECT is_private, is_official FROM public.profiles WHERE id = $1::uuid", followingID).Scan(&isPrivate, &isOfficial) if err != nil { // Fallback: If profile missing, assume public (or return error) isPrivate = false } // 3. Determine status: Official/Public -> Accepted, Private -> Pending newStatus := "pending" if isOfficial || !isPrivate { newStatus = "accepted" } // 4. Insert the follow query := ` INSERT INTO public.follows (follower_id, following_id, status) VALUES ($1::uuid, $2::uuid, $3) ON CONFLICT (follower_id, following_id) DO UPDATE SET status = EXCLUDED.status RETURNING status ` var status string err = r.pool.QueryRow(ctx, query, followerID, followingID, newStatus).Scan(&status) if err != nil { return "", fmt.Errorf("failed to follow user: %w", err) } return status, nil } func (r *UserRepository) AcceptFollowRequest(ctx context.Context, userID, requesterID string) error { query := ` UPDATE public.follows SET status = 'accepted' WHERE following_id = $1::uuid AND follower_id = $2::uuid AND status = 'pending' ` commandTag, err := r.pool.Exec(ctx, query, userID, requesterID) if err != nil { return err } if commandTag.RowsAffected() == 0 { return fmt.Errorf("no pending request found") } return nil } func (r *UserRepository) GetPendingFollowRequests(ctx context.Context, userID string) ([]map[string]any, error) { query := ` SELECT p.id as follower_id, p.handle, p.display_name, p.avatar_url, f.created_at as requested_at FROM public.follows f JOIN public.profiles p ON p.id = f.follower_id WHERE f.following_id = $1::uuid AND f.status = 'pending' ORDER BY f.created_at DESC ` rows, err := r.pool.Query(ctx, query, userID) if err != nil { return nil, err } defer rows.Close() var requests []map[string]any for rows.Next() { var id, handle, displayName, avatarURL string var requestedAt time.Time if err := rows.Scan(&id, &handle, &displayName, &avatarURL, &requestedAt); err != nil { return nil, err } requests = append(requests, map[string]any{ "follower_id": id, "handle": handle, "display_name": displayName, "avatar_url": avatarURL, "requested_at": requestedAt, }) } return requests, nil } func (r *UserRepository) UpdateHarmonyScore(ctx context.Context, userID string, delta int) error { query := ` UPDATE public.trust_state SET harmony_score = GREATEST(0, LEAST(harmony_score + $1, 100)), updated_at = NOW(), last_harmony_calc_at = NOW() WHERE user_id = $2::uuid ` _, err := r.pool.Exec(ctx, query, delta, userID) return err } func (r *UserRepository) RejectFollowRequest(ctx context.Context, userID, requesterID string) error { query := `DELETE FROM public.follows WHERE follower_id = $1::uuid AND following_id = $2::uuid AND status = 'pending'` _, err := r.pool.Exec(ctx, query, requesterID, userID) return err } func (r *UserRepository) UnfollowUser(ctx context.Context, followerID, followingID string) error { query := ` DELETE FROM public.follows WHERE follower_id = $1::uuid AND following_id = $2::uuid ` _, err := r.pool.Exec(ctx, query, followerID, followingID) if err != nil { return fmt.Errorf("failed to unfollow user: %w", err) } return nil } func (r *UserRepository) BlockUser(ctx context.Context, blockerID, blockedID, actorIP string) error { tx, err := r.pool.Begin(ctx) if err != nil { return err } defer tx.Rollback(ctx) // Step 1: Insert Block query := `INSERT INTO public.blocks (blocker_id, blocked_id) VALUES ($1::uuid, $2::uuid) ON CONFLICT DO NOTHING` _, err = tx.Exec(ctx, query, blockerID, blockedID) if err != nil { return err } // Step 2: Log Abuse var handle string _ = tx.QueryRow(ctx, `SELECT handle FROM public.profiles WHERE id = $1::uuid`, blockedID).Scan(&handle) abuseQuery := ` INSERT INTO public.abuse_logs (actor_id, blocked_id, blocked_handle, actor_ip) VALUES ($1::uuid, $2::uuid, $3, $4) ` _, _ = tx.Exec(ctx, abuseQuery, blockerID, blockedID, handle, actorIP) return tx.Commit(ctx) } func (r *UserRepository) UnblockUser(ctx context.Context, blockerID, blockedID string) error { query := `DELETE FROM public.blocks WHERE blocker_id = $1::uuid AND blocked_id = $2::uuid` _, err := r.pool.Exec(ctx, query, blockerID, blockedID) return err } func (r *UserRepository) GetBlockedUsers(ctx context.Context, userID string) ([]models.Profile, error) { query := ` SELECT p.id, p.handle, p.display_name, p.avatar_url FROM public.profiles p JOIN public.blocks b ON p.id = b.blocked_id WHERE b.blocker_id = $1::uuid ` rows, err := r.pool.Query(ctx, query, userID) if err != nil { return nil, err } defer rows.Close() var profiles []models.Profile for rows.Next() { var p models.Profile if err := rows.Scan(&p.ID, &p.Handle, &p.DisplayName, &p.AvatarURL); err != nil { return nil, err } profiles = append(profiles, p) } return profiles, nil } func (r *UserRepository) CreateReport(ctx context.Context, report *models.Report) error { query := ` INSERT INTO public.reports (reporter_id, target_user_id, post_id, comment_id, violation_type, description, status) VALUES ($1::uuid, $2::uuid, $3, $4, $5, $6, 'pending') ` _, err := r.pool.Exec(ctx, query, report.ReporterID, report.TargetUserID, report.PostID, report.CommentID, report.ViolationType, report.Description, ) return err } type ProfileStats struct { PostCount int `json:"post_count"` FollowerCount int `json:"follower_count"` FollowingCount int `json:"following_count"` } func (r *UserRepository) GetProfileStats(ctx context.Context, userID string) (*ProfileStats, error) { stats := &ProfileStats{} query := ` SELECT ( SELECT COUNT(*) FROM public.posts p WHERE p.author_id = $1::uuid AND p.deleted_at IS NULL ) as post_count, ( SELECT COUNT(*) FROM public.follows WHERE following_id = $1::uuid AND status = 'accepted' ) as follower_count, ( SELECT COUNT(*) FROM public.follows WHERE follower_id = $1::uuid AND status = 'accepted' ) as following_count ` err := r.pool.QueryRow(ctx, query, userID).Scan( &stats.PostCount, &stats.FollowerCount, &stats.FollowingCount, ) if err != nil { return nil, err } return stats, nil } func (r *UserRepository) IsFollowing(ctx context.Context, followerID, followingID string) (bool, error) { var exists bool query := `SELECT EXISTS(SELECT 1 FROM public.follows WHERE follower_id = $1::uuid AND following_id = $2::uuid AND status = 'accepted')` err := r.pool.QueryRow(ctx, query, followerID, followingID).Scan(&exists) if err != nil { return false, err } return exists, nil } func (r *UserRepository) IsMutualFollow(ctx context.Context, userA, userB string) (bool, error) { if userA == userB { return true, nil } var exists bool query := ` SELECT EXISTS ( SELECT 1 FROM public.follows WHERE follower_id = $1::uuid AND following_id = $2::uuid AND status = 'accepted' ) AND EXISTS ( SELECT 1 FROM public.follows WHERE follower_id = $2::uuid AND following_id = $1::uuid AND status = 'accepted' )` err := r.pool.QueryRow(ctx, query, userA, userB).Scan(&exists) if err != nil { return false, err } return exists, nil } func (r *UserRepository) GetFollowStatus(ctx context.Context, followerID, followingID string) (string, error) { var status string query := `SELECT status FROM public.follows WHERE follower_id = $1::uuid AND following_id = $2::uuid` err := r.pool.QueryRow(ctx, query, followerID, followingID).Scan(&status) if err != nil { if err == pgx.ErrNoRows { return "", nil } return "", err } return status, nil } type SignalKeysInput struct { IdentityKeyPublic string SignedPrekeyPublic string SignedPrekeyID int SignedPrekeySignature string OneTimePrekeys []byte RegistrationID int } func (r *UserRepository) UpsertKeys(ctx context.Context, userID string, keys SignalKeysInput) error { tx, err := r.pool.Begin(ctx) if err != nil { return err } defer tx.Rollback(ctx) // Step 1: Update identity and registration _, err = tx.Exec(ctx, ` UPDATE public.profiles SET identity_key = $1, registration_id = $2, updated_at = NOW() WHERE id = $3::uuid `, keys.IdentityKeyPublic, keys.RegistrationID, userID) if err != nil { return err } // Step 2: Extract and Upsert Signed Prekey _, err = tx.Exec(ctx, ` INSERT INTO public.signed_prekeys (user_id, key_id, public_key, signature) VALUES ($1::uuid, $2, $3, $4) ON CONFLICT (user_id, key_id) DO UPDATE SET public_key = EXCLUDED.public_key, signature = EXCLUDED.signature, created_at = NOW() `, userID, keys.SignedPrekeyID, keys.SignedPrekeyPublic, keys.SignedPrekeySignature) if err != nil { return err } // Step 3: Clear and Bulk Insert One-Time Prekeys _, err = tx.Exec(ctx, `DELETE FROM public.one_time_prekeys WHERE user_id = $1::uuid`, userID) if err != nil { return err } type OTKEntry struct { KeyID int `json:"key_id"` PublicKey string `json:"public_key"` } var otks []OTKEntry if len(keys.OneTimePrekeys) > 0 { if err := json.Unmarshal(keys.OneTimePrekeys, &otks); err != nil { return fmt.Errorf("failed to unmarshal one_time_prekeys: %w", err) } for _, otk := range otks { _, err = tx.Exec(ctx, ` INSERT INTO public.one_time_prekeys (user_id, key_id, public_key) VALUES ($1::uuid, $2, $3) `, userID, otk.KeyID, otk.PublicKey) if err != nil { return err } } } return tx.Commit(ctx) } // DeleteUsedOTK removes a one-time prekey after it's been used for encryption func (r *UserRepository) DeleteUsedOTK(ctx context.Context, userID string, keyID int) error { _, err := r.pool.Exec(ctx, ` DELETE FROM public.one_time_prekeys WHERE user_id = $1::uuid AND key_id = $2 `, userID, keyID) if err != nil { return fmt.Errorf("failed to delete used OTK: %w", err) } // OTK deleted successfully return nil } func (r *UserRepository) GetSignalKeyBundle(ctx context.Context, userID string) (map[string]interface{}, error) { var ikPub, spkPub, spkSig string var regID sql.NullInt64 var spkID sql.NullInt64 err := r.pool.QueryRow(ctx, ` SELECT p.identity_key as identity_key_public, p.registration_id, sp.key_id as signed_prekey_id, sp.public_key as signed_prekey_public, sp.signature as signed_prekey_signature FROM public.profiles p LEFT JOIN public.signed_prekeys sp ON p.id = sp.user_id WHERE p.id = $1::uuid `, userID).Scan( &ikPub, ®ID, &spkID, &spkPub, &spkSig, ) if err != nil { return nil, fmt.Errorf("failed to get signal key bundle: %w", err) } // Get available OTKs for this user rows, err := r.pool.Query(ctx, ` SELECT key_id, public_key FROM public.one_time_prekeys WHERE user_id = $1::uuid ORDER BY created_at ASC LIMIT 1 `, userID) if err != nil { return nil, fmt.Errorf("failed to get OTKs: %w", err) } defer rows.Close() var otkID int var otkPub string var otk map[string]interface{} if rows.Next() { if err := rows.Scan(&otkID, &otkPub); err != nil { return nil, fmt.Errorf("failed to scan OTK: %w", err) } otk = map[string]interface{}{ "key_id": otkID, "public_key": otkPub, } // OTK retrieved - not logging user ID for security } // Handle NULL values properly var regIDValue int if regID.Valid { regIDValue = int(regID.Int64) } else { regIDValue = 1 // Default value } var spkIDValue int if spkID.Valid { spkIDValue = int(spkID.Int64) } else { spkIDValue = 1 // Default value } bundle := map[string]interface{}{ "identity_key": map[string]interface{}{ "key_id": 1, "public_key": ikPub, }, "registration_id": regIDValue, "signed_prekey": map[string]interface{}{ "key_id": spkIDValue, "public_key": spkPub, "signature": spkSig, }, } // Add OTK if available if otk != nil { bundle["one_time_prekey"] = otk } return bundle, nil } func (r *UserRepository) GetPrivacySettings(ctx context.Context, userID string) (*models.PrivacySettings, error) { query := ` SELECT user_id, show_location, show_interests, profile_visibility, posts_visibility, saved_visibility, follow_request_policy, default_post_visibility, is_private_profile, updated_at FROM public.profile_privacy_settings WHERE user_id = $1::uuid ` var ps models.PrivacySettings err := r.pool.QueryRow(ctx, query, userID).Scan( &ps.UserID, &ps.ShowLocation, &ps.ShowInterests, &ps.ProfileVisibility, &ps.PostsVisibility, &ps.SavedVisibility, &ps.FollowRequestPolicy, &ps.DefaultPostVisibility, &ps.IsPrivateProfile, &ps.UpdatedAt, ) if err != nil { if err.Error() == "no rows in result set" || err.Error() == "pgx: no rows in result set" { // Return default settings for new users (pointers required) uid, _ := uuid.Parse(userID) t := true f := false pub := "public" priv := "private" anyone := "everyone" return &models.PrivacySettings{ UserID: uid, ShowLocation: &t, ShowInterests: &t, ProfileVisibility: &pub, PostsVisibility: &pub, SavedVisibility: &priv, FollowRequestPolicy: &anyone, DefaultPostVisibility: &pub, IsPrivateProfile: &f, UpdatedAt: time.Now(), }, nil } return nil, err } return &ps, nil } func (r *UserRepository) UpdatePrivacySettings(ctx context.Context, ps *models.PrivacySettings) error { query := ` INSERT INTO public.profile_privacy_settings ( user_id, show_location, show_interests, profile_visibility, posts_visibility, saved_visibility, follow_request_policy, default_post_visibility, is_private_profile, updated_at ) VALUES ($1::uuid, $2, $3, $4, $5, $6, $7, $8, $9, NOW()) ON CONFLICT (user_id) DO UPDATE SET show_location = COALESCE(EXCLUDED.show_location, profile_privacy_settings.show_location), show_interests = COALESCE(EXCLUDED.show_interests, profile_privacy_settings.show_interests), profile_visibility = COALESCE(EXCLUDED.profile_visibility, profile_privacy_settings.profile_visibility), posts_visibility = COALESCE(EXCLUDED.posts_visibility, profile_privacy_settings.posts_visibility), saved_visibility = COALESCE(EXCLUDED.saved_visibility, profile_privacy_settings.saved_visibility), follow_request_policy = COALESCE(EXCLUDED.follow_request_policy, profile_privacy_settings.follow_request_policy), default_post_visibility = COALESCE(EXCLUDED.default_post_visibility, profile_privacy_settings.default_post_visibility), is_private_profile = COALESCE(EXCLUDED.is_private_profile, profile_privacy_settings.is_private_profile), updated_at = NOW() ` _, err := r.pool.Exec(ctx, query, ps.UserID, ps.ShowLocation, ps.ShowInterests, ps.ProfileVisibility, ps.PostsVisibility, ps.SavedVisibility, ps.FollowRequestPolicy, ps.DefaultPostVisibility, ps.IsPrivateProfile, ) return err } func (r *UserRepository) GetUserSettings(ctx context.Context, userID string) (*models.UserSettings, error) { query := ` SELECT user_id, theme, language, notifications_enabled, email_notifications, push_notifications, content_filter_level, auto_play_videos, data_saver_mode, default_post_ttl, COALESCE(nsfw_enabled, FALSE), COALESCE(nsfw_blur_enabled, TRUE), updated_at FROM public.user_settings WHERE user_id = $1::uuid ` var us models.UserSettings err := r.pool.QueryRow(ctx, query, userID).Scan( &us.UserID, &us.Theme, &us.Language, &us.NotificationsEnabled, &us.EmailNotifications, &us.PushNotifications, &us.ContentFilterLevel, &us.AutoPlayVideos, &us.DataSaverMode, &us.DefaultPostTtl, &us.NSFWEnabled, &us.NSFWBlurEnabled, &us.UpdatedAt, ) if err != nil { if err.Error() == "no rows in result set" || err.Error() == "pgx: no rows in result set" { // Return default settings for new users (pointers required) uid, _ := uuid.Parse(userID) sys := "system" en := "en" med := "medium" t := true f := false return &models.UserSettings{ UserID: uid, Theme: &sys, Language: &en, NotificationsEnabled: &t, EmailNotifications: &t, PushNotifications: &t, ContentFilterLevel: &med, AutoPlayVideos: &t, DataSaverMode: &f, NSFWEnabled: &f, NSFWBlurEnabled: &t, UpdatedAt: time.Now(), }, nil } return nil, err } return &us, nil } func (r *UserRepository) UpdateUserSettings(ctx context.Context, us *models.UserSettings) error { query := ` INSERT INTO public.user_settings ( user_id, theme, language, notifications_enabled, email_notifications, push_notifications, content_filter_level, auto_play_videos, data_saver_mode, default_post_ttl, nsfw_enabled, nsfw_blur_enabled, updated_at ) VALUES ($1::uuid, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, NOW()) ON CONFLICT (user_id) DO UPDATE SET theme = COALESCE(EXCLUDED.theme, user_settings.theme), language = COALESCE(EXCLUDED.language, user_settings.language), notifications_enabled = COALESCE(EXCLUDED.notifications_enabled, user_settings.notifications_enabled), email_notifications = COALESCE(EXCLUDED.email_notifications, user_settings.email_notifications), push_notifications = COALESCE(EXCLUDED.push_notifications, user_settings.push_notifications), content_filter_level = COALESCE(EXCLUDED.content_filter_level, user_settings.content_filter_level), auto_play_videos = COALESCE(EXCLUDED.auto_play_videos, user_settings.auto_play_videos), data_saver_mode = COALESCE(EXCLUDED.data_saver_mode, user_settings.data_saver_mode), default_post_ttl = COALESCE(EXCLUDED.default_post_ttl, user_settings.default_post_ttl), nsfw_enabled = COALESCE(EXCLUDED.nsfw_enabled, user_settings.nsfw_enabled), nsfw_blur_enabled = COALESCE(EXCLUDED.nsfw_blur_enabled, user_settings.nsfw_blur_enabled), updated_at = NOW() ` _, err := r.pool.Exec(ctx, query, us.UserID, us.Theme, us.Language, us.NotificationsEnabled, us.EmailNotifications, us.PushNotifications, us.ContentFilterLevel, us.AutoPlayVideos, us.DataSaverMode, us.DefaultPostTtl, us.NSFWEnabled, us.NSFWBlurEnabled, ) return err } func (r *UserRepository) UpsertFCMToken(ctx context.Context, userID string, token string, platform string) error { query := ` INSERT INTO public.user_fcm_tokens (user_id, token, device_type, last_updated) VALUES ($1::uuid, $2, $3, NOW()) ON CONFLICT (user_id, token) DO UPDATE SET last_updated = NOW(), device_type = EXCLUDED.device_type ` _, err := r.pool.Exec(ctx, query, userID, token, platform) return err } func (r *UserRepository) GetFCMTokens(ctx context.Context, userID string) ([]string, error) { query := `SELECT token FROM public.user_fcm_tokens WHERE user_id = $1::uuid` rows, err := r.pool.Query(ctx, query, userID) if err != nil { return nil, err } defer rows.Close() var tokens []string for rows.Next() { var token string if err := rows.Scan(&token); err != nil { return nil, err } tokens = append(tokens, token) } return tokens, nil } func (r *UserRepository) DeleteFCMToken(ctx context.Context, userID string, token string) error { _, err := r.pool.Exec(ctx, ` DELETE FROM public.user_fcm_tokens WHERE user_id = $1::uuid AND token = $2 `, userID, token) return err } func (r *UserRepository) StoreRefreshToken(ctx context.Context, userID string, tokenString string, duration time.Duration) error { hash := sha256.Sum256([]byte(tokenString)) hashString := hex.EncodeToString(hash[:]) query := ` INSERT INTO refresh_tokens (token_hash, user_id, expires_at) VALUES ($1, $2::uuid, $3) ` _, err := r.pool.Exec(ctx, query, hashString, userID, time.Now().Add(duration)) return err } func (r *UserRepository) ValidateRefreshToken(ctx context.Context, tokenString string) (*models.RefreshToken, error) { hash := sha256.Sum256([]byte(tokenString)) hashString := hex.EncodeToString(hash[:]) var rt models.RefreshToken query := ` SELECT token_hash, user_id, expires_at, revoked, created_at FROM refresh_tokens WHERE token_hash = $1 ` err := r.pool.QueryRow(ctx, query, hashString).Scan( &rt.TokenHash, &rt.UserID, &rt.ExpiresAt, &rt.Revoked, &rt.CreatedAt, ) if err != nil { return nil, err } if rt.Revoked { return nil, fmt.Errorf("token revoked") } if time.Now().After(rt.ExpiresAt) { return nil, fmt.Errorf("token expired") } return &rt, nil } func (r *UserRepository) RevokeRefreshToken(ctx context.Context, tokenString string) error { hash := sha256.Sum256([]byte(tokenString)) hashString := hex.EncodeToString(hash[:]) query := `UPDATE refresh_tokens SET revoked = true WHERE token_hash = $1` _, err := r.pool.Exec(ctx, query, hashString) return err } func (r *UserRepository) RevokeAllUserTokens(ctx context.Context, userID string) error { query := `UPDATE refresh_tokens SET revoked = true WHERE user_id = $1::uuid` _, err := r.pool.Exec(ctx, query, userID) return err } func (r *UserRepository) CreateAuthToken(ctx context.Context, token *models.AuthToken) error { query := `INSERT INTO public.auth_tokens (token, user_id, type, expires_at, created_at) VALUES ($1, $2, $3, $4, $5)` _, err := r.pool.Exec(ctx, query, token.Token, token.UserID, token.Type, token.ExpiresAt, token.CreatedAt) return err } func (r *UserRepository) GetAuthToken(ctx context.Context, tokenStr string) (*models.AuthToken, error) { query := `SELECT token, user_id, type, expires_at, created_at FROM public.auth_tokens WHERE token = $1` var t models.AuthToken err := r.pool.QueryRow(ctx, query, tokenStr).Scan(&t.Token, &t.UserID, &t.Type, &t.ExpiresAt, &t.CreatedAt) if err != nil { return nil, err } return &t, nil } func (r *UserRepository) DeleteAuthToken(ctx context.Context, tokenStr string) error { _, err := r.pool.Exec(ctx, `DELETE FROM public.auth_tokens WHERE token = $1`, tokenStr) return err } func (r *UserRepository) UpdateUserStatus(ctx context.Context, userID string, status models.UserStatus) error { _, err := r.pool.Exec(ctx, `UPDATE public.users SET status = $1, updated_at = NOW() WHERE id = $2::uuid`, status, userID) return err } func (r *UserRepository) UpdateLastLogin(ctx context.Context, userID string) error { _, err := r.pool.Exec(ctx, `UPDATE public.users SET last_login = NOW() WHERE id = $1::uuid`, userID) return err } func (r *UserRepository) UpsertMFASecret(ctx context.Context, secret *models.MFASecret) error { query := ` INSERT INTO public.user_mfa_secrets (user_id, secret_key, recovery_codes, updated_at) VALUES ($1, $2, $3, NOW()) ON CONFLICT (user_id) DO UPDATE SET secret_key = EXCLUDED.secret_key, recovery_codes = EXCLUDED.recovery_codes, updated_at = NOW() ` _, err := r.pool.Exec(ctx, query, secret.UserID, secret.Secret, secret.RecoveryCodes) return err } func (r *UserRepository) GetMFASecret(ctx context.Context, userID string) (*models.MFASecret, error) { query := `SELECT user_id, secret_key, recovery_codes FROM public.user_mfa_secrets WHERE user_id = $1::uuid` var s models.MFASecret err := r.pool.QueryRow(ctx, query, userID).Scan(&s.UserID, &s.Secret, &s.RecoveryCodes) if err != nil { return nil, err } return &s, nil } // Verification Tokens (New Table) func (r *UserRepository) CreateVerificationToken(ctx context.Context, tokenHash string, userID string, duration time.Duration) error { query := `INSERT INTO public.verification_tokens (token_hash, user_id, expires_at) VALUES ($1, $2, $3)` _, err := r.pool.Exec(ctx, query, tokenHash, userID, time.Now().Add(duration)) return err } func (r *UserRepository) GetVerificationToken(ctx context.Context, tokenHash string) (string, time.Time, error) { query := `SELECT user_id, expires_at FROM public.verification_tokens WHERE token_hash = $1` var userID string var expiresAt time.Time err := r.pool.QueryRow(ctx, query, tokenHash).Scan(&userID, &expiresAt) if err != nil { return "", time.Time{}, err } return userID, expiresAt, nil } func (r *UserRepository) DeleteVerificationToken(ctx context.Context, tokenHash string) error { query := `DELETE FROM public.verification_tokens WHERE token_hash = $1` _, err := r.pool.Exec(ctx, query, tokenHash) return err } // Password Reset Tokens func (r *UserRepository) CreatePasswordResetToken(ctx context.Context, tokenHash string, userID string, duration time.Duration) error { query := `INSERT INTO public.password_reset_tokens (token_hash, user_id, expires_at) VALUES ($1, $2, $3)` _, err := r.pool.Exec(ctx, query, tokenHash, userID, time.Now().Add(duration)) return err } func (r *UserRepository) GetPasswordResetToken(ctx context.Context, tokenHash string) (string, time.Time, error) { query := `SELECT user_id, expires_at FROM public.password_reset_tokens WHERE token_hash = $1` var userID string var expiresAt time.Time err := r.pool.QueryRow(ctx, query, tokenHash).Scan(&userID, &expiresAt) if err != nil { return "", time.Time{}, err } return userID, expiresAt, nil } func (r *UserRepository) DeletePasswordResetToken(ctx context.Context, tokenHash string) error { query := `DELETE FROM public.password_reset_tokens WHERE token_hash = $1` _, err := r.pool.Exec(ctx, query, tokenHash) return err } func (r *UserRepository) UpdateUserPassword(ctx context.Context, userID string, passwordHash string) error { query := `UPDATE public.users SET encrypted_password = $1, updated_at = NOW() WHERE id = $2::uuid` _, err := r.pool.Exec(ctx, query, passwordHash, userID) return err } // WebAuthn Credentials (placeholder comment if more methods follow, or strictly implement the requested method) func (r *UserRepository) FetchAndConsumeOneTimeKey(ctx context.Context, userID string) (*models.OneTimePrekey, error) { tx, err := r.pool.Begin(ctx) if err != nil { return nil, err } defer tx.Rollback(ctx) var otk models.OneTimePrekey // Logic: Execute SELECT ... FROM one_time_prekeys WHERE user_id = $1 ORDER BY created_at ASC LIMIT 1 FOR UPDATE SKIP LOCKED query := ` SELECT key_id, public_key FROM public.one_time_prekeys WHERE user_id = $1::uuid ORDER BY created_at ASC LIMIT 1 FOR UPDATE SKIP LOCKED ` err = tx.QueryRow(ctx, query, userID).Scan(&otk.KeyID, &otk.PublicKey) if err != nil { if err == pgx.ErrNoRows { // If not found: Return nil (this is valid protocol behavior) return nil, tx.Commit(ctx) } return nil, err } // If found: DELETE the row immediately and return the key data. _, err = tx.Exec(ctx, `DELETE FROM public.one_time_prekeys WHERE user_id = $1::uuid AND key_id = $2`, userID, otk.KeyID) if err != nil { return nil, fmt.Errorf("failed to delete one-time key: %w", err) } if err := tx.Commit(ctx); err != nil { return nil, err } return &otk, nil } func (r *UserRepository) CreateWebAuthnCredential(ctx context.Context, cred *models.WebAuthnCredential) error { query := ` INSERT INTO public.webauthn_credentials (user_id, credential_id, public_key, attestation_type, aaguid, sign_count, last_used_at) VALUES ($1, $2, $3, $4, $5, $6, $7) ` _, err := r.pool.Exec(ctx, query, cred.UserID, cred.ID, cred.PublicKey, cred.AttestationType, cred.AAGUID, cred.SignCount, cred.LastUsedAt) return err } func (r *UserRepository) GetWebAuthnCredentials(ctx context.Context, userID string) ([]models.WebAuthnCredential, error) { query := `SELECT id, user_id, credential_id, public_key, attestation_type, aaguid, sign_count, last_used_at FROM public.webauthn_credentials WHERE user_id = $1::uuid` rows, err := r.pool.Query(ctx, query, userID) if err != nil { return nil, err } defer rows.Close() var creds []models.WebAuthnCredential for rows.Next() { var c models.WebAuthnCredential var idStr string err := rows.Scan(&idStr, &c.UserID, &c.ID, &c.PublicKey, &c.AttestationType, &c.AAGUID, &c.SignCount, &c.LastUsedAt) if err != nil { return nil, err } creds = append(creds, c) } return creds, nil } // DeleteUser removes a user by ID. SHOULD BE USED WITH CAUTION (e.g. rollback) func (r *UserRepository) DeleteUser(ctx context.Context, userID uuid.UUID) error { query := `DELETE FROM public.users WHERE id = $1` _, err := r.pool.Exec(ctx, query, userID) return err } func (r *UserRepository) BlockUserByHandle(ctx context.Context, actorID string, handle string, actorIP string) error { var targetID uuid.UUID err := r.pool.QueryRow(ctx, "SELECT id FROM public.profiles WHERE handle = $1", handle).Scan(&targetID) if err != nil { return err } return r.BlockUser(ctx, actorID, targetID.String(), actorIP) } // ======================================================================== // Social Graph: Followers & Following Lists // ======================================================================== type FollowerUser struct { ID uuid.UUID `json:"id"` Handle string `json:"handle"` DisplayName string `json:"display_name"` AvatarURL *string `json:"avatar_url"` HarmonyScore int `json:"harmony_score"` Tier string `json:"tier"` FollowedAt time.Time `json:"followed_at"` } // GetFollowers returns a list of users following the specified user func (r *UserRepository) GetFollowers(ctx context.Context, userID string, limit, offset int) ([]FollowerUser, error) { query := ` SELECT p.id, p.handle, p.display_name, p.avatar_url, COALESCE(t.harmony_score, 50) as harmony_score, COALESCE(t.tier, 'new') as tier, f.created_at as followed_at FROM public.follows f JOIN public.profiles p ON p.id = f.follower_id LEFT JOIN public.trust_state t ON t.user_id = p.id WHERE f.following_id = $1::uuid AND f.status = 'accepted' ORDER BY f.created_at DESC LIMIT $2 OFFSET $3 ` rows, err := r.pool.Query(ctx, query, userID, limit, offset) if err != nil { return nil, err } defer rows.Close() var followers []FollowerUser for rows.Next() { var f FollowerUser if err := rows.Scan(&f.ID, &f.Handle, &f.DisplayName, &f.AvatarURL, &f.HarmonyScore, &f.Tier, &f.FollowedAt); err != nil { return nil, err } followers = append(followers, f) } return followers, nil } // GetFollowing returns a list of users the specified user is following func (r *UserRepository) GetFollowing(ctx context.Context, userID string, limit, offset int) ([]FollowerUser, error) { query := ` SELECT p.id, p.handle, p.display_name, p.avatar_url, COALESCE(t.harmony_score, 50) as harmony_score, COALESCE(t.tier, 'new') as tier, f.created_at as followed_at FROM public.follows f JOIN public.profiles p ON p.id = f.following_id LEFT JOIN public.trust_state t ON t.user_id = p.id WHERE f.follower_id = $1::uuid AND f.status = 'accepted' ORDER BY f.created_at DESC LIMIT $2 OFFSET $3 ` rows, err := r.pool.Query(ctx, query, userID, limit, offset) if err != nil { return nil, err } defer rows.Close() var following []FollowerUser for rows.Next() { var f FollowerUser if err := rows.Scan(&f.ID, &f.Handle, &f.DisplayName, &f.AvatarURL, &f.HarmonyScore, &f.Tier, &f.FollowedAt); err != nil { return nil, err } following = append(following, f) } return following, nil } // ======================================================================== // Circle (Close Friends) Management // ======================================================================== // AddToCircle adds a user to the current user's circle func (r *UserRepository) AddToCircle(ctx context.Context, userID, memberID string) error { // Verify that the user follows the member first isFollowing, err := r.IsFollowing(ctx, userID, memberID) if err != nil { return err } if !isFollowing { return fmt.Errorf("can only add users you follow to your circle") } query := ` INSERT INTO public.circle_members (user_id, member_id) VALUES ($1::uuid, $2::uuid) ON CONFLICT DO NOTHING ` _, err = r.pool.Exec(ctx, query, userID, memberID) return err } // RemoveFromCircle removes a user from the current user's circle func (r *UserRepository) RemoveFromCircle(ctx context.Context, userID, memberID string) error { query := `DELETE FROM public.circle_members WHERE user_id = $1::uuid AND member_id = $2::uuid` _, err := r.pool.Exec(ctx, query, userID, memberID) return err } // GetCircleMembers returns all users in the current user's circle func (r *UserRepository) GetCircleMembers(ctx context.Context, userID string) ([]FollowerUser, error) { query := ` SELECT p.id, p.handle, p.display_name, p.avatar_url, COALESCE(t.harmony_score, 50) as harmony_score, COALESCE(t.tier, 'new') as tier, c.created_at as followed_at FROM public.circle_members c JOIN public.profiles p ON p.id = c.member_id LEFT JOIN public.trust_state t ON t.user_id = p.id WHERE c.user_id = $1::uuid ORDER BY c.created_at DESC ` rows, err := r.pool.Query(ctx, query, userID) if err != nil { return nil, err } defer rows.Close() var members []FollowerUser for rows.Next() { var m FollowerUser if err := rows.Scan(&m.ID, &m.Handle, &m.DisplayName, &m.AvatarURL, &m.HarmonyScore, &m.Tier, &m.FollowedAt); err != nil { return nil, err } members = append(members, m) } return members, nil } // IsInCircle checks if a user is in another user's circle func (r *UserRepository) IsInCircle(ctx context.Context, ownerID, userID string) (bool, error) { var exists bool query := `SELECT EXISTS(SELECT 1 FROM public.circle_members WHERE user_id = $1::uuid AND member_id = $2::uuid)` err := r.pool.QueryRow(ctx, query, ownerID, userID).Scan(&exists) return exists, err } // ======================================================================== // Data Export (Portability) // ======================================================================== type UserExportData struct { Profile *models.Profile `json:"profile"` Posts []ExportedPost `json:"posts"` Following []ExportedFollow `json:"following"` } type ExportedPost struct { ID uuid.UUID `json:"id"` Body string `json:"body"` ImageURL *string `json:"image_url,omitempty"` VideoURL *string `json:"video_url,omitempty"` CreatedAt time.Time `json:"created_at"` } type ExportedFollow struct { Handle string `json:"handle"` DisplayName string `json:"display_name"` FollowedAt time.Time `json:"followed_at"` } // ExportUserData generates complete user data export for portability func (r *UserRepository) ExportUserData(ctx context.Context, userID string) (*UserExportData, error) { export := &UserExportData{} // 1. Get Profile profile, err := r.GetProfileByID(ctx, userID) if err != nil { return nil, fmt.Errorf("failed to get profile: %w", err) } export.Profile = profile // 2. Get All Posts postQuery := ` SELECT id, body, image_url, video_url, created_at FROM public.posts WHERE author_id = $1::uuid AND deleted_at IS NULL ORDER BY created_at DESC ` rows, err := r.pool.Query(ctx, postQuery, userID) if err != nil { return nil, fmt.Errorf("failed to get posts: %w", err) } defer rows.Close() for rows.Next() { var p ExportedPost if err := rows.Scan(&p.ID, &p.Body, &p.ImageURL, &p.VideoURL, &p.CreatedAt); err != nil { return nil, err } export.Posts = append(export.Posts, p) } // 3. Get Following List followQuery := ` SELECT p.handle, p.display_name, f.created_at FROM public.follows f JOIN public.profiles p ON p.id = f.following_id WHERE f.follower_id = $1::uuid AND f.status = 'accepted' ORDER BY f.created_at DESC ` rows, err = r.pool.Query(ctx, followQuery, userID) if err != nil { return nil, fmt.Errorf("failed to get following list: %w", err) } defer rows.Close() for rows.Next() { var f ExportedFollow if err := rows.Scan(&f.Handle, &f.DisplayName, &f.FollowedAt); err != nil { return nil, err } export.Following = append(export.Following, f) } return export, nil } // BanIP records an IP address as banned (used when a user is banned to prevent evasion) func (r *UserRepository) BanIP(ctx context.Context, ipAddress string, userID string, reason string) error { _, err := r.pool.Exec(ctx, ` INSERT INTO banned_ips (ip_address, user_id, reason, banned_at) VALUES ($1, $2::uuid, $3, NOW()) `, ipAddress, userID, reason) return err } // IsIPBanned checks if an IP address has been banned func (r *UserRepository) IsIPBanned(ctx context.Context, ipAddress string) (bool, error) { var exists bool err := r.pool.QueryRow(ctx, ` SELECT EXISTS(SELECT 1 FROM banned_ips WHERE ip_address = $1) `, ipAddress).Scan(&exists) return exists, err } // ======================================================================== // Account Lifecycle: Deactivate, Delete, Destroy // ======================================================================== // DeactivateUser sets user status to deactivated, preserves all data func (r *UserRepository) DeactivateUser(ctx context.Context, userID string) error { _, err := r.pool.Exec(ctx, ` UPDATE public.users SET status = 'deactivated', updated_at = NOW() WHERE id = $1::uuid `, userID) return err } // ReactivateUser sets a deactivated user back to active func (r *UserRepository) ReactivateUser(ctx context.Context, userID string) error { _, err := r.pool.Exec(ctx, ` UPDATE public.users SET status = 'active', deleted_at = NULL, updated_at = NOW() WHERE id = $1::uuid AND status IN ('deactivated', 'pending_deletion') `, userID) return err } // ScheduleDeletion marks account for deletion after 14 days func (r *UserRepository) ScheduleDeletion(ctx context.Context, userID string) error { _, err := r.pool.Exec(ctx, ` UPDATE public.users SET status = 'pending_deletion', deleted_at = NOW() + INTERVAL '14 days', updated_at = NOW() WHERE id = $1::uuid `, userID) return err } // CancelDeletion reverts a pending deletion back to active func (r *UserRepository) CancelDeletion(ctx context.Context, userID string) error { return r.ReactivateUser(ctx, userID) } // GetAccountsPendingPurge returns user IDs whose deletion grace period has expired func (r *UserRepository) GetAccountsPendingPurge(ctx context.Context) ([]string, error) { rows, err := r.pool.Query(ctx, ` SELECT id::text FROM public.users WHERE status = 'pending_deletion' AND deleted_at IS NOT NULL AND deleted_at <= NOW() `) if err != nil { return nil, err } defer rows.Close() var ids []string for rows.Next() { var id string if err := rows.Scan(&id); err != nil { return nil, err } ids = append(ids, id) } return ids, nil } // CascadePurgeUser permanently deletes ALL user data from every table. Irreversible. func (r *UserRepository) CascadePurgeUser(ctx context.Context, userID string) error { tx, err := r.pool.Begin(ctx) if err != nil { return fmt.Errorf("begin tx: %w", err) } defer tx.Rollback(ctx) // Order matters: delete from leaf tables first, then parent tables purgeQueries := []string{ // Post-related (leaf tables first) `DELETE FROM public.post_reactions WHERE post_id IN (SELECT id FROM public.posts WHERE author_id = $1::uuid)`, `DELETE FROM public.post_likes WHERE post_id IN (SELECT id FROM public.posts WHERE author_id = $1::uuid)`, `DELETE FROM public.post_saves WHERE post_id IN (SELECT id FROM public.posts WHERE author_id = $1::uuid)`, `DELETE FROM public.post_mentions WHERE post_id IN (SELECT id FROM public.posts WHERE author_id = $1::uuid)`, `DELETE FROM public.post_interactions WHERE post_id IN (SELECT id FROM public.posts WHERE author_id = $1::uuid)`, `DELETE FROM public.post_metrics WHERE post_id IN (SELECT id FROM public.posts WHERE author_id = $1::uuid)`, `DELETE FROM public.post_hashtags WHERE post_id IN (SELECT id FROM public.posts WHERE author_id = $1::uuid)`, `DELETE FROM public.post_categories WHERE post_id IN (SELECT id FROM public.posts WHERE author_id = $1::uuid)`, // Also remove this user's likes/saves/reactions on OTHER people's posts `DELETE FROM public.post_reactions WHERE user_id = $1::uuid`, `DELETE FROM public.post_likes WHERE user_id = $1::uuid`, `DELETE FROM public.post_saves WHERE user_id = $1::uuid`, `DELETE FROM public.post_interactions WHERE user_id = $1::uuid`, // Beacon votes `DELETE FROM public.beacon_votes WHERE user_id = $1::uuid`, // Comments on user's posts `DELETE FROM public.comments WHERE post_id IN (SELECT id FROM public.posts WHERE author_id = $1::uuid)`, // User's own comments `DELETE FROM public.comments WHERE author_id = $1::uuid`, // Feed engagement `DELETE FROM public.feed_engagement WHERE user_id = $1::uuid`, // Posts themselves `DELETE FROM public.posts WHERE author_id = $1::uuid`, // Messaging / E2EE `DELETE FROM public.secure_messages WHERE sender_id = $1::uuid OR receiver_id = $1::uuid`, `DELETE FROM public.encrypted_messages WHERE sender_id = $1::uuid OR receiver_id = $1::uuid`, `DELETE FROM public.e2ee_session_state WHERE session_id IN (SELECT id FROM public.e2ee_sessions WHERE user_a = $1::uuid OR user_b = $1::uuid)`, `DELETE FROM public.e2ee_sessions WHERE user_a = $1::uuid OR user_b = $1::uuid`, `DELETE FROM public.encrypted_conversations WHERE participant_a = $1::uuid OR participant_b = $1::uuid`, // Signal keys `DELETE FROM public.one_time_prekeys WHERE user_id = $1::uuid`, `DELETE FROM public.signed_prekeys WHERE user_id = $1::uuid`, `DELETE FROM public.signal_keys WHERE user_id = $1::uuid`, // Social graph `DELETE FROM public.circle_members WHERE user_id = $1::uuid OR member_id = $1::uuid`, `DELETE FROM public.follows WHERE follower_id = $1::uuid OR following_id = $1::uuid`, `DELETE FROM public.blocks WHERE blocker_id = $1::uuid OR blocked_id = $1::uuid`, // Notifications `DELETE FROM public.notifications WHERE user_id = $1::uuid OR actor_id = $1::uuid`, `DELETE FROM public.notification_preferences WHERE user_id = $1::uuid`, `DELETE FROM public.user_fcm_tokens WHERE user_id = $1::uuid`, // Moderation & violations `DELETE FROM public.moderation_flags WHERE user_id = $1::uuid`, `DELETE FROM public.pending_moderation WHERE user_id = $1::uuid`, `DELETE FROM public.user_violation_history WHERE user_id = $1::uuid`, `DELETE FROM public.user_violations WHERE user_id = $1::uuid`, `DELETE FROM public.user_appeals WHERE user_id = $1::uuid`, `DELETE FROM public.content_strikes WHERE user_id = $1::uuid`, `DELETE FROM public.reports WHERE reporter_id = $1::uuid`, `DELETE FROM public.abuse_logs WHERE user_id = $1::uuid`, `DELETE FROM public.user_status_history WHERE user_id = $1::uuid`, // Categories & hashtags `DELETE FROM public.user_category_preferences WHERE user_id = $1::uuid`, `DELETE FROM public.user_category_settings WHERE user_id = $1::uuid`, `DELETE FROM public.hashtag_follows WHERE user_id = $1::uuid`, // Backup & recovery `DELETE FROM public.recovery_shard_submissions WHERE session_id IN (SELECT id FROM public.recovery_sessions WHERE user_id = $1::uuid)`, `DELETE FROM public.recovery_sessions WHERE user_id = $1::uuid`, `DELETE FROM public.recovery_guardians WHERE user_id = $1::uuid OR guardian_id = $1::uuid`, `DELETE FROM public.cloud_backups WHERE user_id = $1::uuid`, `DELETE FROM public.backup_preferences WHERE user_id = $1::uuid`, `DELETE FROM public.sync_codes WHERE user_id = $1::uuid`, `DELETE FROM public.user_devices WHERE user_id = $1::uuid`, // Auth & tokens `DELETE FROM public.auth_tokens WHERE user_id = $1::uuid`, `DELETE FROM public.refresh_tokens WHERE user_id = $1::uuid`, `DELETE FROM public.verification_tokens WHERE user_id = $1::uuid`, `DELETE FROM public.password_reset_tokens WHERE user_id = $1::uuid`, `DELETE FROM public.webauthn_credentials WHERE user_id = $1::uuid`, `DELETE FROM public.user_mfa_secrets WHERE user_id = $1::uuid`, // Settings `DELETE FROM public.user_settings WHERE user_id = $1::uuid`, `DELETE FROM public.profile_privacy_settings WHERE user_id = $1::uuid`, // Trust `DELETE FROM public.trust_state WHERE user_id = $1::uuid`, // Username claims `DELETE FROM public.username_claim_requests WHERE user_id = $1::uuid`, // Profile & user (last) `DELETE FROM public.profiles WHERE id = $1::uuid`, `DELETE FROM public.users WHERE id = $1::uuid`, } for _, q := range purgeQueries { if _, err := tx.Exec(ctx, q, userID); err != nil { return fmt.Errorf("purge query failed: %s: %w", q[:60], err) } } return tx.Commit(ctx) }