1e5ef02bdf
Messages between matched users are now encrypted client-side with each user's own key pair before they ever reach the server; the backend only ever stores/relays opaque ciphertext + nonce and cannot read message content, including for the last-message preview on the matches list. - User gains a PublicKey field, registered/rotated via PUT /profile/public-key - Message gains CipherText/Nonce; legacy plaintext Text is kept as-is for messages sent before this change - MatchView exposes the partner's public key and the last message's ciphertext/nonce so the client can decrypt previews itself - Removed the now-dead plaintext SanitizeMessageText utility — the server can no longer sanitize message content since it's opaque ciphertext Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
183 lines
6.2 KiB
Go
183 lines
6.2 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
"dating-app/internal/dto"
|
|
"dating-app/internal/models"
|
|
"dating-app/internal/repository"
|
|
)
|
|
|
|
const MaxPhotosPerUser = 6
|
|
|
|
var (
|
|
ErrTooManyPhotos = errors.New("حداکثر تعداد عکس مجاز رو داری")
|
|
ErrPhotoNotOwned = errors.New("این عکس متعلق به تو نیست")
|
|
ErrInvalidImage = errors.New("فرمت فایل باید jpg، png یا webp باشه")
|
|
ErrImageTooLarge = errors.New("حجم عکس نباید بیشتر از ۵ مگابایت باشه")
|
|
ErrInvalidPublicKey = errors.New("کلید عمومی نامعتبر است")
|
|
)
|
|
|
|
const MaxPhotoSizeBytes = 5 * 1024 * 1024
|
|
|
|
type ProfileService struct {
|
|
photoRepo *repository.PhotoRepository
|
|
userRepo *repository.UserRepository
|
|
storage PhotoStorage
|
|
}
|
|
|
|
func NewProfileService(photoRepo *repository.PhotoRepository, userRepo *repository.UserRepository, storage PhotoStorage) *ProfileService {
|
|
return &ProfileService{photoRepo: photoRepo, userRepo: userRepo, storage: storage}
|
|
}
|
|
|
|
var allowedImageExts = map[string]bool{
|
|
".jpg": true, ".jpeg": true, ".png": true, ".webp": true,
|
|
}
|
|
|
|
func (s *ProfileService) UploadPhoto(userID uint, ext string, data []byte) (*models.Photo, error) {
|
|
if !allowedImageExts[ext] {
|
|
return nil, ErrInvalidImage
|
|
}
|
|
if len(data) > MaxPhotoSizeBytes {
|
|
return nil, ErrImageTooLarge
|
|
}
|
|
|
|
count, err := s.photoRepo.CountByUser(userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if count >= MaxPhotosPerUser {
|
|
return nil, ErrTooManyPhotos
|
|
}
|
|
|
|
url, err := s.storage.Save(userID, ext, data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
photo := &models.Photo{UserID: userID, URL: url, SortOrder: int(count)}
|
|
if err := s.photoRepo.Create(photo); err != nil {
|
|
return nil, err
|
|
}
|
|
return photo, nil
|
|
}
|
|
|
|
func (s *ProfileService) DeletePhoto(userID, photoID uint) error {
|
|
photo, err := s.photoRepo.FindByID(photoID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if photo.UserID != userID {
|
|
return ErrPhotoNotOwned
|
|
}
|
|
if err := s.photoRepo.Delete(photoID); err != nil {
|
|
return err
|
|
}
|
|
return s.storage.Delete(photo.URL)
|
|
}
|
|
|
|
// DeletePhotoAdmin مثل DeletePhoto ولی بدون چک مالکیت؛ برای نظارت محتوایی
|
|
// ادمین که باید بتونه هر عکسی رو صرفنظر از صاحبش حذف کنه.
|
|
func (s *ProfileService) DeletePhotoAdmin(photoID uint) error {
|
|
photo, err := s.photoRepo.FindByID(photoID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := s.photoRepo.Delete(photoID); err != nil {
|
|
return err
|
|
}
|
|
return s.storage.Delete(photo.URL)
|
|
}
|
|
|
|
func (s *ProfileService) ListPhotos(userID uint) ([]models.Photo, error) {
|
|
return s.photoRepo.ListByUser(userID)
|
|
}
|
|
|
|
func (s *ProfileService) UpdateLocation(user *models.User, lat, lng float64) error {
|
|
return s.userRepo.UpdateFields(user, map[string]interface{}{
|
|
"latitude": lat,
|
|
"longitude": lng,
|
|
})
|
|
}
|
|
|
|
// UpdateBio بیوگرافی کوتاه پروفایل رو ذخیره میکنه (طول توسط binding در DTO چک شده)
|
|
func (s *ProfileService) UpdateBio(user *models.User, bio string) error {
|
|
bio = strings.TrimSpace(bio)
|
|
if err := s.userRepo.UpdateFields(user, map[string]interface{}{"bio": bio}); err != nil {
|
|
return err
|
|
}
|
|
user.Bio = &bio
|
|
return nil
|
|
}
|
|
|
|
// CheckIn ورود روزانهی کاربر رو ثبت میکنه و استریک (چند روز پشتسرهم) رو
|
|
// بهروز میکنه: اگه دیروز هم اومده بوده، استریک +۱ میشه؛ اگه امروز قبلاً
|
|
// چکاین کرده، چیزی تغییر نمیکنه؛ وگرنه (وقفه افتاده) استریک از ۱ شروع میشه.
|
|
func (s *ProfileService) CheckIn(user *models.User) (*dto.CheckInResult, error) {
|
|
now := time.Now()
|
|
today := now.Format("2006-01-02")
|
|
|
|
if user.LastActiveDate != nil && user.LastActiveDate.Format("2006-01-02") == today {
|
|
return &dto.CheckInResult{StreakCount: user.StreakCount, IsNewToday: false}, nil
|
|
}
|
|
|
|
newStreak := 1
|
|
if user.LastActiveDate != nil {
|
|
yesterday := now.AddDate(0, 0, -1).Format("2006-01-02")
|
|
if user.LastActiveDate.Format("2006-01-02") == yesterday {
|
|
newStreak = user.StreakCount + 1
|
|
}
|
|
}
|
|
|
|
if err := s.userRepo.UpdateFields(user, map[string]interface{}{
|
|
"streak_count": newStreak,
|
|
"last_active_date": now,
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
user.StreakCount = newStreak
|
|
user.LastActiveDate = &now
|
|
|
|
return &dto.CheckInResult{StreakCount: newStreak, IsNewToday: true}, nil
|
|
}
|
|
|
|
// UpdateNotificationSettings ترجیحات اعلان کاربر رو ذخیره میکنه.
|
|
func (s *ProfileService) UpdateNotificationSettings(user *models.User, req dto.UpdateNotificationSettingsRequest) error {
|
|
if err := s.userRepo.UpdateFields(user, map[string]interface{}{
|
|
"notify_matches": req.NotifyMatches,
|
|
"notify_messages": req.NotifyMessages,
|
|
"notify_likes": req.NotifyLikes,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
user.NotifyMatches = req.NotifyMatches
|
|
user.NotifyMessages = req.NotifyMessages
|
|
user.NotifyLikes = req.NotifyLikes
|
|
return nil
|
|
}
|
|
|
|
// DeleteAccount حساب کاربر رو غیرفعال میکنه (soft) نه پاکسازی کامل رکورد؛
|
|
// چون مچها/پیامهای طرف مقابل نباید یهو پاره بشن. کاربر غیرفعال دیگه اصلاً
|
|
// نمیتونه لاگین کنه (چک IsActive توی AuthRequired و AuthService).
|
|
func (s *ProfileService) DeleteAccount(user *models.User) error {
|
|
return s.userRepo.UpdateFields(user, map[string]interface{}{"is_active": false})
|
|
}
|
|
|
|
// UpdatePublicKey کلید عمومی X25519 کاربر رو برای رمزنگاری سرتاسری پیامها
|
|
// ثبت/جایگزین میکنه. کلید خصوصی متناظرش هیچوقت به سرور نمیرسه؛ اینجا فقط
|
|
// طول کلید خام (باید دقیقاً ۳۲ بایت باشه) بعد از decode چک میشه، نه محتواش.
|
|
func (s *ProfileService) UpdatePublicKey(user *models.User, publicKeyB64 string) error {
|
|
raw, err := base64.StdEncoding.DecodeString(publicKeyB64)
|
|
if err != nil || len(raw) != 32 {
|
|
return ErrInvalidPublicKey
|
|
}
|
|
if err := s.userRepo.UpdateFields(user, map[string]interface{}{"public_key": publicKeyB64}); err != nil {
|
|
return err
|
|
}
|
|
user.PublicKey = &publicKeyB64
|
|
return nil
|
|
}
|