~jan0sch/smederee

Showing details for patch 8391694d2a698e686c411b8e70d51ddaee19e95c.
2023-12-03 (Sun), 7:44 PM - Jens Grassel - 8391694d2a698e686c411b8e70d51ddaee19e95c

Hub: Add FullName

- add FullName to Account
- add database migration
- adjust code
- adjust tests
Summary of changes
2 files added
  • modules/hub/src/main/resources/db/migration/hub/V6__add_full_name.sql
  • modules/hub/src/main/scala/de/smederee/hub/FullName.scala
8 files modified with 50 lines added and 19 lines removed
  • modules/hub/src/main/scala/de/smederee/hub/Account.scala with 3 added and 0 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/DoobieAccountManagementRepository.scala with 3 added and 1 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/DoobieAuthenticationRepository.scala with 5 added and 3 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/DoobieResetPasswordRepository.scala with 3 added and 1 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/SignupRoutes.scala with 1 added and 0 removed lines
  • modules/hub/src/test/scala/de/smederee/hub/BaseSpec.scala with 20 added and 13 removed lines
  • modules/hub/src/test/scala/de/smederee/hub/DoobieVcsMetadataRepositoryTest.scala with 3 added and 0 removed lines
  • modules/hub/src/test/scala/de/smederee/hub/Generators.scala with 12 added and 1 removed lines
diff -rN -u old-smederee/modules/hub/src/main/resources/db/migration/hub/V6__add_full_name.sql new-smederee/modules/hub/src/main/resources/db/migration/hub/V6__add_full_name.sql
--- old-smederee/modules/hub/src/main/resources/db/migration/hub/V6__add_full_name.sql	1970-01-01 00:00:00.000000000 +0000
+++ new-smederee/modules/hub/src/main/resources/db/migration/hub/V6__add_full_name.sql	2025-01-13 11:51:51.486757555 +0000
@@ -0,0 +1,4 @@
+ALTER TABLE "hub"."accounts"
+  ADD COLUMN "full_name" CHARACTER VARYING(128);
+
+COMMENT ON COLUMN "hub"."accounts"."full_name" IS 'The optional human readable full name of an account which must be non empty and is limited to 128 characters.';
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/Account.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/Account.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/Account.scala	2025-01-13 11:51:51.486757555 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/Account.scala	2025-01-13 11:51:51.490757563 +0000
@@ -214,6 +214,8 @@
   *   A unique name which can be used for login and to identify the user.
   * @param email
   *   The email address of the user.
+  * @param fullName
+  *   The optional human readable full name of an account which must be non empty and is limited to 128 characters.
   * @param validatedEmail
   *   This flag indicates if the email address of the user has been validated via a validation email.
   * @param language
@@ -223,6 +225,7 @@
     uid: UserId,
     name: Username,
     email: EmailAddress,
+    fullName: Option[FullName],
     validatedEmail: Boolean,
     language: Option[LanguageCode]
 )
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieAccountManagementRepository.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieAccountManagementRepository.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieAccountManagementRepository.scala	2025-01-13 11:51:51.486757555 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieAccountManagementRepository.scala	2025-01-13 11:51:51.490757563 +0000
@@ -32,6 +32,7 @@
 final class DoobieAccountManagementRepository[F[_]: Sync](tx: Transactor[F]) extends AccountManagementRepository[F] {
     given Meta[EmailAddress]    = Meta[String].timap(EmailAddress.apply)(_.toString)
     given Meta[EncodedKeyBytes] = Meta[String].timap(EncodedKeyBytes.unsafeFrom)(_.toString)
+    given Meta[FullName]        = Meta[String].timap(FullName.apply)(_.toString)
     given Meta[KeyComment]      = Meta[String].timap(KeyComment.apply)(_.toString)
     given Meta[KeyFingerprint]  = Meta[String].timap(KeyFingerprint.apply)(_.toString)
     given Meta[LanguageCode]    = Meta[String].timap(LanguageCode.apply)(_.toString)
@@ -41,7 +42,8 @@
     given Meta[Username]        = Meta[String].timap(Username.apply)(_.toString)
     given Meta[ValidationToken] = Meta[String].timap(ValidationToken.apply)(_.toString)
 
-    private val selectAccountColumns = fr"""SELECT uid, name, email, validated_email, language FROM "hub"."accounts""""
+    private val selectAccountColumns =
+        fr"""SELECT uid, name, email, full_name, validated_email, language FROM "hub"."accounts""""
 
     override def addSshKey(key: PublicSshKey): F[Int] =
         sql"""INSERT INTO "hub"."ssh_keys" (id, uid, key_type, key, fingerprint, comment, created_at)
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieAuthenticationRepository.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieAuthenticationRepository.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieAuthenticationRepository.scala	2025-01-13 11:51:51.486757555 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieAuthenticationRepository.scala	2025-01-13 11:51:51.490757563 +0000
@@ -32,6 +32,7 @@
 
 final class DoobieAuthenticationRepository[F[_]: Sync](tx: Transactor[F]) extends AuthenticationRepository[F] {
     given Meta[EmailAddress] = Meta[String].timap(EmailAddress.apply)(_.toString)
+    given Meta[FullName]     = Meta[String].timap(FullName.apply)(_.toString)
     given Meta[LanguageCode] = Meta[String].timap(LanguageCode.apply)(_.toString)
     given Meta[PasswordHash] = Meta[String].timap(PasswordHash.apply)(_.toString)
     given Meta[SessionId]    = Meta[String].timap(SessionId.apply)(_.toString)
@@ -39,9 +40,10 @@
     given Meta[UserId]       = Meta[UUID].timap(UserId.apply)(_.toUUID)
     given Meta[Username]     = Meta[String].timap(Username.apply)(_.toString)
 
-    private val lockedFilter         = fr"""locked_at IS NOT NULL"""
-    private val notLockedFilter      = fr"""locked_at IS NULL"""
-    private val selectAccountColumns = fr"""SELECT uid, name, email, validated_email, language FROM "hub"."accounts""""
+    private val lockedFilter    = fr"""locked_at IS NOT NULL"""
+    private val notLockedFilter = fr"""locked_at IS NULL"""
+    private val selectAccountColumns =
+        fr"""SELECT uid, name, email, full_name, validated_email, language FROM "hub"."accounts""""
 
     override def allAccounts(): Stream[F, Account] = {
         val query = selectAccountColumns ++ fr"""ORDER BY "name" ASC"""
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieResetPasswordRepository.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieResetPasswordRepository.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieResetPasswordRepository.scala	2025-01-13 11:51:51.486757555 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieResetPasswordRepository.scala	2025-01-13 11:51:51.490757563 +0000
@@ -31,6 +31,7 @@
 
 final class DoobieResetPasswordRepository[F[_]: Sync](tx: Transactor[F]) extends ResetPasswordRepository[F] {
     given Meta[EmailAddress] = Meta[String].timap(EmailAddress.apply)(_.toString)
+    given Meta[FullName]     = Meta[String].timap(FullName.apply)(_.toString)
     given Meta[LanguageCode] = Meta[String].timap(LanguageCode.apply)(_.toString)
     given Meta[PasswordHash] = Meta[String].timap(PasswordHash.apply)(_.toString)
     given Meta[ResetToken]   = Meta[String].timap(ResetToken.apply)(_.toString)
@@ -43,7 +44,8 @@
     private val resetTokenExpiryNotSetFilter = fr"""reset_expiry IS NULL"""
     private val resetTokenExpirySetFilter    = fr"""reset_expiry IS NOT NULL"""
     private val resetTokenNotExpiredFilter   = fr"""reset_expiry > NOW()"""
-    private val selectAccountColumns = fr"""SELECT uid, name, email, validated_email, language FROM "hub"."accounts""""
+    private val selectAccountColumns =
+        fr"""SELECT uid, name, email, full_name, validated_email, language FROM "hub"."accounts""""
 
     override def findByNameAndResetPasswordToken(name: Username, token: ResetToken): F[Option[Account]] = {
         val nameFilter       = fr"""name = $name"""
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/FullName.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/FullName.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/FullName.scala	1970-01-01 00:00:00.000000000 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/FullName.scala	2025-01-13 11:51:51.490757563 +0000
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2022  Contributors as noted in the AUTHORS.md file
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.smederee.hub
+
+/** The human readable full name of an actor (account, organisation) which must be non empty and is limited to 128
+  * characters.
+  */
+opaque type FullName = String
+object FullName {
+    val MaximumLength: Int = 128
+
+    /** Create an instance of FullName from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a FullName.
+      * @return
+      *   The appropriate instance of FullName.
+      */
+    def apply(source: String): FullName = source
+
+    /** Try to create an instance of FullName from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a FullName.
+      * @return
+      *   An option to the successfully converted FullName.
+      */
+    def from(source: String): Option[FullName] = Option(source).filter(_.nonEmpty).filter(_.length <= MaximumLength)
+}
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/SignupRoutes.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/SignupRoutes.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/SignupRoutes.scala	2025-01-13 11:51:51.486757555 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/SignupRoutes.scala	2025-01-13 11:51:51.490757563 +0000
@@ -114,6 +114,7 @@
                                             uid = uid,
                                             name = signupForm.name,
                                             email = signupForm.email,
+                                            fullName = None,
                                             validatedEmail = false,
                                             language = None
                                         )
diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/hub/BaseSpec.scala new-smederee/modules/hub/src/test/scala/de/smederee/hub/BaseSpec.scala
--- old-smederee/modules/hub/src/test/scala/de/smederee/hub/BaseSpec.scala	2025-01-13 11:51:51.486757555 +0000
+++ new-smederee/modules/hub/src/test/scala/de/smederee/hub/BaseSpec.scala	2025-01-13 11:51:51.490757563 +0000
@@ -166,41 +166,46 @@
                     (unlockToken, validationToken) match {
                         case (None, None) =>
                             con.prepareStatement(
-                                """INSERT INTO "hub"."accounts" (uid, name, email, password, failed_attempts, created_at, updated_at, validated_email) VALUES(?, ?, ?, ?, ?, NOW(), NOW(), ?)"""
+                                """INSERT INTO "hub"."accounts" (uid, name, email, full_name, password, failed_attempts, created_at, updated_at, validated_email) VALUES(?, ?, ?, ?, ?, ?, NOW(), NOW(), ?)"""
                             )
                         case (Some(_), None) =>
                             con.prepareStatement(
-                                """INSERT INTO "hub"."accounts" (uid, name, email, password, failed_attempts, validated_email, locked_at, unlock_token, created_at, updated_at) VALUES(?, ?, ?, ?, ?, ?, NOW(), ?, NOW(), NOW())"""
+                                """INSERT INTO "hub"."accounts" (uid, name, email, full_name, password, failed_attempts, validated_email, locked_at, unlock_token, created_at, updated_at) VALUES(?, ?, ?, ?, ?, ?, ?, NOW(), ?, NOW(), NOW())"""
                             )
                         case (None, Some(_)) =>
                             con.prepareStatement(
-                                """INSERT INTO "hub"."accounts" (uid, name, email, password, failed_attempts, validated_email, locked_at, validation_token, created_at, updated_at) VALUES(?, ?, ?, ?, ?, ?, NOW(), ?, NOW(), NOW())"""
+                                """INSERT INTO "hub"."accounts" (uid, name, email, full_name, password, failed_attempts, validated_email, locked_at, validation_token, created_at, updated_at) VALUES(?, ?, ?, ?, ?, ?, ?, NOW(), ?, NOW(), NOW())"""
                             )
                         case (Some(_), Some(_)) =>
                             con.prepareStatement(
-                                """INSERT INTO "hub"."accounts" (uid, name, email, password, failed_attempts, validated_email, locked_at, unlock_token, validation_token, created_at, updated_at) VALUES(?, ?, ?, ?, ?, ?, NOW(), ?, NOW(), NOW())"""
+                                """INSERT INTO "hub"."accounts" (uid, name, email, full_name, password, failed_attempts, validated_email, locked_at, unlock_token, validation_token, created_at, updated_at) VALUES(?, ?, ?, ?, ?, ?, ?, NOW(), ?, NOW(), NOW())"""
                             )
                     }
                 }
                 _ <- IO.delay(statement.setObject(1, account.uid))
                 _ <- IO.delay(statement.setString(2, account.name.toString))
                 _ <- IO.delay(statement.setString(3, account.email.toString))
-                _ <- IO.delay(statement.setString(4, hash.toString))
-                _ <- IO.delay(statement.setInt(5, attempts.getOrElse(1)))
-                _ <- IO.delay(statement.setBoolean(6, account.validatedEmail))
+                _ <- IO.delay(
+                    account.fullName.fold(statement.setNull(4, java.sql.Types.VARCHAR))(name =>
+                        statement.setString(4, name.toString)
+                    )
+                )
+                _ <- IO.delay(statement.setString(5, hash.toString))
+                _ <- IO.delay(statement.setInt(6, attempts.getOrElse(1)))
+                _ <- IO.delay(statement.setBoolean(7, account.validatedEmail))
                 _ <- (unlockToken, validationToken) match {
                     case (None, None)     => IO.unit
-                    case (Some(ut), None) => IO.delay(statement.setString(7, ut.toString))
-                    case (None, Some(vt)) => IO.delay(statement.setString(7, vt.toString))
+                    case (Some(ut), None) => IO.delay(statement.setString(8, ut.toString))
+                    case (None, Some(vt)) => IO.delay(statement.setString(8, vt.toString))
                     case (Some(ut), Some(vt)) =>
                         IO.delay {
-                            statement.setString(7, ut.toString)
-                            statement.setString(8, vt.toString)
+                            statement.setString(8, ut.toString)
+                            statement.setString(9, vt.toString)
                         }
                 }
                 _ <- IO.delay {
                     unlockToken.foreach { token =>
-                        statement.setString(7, token.toString)
+                        statement.setString(8, token.toString)
                     }
                 }
                 r <- IO.delay(statement.executeUpdate())
@@ -251,7 +256,7 @@
             for {
                 statement <- IO.delay(
                     con.prepareStatement(
-                        """SELECT uid, name, email, password, created_at, updated_at, validated_email, language FROM "hub"."accounts" WHERE uid = ? LIMIT 1"""
+                        """SELECT uid, name, email, full_name, password, created_at, updated_at, validated_email, language FROM "hub"."accounts" WHERE uid = ? LIMIT 1"""
                     )
                 )
                 _      <- IO.delay(statement.setObject(1, uid))
@@ -263,11 +268,13 @@
                                 LanguageCode.from(result.getString("language"))
                             else
                                 None
+                        val fullName = FullName.from(result.getString("full_name"))
                         Option(
                             Account(
                                 uid = uid,
                                 name = Username(result.getString("name")),
                                 email = EmailAddress(result.getString("email")),
+                                fullName = fullName,
                                 validatedEmail = result.getBoolean("validated_email"),
                                 language = language
                             )
diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/hub/DoobieVcsMetadataRepositoryTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/hub/DoobieVcsMetadataRepositoryTest.scala
--- old-smederee/modules/hub/src/test/scala/de/smederee/hub/DoobieVcsMetadataRepositoryTest.scala	2025-01-13 11:51:51.486757555 +0000
+++ new-smederee/modules/hub/src/test/scala/de/smederee/hub/DoobieVcsMetadataRepositoryTest.scala	2025-01-13 11:51:51.490757563 +0000
@@ -392,6 +392,7 @@
                         repo.owner.uid,
                         repo.owner.name,
                         EmailAddress(s"${repo.owner.name}@example.com"),
+                        account.fullName,
                         validatedEmail = true,
                         None
                     )
@@ -436,6 +437,7 @@
                         repo.owner.uid,
                         repo.owner.name,
                         EmailAddress(s"${repo.owner.name}@example.com"),
+                        account.fullName,
                         validatedEmail = true,
                         None
                     )
@@ -483,6 +485,7 @@
                         repo.owner.uid,
                         repo.owner.name,
                         EmailAddress(s"${repo.owner.name}@example.com"),
+                        account.fullName,
                         validatedEmail = true,
                         None
                     )
diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/hub/Generators.scala new-smederee/modules/hub/src/test/scala/de/smederee/hub/Generators.scala
--- old-smederee/modules/hub/src/test/scala/de/smederee/hub/Generators.scala	2025-01-13 11:51:51.486757555 +0000
+++ new-smederee/modules/hub/src/test/scala/de/smederee/hub/Generators.scala	2025-01-13 11:51:51.490757563 +0000
@@ -102,6 +102,9 @@
         suffix = s"$domain.$topLevelDomain"
     } yield EmailAddress(s"$prefix@$suffix")
 
+    val genValidFullName: Gen[FullName] =
+        Gen.nonEmptyListOf(Gen.alphaNumChar).map(_.take(FullName.MaximumLength).mkString).map(FullName.apply)
+
     val genValidUsername: Gen[Username] = for {
         length <- Gen.choose(2, 30)
         prefix <- Gen.alphaChar
@@ -114,9 +117,17 @@
         id             <- genUserId
         email          <- genValidEmail
         name           <- genValidUsername
+        fullName       <- Gen.option(genValidFullName)
         validatedEmail <- Gen.oneOf(List(false, true))
         language       <- Gen.option(genLanguageCode)
-    } yield Account(uid = id, name = name, email = email, validatedEmail = validatedEmail, language = language)
+    } yield Account(
+        uid = id,
+        name = name,
+        email = email,
+        fullName = fullName,
+        validatedEmail = validatedEmail,
+        language = language
+    )
 
     given Arbitrary[Account] = Arbitrary(genValidAccount)