329 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			329 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| 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 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
 | |
| }
 | 
