diff --git a/Makefile b/Makefile index f7bf1ff9f..0a524865a 100644 --- a/Makefile +++ b/Makefile @@ -85,7 +85,37 @@ start-temporal: start-temporal-server: cd $(SERVER_DIR) && $(BACKEND_ENV_VARS) go run ./cmd/temporal-worker/main.go +COOKIE_JAR := /tmp/olake_cookies.txt + # Create a user with specified username, password and email (e.g. make create-user username=admin password=admin123 email=admin@example.com) create-user: - @curl -s -X POST http://localhost:8000/signup -H "Content-Type: application/json" -d "{\"username\":\"$(username)\",\"password\":\"$(password)\",\"email\":\"$(email)\"}" | grep -q "\"success\": true" && echo "User $(username) created successfully" || echo "Failed to create user $(username)" + @curl -s -X POST http://localhost:8000/signup \ + -H "Content-Type: application/json" \ + -d "{\"username\":\"$(username)\",\"password\":\"$(password)\",\"email\":\"$(email)\"}" | grep -q "\"success\": true" && echo "User $(username) created successfully" || echo "Failed to create user $(username)" + + +# helper target that logs in with provided credentials and stores cookie +# prints an error and exits if login fails or no cookie received +login: + @echo "logging in as '$(oldusername)'..." + @rm -f $(COOKIE_JAR) + @curl -c $(COOKIE_JAR) -s -X POST http://localhost:8000/login \ + -H 'Content-Type: application/json' \ + -d '{"username":"$(oldusername)","password":"$(oldpassword)"}' \ + | tee /dev/stderr | grep -q "\"success\": true" \ + && [ -s $(COOKIE_JAR) ] \ + && echo "login succeeded" \ + || (echo "login failed or no session cookie written"; exit 1) + +# Update an existing user's credentials. +# Pass oldusername, oldpassword, newusername and newpassword variables. +# Example: make update-user oldusername=admin oldpassword=secret newusername=alice newpassword=newpass +update-user: login + @echo "updating credentials to $(newusername)..." + @curl -b $(COOKIE_JAR) -s -X PUT http://localhost:8000/user/credentials \ + -H "Content-Type: application/json" \ + -d "{\"username\":\"$(newusername)\",\"password\":\"$(newpassword)\"}" \ + | tee /dev/stderr | grep -q "\"success\": true" \ + && echo "Credentials updated successfully" \ + || echo "Failed to update credentials" diff --git a/server/docs/docs.go b/server/docs/docs.go index 8a18cff00..f1fba483f 100644 --- a/server/docs/docs.go +++ b/server/docs/docs.go @@ -2669,9 +2669,64 @@ const docTemplate = `{ } } } + }, + "/user/credentials": { + "put": { + "description": "Change the authenticated user's credentials. User must be logged in via session.", + "tags": [ + "Authentication" + ], + "summary": "Update user credentials", + "parameters": [ + { + "description": "new username/password", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateCredentialsRequest" + } + } + ], + "responses": { + "200": { + "description": "credentials updated successfully", + "schema": { + "$ref": "#/definitions/dto.JSONResponse" + } + }, + "400": { + "description": "invalid request", + "schema": { + "$ref": "#/definitions/dto.Error400Response" + } + }, + "401": { + "description": "not authenticated", + "schema": { + "$ref": "#/definitions/dto.Error401Response" + } + }, + "500": { + "description": "failed to update credentials", + "schema": { + "$ref": "#/definitions/dto.Error500Response" + } + } + } + } } }, "definitions": { + "dto.AdvancedSettings": { + "type": "object", + "properties": { + "max_discover_threads": { + "type": "integer", + "example": 50 + } + } + }, "dto.CheckUniqueJobNameResponse": { "type": "object", "properties": { @@ -2753,6 +2808,9 @@ const docTemplate = `{ "type": "boolean", "example": true }, + "advanced_settings": { + "$ref": "#/definitions/dto.AdvancedSettings" + }, "destination": { "$ref": "#/definitions/dto.DriverConfig" }, @@ -3046,6 +3104,9 @@ const docTemplate = `{ "type": "boolean", "example": true }, + "advanced_settings": { + "$ref": "#/definitions/dto.AdvancedSettings" + }, "created_at": { "type": "string", "example": "2024-01-01T00:00:00Z" @@ -3390,6 +3451,10 @@ const docTemplate = `{ "type": "string", "example": "my-sync-job" }, + "max_discover_threads": { + "type": "integer", + "example": 50 + }, "name": { "type": "string", "example": "my-postgres-source" @@ -3458,6 +3523,23 @@ const docTemplate = `{ } } }, + "dto.UpdateCredentialsRequest": { + "type": "object", + "required": [ + "password", + "username" + ], + "properties": { + "password": { + "type": "string", + "example": "newpassword" + }, + "username": { + "type": "string", + "example": "newadmin" + } + } + }, "dto.UpdateDestinationRequest": { "type": "object", "required": [ @@ -3499,6 +3581,9 @@ const docTemplate = `{ "type": "boolean", "example": true }, + "advanced_settings": { + "$ref": "#/definitions/dto.AdvancedSettings" + }, "destination": { "$ref": "#/definitions/dto.DriverConfig" }, diff --git a/server/docs/swagger.json b/server/docs/swagger.json index 1a1d50110..d8f402e1d 100644 --- a/server/docs/swagger.json +++ b/server/docs/swagger.json @@ -2660,9 +2660,64 @@ } } } + }, + "/user/credentials": { + "put": { + "description": "Change the authenticated user's credentials. User must be logged in via session.", + "tags": [ + "Authentication" + ], + "summary": "Update user credentials", + "parameters": [ + { + "description": "new username/password", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.UpdateCredentialsRequest" + } + } + ], + "responses": { + "200": { + "description": "credentials updated successfully", + "schema": { + "$ref": "#/definitions/dto.JSONResponse" + } + }, + "400": { + "description": "invalid request", + "schema": { + "$ref": "#/definitions/dto.Error400Response" + } + }, + "401": { + "description": "not authenticated", + "schema": { + "$ref": "#/definitions/dto.Error401Response" + } + }, + "500": { + "description": "failed to update credentials", + "schema": { + "$ref": "#/definitions/dto.Error500Response" + } + } + } + } } }, "definitions": { + "dto.AdvancedSettings": { + "type": "object", + "properties": { + "max_discover_threads": { + "type": "integer", + "example": 50 + } + } + }, "dto.CheckUniqueJobNameResponse": { "type": "object", "properties": { @@ -2744,6 +2799,9 @@ "type": "boolean", "example": true }, + "advanced_settings": { + "$ref": "#/definitions/dto.AdvancedSettings" + }, "destination": { "$ref": "#/definitions/dto.DriverConfig" }, @@ -3037,6 +3095,9 @@ "type": "boolean", "example": true }, + "advanced_settings": { + "$ref": "#/definitions/dto.AdvancedSettings" + }, "created_at": { "type": "string", "example": "2024-01-01T00:00:00Z" @@ -3381,6 +3442,10 @@ "type": "string", "example": "my-sync-job" }, + "max_discover_threads": { + "type": "integer", + "example": 50 + }, "name": { "type": "string", "example": "my-postgres-source" @@ -3449,6 +3514,23 @@ } } }, + "dto.UpdateCredentialsRequest": { + "type": "object", + "required": [ + "password", + "username" + ], + "properties": { + "password": { + "type": "string", + "example": "newpassword" + }, + "username": { + "type": "string", + "example": "newadmin" + } + } + }, "dto.UpdateDestinationRequest": { "type": "object", "required": [ @@ -3490,6 +3572,9 @@ "type": "boolean", "example": true }, + "advanced_settings": { + "$ref": "#/definitions/dto.AdvancedSettings" + }, "destination": { "$ref": "#/definitions/dto.DriverConfig" }, diff --git a/server/internal/handlers/auth.go b/server/internal/handlers/auth.go index 7f7673d9c..16a15bba9 100644 --- a/server/internal/handlers/auth.go +++ b/server/internal/handlers/auth.go @@ -128,6 +128,47 @@ func (h *Handler) Signup() { }) } +// @Summary Update user credentials +// @Tags Authentication +// @Description Change the authenticated user's credentials. User must be logged in via session. +// @Param body body dto.UpdateCredentialsRequest true "new username/password" +// @Success 200 {object} dto.JSONResponse "credentials updated successfully" +// @Failure 400 {object} dto.Error400Response "invalid request" +// @Failure 401 {object} dto.Error401Response "not authenticated" +// @Failure 500 {object} dto.Error500Response "failed to update credentials" +// @Router /user/credentials [put] +func (h *Handler) UpdateCredentials() { + // ensure user is authenticated via session + userID := h.GetSession(constants.SessionUserID) + if userID == nil { + utils.ErrorResponse(&h.Controller, http.StatusUnauthorized, "Not authenticated", errors.New("not authenticated")) + return + } + userIDInt, ok := userID.(int) + if !ok { + utils.ErrorResponse(&h.Controller, http.StatusUnauthorized, "Invalid session data", errors.New("invalid session user id")) + return + } + + var req dto.UpdateCredentialsRequest + if err := UnmarshalAndValidate(h.Ctx.Input.RequestBody, &req); err != nil { + utils.ErrorResponse(&h.Controller, http.StatusBadRequest, constants.ValidationInvalidRequestFormat, err) + return + } + + if req.Username == "" && req.Password == "" { + utils.ErrorResponse(&h.Controller, http.StatusBadRequest, "nothing to update", errors.New("no credentials provided")) + return + } + + if err := h.etl.UpdateCredentials(h.Ctx.Request.Context(), userIDInt, req.Username, req.Password); err != nil { + utils.ErrorResponse(&h.Controller, http.StatusInternalServerError, fmt.Sprintf("failed to update credentials: %s", err), err) + return + } + + utils.SuccessResponse(&h.Controller, "credentials updated successfully", nil) +} + // @Summary Get telemetry ID // @Tags Internal // @Description Retrieve the unique telemetry identifier and current UI version. diff --git a/server/internal/models/dto/requests.go b/server/internal/models/dto/requests.go index eabea0344..8ac5cd5c5 100644 --- a/server/internal/models/dto/requests.go +++ b/server/internal/models/dto/requests.go @@ -15,6 +15,11 @@ type LoginRequest struct { Password string `json:"password" validate:"required" example:"password"` } +type UpdateCredentialsRequest struct { + Username string `json:"username" validate:"required" example:"newadmin"` + Password string `json:"password" validate:"required" example:"newpassword"` +} + type SpecRequest struct { // enum: postgres,mongodb,mysql,mssql,db2,s3,kafka,iceberg Type string `json:"type" validate:"required" example:"postgres"` diff --git a/server/internal/services/etl/auth.go b/server/internal/services/etl/auth.go index b5fb1a83d..62a368e70 100644 --- a/server/internal/services/etl/auth.go +++ b/server/internal/services/etl/auth.go @@ -62,3 +62,30 @@ func (s *ETLService) ValidateUser(userID int) error { } return nil } + +func (s *ETLService) UpdateCredentials(ctx context.Context, userID int, newUsername, newPassword string) error { + user, err := s.db.GetUserByID(userID) + if err != nil { + return fmt.Errorf("failed to load user: %s", err) + } + + if newUsername != "" { + user.Username = newUsername + } + + if newPassword != "" { + hashed, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) + if err != nil { + return fmt.Errorf("failed to hash new password: %s", err) + } + user.Password = string(hashed) + } + + if err := s.db.UpdateUser(user); err != nil { + return fmt.Errorf("failed to update credentials: %s", err) + } + + telemetry.TrackUserUpdate(ctx, user) + + return nil +} diff --git a/server/routes/router.go b/server/routes/router.go index 5855deafa..b0ea728f8 100644 --- a/server/routes/router.go +++ b/server/routes/router.go @@ -54,6 +54,7 @@ func Init(h *handlers.Handler) { web.Router("/signup", h, "post:Signup") web.Router("/auth/check", h, "get:CheckAuth") web.Router("/telemetry-id", h, "get:GetTelemetryID") + web.Router("/user/credentials", h, "put:UpdateCredentials") // User routes web.Router("/api/v1/users", h, "post:CreateUser") diff --git a/server/utils/telemetry/auth.go b/server/utils/telemetry/auth.go index a8e36192c..2a1acbc2b 100644 --- a/server/utils/telemetry/auth.go +++ b/server/utils/telemetry/auth.go @@ -26,3 +26,22 @@ func TrackUserLogin(ctx context.Context, user *models.User) { } }() } + +// TrackUserUpdate records when a user updates their credentials. +func TrackUserUpdate(ctx context.Context, user *models.User) { + go func() { + if instance == nil || user == nil { + return + } + + properties := map[string]interface{}{ + "user_id": user.ID, + "email": user.Email, + } + + err := TrackEvent(ctx, "user_update", properties) + if err != nil { + logger.Debug("Failed to track user update event: %s", err) + } + }() +}