diff --git a/backend/Dockerfile b/backend/Dockerfile index 15a490d..9be2b1d 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,5 +1,5 @@ # Base image with Java 21 -FROM openjdk:21-jdk-slim +FROM eclipse-temurin:21-jdk-jammy # Install necessary tools: curl, unzip RUN apt-get update && apt-get install -y curl unzip && rm -rf /var/lib/apt/lists/* diff --git a/backend/app/controllers/UserProfileController.scala b/backend/app/controllers/UserProfileController.scala new file mode 100644 index 0000000..74e8f91 --- /dev/null +++ b/backend/app/controllers/UserProfileController.scala @@ -0,0 +1,72 @@ +package controllers + +import javax.inject.{Inject, Singleton} +import play.api.mvc._ +import play.api.libs.json._ +import services.UserProfileService +import models.entities.UserProfile + +import java.time.LocalDateTime +import scala.concurrent.{ExecutionContext, Future} + +@Singleton +class UserProfileController @Inject()( + val controllerComponents: ControllerComponents, + userProfileService: UserProfileService, + authenticatedAction: AuthenticatedActionWithUser +)(implicit ec: ExecutionContext) extends BaseController { + + implicit val userProfileWrites: OWrites[UserProfile] = Json.writes[UserProfile] + + // Request DTO for partial updates + private case class UpdateUserProfileRequest(userLanguage: Option[String], themeMode: Option[String]) + private object UpdateUserProfileRequest { + implicit val reads: Reads[UpdateUserProfileRequest] = Json.reads[UpdateUserProfileRequest] + } + + def getUserProfile: Action[AnyContent] = authenticatedAction.async { request => + userProfileService.getUserProfile(request.userToken.userId).map { + case Some(profile) => Ok(Json.toJson(profile)) + case None => NotFound + } + } + + def createProfile: Action[JsValue] = authenticatedAction.async(parse.json) { request => + request.body.validate[UpdateUserProfileRequest].fold( + errors => Future.successful(BadRequest(JsError.toJson(errors))), + dto => { + val newProfile = UserProfile( + userId = request.userToken.userId, + userLanguage = dto.userLanguage.get, + themeMode = dto.themeMode.get, + createdAt = LocalDateTime.now(), + updatedAt = LocalDateTime.now() + ) + userProfileService.createProfile(newProfile).map { profile => + Created(Json.toJson(profile)) + } + } + ) + } + + def updateProfile: Action[JsValue] = authenticatedAction.async(parse.json) { request => + request.body.validate[UpdateUserProfileRequest].fold( + errors => Future.successful(BadRequest(JsError.toJson(errors))), + dto => { + userProfileService.getUserProfile(request.userToken.userId).flatMap { + case None => Future.successful(NotFound) + case Some(existing) => + val updated = existing.copy( + userLanguage = dto.userLanguage.getOrElse(existing.userLanguage), + themeMode = dto.themeMode.getOrElse(existing.themeMode), + updatedAt = java.time.LocalDateTime.now() + ) + userProfileService.updateProfile(updated).map { + case Some(profile) => Ok(Json.toJson(profile)) + case None => InternalServerError(Json.obj("error" -> "Failed to update profile")) + } + } + } + ) + } +} \ No newline at end of file diff --git a/backend/app/exception/AppException.scala b/backend/app/exception/AppException.scala index f8c4106..546909f 100644 --- a/backend/app/exception/AppException.scala +++ b/backend/app/exception/AppException.scala @@ -2,6 +2,8 @@ package exception import play.api.http.Status -case class AppException(message: String, statusCode: Int = Status.BAD_REQUEST) extends RuntimeException { - -} +/** + * Base application exception with an associated HTTP status code. + * Specific exceptions extend this so handlers can map them to proper HTTP responses. + */ +case class AppException(message: String, val statusCode: Int = Status.BAD_REQUEST) extends RuntimeException(message) diff --git a/backend/app/exception/BadRequestException.scala b/backend/app/exception/BadRequestException.scala new file mode 100644 index 0000000..1962f3e --- /dev/null +++ b/backend/app/exception/BadRequestException.scala @@ -0,0 +1,6 @@ +package exception + +import play.api.http.Status + +case class BadRequestException(message: String = "Bad Request", statusCode: Int = Status.BAD_REQUEST) + extends RuntimeException(message) diff --git a/backend/app/exception/InternalException.scala b/backend/app/exception/InternalException.scala new file mode 100644 index 0000000..d0f6fe5 --- /dev/null +++ b/backend/app/exception/InternalException.scala @@ -0,0 +1,6 @@ +package exception + +import play.api.http.Status + +case class InternalErrorException(message: String = "Internal Server Error", statusCode: Int = Status.INTERNAL_SERVER_ERROR) + extends RuntimeException(message) diff --git a/backend/app/exception/NotFoundException.scala b/backend/app/exception/NotFoundException.scala new file mode 100644 index 0000000..0f17f62 --- /dev/null +++ b/backend/app/exception/NotFoundException.scala @@ -0,0 +1,6 @@ +package exception + +import play.api.http.Status + +case class NotFoundException(message: String = "Not Found", statusCode: Int = Status.NOT_FOUND) + extends RuntimeException(message) diff --git a/backend/app/exception/ResourceInactiveException.scala b/backend/app/exception/ResourceInactiveException.scala new file mode 100644 index 0000000..3e3b1e7 --- /dev/null +++ b/backend/app/exception/ResourceInactiveException.scala @@ -0,0 +1,6 @@ +package exception + +import play.api.http.Status + +case class ResourceInactiveException(message: String = "Resource Inactive", statusCode: Int = Status.CONFLICT) + extends RuntimeException(message) diff --git a/backend/app/models/entities/UserProfile.scala b/backend/app/models/entities/UserProfile.scala new file mode 100644 index 0000000..58a22e3 --- /dev/null +++ b/backend/app/models/entities/UserProfile.scala @@ -0,0 +1,18 @@ +package models.entities + +import play.api.libs.json.{Json, OFormat} + +import java.time.LocalDateTime + +case class UserProfile( + id: Option[Int] = None, + userId: Int, + userLanguage: String, + themeMode: String, + createdAt: LocalDateTime = LocalDateTime.now(), + updatedAt: LocalDateTime = LocalDateTime.now() +) + +object UserProfile { + implicit val userProfileFormat: OFormat[Project] = Json.format[Project] +} diff --git a/backend/app/models/tables/TableRegistry.scala b/backend/app/models/tables/TableRegistry.scala index e980f0f..ff503c2 100644 --- a/backend/app/models/tables/TableRegistry.scala +++ b/backend/app/models/tables/TableRegistry.scala @@ -19,6 +19,7 @@ object TableRegistry { lazy val notifications = TableQuery[NotificationTable] lazy val activityLogs = TableQuery[ActivityLogTable] lazy val userTasks = TableQuery[UserTaskTable] + lazy val userProfiles = TableQuery[UserProfileTable] // All tables for schema creation/evolution val allTables = Seq( diff --git a/backend/app/models/tables/UserProfileTable.scala b/backend/app/models/tables/UserProfileTable.scala new file mode 100644 index 0000000..074dab6 --- /dev/null +++ b/backend/app/models/tables/UserProfileTable.scala @@ -0,0 +1,31 @@ +package models.tables + +import models.entities.UserProfile +import java.time.LocalDateTime +import db.MyPostgresProfile.api._ + +class UserProfileTable(tag: Tag) extends Table[UserProfile](tag, "user_profiles") { + def id = column[Int]("id", O.PrimaryKey, O.AutoInc) + def userId = column[Int]("user_id") + def userLanguage = column[String]("user_language") + def themeMode = column[String]("theme_mode") + def createdAt = column[LocalDateTime]("created_at") + def updatedAt = column[LocalDateTime]("updated_at") + + def * = ( + id.?, + userId, + userLanguage, + themeMode, + createdAt, + updatedAt + ) <> ((UserProfile.apply _).tupled, UserProfile.unapply) + + private def userFk = foreignKey( + "user_profile_user_fk", + userId, + TableQuery[UserTable] + )(_.id, onDelete = ForeignKeyAction.Cascade) + + private def userIdIdx = index("user_profile_user_id_idx", userId, unique = true) +} diff --git a/backend/app/repositories/UserProfileRepository.scala b/backend/app/repositories/UserProfileRepository.scala new file mode 100644 index 0000000..5170489 --- /dev/null +++ b/backend/app/repositories/UserProfileRepository.scala @@ -0,0 +1,42 @@ +package repositories + +import javax.inject.{Inject, Singleton} +import models.entities.UserProfile +import play.api.db.slick.{DatabaseConfigProvider, HasDatabaseConfigProvider} +import models.tables.TableRegistry.userProfiles +import slick.jdbc.JdbcProfile + +import scala.concurrent.{ExecutionContext, Future} + +@Singleton +class UserProfileRepository @Inject()( + protected val dbConfigProvider: DatabaseConfigProvider +)(implicit ec: ExecutionContext) extends HasDatabaseConfigProvider[JdbcProfile] { + + import profile.api._ + + def create(userProfile: UserProfile): Future[UserProfile] = { + val query = userProfiles returning userProfiles.map(_.id) into ((profile, id) => profile.copy(id = Some(id))) + db.run(query += userProfile) + } + + def update(userProfile: UserProfile): Future[Int] = { + val query = userProfiles.filter(_.id === userProfile.id).update(userProfile) + db.run(query) + } + + def findByUserId(userId: Int): Future[Option[UserProfile]] = { + val query = userProfiles.filter(_.userId === userId) + db.run(query.result.headOption) + } + + def findById(id: Int): Future[Option[UserProfile]] = { + val query = userProfiles.filter(_.id === id) + db.run(query.result.headOption) + } + + def delete(id: Int): Future[Int] = { + val query = userProfiles.filter(_.id === id).delete + db.run(query) + } +} \ No newline at end of file diff --git a/backend/app/services/UserProfileService.scala b/backend/app/services/UserProfileService.scala new file mode 100644 index 0000000..22c47e6 --- /dev/null +++ b/backend/app/services/UserProfileService.scala @@ -0,0 +1,27 @@ +package services + +import javax.inject.{Inject, Singleton} +import models.entities.UserProfile +import repositories.UserProfileRepository +import scala.concurrent.{ExecutionContext, Future} + +@Singleton +class UserProfileService @Inject()( + userProfileRepository: UserProfileRepository +)(implicit ec: ExecutionContext) { + + def createProfile(newProfile: UserProfile): Future[UserProfile] = { + userProfileRepository.create(newProfile) + } + + def updateProfile(userProfile: UserProfile): Future[Option[UserProfile]] = { + userProfileRepository.update(userProfile).flatMap { + case 0 => Future.successful(None) + case _ => userProfileRepository.findById(userProfile.id.get) + } + } + + def getUserProfile(userId: Int): Future[Option[UserProfile]] = { + userProfileRepository.findByUserId(userId) + } +} \ No newline at end of file diff --git a/backend/app/services/WorkspaceService.scala b/backend/app/services/WorkspaceService.scala index ac9c8f7..d5014be 100644 --- a/backend/app/services/WorkspaceService.scala +++ b/backend/app/services/WorkspaceService.scala @@ -14,6 +14,7 @@ import slick.jdbc.JdbcProfile import java.time.Instant import javax.inject.{Inject, Singleton} import scala.concurrent.{ExecutionContext, Future} +import exception.AppException @Singleton class WorkspaceService @Inject()( diff --git a/backend/conf/application.conf b/backend/conf/application.conf index b84c292..1f307cf 100644 --- a/backend/conf/application.conf +++ b/backend/conf/application.conf @@ -36,7 +36,7 @@ slick.dbs.default.db.url = "jdbc:postgresql://localhost:5432/smart_taskhub" slick.dbs.default.db.url = ${?DB_URL} slick.dbs.default.db.user = "postgres" slick.dbs.default.db.user = ${?DB_USER} -slick.dbs.default.db.password = "postgres" +slick.dbs.default.db.password = "admin" slick.dbs.default.db.password = ${?DB_PASSWORD} # Play evolutions diff --git a/backend/conf/evolutions/default/7_create_user_profile_table.sql b/backend/conf/evolutions/default/7_create_user_profile_table.sql new file mode 100644 index 0000000..2917845 --- /dev/null +++ b/backend/conf/evolutions/default/7_create_user_profile_table.sql @@ -0,0 +1,9 @@ +CREATE TABLE user_profiles ( + id SERIAL PRIMARY KEY, + user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + user_language VARCHAR(10) NOT NULL DEFAULT 'en', + theme_mode VARCHAR(10) NOT NULL DEFAULT 'Light', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT user_profile_user_id_idx UNIQUE (user_id) +); diff --git a/backend/conf/routes b/backend/conf/routes index 3eb3087..a2f2e34 100644 --- a/backend/conf/routes +++ b/backend/conf/routes @@ -66,5 +66,10 @@ GET /api/projects/:projectId/columns/tasks/active controllers.TaskControll GET /api/projects/:projectId/columns/:columnsId/tasks controllers.TaskController.getActiveTasksInColumn(projectId: Int, columnsId: Int, limit: Int ?= 20, page: Int ?= 1) GET /api/tasks controllers.TaskController.search(page: Int ?= 2, size: Int ?= 10, keyword: String ?= "", projectIds: Option[List[Int]]) +# User Profile routes +GET /api/user/profile controllers.UserProfileController.getUserProfile() +POST /api/user/profile controllers.UserProfileController.createProfile() +PUT /api/user/profile controllers.UserProfileController.updateProfile() + #Web socket GET /ws/project/:projectId controllers.WebSocketController.joinProject(projectId: Int) \ No newline at end of file diff --git a/backend/postman_collections/Smart Taskhub API.postman_collection.json b/backend/postman_collections/Smart Taskhub API.postman_collection.json index 3d931cf..d1f2a87 100644 --- a/backend/postman_collections/Smart Taskhub API.postman_collection.json +++ b/backend/postman_collections/Smart Taskhub API.postman_collection.json @@ -1,10 +1,10 @@ { "info": { - "_postman_id": "d1795af4-fe16-437c-9cbf-b90c156f1249", + "_postman_id": "f42570ae-fb8f-49b0-9842-2f30d0c05235", "name": "Smart Taskhub API", "description": "API documentation for Smart Taskhub, a task and project management application.", - "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json", - "_exporter_id": "38432428" + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "24956801" }, "item": [ { @@ -34,7 +34,16 @@ } } }, - "url": "{{baseUrl}}/auth/register", + "url": { + "raw": "{{baseUrl}}/auth/register", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "auth", + "register" + ] + }, "description": "Creates a new user account" }, "response": [ @@ -62,7 +71,16 @@ } } }, - "url": "{{baseUrl}}/auth/register" + "url": { + "raw": "{{baseUrl}}/auth/register", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "auth", + "register" + ] + } }, "status": "Created", "code": 201, @@ -100,7 +118,16 @@ } } }, - "url": "{{baseUrl}}/auth/register" + "url": { + "raw": "{{baseUrl}}/auth/register", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "auth", + "register" + ] + } }, "status": "Bad Request", "code": 400, @@ -138,7 +165,16 @@ } } }, - "url": "{{baseUrl}}/auth/register" + "url": { + "raw": "{{baseUrl}}/auth/register", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "auth", + "register" + ] + } }, "status": "Conflict", "code": 409, @@ -178,7 +214,16 @@ } } }, - "url": "{{baseUrl}}/auth/login", + "url": { + "raw": "{{baseUrl}}/auth/login", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "auth", + "login" + ] + }, "description": "Authenticates user and returns JWT token" }, "response": [ @@ -206,7 +251,16 @@ } } }, - "url": "{{baseUrl}}/auth/login" + "url": { + "raw": "{{baseUrl}}/auth/login", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "auth", + "login" + ] + } }, "status": "OK", "code": 200, @@ -252,7 +306,16 @@ } } }, - "url": "{{baseUrl}}/auth/login" + "url": { + "raw": "{{baseUrl}}/auth/login", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "auth", + "login" + ] + } }, "status": "Bad Request", "code": 400, @@ -290,7 +353,16 @@ } } }, - "url": "{{baseUrl}}/auth/login" + "url": { + "raw": "{{baseUrl}}/auth/login", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "auth", + "login" + ] + } }, "status": "Unauthorized", "code": 401, @@ -316,7 +388,16 @@ "value": "application/json" } ], - "url": "{{baseUrl}}/auth/logout", + "url": { + "raw": "{{baseUrl}}/auth/logout", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "auth", + "logout" + ] + }, "description": "Logs out the current user and invalidates the session" }, "response": [ @@ -330,7 +411,16 @@ "value": "application/json" } ], - "url": "{{baseUrl}}/auth/logout" + "url": { + "raw": "{{baseUrl}}/auth/logout", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "auth", + "logout" + ] + } }, "status": "OK", "code": 200, @@ -364,7 +454,16 @@ "value": "application/json" } ], - "url": "{{baseUrl}}/auth/me", + "url": { + "raw": "{{baseUrl}}/auth/me", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "auth", + "me" + ] + }, "description": "Returns information about the currently authenticated user" }, "response": [ @@ -378,7 +477,16 @@ "value": "application/json" } ], - "url": "{{baseUrl}}/auth/me" + "url": { + "raw": "{{baseUrl}}/auth/me", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "auth", + "me" + ] + } }, "status": "OK", "code": 200, @@ -402,7 +510,16 @@ "value": "application/json" } ], - "url": "{{baseUrl}}/auth/me" + "url": { + "raw": "{{baseUrl}}/auth/me", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "auth", + "me" + ] + } }, "status": "Unauthorized", "code": 401, @@ -428,7 +545,16 @@ "value": "application/json" } ], - "url": "{{baseUrl}}/auth/refresh", + "url": { + "raw": "{{baseUrl}}/auth/refresh", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "auth", + "refresh" + ] + }, "description": "Refreshes the current authentication token" }, "response": [ @@ -442,7 +568,16 @@ "value": "application/json" } ], - "url": "{{baseUrl}}/auth/refresh" + "url": { + "raw": "{{baseUrl}}/auth/refresh", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "auth", + "refresh" + ] + } }, "status": "OK", "code": 200, @@ -474,7 +609,16 @@ "value": "application/json" } ], - "url": "{{baseUrl}}/auth/refresh" + "url": { + "raw": "{{baseUrl}}/auth/refresh", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "auth", + "refresh" + ] + } }, "status": "Unauthorized", "code": 401, @@ -500,7 +644,16 @@ "value": "application/json" } ], - "url": "{{baseUrl}}/auth/check", + "url": { + "raw": "{{baseUrl}}/auth/check", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "auth", + "check" + ] + }, "description": "Verifies if the user is currently authenticated" }, "response": [ @@ -514,7 +667,16 @@ "value": "application/json" } ], - "url": "{{baseUrl}}/auth/check" + "url": { + "raw": "{{baseUrl}}/auth/check", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "auth", + "check" + ] + } }, "status": "OK", "code": 200, @@ -538,7 +700,16 @@ "value": "application/json" } ], - "url": "{{baseUrl}}/auth/check" + "url": { + "raw": "{{baseUrl}}/auth/check", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "auth", + "check" + ] + } }, "status": "Unauthorized", "code": 401, @@ -564,7 +735,16 @@ "value": "application/json" } ], - "url": "{{baseUrl}}/auth/role", + "url": { + "raw": "{{baseUrl}}/auth/role", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "auth", + "role" + ] + }, "description": "Retrieves the role of the current user" }, "response": [ @@ -578,7 +758,16 @@ "value": "application/json" } ], - "url": "{{baseUrl}}/auth/role" + "url": { + "raw": "{{baseUrl}}/auth/role", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "auth", + "role" + ] + } }, "status": "OK", "code": 200, @@ -602,7 +791,16 @@ "value": "application/json" } ], - "url": "{{baseUrl}}/auth/role" + "url": { + "raw": "{{baseUrl}}/auth/role", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "auth", + "role" + ] + } }, "status": "Unauthorized", "code": 401, @@ -626,7 +824,16 @@ "value": "application/json" } ], - "url": "{{baseUrl}}/auth/role" + "url": { + "raw": "{{baseUrl}}/auth/role", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "auth", + "role" + ] + } }, "status": "Not Found", "code": 404, @@ -658,7 +865,15 @@ "value": "application/json" } ], - "url": "{{baseUrl}}/workspaces", + "url": { + "raw": "{{baseUrl}}/workspaces", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "workspaces" + ] + }, "description": "Retrieves all workspaces for the authenticated user" }, "response": [ @@ -672,7 +887,15 @@ "value": "application/json" } ], - "url": "{{baseUrl}}/workspaces" + "url": { + "raw": "{{baseUrl}}/workspaces", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "workspaces" + ] + } }, "status": "OK", "code": 200, @@ -696,7 +919,15 @@ "value": "application/json" } ], - "url": "{{baseUrl}}/workspaces" + "url": { + "raw": "{{baseUrl}}/workspaces", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "workspaces" + ] + } }, "status": "Unauthorized", "code": 401, @@ -736,7 +967,15 @@ } } }, - "url": "{{baseUrl}}/workspaces", + "url": { + "raw": "{{baseUrl}}/workspaces", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "workspaces" + ] + }, "description": "Creates a new workspace for the authenticated user" }, "response": [ @@ -764,7 +1003,15 @@ } } }, - "url": "{{baseUrl}}/workspaces" + "url": { + "raw": "{{baseUrl}}/workspaces", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "workspaces" + ] + } }, "status": "Created", "code": 201, @@ -802,7 +1049,15 @@ } } }, - "url": "{{baseUrl}}/workspaces" + "url": { + "raw": "{{baseUrl}}/workspaces", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "workspaces" + ] + } }, "status": "Bad Request", "code": 400, @@ -840,7 +1095,15 @@ } } }, - "url": "{{baseUrl}}/workspaces" + "url": { + "raw": "{{baseUrl}}/workspaces", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "workspaces" + ] + } }, "status": "Unauthorized", "code": 401, @@ -878,7 +1141,15 @@ } } }, - "url": "{{baseUrl}}/workspaces" + "url": { + "raw": "{{baseUrl}}/workspaces", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "workspaces" + ] + } }, "status": "Conflict", "code": 409, @@ -2630,7 +2901,16 @@ "value": "application/json" } ], - "url": "{{baseUrl}}/projects/completed", + "url": { + "raw": "{{baseUrl}}/projects/completed", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "projects", + "completed" + ] + }, "description": "Retrieves all completed projects for the authenticated user" }, "response": [ @@ -2644,7 +2924,16 @@ "value": "application/json" } ], - "url": "{{baseUrl}}/projects/completed" + "url": { + "raw": "{{baseUrl}}/projects/completed", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "projects", + "completed" + ] + } }, "status": "OK", "code": 200, @@ -2668,7 +2957,16 @@ "value": "application/json" } ], - "url": "{{baseUrl}}/projects/completed" + "url": { + "raw": "{{baseUrl}}/projects/completed", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "projects", + "completed" + ] + } }, "status": "Unauthorized", "code": 401, @@ -2828,7 +3126,15 @@ "request": { "method": "OPTIONS", "header": [], - "url": "{{baseUrl}}/url-preview", + "url": { + "raw": "{{baseUrl}}/url-preview", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "url-preview" + ] + }, "description": "Handles CORS preflight requests for URL preview endpoint" }, "response": [ @@ -2837,7 +3143,15 @@ "originalRequest": { "method": "OPTIONS", "header": [], - "url": "{{baseUrl}}/url-preview" + "url": { + "raw": "{{baseUrl}}/url-preview", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "url-preview" + ] + } }, "status": "OK", "code": 200, @@ -6101,6 +6415,92 @@ } ], "description": "Task management endpoints" + }, + { + "name": "User Profile", + "item": [ + { + "name": "Get User Profile", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:9000/api/user/profile", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "9000", + "path": [ + "api", + "user", + "profile" + ] + } + }, + "response": [] + }, + { + "name": "Update User Profile", + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"userLanguage\": \"en\",\r\n \"themeMode\": \"Dark\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:9000/api/user/profile", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "9000", + "path": [ + "api", + "user", + "profile" + ] + } + }, + "response": [] + }, + { + "name": "Update User Profile Copy", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"userLanguage\": \"en\",\r\n \"themeMode\": \"Dark\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:9000/api/user/profile", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "9000", + "path": [ + "api", + "user", + "profile" + ] + } + }, + "response": [] + } + ] } ], "variable": [