diff --git a/src/main/scala/gitbucket/core/controller/AccountController.scala b/src/main/scala/gitbucket/core/controller/AccountController.scala index ca04d8a..e3a7304 100644 --- a/src/main/scala/gitbucket/core/controller/AccountController.scala +++ b/src/main/scala/gitbucket/core/controller/AccountController.scala @@ -128,6 +128,15 @@ "highlighterTheme" -> trim(label("Theme", text(required))) )(SyntaxHighlighterThemeForm.apply) + val resetPasswordEmailForm = mapping( + "mailAddress" -> trim(label("Email", text(required))) + )(ResetPasswordEmailForm.apply) + + val resetPasswordForm = mapping( + "token" -> trim(label("Token", text(required))), + "password" -> trim(label("Password", text(required, maxlength(40)))) + )(ResetPasswordForm.apply) + case class NewGroupForm( groupName: String, description: Option[String], @@ -143,6 +152,13 @@ members: String, clearImage: Boolean ) + case class ResetPasswordEmailForm( + mailAddress: String + ) + case class ResetPasswordForm( + token: String, + password: String + ) val newGroupForm = mapping( "groupName" -> trim(label("Group name", text(required, maxlength(100), identifier, uniqueUserName, reservedNames))), @@ -602,7 +618,7 @@ }) get("/register") { - if (context.settings.allowAccountRegistration) { + if (context.settings.basicBehavior.allowAccountRegistration) { if (context.loginAccount.isDefined) { redirect("/") } else { @@ -612,7 +628,7 @@ } post("/register", newForm) { form => - if (context.settings.allowAccountRegistration) { + if (context.settings.basicBehavior.allowAccountRegistration) { createAccount( form.userName, pbkdf2_sha256(form.password), @@ -628,6 +644,63 @@ } else NotFound() } + get("/reset") { + if (context.settings.basicBehavior.allowResetPassword) { + html.reset() + } else NotFound() + } + + post("/reset", resetPasswordEmailForm) { form => + if (context.settings.basicBehavior.allowResetPassword) { + getAccountByMailAddress(form.mailAddress).foreach { account => + val token = generateResetPasswordToken(form.mailAddress) + val mailer = new Mailer(context.settings) + mailer.send( + form.mailAddress, + "Reset password", + s"""Hello, ${account.fullName}! + | + |You requested to reset the password for your GitBucket account. + |If you are not sure about the request, you can ignore this email. + |Otherwise, click the following link to set the new password: + |${context.baseUrl}/reset/form/${token} + |""".stripMargin + ) + } + redirect("/reset/sent") + } else NotFound() + } + + get("/reset/sent") { + if (context.settings.basicBehavior.allowResetPassword) { + html.resetsent() + } else NotFound() + } + + get("/reset/form/:token") { + if (context.settings.basicBehavior.allowResetPassword) { + val token = params("token") + decodeResetPasswordToken(token) + .map { _ => + html.resetform(token) + } + .getOrElse(NotFound()) + } else NotFound() + } + + post("/reset/form", resetPasswordForm) { form => + if (context.settings.basicBehavior.allowResetPassword) { + decodeResetPasswordToken(form.token) + .flatMap { mailAddress => + getAccountByMailAddress(mailAddress).map { account => + updateAccount(account.copy(password = form.password)) + html.resetcomplete() + } + } + .getOrElse(NotFound()) + } else NotFound() + } + get("/groups/new")(usersOnly { context.withLoginAccount { loginAccount => html.creategroup(List(GroupMember("", loginAccount.userName, true))) @@ -713,7 +786,7 @@ */ get("/new")(usersOnly { context.withLoginAccount { loginAccount => - html.newrepo(getGroupsByUserName(loginAccount.userName), context.settings.isCreateRepoOptionPublic) + html.newrepo(getGroupsByUserName(loginAccount.userName), context.settings.basicBehavior.isCreateRepoOptionPublic) } }) @@ -723,7 +796,7 @@ post("/new", newRepositoryForm)(usersOnly { form => context.withLoginAccount { loginAccount => - if (context.settings.repositoryOperation.create || loginAccount.isAdmin) { + if (context.settings.basicBehavior.repositoryOperation.create || loginAccount.isAdmin) { LockUtil.lock(s"${form.owner}/${form.name}") { if (getRepository(form.owner, form.name).isDefined) { // redirect to the repository if repository already exists @@ -753,7 +826,7 @@ get("/:owner/:repository/fork")(readableUsersOnly { repository => context.withLoginAccount { loginAccount => - if (repository.repository.options.allowFork && (context.settings.repositoryOperation.fork || loginAccount.isAdmin)) { + if (repository.repository.options.allowFork && (context.settings.basicBehavior.repositoryOperation.fork || loginAccount.isAdmin)) { val loginUserName = loginAccount.userName val groups = getGroupsByUserName(loginUserName) groups match { @@ -780,7 +853,7 @@ post("/:owner/:repository/fork", accountForm)(readableUsersOnly { (form, repository) => context.withLoginAccount { loginAccount => - if (repository.repository.options.allowFork && (context.settings.repositoryOperation.fork || loginAccount.isAdmin)) { + if (repository.repository.options.allowFork && (context.settings.basicBehavior.repositoryOperation.fork || loginAccount.isAdmin)) { val loginUserName = loginAccount.userName val accountName = form.accountName diff --git a/src/main/scala/gitbucket/core/controller/DashboardController.scala b/src/main/scala/gitbucket/core/controller/DashboardController.scala index 576619f..5fc1da1 100644 --- a/src/main/scala/gitbucket/core/controller/DashboardController.scala +++ b/src/main/scala/gitbucket/core/controller/DashboardController.scala @@ -40,7 +40,7 @@ context.loginAccount, None, withoutPhysicalInfo = true, - limit = context.settings.limitVisibleRepositories + limit = context.settings.basicBehavior.limitVisibleRepositories ) html.repos(getGroupNames(loginAccount.userName), repos, repos) } @@ -129,7 +129,7 @@ context.loginAccount, None, withoutPhysicalInfo = true, - limit = context.settings.limitVisibleRepositories + limit = context.settings.basicBehavior.limitVisibleRepositories ) ) } @@ -171,7 +171,7 @@ context.loginAccount, None, withoutPhysicalInfo = true, - limit = context.settings.limitVisibleRepositories + limit = context.settings.basicBehavior.limitVisibleRepositories ) ) } diff --git a/src/main/scala/gitbucket/core/controller/IndexController.scala b/src/main/scala/gitbucket/core/controller/IndexController.scala index 124c2d7..a4e3ae3 100644 --- a/src/main/scala/gitbucket/core/controller/IndexController.scala +++ b/src/main/scala/gitbucket/core/controller/IndexController.scala @@ -69,7 +69,7 @@ Some(account), None, withoutPhysicalInfo = true, - limit = context.settings.limitVisibleRepositories + limit = context.settings.basicBehavior.limitVisibleRepositories ), showBannerToCreatePersonalAccessToken = hasAccountFederation(account.userName) && !hasAccessToken( account.userName @@ -289,11 +289,11 @@ context.loginAccount, None, withoutPhysicalInfo = true, - limit = context.settings.limitVisibleRepositories + limit = context.settings.basicBehavior.limitVisibleRepositories ) val repositories = { - context.settings.limitVisibleRepositories match { + context.settings.basicBehavior.limitVisibleRepositories match { case true => getVisibleRepositories( context.loginAccount, diff --git a/src/main/scala/gitbucket/core/controller/PreProcessController.scala b/src/main/scala/gitbucket/core/controller/PreProcessController.scala index 4a368b0..794c950 100644 --- a/src/main/scala/gitbucket/core/controller/PreProcessController.scala +++ b/src/main/scala/gitbucket/core/controller/PreProcessController.scala @@ -29,7 +29,7 @@ * If anonymous access is allowed, pass all requests. * But if it's not allowed, demands authentication except some paths. */ - get(!context.settings.allowAnonymousAccess, context.loginAccount.isEmpty) { + get(!context.settings.basicBehavior.allowAnonymousAccess, context.loginAccount.isEmpty) { if (!context.currentPath.startsWith("/assets") && !context.currentPath.startsWith("/signin") && !context.currentPath.startsWith("/register") && !context.currentPath.endsWith("/info/refs") && !context.currentPath.startsWith("/plugin-assets") && diff --git a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala index 2d57129..9b243af 100644 --- a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala @@ -390,7 +390,7 @@ post("/:owner/:repository/settings/rename", renameForm)(ownerOnly { (form, repository) => context.withLoginAccount { loginAccount => - if (context.settings.repositoryOperation.rename || loginAccount.isAdmin) { + if (context.settings.basicBehavior.repositoryOperation.rename || loginAccount.isAdmin) { if (repository.name != form.repositoryName) { // Update database and move git repository renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName) @@ -414,7 +414,7 @@ post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) => context.withLoginAccount { loginAccount => - if (context.settings.repositoryOperation.transfer || loginAccount.isAdmin) { + if (context.settings.basicBehavior.repositoryOperation.transfer || loginAccount.isAdmin) { // Change repository owner if (repository.owner != form.newOwner) { // Update database and move git repository @@ -438,7 +438,7 @@ */ post("/:owner/:repository/settings/delete")(ownerOnly { repository => context.withLoginAccount { loginAccount => - if (context.settings.repositoryOperation.delete || loginAccount.isAdmin) { + if (context.settings.basicBehavior.repositoryOperation.delete || loginAccount.isAdmin) { // Delete the repository and related files deleteRepository(repository.repository) redirect(s"/${repository.owner}") diff --git a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala index 52b81a5..40c19d3 100644 --- a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala @@ -34,19 +34,22 @@ private val form = mapping( "baseUrl" -> trim(label("Base URL", optional(text()))), "information" -> trim(label("Information", optional(text()))), - "allowAccountRegistration" -> trim(label("Account registration", boolean())), - "allowAnonymousAccess" -> trim(label("Anonymous access", boolean())), - "isCreateRepoOptionPublic" -> trim(label("Default visibility of new repository", boolean())), - "repositoryOperation" -> mapping( - "create" -> trim(label("Allow all users to create repository", boolean())), - "delete" -> trim(label("Allow all users to delete repository", boolean())), - "rename" -> trim(label("Allow all users to rename repository", boolean())), - "transfer" -> trim(label("Allow all users to transfer repository", boolean())), - "fork" -> trim(label("Allow all users to fork repository", boolean())) - )(RepositoryOperation.apply), - "gravatar" -> trim(label("Gravatar", boolean())), - "notification" -> trim(label("Notification", boolean())), - "limitVisibleRepositories" -> trim(label("limitVisibleRepositories", boolean())), + "basicBehavior" -> mapping( + "allowAccountRegistration" -> trim(label("Account registration", boolean())), + "allowResetPassword" -> trim(label("Reset password", boolean())), + "allowAnonymousAccess" -> trim(label("Anonymous access", boolean())), + "isCreateRepoOptionPublic" -> trim(label("Default visibility of new repository", boolean())), + "repositoryOperation" -> mapping( + "create" -> trim(label("Allow all users to create repository", boolean())), + "delete" -> trim(label("Allow all users to delete repository", boolean())), + "rename" -> trim(label("Allow all users to rename repository", boolean())), + "transfer" -> trim(label("Allow all users to transfer repository", boolean())), + "fork" -> trim(label("Allow all users to fork repository", boolean())) + )(RepositoryOperation.apply), + "gravatar" -> trim(label("Gravatar", boolean())), + "notification" -> trim(label("Notification", boolean())), + "limitVisibleRepositories" -> trim(label("limitVisibleRepositories", boolean())), + )(BasicBehavior.apply), "ssh" -> mapping( "enabled" -> trim(label("SSH access", boolean())), "bindAddress" -> mapping( @@ -334,7 +337,12 @@ post("/admin/system/sendmail", sendMailForm)(adminOnly { form => try { - new Mailer(context.settings.copy(smtp = Some(form.smtp), notification = true)).send( + new Mailer( + context.settings.copy( + smtp = Some(form.smtp), + basicBehavior = context.settings.basicBehavior.copy(notification = true) + ) + ).send( to = form.testAddress, subject = "Test message from GitBucket", textMsg = "This is a test message from GitBucket.", diff --git a/src/main/scala/gitbucket/core/service/AccountService.scala b/src/main/scala/gitbucket/core/service/AccountService.scala index 78f9728..77730dc 100644 --- a/src/main/scala/gitbucket/core/service/AccountService.scala +++ b/src/main/scala/gitbucket/core/service/AccountService.scala @@ -7,9 +7,14 @@ import gitbucket.core.model.Profile.dateColumnType import gitbucket.core.util.{LDAPUtil, StringUtil} import StringUtil._ +import com.nimbusds.jose.{Algorithm, JWSAlgorithm, JWSHeader} +import com.nimbusds.jose.crypto.{MACSigner, MACVerifier} +import com.nimbusds.jwt.{JWTClaimsSet, SignedJWT} import gitbucket.core.plugin.PluginRegistry import gitbucket.core.service.SystemSettingsService.SystemSettings +import java.security.SecureRandom + trait AccountService { private val logger = LoggerFactory.getLogger(classOf[AccountService]) @@ -337,6 +342,33 @@ } } + def generateResetPasswordToken(mailAddress: String): String = { + val claimsSet = new JWTClaimsSet.Builder() + .claim("mailAddress", mailAddress) + .expirationTime(new java.util.Date(System.currentTimeMillis() + 10 * 1000)) + .build() + + val signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet) + signedJWT.sign(new MACSigner(AccountService.jwtSecretKey)) + + signedJWT.serialize() + } + + def decodeResetPasswordToken(token: String): Option[String] = { + try { + val signedJWT = SignedJWT.parse(token) + val verifier = new MACVerifier(AccountService.jwtSecretKey) + if (signedJWT.verify(verifier) && new java.util.Date().before(signedJWT.getJWTClaimsSet().getExpirationTime())) { + Some(signedJWT.getPayload.toJSONObject.get("mailAddress").toString) + } else None + } catch { + case _: Exception => None + } + } } -object AccountService extends AccountService +object AccountService extends AccountService { + // 256-bit key for HS256 which must be pre-shared + val jwtSecretKey = new Array[Byte](32) + new SecureRandom().nextBytes(jwtSecretKey) +} diff --git a/src/main/scala/gitbucket/core/service/SystemSettingsService.scala b/src/main/scala/gitbucket/core/service/SystemSettingsService.scala index 1b3b815..05faaf9 100644 --- a/src/main/scala/gitbucket/core/service/SystemSettingsService.scala +++ b/src/main/scala/gitbucket/core/service/SystemSettingsService.scala @@ -18,17 +18,18 @@ val props = new java.util.Properties() settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", ""))) settings.information.foreach(x => props.setProperty(Information, x)) - props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString) - props.setProperty(AllowAnonymousAccess, settings.allowAnonymousAccess.toString) - props.setProperty(IsCreateRepoOptionPublic, settings.isCreateRepoOptionPublic.toString) - props.setProperty(RepositoryOperationCreate, settings.repositoryOperation.create.toString) - props.setProperty(RepositoryOperationDelete, settings.repositoryOperation.delete.toString) - props.setProperty(RepositoryOperationRename, settings.repositoryOperation.rename.toString) - props.setProperty(RepositoryOperationTransfer, settings.repositoryOperation.transfer.toString) - props.setProperty(RepositoryOperationFork, settings.repositoryOperation.fork.toString) - props.setProperty(Gravatar, settings.gravatar.toString) - props.setProperty(Notification, settings.notification.toString) - props.setProperty(LimitVisibleRepositories, settings.limitVisibleRepositories.toString) + props.setProperty(AllowAccountRegistration, settings.basicBehavior.allowAccountRegistration.toString) + props.setProperty(AllowResetPassword, settings.basicBehavior.allowResetPassword.toString) + props.setProperty(AllowAnonymousAccess, settings.basicBehavior.allowAnonymousAccess.toString) + props.setProperty(IsCreateRepoOptionPublic, settings.basicBehavior.isCreateRepoOptionPublic.toString) + props.setProperty(RepositoryOperationCreate, settings.basicBehavior.repositoryOperation.create.toString) + props.setProperty(RepositoryOperationDelete, settings.basicBehavior.repositoryOperation.delete.toString) + props.setProperty(RepositoryOperationRename, settings.basicBehavior.repositoryOperation.rename.toString) + props.setProperty(RepositoryOperationTransfer, settings.basicBehavior.repositoryOperation.transfer.toString) + props.setProperty(RepositoryOperationFork, settings.basicBehavior.repositoryOperation.fork.toString) + props.setProperty(Gravatar, settings.basicBehavior.gravatar.toString) + props.setProperty(Notification, settings.basicBehavior.notification.toString) + props.setProperty(LimitVisibleRepositories, settings.basicBehavior.limitVisibleRepositories.toString) props.setProperty(SshEnabled, settings.ssh.enabled.toString) settings.ssh.bindAddress.foreach { bindAddress => props.setProperty(SshBindAddressHost, bindAddress.host.trim()) @@ -109,19 +110,22 @@ SystemSettings( getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")), getOptionValue(props, Information, None), - getValue(props, AllowAccountRegistration, false), - getValue(props, AllowAnonymousAccess, true), - getValue(props, IsCreateRepoOptionPublic, true), - RepositoryOperation( - create = getValue(props, RepositoryOperationCreate, true), - delete = getValue(props, RepositoryOperationDelete, true), - rename = getValue(props, RepositoryOperationRename, true), - transfer = getValue(props, RepositoryOperationTransfer, true), - fork = getValue(props, RepositoryOperationFork, true) + BasicBehavior( + getValue(props, AllowAccountRegistration, false), + getValue(props, AllowResetPassword, false), + getValue(props, AllowAnonymousAccess, true), + getValue(props, IsCreateRepoOptionPublic, true), + RepositoryOperation( + create = getValue(props, RepositoryOperationCreate, true), + delete = getValue(props, RepositoryOperationDelete, true), + rename = getValue(props, RepositoryOperationRename, true), + transfer = getValue(props, RepositoryOperationTransfer, true), + fork = getValue(props, RepositoryOperationFork, true) + ), + getValue(props, Gravatar, false), + getValue(props, Notification, false), + getValue(props, LimitVisibleRepositories, false) ), - getValue(props, Gravatar, false), - getValue(props, Notification, false), - getValue(props, LimitVisibleRepositories, false), Ssh( enabled = getValue(props, SshEnabled, false), bindAddress = { @@ -214,13 +218,7 @@ case class SystemSettings( baseUrl: Option[String], information: Option[String], - allowAccountRegistration: Boolean, - allowAnonymousAccess: Boolean, - isCreateRepoOptionPublic: Boolean, - repositoryOperation: RepositoryOperation, - gravatar: Boolean, - notification: Boolean, - limitVisibleRepositories: Boolean, + basicBehavior: BasicBehavior, ssh: Ssh, useSMTP: Boolean, smtp: Option[Smtp], @@ -264,6 +262,17 @@ ssh.getUrl(owner: String, name: String) } + case class BasicBehavior( + allowAccountRegistration: Boolean, + allowResetPassword: Boolean, + allowAnonymousAccess: Boolean, + isCreateRepoOptionPublic: Boolean, + repositoryOperation: RepositoryOperation, + gravatar: Boolean, + notification: Boolean, + limitVisibleRepositories: Boolean, + ) + case class RepositoryOperation( create: Boolean, delete: Boolean, @@ -383,6 +392,7 @@ private val BaseURL = "base_url" private val Information = "information" private val AllowAccountRegistration = "allow_account_registration" + private val AllowResetPassword = "allow_reset_password" private val AllowAnonymousAccess = "allow_anonymous_access" private val IsCreateRepoOptionPublic = "is_create_repository_option_public" private val RepositoryOperationCreate = "repository_operation_create" diff --git a/src/main/scala/gitbucket/core/servlet/GitAuthenticationFilter.scala b/src/main/scala/gitbucket/core/servlet/GitAuthenticationFilter.scala index 16a83a5..3b205c8 100644 --- a/src/main/scala/gitbucket/core/servlet/GitAuthenticationFilter.scala +++ b/src/main/scala/gitbucket/core/servlet/GitAuthenticationFilter.scala @@ -98,29 +98,30 @@ Database() withSession { implicit session => getRepository(repositoryOwner, repositoryName.replaceFirst("(\\.wiki)?\\.git$", "")) match { case Some(repository) => { - val execute = if (!isUpdating && !repository.repository.isPrivate && settings.allowAnonymousAccess) { - // Authentication is not required - true - } else { - // Authentication is required - val passed = for { - authorizationHeader <- Option(request.getHeader("Authorization")) - account <- authenticateByHeader(authorizationHeader, settings) - } yield - if (isUpdating) { - if (hasDeveloperRole(repository.owner, repository.name, Some(account))) { - request.setAttribute(Keys.Request.UserName, account.userName) - request.setAttribute(Keys.Request.RepositoryLockKey, s"${repository.owner}/${repository.name}") - true - } else false - } else if (repository.repository.isPrivate) { - if (hasGuestRole(repository.owner, repository.name, Some(account))) { - request.setAttribute(Keys.Request.UserName, account.userName) - true - } else false - } else true - passed.getOrElse(false) - } + val execute = + if (!isUpdating && !repository.repository.isPrivate && settings.basicBehavior.allowAnonymousAccess) { + // Authentication is not required + true + } else { + // Authentication is required + val passed = for { + authorizationHeader <- Option(request.getHeader("Authorization")) + account <- authenticateByHeader(authorizationHeader, settings) + } yield + if (isUpdating) { + if (hasDeveloperRole(repository.owner, repository.name, Some(account))) { + request.setAttribute(Keys.Request.UserName, account.userName) + request.setAttribute(Keys.Request.RepositoryLockKey, s"${repository.owner}/${repository.name}") + true + } else false + } else if (repository.repository.isPrivate) { + if (hasGuestRole(repository.owner, repository.name, Some(account))) { + request.setAttribute(Keys.Request.UserName, account.userName) + true + } else false + } else true + passed.getOrElse(false) + } if (execute) { () => chain.doFilter(request, response) diff --git a/src/main/scala/gitbucket/core/util/Mailer.scala b/src/main/scala/gitbucket/core/util/Mailer.scala index 78c31e2..3b448a4 100644 --- a/src/main/scala/gitbucket/core/util/Mailer.scala +++ b/src/main/scala/gitbucket/core/util/Mailer.scala @@ -41,7 +41,7 @@ htmlMsg: Option[String] = None, loginAccount: Option[Account] = None ): Option[HtmlEmail] = { - if (settings.notification) { + if (settings.basicBehavior.notification) { settings.smtp.map { smtp => val email = new HtmlEmail email.setHostName(smtp.host) diff --git a/src/main/scala/gitbucket/core/view/AvatarImageProvider.scala b/src/main/scala/gitbucket/core/view/AvatarImageProvider.scala index e74a4a8..467bc8c 100644 --- a/src/main/scala/gitbucket/core/view/AvatarImageProvider.scala +++ b/src/main/scala/gitbucket/core/view/AvatarImageProvider.scala @@ -18,7 +18,7 @@ val src = if (mailAddress.isEmpty) { // by user name getAccountByUserNameFromCache(userName).map { account => - if (account.image.isEmpty && context.settings.gravatar) { + if (account.image.isEmpty && context.settings.basicBehavior.gravatar) { s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}&d=retro&r=g""" } else { s"""${context.path}/${account.userName}/_avatar?${helpers.hashDate(account.updatedDate)}""" @@ -29,13 +29,13 @@ } else { // by mail address getAccountByMailAddressFromCache(mailAddress).map { account => - if (account.image.isEmpty && context.settings.gravatar) { + if (account.image.isEmpty && context.settings.basicBehavior.gravatar) { s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}&d=retro&r=g""" } else { s"""${context.path}/${account.userName}/_avatar?${helpers.hashDate(account.updatedDate)}""" } } getOrElse { - if (context.settings.gravatar) { + if (context.settings.basicBehavior.gravatar) { s"""https://www.gravatar.com/avatar/${StringUtil.md5(mailAddress.toLowerCase)}?s=${size}&d=retro&r=g""" } else { s"""${context.path}/_unknown/_avatar""" diff --git a/src/main/twirl/gitbucket/core/account/reset.scala.html b/src/main/twirl/gitbucket/core/account/reset.scala.html new file mode 100644 index 0000000..dd241a6 --- /dev/null +++ b/src/main/twirl/gitbucket/core/account/reset.scala.html @@ -0,0 +1,16 @@ +@()(implicit context: gitbucket.core.controller.Context) +@gitbucket.core.html.main("Reset your password"){ +
+ Password has been updated. Sign-in with new password. +
++ Send an email to you. Check your mailbox. +
+