Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions server/controllers/e2ee.go
Original file line number Diff line number Diff line change
Expand Up @@ -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})
}
9 changes: 9 additions & 0 deletions server/migrations/011_create_user_verifications_up.sql
Original file line number Diff line number Diff line change
@@ -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);
11 changes: 11 additions & 0 deletions server/models/e2ee.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
54 changes: 54 additions & 0 deletions server/repository/e2ee.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
3 changes: 3 additions & 0 deletions server/routes/e2ee.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
26 changes: 26 additions & 0 deletions server/services/e2ee.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Loading