diff --git a/server/controllers/e2ee.go b/server/controllers/e2ee.go index af77806..2bd37fd 100644 --- a/server/controllers/e2ee.go +++ b/server/controllers/e2ee.go @@ -69,3 +69,43 @@ func HandleGetPrekeyBundle(w http.ResponseWriter, r *http.Request) { _ = json.NewEncoder(w).Encode(models.JSONResponse{Success: true, Data: bundle}) } + +func HandleSetVerification(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + uid, _ := r.Context().Value(middlewares.UserIDKey).(string) + + var req models.VerifyContactReq + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + w.WriteHeader(http.StatusBadRequest) + _ = json.NewEncoder(w).Encode(models.JSONResponse{Success: false, Error: "Invalid JSON payload structure"}) + return + } + + svc := services.NewE2EEService() + if err := svc.SetVerification(r.Context(), uid, req); err != nil { + w.WriteHeader(http.StatusBadRequest) + _ = json.NewEncoder(w).Encode(models.JSONResponse{Success: false, Error: err.Error()}) + return + } + + _ = json.NewEncoder(w).Encode(models.JSONResponse{ + Success: true, + Data: map[string]interface{}{"message": "Contact verification status updated"}, + }) +} + +func HandleGetVerificationStatus(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + uid, _ := r.Context().Value(middlewares.UserIDKey).(string) + targetUID := chi.URLParam(r, "userId") + + svc := services.NewE2EEService() + resp, err := svc.GetVerification(r.Context(), uid, targetUID) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + _ = json.NewEncoder(w).Encode(models.JSONResponse{Success: false, Error: err.Error()}) + return + } + + _ = json.NewEncoder(w).Encode(models.JSONResponse{Success: true, Data: resp}) +} diff --git a/server/migrations/011_create_user_verifications_up.sql b/server/migrations/011_create_user_verifications_up.sql new file mode 100644 index 0000000..45e0d50 --- /dev/null +++ b/server/migrations/011_create_user_verifications_up.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS user_verifications ( + user_id UUID REFERENCES users(id) ON DELETE CASCADE, + verified_user_id UUID REFERENCES users(id) ON DELETE CASCADE, + is_verified BOOLEAN DEFAULT TRUE NOT NULL, + updated_at TIMESTAMP DEFAULT NOW(), + PRIMARY KEY (user_id, verified_user_id) +); + +CREATE INDEX IF NOT EXISTS idx_user_verifications ON user_verifications(user_id, verified_user_id); \ No newline at end of file diff --git a/server/models/e2ee.go b/server/models/e2ee.go index d6a89bb..192424c 100644 --- a/server/models/e2ee.go +++ b/server/models/e2ee.go @@ -22,3 +22,14 @@ type PrekeyBundle struct { OneTimePrekeyID *int `json:"one_time_prekey_id,omitempty"` OneTimePrekeyBody *string `json:"one_time_prekey_body,omitempty"` } + +type VerifyContactReq struct { + VerifiedUserID string `json:"verified_user_id"` + IsVerified bool `json:"is_verified"` +} + +type VerificationStatusResp struct { + UserID string `json:"user_id"` + VerifiedUserID string `json:"verified_user_id"` + IsVerified bool `json:"is_verified"` +} diff --git a/server/repository/e2ee.go b/server/repository/e2ee.go index 34e1097..956cbe3 100644 --- a/server/repository/e2ee.go +++ b/server/repository/e2ee.go @@ -22,6 +22,10 @@ func (r *E2EERepository) SaveDeviceKeys(ctx context.Context, uid string, req mod } defer tx.Rollback(ctx) + var existingKey string + checkQuery := `SELECT identity_key FROM user_devices WHERE user_id = $1::uuid AND device_id = $2;` + _ = tx.QueryRow(ctx, checkQuery, uid, req.DeviceID).Scan(&existingKey) + deviceQuery := ` INSERT INTO user_devices (user_id, device_id, identity_key, signed_prekey, signed_prekey_signature, updated_at) VALUES ($1::uuid, $2, $3, $4, $5, NOW()) @@ -33,6 +37,18 @@ func (r *E2EERepository) SaveDeviceKeys(ctx context.Context, uid string, req mod return err } + if existingKey != req.IdentityKey { + invalidateQuery := ` + UPDATE user_verifications + SET is_verified = FALSE, updated_at = NOW() + WHERE verified_user_id = $1::uuid; + ` + _, err = tx.Exec(ctx, invalidateQuery, uid) + if err != nil { + return err + } + } + if len(req.OneTimePrekeys) > 0 { otpQuery := `INSERT INTO one_time_prekeys (user_id, device_id, key_id, key_content) VALUES ($1::uuid, $2, $3, $4);` for _, k := range req.OneTimePrekeys { @@ -100,3 +116,41 @@ func (r *E2EERepository) GetOTPCount(ctx context.Context, uid, deviceID string) err := config.DB.QueryRow(ctx, query, uid, deviceID).Scan(&count) return count, err } + +func (r *E2EERepository) SetVerificationStatus(ctx context.Context, uid, targetUID string, isVerified bool) error { + query := ` + INSERT INTO user_verifications (user_id, verified_user_id, is_verified, updated_at) + VALUES ($1::uuid, $2::uuid, $3, NOW()) + ON CONFLICT (user_id, verified_user_id) + DO UPDATE SET is_verified = $3, updated_at = NOW(); + ` + _, err := config.DB.Exec(ctx, query, uid, targetUID, isVerified) + return err +} + +func (r *E2EERepository) GetVerificationStatus(ctx context.Context, uid, targetUID string) (bool, error) { + query := ` + SELECT is_verified + FROM user_verifications + WHERE user_id = $1::uuid AND verified_user_id = $2::uuid; + ` + var isVerified bool + err := config.DB.QueryRow(ctx, query, uid, targetUID).Scan(&isVerified) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return false, nil + } + return false, err + } + return isVerified, nil +} + +func (r *E2EERepository) InvalidateUserVerifications(ctx context.Context, uid string) error { + query := ` + UPDATE user_verifications + SET is_verified = FALSE, updated_at = NOW() + WHERE verified_user_id = $1::uuid; + ` + _, err := config.DB.Exec(ctx, query, uid) + return err +} diff --git a/server/routes/e2ee.go b/server/routes/e2ee.go index d91dd7f..1f0ae3e 100644 --- a/server/routes/e2ee.go +++ b/server/routes/e2ee.go @@ -11,4 +11,7 @@ func E2EERoute(router chi.Router) { router.Post("/keys", controllers.HandleUploadE2EEKeys) router.Get("/bundle/{userId}", controllers.HandleGetPrekeyBundle) + + router.Post("/verify", controllers.HandleSetVerification) + router.Get("/verify/{userId}", controllers.HandleGetVerificationStatus) } diff --git a/server/services/e2ee.go b/server/services/e2ee.go index 60a7af8..225c8a1 100644 --- a/server/services/e2ee.go +++ b/server/services/e2ee.go @@ -37,3 +37,29 @@ func (s *E2EEService) CheckKeysExhaustion(ctx context.Context, uid string, devic } return count, count < 10, nil } + +func (s *E2EEService) SetVerification(ctx context.Context, uid string, req models.VerifyContactReq) error { + if req.VerifiedUserID == "" { + return errors.New("target verified_user_id is required") + } + if uid == req.VerifiedUserID { + return errors.New("cannot verify own user identity") + } + return s.repo.SetVerificationStatus(ctx, uid, req.VerifiedUserID, req.IsVerified) +} + +func (s *E2EEService) GetVerification(ctx context.Context, uid, targetUID string) (*models.VerificationStatusResp, error) { + if targetUID == "" { + return nil, errors.New("target user_id is required") + } + isVerified, err := s.repo.GetVerificationStatus(ctx, uid, targetUID) + if err != nil { + return nil, err + } + + return &models.VerificationStatusResp{ + UserID: uid, + VerifiedUserID: targetUID, + IsVerified: isVerified, + }, nil +}