~jan0sch/smederee
Showing details for patch 8391694d2a698e686c411b8e70d51ddaee19e95c.
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)