diff --git a/rocket-backend/integration-tests/internal/database-tests/image_store_table_test.go b/rocket-backend/integration-tests/internal/database-tests/image_store_table_test.go index ade74c3..044dc28 100644 --- a/rocket-backend/integration-tests/internal/database-tests/image_store_table_test.go +++ b/rocket-backend/integration-tests/internal/database-tests/image_store_table_test.go @@ -4,9 +4,9 @@ import ( "bytes" "time" + "github.com/google/uuid" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/google/uuid" ) var _ = Describe("Image Store Table Integration", func() { @@ -92,4 +92,61 @@ var _ = Describe("Image Store Table Integration", func() { err := row.Scan(&gotID, &gotName, &gotData) Expect(err).ToNot(BeNil()) }) + + It("should save and delete a user image", func() { + imageID := uuid.New() + imgName := "todelete.png" + imgData := []byte{0x89, 0x50, 0x4E, 0x47} + + _, err := testDbInstance.Exec(` + INSERT INTO image_store (id, image_name, image_data) + VALUES ($1, $2, $3) + `, imageID, imgName, imgData) + Expect(err).To(BeNil()) + + // Link image to user in settings + _, err = testDbInstance.Exec(` + UPDATE settings SET image_id = $1 WHERE user_id = $2 + `, imageID, userID) + Expect(err).To(BeNil()) + + // Delete user image + _, err = testDbInstance.Exec(` + UPDATE settings SET image_id = NULL WHERE user_id = $1 + `, userID) + Expect(err).To(BeNil()) + _, err = testDbInstance.Exec(` + DELETE FROM image_store WHERE id = $1 + `, imageID) + Expect(err).To(BeNil()) + + // Confirm deletion + row := testDbInstance.QueryRow(` + SELECT COUNT(*) FROM image_store WHERE id = $1 + `, imageID) + var count int + err = row.Scan(&count) + Expect(err).To(BeNil()) + Expect(count).To(Equal(0)) + }) + + It("should return error when getting image for user with no image", func() { + // Remove image_id from settings if present + _, err := testDbInstance.Exec(` + UPDATE settings SET image_id = NULL WHERE user_id = $1 + `, userID) + Expect(err).To(BeNil()) + + row := testDbInstance.QueryRow(` + SELECT i.id, i.image_name, i.image_data + FROM settings s + JOIN image_store i ON s.image_id = i.id + WHERE s.user_id = $1 + `, userID) + var gotID uuid.UUID + var gotName string + var gotData []byte + err = row.Scan(&gotID, &gotName, &gotData) + Expect(err).ToNot(BeNil()) + }) }) diff --git a/rocket-backend/integration-tests/internal/database-tests/user_table_test.go b/rocket-backend/integration-tests/internal/database-tests/user_table_test.go index 0979789..ee10fdb 100644 --- a/rocket-backend/integration-tests/internal/database-tests/user_table_test.go +++ b/rocket-backend/integration-tests/internal/database-tests/user_table_test.go @@ -82,4 +82,114 @@ var _ = Describe("Users Table Integration", func() { Expect(err).ToNot(BeNil()) Expect(err.Error()).To(ContainSubstring("violates foreign key constraint")) }) + + It("should update and retrieve user name", func() { + username := "useruser" + email := "useruser@example.com" + rocketpoints := 42 + + _, err := testDbInstance.Exec(` + INSERT INTO users (id, username, email, rocketpoints) + VALUES ($1, $2, $3, $4) + `, userID, username, email, rocketpoints) + Expect(err).To(BeNil()) + + _, err = testDbInstance.Exec(` + UPDATE users SET username = $1 WHERE id = $2 + `, "newname", userID) + Expect(err).To(BeNil()) + + row := testDbInstance.QueryRow(` + SELECT username FROM users WHERE id = $1 + `, userID) + var gotUsername string + err = row.Scan(&gotUsername) + Expect(err).To(BeNil()) + Expect(gotUsername).To(Equal("newname")) + }) + + It("should update and retrieve user email in both users and credentials", func() { + username := "useruser" + email := "useruser@example.com" + rocketpoints := 42 + + _, err := testDbInstance.Exec(` + INSERT INTO users (id, username, email, rocketpoints) + VALUES ($1, $2, $3, $4) + `, userID, username, email, rocketpoints) + Expect(err).To(BeNil()) + + _, err = testDbInstance.Exec(` + UPDATE users SET email = $1 WHERE id = $2 + `, "newemail@example.com", userID) + Expect(err).To(BeNil()) + _, err = testDbInstance.Exec(` + UPDATE credentials SET email = $1 WHERE id = $2 + `, "newemail@example.com", userID) + Expect(err).To(BeNil()) + + row := testDbInstance.QueryRow(` + SELECT email FROM users WHERE id = $1 + `, userID) + var gotEmail string + err = row.Scan(&gotEmail) + Expect(err).To(BeNil()) + Expect(gotEmail).To(Equal("newemail@example.com")) + + row = testDbInstance.QueryRow(` + SELECT email FROM credentials WHERE id = $1 + `, userID) + err = row.Scan(&gotEmail) + Expect(err).To(BeNil()) + Expect(gotEmail).To(Equal("newemail@example.com")) + }) + + It("should get user ID by username", func() { + username := "useruser" + email := "useruser@example.com" + rocketpoints := 42 + + _, err := testDbInstance.Exec(` + INSERT INTO users (id, username, email, rocketpoints) + VALUES ($1, $2, $3, $4) + `, userID, username, email, rocketpoints) + Expect(err).To(BeNil()) + + row := testDbInstance.QueryRow(` + SELECT id FROM users WHERE username = $1 + `, username) + var gotID uuid.UUID + err = row.Scan(&gotID) + Expect(err).To(BeNil()) + Expect(gotID).To(Equal(userID)) + }) + + It("should delete a user and its credentials", func() { + username := "useruser" + email := "useruser@example.com" + rocketpoints := 42 + + _, err := testDbInstance.Exec(` + INSERT INTO users (id, username, email, rocketpoints) + VALUES ($1, $2, $3, $4) + `, userID, username, email, rocketpoints) + Expect(err).To(BeNil()) + + _, err = testDbInstance.Exec(` + DELETE FROM users WHERE id = $1 + `, userID) + Expect(err).To(BeNil()) + _, err = testDbInstance.Exec(` + DELETE FROM credentials WHERE id = $1 + `, userID) + Expect(err).To(BeNil()) + + row := testDbInstance.QueryRow(` + SELECT COUNT(*) FROM users WHERE id = $1 + `, userID) + var count int + err = row.Scan(&count) + Expect(err).To(BeNil()) + Expect(count).To(Equal(0)) + }) }) diff --git a/rocket-backend/integration-tests/internal/server-tests/settings_routes_test.go b/rocket-backend/integration-tests/internal/server-tests/settings_routes_test.go index d4b9336..a3f046c 100644 --- a/rocket-backend/integration-tests/internal/server-tests/settings_routes_test.go +++ b/rocket-backend/integration-tests/internal/server-tests/settings_routes_test.go @@ -95,4 +95,91 @@ var _ = Describe("Settings Handlers API", func() { _ = json.NewDecoder(resp.Body).Decode(&result) Expect(result["message"]).To(Equal("Image updated successfully")) }) + + It("should delete user image", func() { + // First, upload an image so there is something to delete + imgPath := filepath.Join(os.TempDir(), "testimg_del.png") + os.WriteFile(imgPath, []byte{0x89, 0x50, 0x4E, 0x47}, 0644) + file, _ := os.Open(imgPath) + defer file.Close() + + var b bytes.Buffer + w := multipart.NewWriter(&b) + fw, _ := w.CreateFormFile("image", "testimg_del.png") + file.Seek(0, 0) + _, _ = file.WriteTo(fw) + w.Close() + + req, _ := http.NewRequest("POST", baseURL+"/protected/settings/image", &b) + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", w.FormDataContentType()) + resp, err := http.DefaultClient.Do(req) + Expect(err).To(BeNil()) + resp.Body.Close() + + // Now, delete the image + req, _ = http.NewRequest("DELETE", baseURL+"/protected/settings/image", nil) + req.Header.Set("Authorization", "Bearer "+token) + resp, err = http.DefaultClient.Do(req) + Expect(err).To(BeNil()) + defer resp.Body.Close() + Expect(resp.StatusCode).To(Equal(200)) + var result map[string]any + _ = json.NewDecoder(resp.Body).Decode(&result) + Expect(result["message"]).To(Equal("Image deleted successfully")) + }) + + It("should update user info (name and email)", func() { + payload := map[string]any{ + "name": "New Name", + "email": "newemail@example.com", + } + body, _ := json.Marshal(payload) + req, _ := http.NewRequest("POST", baseURL+"/protected/settings/userinfo", bytes.NewReader(body)) + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + resp, err := http.DefaultClient.Do(req) + Expect(err).To(BeNil()) + defer resp.Body.Close() + Expect(resp.StatusCode).To(Equal(200)) + var result map[string]any + _ = json.NewDecoder(resp.Body).Decode(&result) + Expect(result["message"]).To(Equal("User info updated successfully")) + }) + + It("should update user password with correct current password", func() { + payload := map[string]any{ + "currentPassword": "password123", + "newPassword": "newpass456", + } + body, _ := json.Marshal(payload) + req, _ := http.NewRequest("POST", baseURL+"/protected/settings/userinfo", bytes.NewReader(body)) + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + resp, err := http.DefaultClient.Do(req) + Expect(err).To(BeNil()) + defer resp.Body.Close() + Expect(resp.StatusCode).To(Equal(200)) + var result map[string]any + _ = json.NewDecoder(resp.Body).Decode(&result) + Expect(result["message"]).To(Equal("User info updated successfully")) + }) + + It("should reject password update with wrong current password", func() { + payload := map[string]any{ + "currentPassword": "wrongpassword", + "newPassword": "newpass456", + } + body, _ := json.Marshal(payload) + req, _ := http.NewRequest("POST", baseURL+"/protected/settings/userinfo", bytes.NewReader(body)) + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + resp, err := http.DefaultClient.Do(req) + Expect(err).To(BeNil()) + defer resp.Body.Close() + Expect(resp.StatusCode).To(Equal(401)) + var result map[string]any + _ = json.NewDecoder(resp.Body).Decode(&result) + Expect(result["error"]).To(Equal("Current password incorrect")) + }) }) diff --git a/rocket-backend/integration-tests/internal/server-tests/user_routes_test.go b/rocket-backend/integration-tests/internal/server-tests/user_routes_test.go index 94214e8..cacebe2 100644 --- a/rocket-backend/integration-tests/internal/server-tests/user_routes_test.go +++ b/rocket-backend/integration-tests/internal/server-tests/user_routes_test.go @@ -118,4 +118,26 @@ var _ = Describe("Protected Handlers API", func() { _ = json.NewDecoder(resp.Body).Decode(&users) Expect(users).To(BeNil()) }) + + It("should delete the user account", func() { + delToken := registerAndLogin("deleteuser@example.com", "password123", "deleteuser") + + req, _ := http.NewRequest("DELETE", baseURL+"/protected/user", nil) + req.Header.Set("Authorization", "Bearer "+delToken) + resp, err := http.DefaultClient.Do(req) + Expect(err).To(BeNil()) + defer resp.Body.Close() + Expect(resp.StatusCode).To(Equal(200)) + var result map[string]any + _ = json.NewDecoder(resp.Body).Decode(&result) + Expect(result["message"]).To(Equal("User deleted successfully")) + + // Try to access a protected endpoint with the same token, should be unauthorized + req2, _ := http.NewRequest("GET", baseURL+"/protected/user", nil) + req2.Header.Set("Authorization", "Bearer "+delToken) + resp2, err := http.DefaultClient.Do(req2) + Expect(err).To(BeNil()) + defer resp2.Body.Close() + Expect(resp2.StatusCode).To(Equal(401)) + }) }) diff --git a/rocket-backend/internal/database/database.go b/rocket-backend/internal/database/database.go index 46735ee..3b17640 100644 --- a/rocket-backend/internal/database/database.go +++ b/rocket-backend/internal/database/database.go @@ -42,6 +42,11 @@ type Service interface { GetUserIDByName(name string) (uuid.UUID, error) GetTopUsers(limit int) ([]types.User, error) GetAllUsers(excludeUserID *uuid.UUID) ([]types.User, error) + DeleteUser(userID uuid.UUID) error + UpdateUserName(userID uuid.UUID, newName string) error + UpdateUserEmail(userID uuid.UUID, newEmail string) error + CheckUserPassword(userID uuid.UUID, currentPassword string) (bool, error) + UpdateUserPassword(userID uuid.UUID, newPassword string) error // daily_steps UpdateDailySteps(userID uuid.UUID, steps int) error @@ -55,6 +60,7 @@ type Service interface { UpdateSettingsImage(userId uuid.UUID, imageID uuid.UUID) error UpdateStepGoal(userId uuid.UUID, stepGoal int) error UpdateImage(userId uuid.UUID, imageID uuid.UUID) error + DeleteUserImage(userID uuid.UUID) error // images SaveImage(filename string, data []byte) (uuid.UUID, error) diff --git a/rocket-backend/internal/database/image_store_table.go b/rocket-backend/internal/database/image_store_table.go index 617b609..cb53788 100644 --- a/rocket-backend/internal/database/image_store_table.go +++ b/rocket-backend/internal/database/image_store_table.go @@ -56,3 +56,39 @@ func (s *service) GetUserImage(userID uuid.UUID) (*types.UserImage, error) { return &img, nil } + +func (s *service) DeleteUserImage(userID uuid.UUID) error { + var imageID uuid.UUID + err := s.db.QueryRow(` + SELECT image_id FROM settings WHERE user_id = $1 + `, userID).Scan(&imageID) + if err != nil { + if err == sql.ErrNoRows { + logger.Warn("No settings found for user:", userID) + return nil + } + logger.Error("Failed to get image_id from settings", err) + return fmt.Errorf("%w: %v", custom_error.ErrFailedToRetrieveData, err) + } + if imageID == uuid.Nil { + return nil + } + + _, err = s.db.Exec(` + UPDATE settings SET image_id = NULL WHERE user_id = $1 + `, userID) + if err != nil { + logger.Error("Failed to remove image reference from settings", err) + return fmt.Errorf("%w: %v", custom_error.ErrFailedToDelete, err) + } + + _, err = s.db.Exec(` + DELETE FROM image_store WHERE id = $1 + `, imageID) + if err != nil { + logger.Error("Failed to delete image", err) + return fmt.Errorf("%w: %v", custom_error.ErrFailedToDelete, err) + } + + return nil +} diff --git a/rocket-backend/internal/database/user_table.go b/rocket-backend/internal/database/user_table.go index ddbee8e..2a48ca6 100644 --- a/rocket-backend/internal/database/user_table.go +++ b/rocket-backend/internal/database/user_table.go @@ -9,6 +9,7 @@ import ( "github.com/google/uuid" _ "github.com/jackc/pgx/v5/stdlib" _ "github.com/joho/godotenv/autoload" + "golang.org/x/crypto/bcrypt" ) func (s *service) SaveUserProfile(user types.User) error { @@ -84,6 +85,72 @@ func (s *service) GetTopUsers(limit int) ([]types.User, error) { return users, nil } +// UpdateUserName updates the username for a given user UUID. +func (s *service) UpdateUserName(userID uuid.UUID, newName string) error { + query := `UPDATE users SET username = $2 WHERE id = $1` + _, err := s.db.Exec(query, userID, newName) + if err != nil { + return fmt.Errorf("%w: %v", custom_error.ErrFailedToUpdate, err) + } + return nil +} + +// UpdateUserEmail updates the email for a given user UUID in both users and credentials tables. +func (s *service) UpdateUserEmail(userID uuid.UUID, newEmail string) error { + tx, err := s.db.Begin() + if err != nil { + return fmt.Errorf("failed to begin transaction: %w", err) + } + defer tx.Rollback() + + // Update in users table + queryUsers := `UPDATE users SET email = $2 WHERE id = $1` + if _, err := tx.Exec(queryUsers, userID, newEmail); err != nil { + return fmt.Errorf("%w: %v", custom_error.ErrFailedToUpdate, err) + } + + // Update in credentials table + queryCreds := `UPDATE credentials SET email = $2 WHERE id = $1` + if _, err := tx.Exec(queryCreds, userID, newEmail); err != nil { + return fmt.Errorf("%w: %v", custom_error.ErrFailedToUpdate, err) + } + + if err := tx.Commit(); err != nil { + return fmt.Errorf("failed to commit transaction: %w", err) + } + return nil +} + +// CheckUserPassword checks if the provided password matches the user's current password using bcrypt. +func (s *service) CheckUserPassword(userID uuid.UUID, currentPassword string) (bool, error) { + var hashedPassword string + query := `SELECT password FROM credentials WHERE id = $1` + err := s.db.QueryRow(query, userID).Scan(&hashedPassword) + if err != nil { + return false, fmt.Errorf("%w: %v", custom_error.ErrFailedToRetrieveData, err) + } + // Use bcrypt for password verification + if err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(currentPassword)); err != nil { + return false, nil + } + return true, nil +} + +// UpdateUserPassword updates the user's password in the credentials table using bcrypt. +func (s *service) UpdateUserPassword(userID uuid.UUID, newPassword string) error { + // Hash the new password using bcrypt + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) + if err != nil { + return fmt.Errorf("failed to hash new password: %w", err) + } + query := `UPDATE credentials SET password = $2 WHERE id = $1` + _, err = s.db.Exec(query, userID, string(hashedPassword)) + if err != nil { + return fmt.Errorf("%w: %v", custom_error.ErrFailedToUpdate, err) + } + return nil +} + func (s *service) GetAllUsers(excludeUserID *uuid.UUID) ([]types.User, error) { var users []types.User var rows *sql.Rows @@ -116,3 +183,21 @@ func (s *service) GetAllUsers(excludeUserID *uuid.UUID) ([]types.User, error) { return users, nil } + +func (s *service) DeleteUser(userID uuid.UUID) error { + // First, delete from users (this will cascade to all dependent tables) + query := `DELETE FROM users WHERE id = $1` + _, err := s.db.Exec(query, userID) + if err != nil { + return fmt.Errorf("%w: %v", custom_error.ErrFailedToDelete, err) + } + + // Then, delete from credentials (since users references credentials, not the other way around) + credQuery := `DELETE FROM credentials WHERE id = $1` + _, credErr := s.db.Exec(credQuery, userID) + if credErr != nil { + return fmt.Errorf("%w: %v", custom_error.ErrFailedToDelete, credErr) + } + + return nil +} diff --git a/rocket-backend/internal/server/routes.go b/rocket-backend/internal/server/routes.go index 5601395..865ec38 100644 --- a/rocket-backend/internal/server/routes.go +++ b/rocket-backend/internal/server/routes.go @@ -37,8 +37,11 @@ func (s *Server) RegisterRoutes() http.Handler { protected.GET("/settings", s.GetSettingsHandler) protected.POST("/settings/step-goal", s.UpdateStepGoalHandler) protected.POST("/settings/image", s.UpdateImageHandler) + protected.DELETE("/settings/image", s.DeleteImageHandler) + protected.POST("/settings/userinfo", s.UpdateUserInfoHandler) protected.GET("/user", s.GetUserHandler) + protected.DELETE("/user", s.DeleteUserHandler) protected.GET("/user/:name", s.GetUserByNameHandler) protected.POST("/user/statistics", s.GetUserStatisticsHandler) protected.POST("/user/image", s.GetUserImageHandler) diff --git a/rocket-backend/internal/server/settings_handlers.go b/rocket-backend/internal/server/settings_handlers.go index 6ea8891..21a7deb 100644 --- a/rocket-backend/internal/server/settings_handlers.go +++ b/rocket-backend/internal/server/settings_handlers.go @@ -122,3 +122,91 @@ func (s *Server) UpdateImageHandler(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "Image updated successfully"}) } + +func (s *Server) DeleteImageHandler(c *gin.Context) { + userID, exists := c.Get("userID") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"}) + return + } + + userUUID, err := uuid.Parse(userID.(string)) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID format"}) + return + } + + logger.Info("Deleting image for user", "userID", userUUID) + + err = s.db.DeleteUserImage(userUUID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete image"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Image deleted successfully"}) +} + +func (s *Server) UpdateUserInfoHandler(c *gin.Context) { + userID, exists := c.Get("userID") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"}) + return + } + userUUID, err := uuid.Parse(userID.(string)) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID format"}) + return + } + + var req struct { + Name *string `json:"name"` + Email *string `json:"email"` + CurrentPassword *string `json:"currentPassword"` + NewPassword *string `json:"newPassword"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) + return + } + + // Update name + if req.Name != nil { + if err := s.db.UpdateUserName(userUUID, *req.Name); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update name"}) + return + } + } + + // Update email + if req.Email != nil { + if err := s.db.UpdateUserEmail(userUUID, *req.Email); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update email"}) + return + } + } + + // Update password + if req.NewPassword != nil { + if req.CurrentPassword == nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Current password required"}) + return + } + // Validate current password + ok, err := s.db.CheckUserPassword(userUUID, *req.CurrentPassword) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check current password"}) + return + } + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Current password incorrect"}) + return + } + if err := s.db.UpdateUserPassword(userUUID, *req.NewPassword); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update password"}) + return + } + } + + c.JSON(http.StatusOK, gin.H{"message": "User info updated successfully"}) +} diff --git a/rocket-backend/internal/server/user_handlers.go b/rocket-backend/internal/server/user_handlers.go index 69cd1f2..413a276 100644 --- a/rocket-backend/internal/server/user_handlers.go +++ b/rocket-backend/internal/server/user_handlers.go @@ -263,3 +263,29 @@ func (s *Server) GetUserByNameHandler(c *gin.Context) { c.JSON(http.StatusOK, userWithImage) } + +func (s *Server) DeleteUserHandler(c *gin.Context) { + userID, exists := c.Get("userID") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"}) + return + } + + userUUID, err := uuid.Parse(userID.(string)) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID format"}) + return + } + + err = s.db.DeleteUser(userUUID) + if err != nil { + if errors.Is(err, custom_error.ErrUserNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) + } else { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete user"}) + } + return + } + + c.JSON(http.StatusOK, gin.H{"message": "User deleted successfully"}) +} diff --git a/rocket-backend/migrations/0002_create_infotables.up.sql b/rocket-backend/migrations/0002_create_infotables.up.sql index 383121b..005dc0b 100644 --- a/rocket-backend/migrations/0002_create_infotables.up.sql +++ b/rocket-backend/migrations/0002_create_infotables.up.sql @@ -60,7 +60,7 @@ CREATE TABLE friends ( CREATE TABLE runs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid (), created_at TIMESTAMP DEFAULT now (), - user_id UUID NOT NULL REFERENCES users (id), + user_id UUID NOT NULL REFERENCES users (id) ON DELETE CASCADE, route GEOMETRY (LINESTRING, 4326), duration VARCHAR(16), distance REAL @@ -92,7 +92,7 @@ CREATE TABLE chat_messages_reactions ( CREATE TABLE planned_runs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), created_at TIMESTAMP DEFAULT now(), - user_id UUID NOT NULL REFERENCES users(id), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, route GEOMETRY(LINESTRING, 4326), name VARCHAR(255) NOT NULL, distance REAL, diff --git a/website/index.html b/website/index.html index c266cf2..81af30a 100644 --- a/website/index.html +++ b/website/index.html @@ -1,12 +1,15 @@ - +
- + + + +