package db import ( "context" "crypto/rand" "fmt" "math/big" "strings" "time" "golang.org/x/crypto/bcrypt" ) type User struct { ID int `json:"id"` Username string `json:"username"` Email string `json:"email"` IsVerified bool `json:"is_verified"` CreatedAt time.Time `json:"created_at"` } type CreateUserRequest struct { Username string `json:"username"` Email string `json:"email"` Password string `json:"password"` } type LoginRequest struct { Username string `json:"username"` Password string `json:"password"` } type VerifyCodeRequest struct { Email string `json:"email"` Code string `json:"code"` } type VerificationCode struct { ID int `json:"id"` UserID int `json:"user_id"` Code string `json:"code"` ExpiresAt time.Time `json:"expires_at"` Used bool `json:"used"` CreatedAt time.Time `json:"created_at"` } func GenerateVerificationCode() (string, error) { const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" const length = 6 result := make([]byte, length) for i := range result { num, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) if err != nil { return "", err } result[i] = charset[num.Int64()] } return string(result), nil } func CreateUser(ctx context.Context, req CreateUserRequest) (*User, error) { hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { return nil, err } var user User err = Pool.QueryRow(ctx, "INSERT INTO users (username, email, password_hash) VALUES ($1, $2, $3) RETURNING id, username, email, is_verified, created_at", req.Username, strings.ToLower(req.Email), string(hashedPassword), ).Scan(&user.ID, &user.Username, &user.Email, &user.IsVerified, &user.CreatedAt) if err != nil { return nil, err } return &user, nil } func CreateVerificationCode(ctx context.Context, userID int) (*VerificationCode, error) { code, err := GenerateVerificationCode() if err != nil { return nil, err } expiresAt := time.Now().Add(10 * time.Minute) var verificationCode VerificationCode err = Pool.QueryRow(ctx, "INSERT INTO verification_codes (user_id, code, expires_at) VALUES ($1, $2, $3) RETURNING id, user_id, code, expires_at, used, created_at", userID, code, expiresAt, ).Scan(&verificationCode.ID, &verificationCode.UserID, &verificationCode.Code, &verificationCode.ExpiresAt, &verificationCode.Used, &verificationCode.CreatedAt) if err != nil { return nil, err } return &verificationCode, nil } func VerifyCode(ctx context.Context, email, code string) error { tx, err := Pool.Begin(ctx) if err != nil { return err } defer tx.Rollback(ctx) var userID int var codeID int var expiresAt time.Time var used bool err = tx.QueryRow(ctx, ` SELECT u.id, vc.id, vc.expires_at, vc.used FROM users u JOIN verification_codes vc ON u.id = vc.user_id WHERE u.email = $1 AND vc.code = $2 ORDER BY vc.created_at DESC LIMIT 1 `, strings.ToLower(email), code).Scan(&userID, &codeID, &expiresAt, &used) if err != nil { return fmt.Errorf("invalid verification code") } if used { return fmt.Errorf("verification code already used") } if time.Now().After(expiresAt) { return fmt.Errorf("verification code expired") } _, err = tx.Exec(ctx, "UPDATE verification_codes SET used = true WHERE id = $1", codeID) if err != nil { return err } _, err = tx.Exec(ctx, "UPDATE users SET is_verified = true WHERE id = $1", userID) if err != nil { return err } return tx.Commit(ctx) } func GetUserByUsername(ctx context.Context, username string) (*User, error) { var user User err := Pool.QueryRow(ctx, "SELECT id, username, email, is_verified, created_at FROM users WHERE username = $1", username, ).Scan(&user.ID, &user.Username, &user.Email, &user.IsVerified, &user.CreatedAt) if err != nil { return nil, err } return &user, nil } func GetUserByEmail(ctx context.Context, email string) (*User, error) { var user User err := Pool.QueryRow(ctx, "SELECT id, username, email, is_verified, created_at FROM users WHERE email = $1", strings.ToLower(email), ).Scan(&user.ID, &user.Username, &user.Email, &user.IsVerified, &user.CreatedAt) if err != nil { return nil, err } return &user, nil } type Termail struct { ID int `json:"id"` SenderID int `json:"sender_id"` ReceiverID int `json:"receiver_id"` Subject string `json:"subject"` Content string `json:"content"` IsRead bool `json:"is_read"` SentAt time.Time `json:"sent_at"` Sender string `json:"sender,omitempty"` Receiver string `json:"receiver,omitempty"` } type SendTermailRequest struct { ReceiverUsername string `json:"receiver_username"` Subject string `json:"subject"` Content string `json:"content"` } func GetTermail(ctx context.Context, termailID, userID int) (*Termail, error) { var t Termail err := Pool.QueryRow(ctx, ` SELECT t.id, t.sender_id, t.receiver_id, t.subject, t.content, t.is_read, t.sent_at, u.username as sender FROM termails t JOIN users u ON t.sender_id = u.id WHERE t.id = $1 AND t.receiver_id = $2 `, termailID, userID).Scan(&t.ID, &t.SenderID, &t.ReceiverID, &t.Subject, &t.Content, &t.IsRead, &t.SentAt, &t.Sender) if err != nil { return nil, err } return &t, nil } func SendTermail(ctx context.Context, senderID int, req SendTermailRequest) (*Termail, error) { receiver, err := GetUserByUsername(ctx, req.ReceiverUsername) if err != nil { return nil, fmt.Errorf("user not found: %s", req.ReceiverUsername) } var termail Termail err = Pool.QueryRow(ctx, "INSERT INTO termails (sender_id, receiver_id, subject, content) VALUES ($1, $2, $3, $4) RETURNING id, sender_id, receiver_id, subject, content, is_read, sent_at", senderID, receiver.ID, req.Subject, req.Content, ).Scan(&termail.ID, &termail.SenderID, &termail.ReceiverID, &termail.Subject, &termail.Content, &termail.IsRead, &termail.SentAt) if err != nil { return nil, err } return &termail, nil } func GetInbox(ctx context.Context, userID int, limit, offset int) ([]Termail, error) { query := ` SELECT t.id, t.sender_id, t.receiver_id, t.subject, t.content, t.is_read, t.sent_at, u.username as sender FROM termails t JOIN users u ON t.sender_id = u.id WHERE t.receiver_id = $1 ORDER BY t.sent_at DESC LIMIT $2 OFFSET $3 ` rows, err := Pool.Query(ctx, query, userID, limit, offset) if err != nil { return nil, err } defer rows.Close() var termails []Termail for rows.Next() { var t Termail err := rows.Scan(&t.ID, &t.SenderID, &t.ReceiverID, &t.Subject, &t.Content, &t.IsRead, &t.SentAt, &t.Sender) if err != nil { return nil, err } termails = append(termails, t) } return termails, nil } func MarkTermailAsRead(ctx context.Context, termailID, userID int) error { _, err := Pool.Exec(ctx, "UPDATE termails SET is_read = true WHERE id = $1 AND receiver_id = $2", termailID, userID, ) return err } func DeleteTermail(ctx context.Context, termailID, userID int) error { result, err := Pool.Exec(ctx, "DELETE FROM termails WHERE id = $1 AND receiver_id = $2", termailID, userID, ) if err != nil { return err } rowsAffected := result.RowsAffected() if rowsAffected == 0 { return fmt.Errorf("termail not found or access denied") } return nil } func SearchTermails(ctx context.Context, userID int, query string, limit, offset int) ([]Termail, error) { sqlQuery := ` SELECT t.id, t.sender_id, t.receiver_id, t.subject, t.content, t.is_read, t.sent_at, u.username as sender FROM termails t JOIN users u ON t.sender_id = u.id WHERE t.receiver_id = $1 AND (t.subject ILIKE $2 OR t.content ILIKE $2 OR u.username ILIKE $2) ORDER BY t.sent_at DESC LIMIT $3 OFFSET $4 ` searchPattern := "%" + query + "%" rows, err := Pool.Query(ctx, sqlQuery, userID, searchPattern, limit, offset) if err != nil { return nil, err } defer rows.Close() var termails []Termail for rows.Next() { var t Termail err := rows.Scan(&t.ID, &t.SenderID, &t.ReceiverID, &t.Subject, &t.Content, &t.IsRead, &t.SentAt, &t.Sender) if err != nil { return nil, err } termails = append(termails, t) } return termails, nil } func CleanupUnverifiedUsers(ctx context.Context) error { _, err := Pool.Exec(ctx, ` DELETE FROM users WHERE is_verified = false AND created_at < NOW() - INTERVAL '1 hour' `) return err } func ValidateUserCredentials(ctx context.Context, username, password string) (*User, error) { var user User var passwordHash string err := Pool.QueryRow(ctx, "SELECT id, username, email, password_hash, is_verified, created_at FROM users WHERE username = $1", username, ).Scan(&user.ID, &user.Username, &user.Email, &passwordHash, &user.IsVerified, &user.CreatedAt) if err != nil { return nil, err } if !user.IsVerified { return nil, fmt.Errorf("account not verified") } err = bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(password)) if err != nil { return nil, err } return &user, nil }