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 @@ - + - + + + + Rocket App diff --git a/website/public/AppIcon.png b/website/public/AppIcon.png new file mode 100644 index 0000000..dfc2a6b Binary files /dev/null and b/website/public/AppIcon.png differ diff --git a/website/public/robots.txt b/website/public/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/website/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/website/src/api/backend-api.ts b/website/src/api/backend-api.ts index f47bf64..2196b2f 100644 --- a/website/src/api/backend-api.ts +++ b/website/src/api/backend-api.ts @@ -117,5 +117,33 @@ export default { }, getAllUsers(): Promise { return protectedAxiosApi.get('/users', { withCredentials: true }) + }, + deleteUser(): Promise { + return protectedAxiosApi.delete('/user', { withCredentials: true }) + }, + deleteImage(): Promise { + return protectedAxiosApi.delete('/settings/image', { withCredentials: true }) + }, + uploadImage(imageFile: File): Promise { + const formData = new FormData() + formData.append('image', imageFile) + + return protectedAxiosApi.post('/settings/image', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + withCredentials: true + }) + }, + updateStepGoal(value: number): Promise { + return protectedAxiosApi.post( + '/settings/step-goal', + { stepGoal: value }, + { withCredentials: true } + ) + }, + getSettings(): Promise { + return protectedAxiosApi.get('/settings', { withCredentials: true }) + }, + updateUserInfo(data: { name?: string; email?: string; currentPassword?: string; newPassword?: string }): Promise { + return protectedAxiosApi.post('/settings/userinfo', data, { withCredentials: true }) } } diff --git a/website/src/components/Navbar.vue b/website/src/components/Navbar.vue index a8bffa4..901eb86 100644 --- a/website/src/components/Navbar.vue +++ b/website/src/components/Navbar.vue @@ -181,6 +181,7 @@ onBeforeUnmount(() => { font-size: 1rem; padding: 0.25rem 0.5rem; transition: color 0.2s; + border-bottom: 2.5px solid transparent; } .nav-link:hover { @@ -188,6 +189,13 @@ onBeforeUnmount(() => { text-decoration: underline; } +.router-link-exact-active.nav-link, +.router-link-active.nav-link { + color: #fff; + border-bottom: 2.5px solid #ffb347; + text-decoration: none; +} + .user-info { display: flex; align-items: center; diff --git a/website/src/components/chat/ChatMessage.vue b/website/src/components/chat/ChatMessage.vue index 9bab9c4..95075c7 100644 --- a/website/src/components/chat/ChatMessage.vue +++ b/website/src/components/chat/ChatMessage.vue @@ -24,15 +24,31 @@ class="chat-username me-2" :style="{ color: getColor(username) }" >{{ username }} - {{ message }} + {{ formatTime(timestamp) }} + +
+ +
diff --git a/website/src/components/settings/AccountSettings.vue b/website/src/components/settings/AccountSettings.vue new file mode 100644 index 0000000..02e0958 --- /dev/null +++ b/website/src/components/settings/AccountSettings.vue @@ -0,0 +1,110 @@ + + + + + diff --git a/website/src/components/settings/ProfileSettings.vue b/website/src/components/settings/ProfileSettings.vue new file mode 100644 index 0000000..651fa14 --- /dev/null +++ b/website/src/components/settings/ProfileSettings.vue @@ -0,0 +1,165 @@ + + + + + diff --git a/website/src/components/settings/SecuritySettings.vue b/website/src/components/settings/SecuritySettings.vue new file mode 100644 index 0000000..50428f8 --- /dev/null +++ b/website/src/components/settings/SecuritySettings.vue @@ -0,0 +1,139 @@ + + + + + \ No newline at end of file diff --git a/website/src/pages/ChatView.vue b/website/src/pages/ChatView.vue index 2fd9bb8..f668f1b 100644 --- a/website/src/pages/ChatView.vue +++ b/website/src/pages/ChatView.vue @@ -29,10 +29,10 @@ onMounted(async () => { diff --git a/website/src/pages/FriendlistView.vue b/website/src/pages/FriendlistView.vue index d58ab0b..7ec9a02 100644 --- a/website/src/pages/FriendlistView.vue +++ b/website/src/pages/FriendlistView.vue @@ -54,7 +54,8 @@ const allUsers = ref([]); const fetchFriends = async () => { try { const res = await backendApi.getFriends(); - friends.value = res.data.map((f: any) => ({ + const data = res.data ?? []; + friends.value = data.map((f: any) => ({ id: f.id, username: f.username, email: f.email, @@ -66,13 +67,13 @@ const fetchFriends = async () => { console.error('Failed to fetch friends:', e); friends.value = []; } - }; const fetchAllUsers = async () => { try { const res = await backendApi.getAllUsers(); - allUsers.value = res.data.map((u: any) => ({ + const data = res.data ?? []; + allUsers.value = data.map((u: any) => ({ id: u.id, username: u.username, email: u.email, @@ -84,7 +85,6 @@ const fetchAllUsers = async () => { console.error('Failed to fetch all users:', e); allUsers.value = []; } - }; const loading = ref(true); diff --git a/website/src/pages/HighscoreView.vue b/website/src/pages/HighscoreView.vue index d669174..126d3a7 100644 --- a/website/src/pages/HighscoreView.vue +++ b/website/src/pages/HighscoreView.vue @@ -27,17 +27,25 @@ /> + diff --git a/website/src/utils/userUtils.ts b/website/src/utils/userUtils.ts index b999fca..4f11a8d 100644 --- a/website/src/utils/userUtils.ts +++ b/website/src/utils/userUtils.ts @@ -1,6 +1,8 @@ export const chatColors = [ '#2a5298', '#f39c12', '#27ae60', '#8e44ad', '#e74c3c', '#16a085', - '#d35400', '#2980b9', '#c0392b', '#1abc9c', '#9b59b6', '#34495e' + '#d35400', '#2980b9', '#c0392b', '#1abc9c', '#9b59b6', '#34495e', + '#e67e22', '#3498db', '#f1c40f', '#7f8c8d', '#e84393', '#00b894', + '#fdcb6e', '#0984e3', '#6c5ce7', '#00cec9', '#b2bec3', '#636e72' ] export function getColor(name: string) {