From 1f23e16e3500a9f063e8f78013b0c56b456918bd Mon Sep 17 00:00:00 2001 From: LongLeVanQuoc Date: Wed, 5 Nov 2025 14:57:26 +0700 Subject: [PATCH 1/5] [Sprint 8][BE] Create table user_setting and add crud for table --- .../controllers/UserProfileController.scala | 53 +++++++++++++++++++ backend/app/exception/AppException.scala | 8 +-- .../app/exception/BadRequestException.scala | 6 +++ backend/app/exception/InternalException.scala | 6 +++ backend/app/exception/NotFoundException.scala | 6 +++ .../exception/ResourceInactiveException.scala | 6 +++ backend/app/models/entities/UserProfile.scala | 19 +++++++ .../app/models/tables/UserProfileTable.scala | 43 +++++++++++++++ .../repositories/UserProfileRepository.scala | 41 ++++++++++++++ backend/app/services/UserProfileService.scala | 27 ++++++++++ .../default/7_create_user_profile_table.sql | 11 ++++ backend/conf/routes | 4 ++ 12 files changed, 227 insertions(+), 3 deletions(-) create mode 100644 backend/app/controllers/UserProfileController.scala create mode 100644 backend/app/exception/BadRequestException.scala create mode 100644 backend/app/exception/InternalException.scala create mode 100644 backend/app/exception/NotFoundException.scala create mode 100644 backend/app/exception/ResourceInactiveException.scala create mode 100644 backend/app/models/entities/UserProfile.scala create mode 100644 backend/app/models/tables/UserProfileTable.scala create mode 100644 backend/app/repositories/UserProfileRepository.scala create mode 100644 backend/app/services/UserProfileService.scala create mode 100644 backend/conf/evolutions/default/7_create_user_profile_table.sql diff --git a/backend/app/controllers/UserProfileController.scala b/backend/app/controllers/UserProfileController.scala new file mode 100644 index 0000000..4fe983c --- /dev/null +++ b/backend/app/controllers/UserProfileController.scala @@ -0,0 +1,53 @@ +package controllers + +import javax.inject.{Inject, Singleton} +import play.api.mvc._ +import play.api.libs.json._ +import services.UserProfileService +import models.entities.UserProfile + +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 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..aaef251 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. + */ +sealed abstract 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..00b7360 --- /dev/null +++ b/backend/app/models/entities/UserProfile.scala @@ -0,0 +1,19 @@ +package models.entities + +import java.time.LocalDateTime + +case class UserProfile( + id: Int, + userId: Int, + userLanguage: String, + themeMode: String, + createdAt: LocalDateTime = LocalDateTime.now(), + updatedAt: LocalDateTime = LocalDateTime.now(), + createdBy: Option[Int] = None, + updatedBy: Option[Int] = None, +) + +object UserProfile { + def apply(id: Int, userId: Int, userLanguage: String, themeMode: String, createdAt: LocalDateTime, updatedAt: LocalDateTime): UserProfile = + UserProfile(id, userId, userLanguage, themeMode, createdAt, updatedAt, None, None) +} diff --git a/backend/app/models/tables/UserProfileTable.scala b/backend/app/models/tables/UserProfileTable.scala new file mode 100644 index 0000000..f5c0b0d --- /dev/null +++ b/backend/app/models/tables/UserProfileTable.scala @@ -0,0 +1,43 @@ +package models.tables + +import models.entities.UserProfile +import play.api.db.slick.HasDatabaseConfig +import slick.jdbc.PostgresProfile +import java.time.LocalDateTime + +trait UserProfileTable { self: HasDatabaseConfig[PostgresProfile] => + import profile.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 + ) <> ( + { case (id, userId, userLanguage, themeMode, createdAt, updatedAt) => + UserProfile(id, userId, userLanguage, themeMode, createdAt, updatedAt) + }, + (up: UserProfile) => Some((up.id, up.userId, up.userLanguage, up.themeMode, up.createdAt, up.updatedAt)) + ) + + 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) + } + + lazy val userProfiles = TableQuery[UserProfileTable] +} \ No newline at end of file diff --git a/backend/app/repositories/UserProfileRepository.scala b/backend/app/repositories/UserProfileRepository.scala new file mode 100644 index 0000000..93ef9d6 --- /dev/null +++ b/backend/app/repositories/UserProfileRepository.scala @@ -0,0 +1,41 @@ +package repositories + +import javax.inject.{Inject, Singleton} +import models.entities.UserProfile +import models.tables.UserProfileTable +import play.api.db.slick.{DatabaseConfigProvider, HasDatabaseConfig} +import slick.jdbc.PostgresProfile +import scala.concurrent.{ExecutionContext, Future} + +@Singleton +class UserProfileRepository @Inject()( + protected val dbConfigProvider: DatabaseConfigProvider +)(implicit ec: ExecutionContext) extends HasDatabaseConfig[PostgresProfile] with UserProfileTable { + + import profile.api._ + + def create(userProfile: UserProfile): Future[UserProfile] = { + val query = userProfiles returning userProfiles.map(_.id) into ((profile, id) => profile.copy(id = 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..f87b409 --- /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) + } + } + + def getUserProfile(userId: Int): Future[Option[UserProfile]] = { + userProfileRepository.findByUserId(userId) + } +} \ No newline at end of file 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..fef9455 --- /dev/null +++ b/backend/conf/evolutions/default/7_create_user_profile_table.sql @@ -0,0 +1,11 @@ +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) +); + +DROP TABLE IF EXISTS user_profiles; \ No newline at end of file diff --git a/backend/conf/routes b/backend/conf/routes index 3eb3087..47e897b 100644 --- a/backend/conf/routes +++ b/backend/conf/routes @@ -66,5 +66,9 @@ 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() +PUT /api/user/profile controllers.UserProfileController.updateProfile() + #Web socket GET /ws/project/:projectId controllers.WebSocketController.joinProject(projectId: Int) \ No newline at end of file From 97714c34446e77e5e0afe20cc714cd8eb52dfc36 Mon Sep 17 00:00:00 2001 From: LongLeVanQuoc Date: Fri, 7 Nov 2025 13:58:54 +0700 Subject: [PATCH 2/5] [Sprint 8][BE] Create table user_setting and add crud for table --- backend/conf/evolutions/default/7_create_user_profile_table.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/conf/evolutions/default/7_create_user_profile_table.sql b/backend/conf/evolutions/default/7_create_user_profile_table.sql index fef9455..4375170 100644 --- a/backend/conf/evolutions/default/7_create_user_profile_table.sql +++ b/backend/conf/evolutions/default/7_create_user_profile_table.sql @@ -8,4 +8,4 @@ CREATE TABLE user_profiles ( CONSTRAINT user_profile_user_id_idx UNIQUE (user_id) ); -DROP TABLE IF EXISTS user_profiles; \ No newline at end of file +DROP TABLE IF EXISTS user_profiles; From 78d1877148d6e6efe5b6860ae770334d90e1ced8 Mon Sep 17 00:00:00 2001 From: nguyenvanhadncntt Date: Mon, 10 Nov 2025 10:38:16 +0700 Subject: [PATCH 3/5] fix build fail --- backend/Dockerfile | 2 +- backend/app/exception/AppException.scala | 2 +- backend/app/models/entities/UserProfile.scala | 5 ++++- backend/app/services/WorkspaceService.scala | 1 + backend/conf/application.conf | 2 +- 5 files changed, 8 insertions(+), 4 deletions(-) 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/exception/AppException.scala b/backend/app/exception/AppException.scala index aaef251..546909f 100644 --- a/backend/app/exception/AppException.scala +++ b/backend/app/exception/AppException.scala @@ -6,4 +6,4 @@ import play.api.http.Status * Base application exception with an associated HTTP status code. * Specific exceptions extend this so handlers can map them to proper HTTP responses. */ -sealed abstract class AppException(message: String, val statusCode: Int = Status.BAD_REQUEST) extends RuntimeException(message) +case class AppException(message: String, val statusCode: Int = Status.BAD_REQUEST) extends RuntimeException(message) diff --git a/backend/app/models/entities/UserProfile.scala b/backend/app/models/entities/UserProfile.scala index 00b7360..f5250fc 100644 --- a/backend/app/models/entities/UserProfile.scala +++ b/backend/app/models/entities/UserProfile.scala @@ -1,5 +1,7 @@ package models.entities +import play.api.libs.json.{Json, OFormat} + import java.time.LocalDateTime case class UserProfile( @@ -10,10 +12,11 @@ case class UserProfile( createdAt: LocalDateTime = LocalDateTime.now(), updatedAt: LocalDateTime = LocalDateTime.now(), createdBy: Option[Int] = None, - updatedBy: Option[Int] = None, + updatedBy: Option[Int] = None ) object UserProfile { + implicit val userProfileFormat: OFormat[Project] = Json.format[Project] def apply(id: Int, userId: Int, userLanguage: String, themeMode: String, createdAt: LocalDateTime, updatedAt: LocalDateTime): UserProfile = UserProfile(id, userId, userLanguage, themeMode, createdAt, updatedAt, None, None) } 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 From fc9e1e8bb5a4b35cc8e202ffad613045e8e84623 Mon Sep 17 00:00:00 2001 From: nguyenvanhadncntt Date: Mon, 10 Nov 2025 13:58:14 +0700 Subject: [PATCH 4/5] Add Create User Profile --- .../controllers/UserProfileController.scala | 19 ++++++ backend/app/models/entities/UserProfile.scala | 8 +-- backend/app/models/tables/TableRegistry.scala | 1 + .../app/models/tables/UserProfileTable.scala | 58 ++++++++----------- .../repositories/UserProfileRepository.scala | 11 ++-- backend/app/services/UserProfileService.scala | 2 +- .../default/7_create_user_profile_table.sql | 2 - backend/conf/routes | 1 + 8 files changed, 53 insertions(+), 49 deletions(-) diff --git a/backend/app/controllers/UserProfileController.scala b/backend/app/controllers/UserProfileController.scala index 4fe983c..74e8f91 100644 --- a/backend/app/controllers/UserProfileController.scala +++ b/backend/app/controllers/UserProfileController.scala @@ -6,6 +6,7 @@ import play.api.libs.json._ import services.UserProfileService import models.entities.UserProfile +import java.time.LocalDateTime import scala.concurrent.{ExecutionContext, Future} @Singleton @@ -30,6 +31,24 @@ class UserProfileController @Inject()( } } + 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))), diff --git a/backend/app/models/entities/UserProfile.scala b/backend/app/models/entities/UserProfile.scala index f5250fc..58a22e3 100644 --- a/backend/app/models/entities/UserProfile.scala +++ b/backend/app/models/entities/UserProfile.scala @@ -5,18 +5,14 @@ import play.api.libs.json.{Json, OFormat} import java.time.LocalDateTime case class UserProfile( - id: Int, + id: Option[Int] = None, userId: Int, userLanguage: String, themeMode: String, createdAt: LocalDateTime = LocalDateTime.now(), - updatedAt: LocalDateTime = LocalDateTime.now(), - createdBy: Option[Int] = None, - updatedBy: Option[Int] = None + updatedAt: LocalDateTime = LocalDateTime.now() ) object UserProfile { implicit val userProfileFormat: OFormat[Project] = Json.format[Project] - def apply(id: Int, userId: Int, userLanguage: String, themeMode: String, createdAt: LocalDateTime, updatedAt: LocalDateTime): UserProfile = - UserProfile(id, userId, userLanguage, themeMode, createdAt, updatedAt, None, None) } 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 index f5c0b0d..b5f2515 100644 --- a/backend/app/models/tables/UserProfileTable.scala +++ b/backend/app/models/tables/UserProfileTable.scala @@ -1,43 +1,31 @@ package models.tables import models.entities.UserProfile -import play.api.db.slick.HasDatabaseConfig -import slick.jdbc.PostgresProfile import java.time.LocalDateTime +import db.MyPostgresProfile.api._ -trait UserProfileTable { self: HasDatabaseConfig[PostgresProfile] => - import profile.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") - 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) - def * = ( - id, - userId, - userLanguage, - themeMode, - createdAt, - updatedAt - ) <> ( - { case (id, userId, userLanguage, themeMode, createdAt, updatedAt) => - UserProfile(id, userId, userLanguage, themeMode, createdAt, updatedAt) - }, - (up: UserProfile) => Some((up.id, up.userId, up.userLanguage, up.themeMode, up.createdAt, up.updatedAt)) - ) + private def userFk = foreignKey( + "user_profile_user_fk", + userId, + TableQuery[UserTable] + )(_.id, onDelete = ForeignKeyAction.Cascade) - 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) - } - - lazy val userProfiles = TableQuery[UserProfileTable] -} \ No newline at end of file + 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 index 93ef9d6..5170489 100644 --- a/backend/app/repositories/UserProfileRepository.scala +++ b/backend/app/repositories/UserProfileRepository.scala @@ -2,20 +2,21 @@ package repositories import javax.inject.{Inject, Singleton} import models.entities.UserProfile -import models.tables.UserProfileTable -import play.api.db.slick.{DatabaseConfigProvider, HasDatabaseConfig} -import slick.jdbc.PostgresProfile +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 HasDatabaseConfig[PostgresProfile] with UserProfileTable { +)(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 = id)) + val query = userProfiles returning userProfiles.map(_.id) into ((profile, id) => profile.copy(id = Some(id))) db.run(query += userProfile) } diff --git a/backend/app/services/UserProfileService.scala b/backend/app/services/UserProfileService.scala index f87b409..22c47e6 100644 --- a/backend/app/services/UserProfileService.scala +++ b/backend/app/services/UserProfileService.scala @@ -17,7 +17,7 @@ class UserProfileService @Inject()( def updateProfile(userProfile: UserProfile): Future[Option[UserProfile]] = { userProfileRepository.update(userProfile).flatMap { case 0 => Future.successful(None) - case _ => userProfileRepository.findById(userProfile.id) + case _ => userProfileRepository.findById(userProfile.id.get) } } diff --git a/backend/conf/evolutions/default/7_create_user_profile_table.sql b/backend/conf/evolutions/default/7_create_user_profile_table.sql index 4375170..2917845 100644 --- a/backend/conf/evolutions/default/7_create_user_profile_table.sql +++ b/backend/conf/evolutions/default/7_create_user_profile_table.sql @@ -7,5 +7,3 @@ CREATE TABLE user_profiles ( updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT user_profile_user_id_idx UNIQUE (user_id) ); - -DROP TABLE IF EXISTS user_profiles; diff --git a/backend/conf/routes b/backend/conf/routes index 47e897b..a2f2e34 100644 --- a/backend/conf/routes +++ b/backend/conf/routes @@ -68,6 +68,7 @@ GET /api/tasks controllers.TaskController.search(page: Int ?= 2, size: 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 From 64954ec88b18adc00037b9ac82452c092ee469c8 Mon Sep 17 00:00:00 2001 From: nguyenvanhadncntt Date: Mon, 10 Nov 2025 14:01:26 +0700 Subject: [PATCH 5/5] update postman --- .../app/models/tables/UserProfileTable.scala | 2 +- .../Smart Taskhub API.postman_collection.json | 478 ++++++++++++++++-- 2 files changed, 440 insertions(+), 40 deletions(-) diff --git a/backend/app/models/tables/UserProfileTable.scala b/backend/app/models/tables/UserProfileTable.scala index b5f2515..074dab6 100644 --- a/backend/app/models/tables/UserProfileTable.scala +++ b/backend/app/models/tables/UserProfileTable.scala @@ -18,7 +18,7 @@ class UserProfileTable(tag: Tag) extends Table[UserProfile](tag, "user_profiles" userLanguage, themeMode, createdAt, - updatedAt, + updatedAt ) <> ((UserProfile.apply _).tupled, UserProfile.unapply) private def userFk = foreignKey( 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": [