Reset password email by users themselves (#3023)

This commit is contained in:
Naoki Takezoe
2022-03-29 01:04:57 +09:00
committed by GitHub
parent bd06e6d4dc
commit b5ee6431c4
24 changed files with 354 additions and 144 deletions

View File

@@ -128,6 +128,15 @@ trait AccountControllerBase extends AccountManagementControllerBase {
"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 @@ trait AccountControllerBase extends AccountManagementControllerBase {
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 @@ trait AccountControllerBase extends AccountManagementControllerBase {
})
get("/register") {
if (context.settings.allowAccountRegistration) {
if (context.settings.basicBehavior.allowAccountRegistration) {
if (context.loginAccount.isDefined) {
redirect("/")
} else {
@@ -612,7 +628,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
}
post("/register", newForm) { form =>
if (context.settings.allowAccountRegistration) {
if (context.settings.basicBehavior.allowAccountRegistration) {
createAccount(
form.userName,
pbkdf2_sha256(form.password),
@@ -628,6 +644,63 @@ trait AccountControllerBase extends AccountManagementControllerBase {
} 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 @@ trait AccountControllerBase extends AccountManagementControllerBase {
*/
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 @@ trait AccountControllerBase extends AccountManagementControllerBase {
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 @@ trait AccountControllerBase extends AccountManagementControllerBase {
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 @@ trait AccountControllerBase extends AccountManagementControllerBase {
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

View File

@@ -40,7 +40,7 @@ trait DashboardControllerBase extends ControllerBase {
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 @@ trait DashboardControllerBase extends ControllerBase {
context.loginAccount,
None,
withoutPhysicalInfo = true,
limit = context.settings.limitVisibleRepositories
limit = context.settings.basicBehavior.limitVisibleRepositories
)
)
}
@@ -171,7 +171,7 @@ trait DashboardControllerBase extends ControllerBase {
context.loginAccount,
None,
withoutPhysicalInfo = true,
limit = context.settings.limitVisibleRepositories
limit = context.settings.basicBehavior.limitVisibleRepositories
)
)
}

View File

@@ -69,7 +69,7 @@ trait IndexControllerBase extends ControllerBase {
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 @@ trait IndexControllerBase extends ControllerBase {
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,

View File

@@ -29,7 +29,7 @@ trait PreProcessControllerBase extends ControllerBase {
* 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") &&

View File

@@ -390,7 +390,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
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 @@ trait RepositorySettingsControllerBase extends ControllerBase {
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 @@ trait RepositorySettingsControllerBase extends ControllerBase {
*/
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}")

View File

@@ -34,19 +34,22 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
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 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
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.",

View File

@@ -7,9 +7,14 @@ import gitbucket.core.model.Profile.profile.blockingApi._
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 @@ trait AccountService {
}
}
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)
}

View File

@@ -18,17 +18,18 @@ trait SystemSettingsService {
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 @@ trait SystemSettingsService {
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 @@ object SystemSettingsService {
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 @@ object SystemSettingsService {
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 @@ object SystemSettingsService {
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"

View File

@@ -98,29 +98,30 @@ class GitAuthenticationFilter extends Filter with RepositoryService with Account
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)

View File

@@ -41,7 +41,7 @@ class Mailer(settings: SystemSettings) {
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)

View File

@@ -18,7 +18,7 @@ trait AvatarImageProvider { self: RequestCache =>
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 @@ trait AvatarImageProvider { self: RequestCache =>
} 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"""

View File

@@ -0,0 +1,16 @@
@()(implicit context: gitbucket.core.controller.Context)
@gitbucket.core.html.main("Reset your password"){
<div class="content-wrapper main-center">
<div class="content body">
<h2>Reset your password</h2>
<form action="@context.path/reset" method="POST" validate="true" autocomplete="off">
<fieldset class="form-group">
Enter your email address to reset your password.
<input type="text" name="mailAddress" id="mailAddress" class="form-control" value="" value="" style="max-width: 400px;"/>
<span id="error-mailAddress" class="error"></span>
</fieldset>
<input type="submit" class="btn btn-success" value="Submit"/>
</form>
</div>
</div>
}

View File

@@ -0,0 +1,11 @@
@()(implicit context: gitbucket.core.controller.Context)
@gitbucket.core.html.main("Reset your password"){
<div class="content-wrapper main-center">
<div class="content body">
<h2>Reset your password</h2>
<p>
Password has been updated. <a href="@context.path/signin">Sign-in</a> with new password.
</p>
</div>
</div>
}

View File

@@ -0,0 +1,17 @@
@(token: String)(implicit context: gitbucket.core.controller.Context)
@gitbucket.core.html.main("Reset your password"){
<div class="content-wrapper main-center">
<div class="content body">
<h2>Reset your password</h2>
<form action="@context.path/reset/form" method="POST" validate="true" autocomplete="off">
<fieldset class="form-group">
Enter new password:
<input type="password" name="password" id="password" class="form-control" value="" style="max-width: 400px;"/>
<span id="error-password" class="error"></span>
</fieldset>
<input type="submit" class="btn btn-success" value="Submit"/>
<input type="hidden" name="token" id="token" value="@token"/>
</form>
</div>
</div>
}

View File

@@ -0,0 +1,11 @@
@()(implicit context: gitbucket.core.controller.Context)
@gitbucket.core.html.main("Reset your password"){
<div class="content-wrapper main-center">
<div class="content body">
<h2>Reset your password</h2>
<p>
Send an email to you. Check your mailbox.
</p>
</div>
</div>
}

View File

@@ -6,7 +6,7 @@
<label class="strong">Services</label>
<fieldset>
<label class="checkbox">
<input type="checkbox" name="gravatar"@if(context.settings.gravatar){ checked}/>
<input type="checkbox" name="basicBehavior.gravatar"@if(context.settings.basicBehavior.gravatar){ checked}/>
Use Gravatar for profile images
</label>
</fieldset>
@@ -132,7 +132,7 @@
<label class="strong">Notifications</label>
<fieldset>
<label class="checkbox" for="notification">
<input type="checkbox" id="notification" name="notification"@if(context.settings.notification){ checked}/>
<input type="checkbox" id="notification" name="basicBehavior.notification"@if(context.settings.basicBehavior.notification){ checked}/>
Send notifications
</label>
</fieldset>

View File

@@ -108,15 +108,30 @@
<label class="strong">Account registration</label>
<fieldset>
<label class="radio">
<input type="radio" name="allowAccountRegistration" value="true"@if(context.settings.allowAccountRegistration){ checked}>
<input type="radio" name="basicBehavior.allowAccountRegistration" value="true"@if(context.settings.basicBehavior.allowAccountRegistration){ checked}>
<span class="strong">Allow</span> <span class="normal">- Users can create accounts by themselves.</span>
</label>
<label class="radio">
<input type="radio" name="allowAccountRegistration" value="false"@if(!context.settings.allowAccountRegistration){ checked}>
<input type="radio" name="basicBehavior.allowAccountRegistration" value="false"@if(!context.settings.basicBehavior.allowAccountRegistration){ checked}>
<span class="strong">Deny</span> <span class="normal">- Only administrators can create accounts.</span>
</label>
</fieldset>
<!--====================================================================-->
<!-- Reset password -->
<!--====================================================================-->
<hr>
<label class="strong">Reset password</label>
<fieldset>
<label class="radio">
<input type="radio" name="basicBehavior.allowResetPassword" value="true"@if(context.settings.basicBehavior.allowResetPassword){ checked}>
<span class="strong">Allow</span> <span class="normal">- Allow users to reset password. (SMTP setting is required)</span>
</label>
<label class="radio">
<input type="radio" name="basicBehavior.allowResetPassword" value="false"@if(!context.settings.basicBehavior.allowResetPassword){ checked}>
<span class="strong">Deny</span> <span class="normal">- Doesn't allow users to reset password.</span>
</label>
</fieldset>
<!--====================================================================-->
<!-- Repository operations -->
<!--====================================================================-->
<hr>
@@ -126,11 +141,11 @@
<label class="control-label col-md-2">Create</label>
<div class="col-md-10">
<label class="radio">
<input type="radio" name="repositoryOperation.create" value="true"@if(context.settings.repositoryOperation.create){ checked}>
<input type="radio" name="basicBehavior.repositoryOperation.create" value="true"@if(context.settings.basicBehavior.repositoryOperation.create){ checked}>
<span class="strong">All users</span> <span class="normal">- All users can create repository.</span>
</label>
<label class="radio">
<input type="radio" name="repositoryOperation.create" value="false"@if(!context.settings.repositoryOperation.create){ checked}>
<input type="radio" name="basicBehavior.repositoryOperation.create" value="false"@if(!context.settings.basicBehavior.repositoryOperation.create){ checked}>
<span class="strong">Admin only</span> <span class="normal">- Only administrators can create repository.</span>
</label>
</div>
@@ -139,11 +154,11 @@
<label class="control-label col-md-2">Delete</label>
<div class="col-md-10">
<label class="radio">
<input type="radio" name="repositoryOperation.delete" value="true"@if(context.settings.repositoryOperation.delete){ checked}>
<input type="radio" name="basicBehavior.repositoryOperation.delete" value="true"@if(context.settings.basicBehavior.repositoryOperation.delete){ checked}>
<span class="strong">All users</span> <span class="normal">- All users can delete repository.</span>
</label>
<label class="radio">
<input type="radio" name="repositoryOperation.delete" value="false"@if(!context.settings.repositoryOperation.delete){ checked}>
<input type="radio" name="basicBehavior.repositoryOperation.delete" value="false"@if(!context.settings.basicBehavior.repositoryOperation.delete){ checked}>
<span class="strong">Admin only</span> <span class="normal">- Only administrators can delete repository.</span>
</label>
</div>
@@ -152,11 +167,11 @@
<label class="control-label col-md-2">Rename</label>
<div class="col-md-10">
<label class="radio">
<input type="radio" name="repositoryOperation.rename" value="true"@if(context.settings.repositoryOperation.rename){ checked}>
<input type="radio" name="basicBehavior.repositoryOperation.rename" value="true"@if(context.settings.basicBehavior.repositoryOperation.rename){ checked}>
<span class="strong">All users</span> <span class="normal">- All users can rename repository.</span>
</label>
<label class="radio">
<input type="radio" name="repositoryOperation.rename" value="false"@if(!context.settings.repositoryOperation.rename){ checked}>
<input type="radio" name="basicBehavior.repositoryOperation.rename" value="false"@if(!context.settings.basicBehavior.repositoryOperation.rename){ checked}>
<span class="strong">Admin only</span> <span class="normal">- Only administrators can rename repository.</span>
</label>
</div>
@@ -165,11 +180,11 @@
<label class="control-label col-md-2">Transfer</label>
<div class="col-md-10">
<label class="radio">
<input type="radio" name="repositoryOperation.transfer" value="true"@if(context.settings.repositoryOperation.transfer){ checked}>
<input type="radio" name="basicBehavior.repositoryOperation.transfer" value="true"@if(context.settings.basicBehavior.repositoryOperation.transfer){ checked}>
<span class="strong">All users</span> <span class="normal">- All users can transfer repository.</span>
</label>
<label class="radio">
<input type="radio" name="repositoryOperation.transfer" value="false"@if(!context.settings.repositoryOperation.transfer){ checked}>
<input type="radio" name="basicBehavior.repositoryOperation.transfer" value="false"@if(!context.settings.basicBehavior.repositoryOperation.transfer){ checked}>
<span class="strong">Admin only</span> <span class="normal">- Only administrators can transfer repository.</span>
</label>
</div>
@@ -178,11 +193,11 @@
<label class="control-label col-md-2">Fork</label>
<div class="col-md-10">
<label class="radio">
<input type="radio" name="repositoryOperation.fork" value="true"@if(context.settings.repositoryOperation.fork){ checked}>
<input type="radio" name="basicBehavior.repositoryOperation.fork" value="true"@if(context.settings.basicBehavior.repositoryOperation.fork){ checked}>
<span class="strong">All users</span> <span class="normal">- All users can fork repository.</span>
</label>
<label class="radio">
<input type="radio" name="repositoryOperation.fork" value="false"@if(!context.settings.repositoryOperation.fork){ checked}>
<input type="radio" name="basicBehavior.repositoryOperation.fork" value="false"@if(!context.settings.basicBehavior.repositoryOperation.fork){ checked}>
<span class="strong">Admin only</span> <span class="normal">- Only administrators can fork repository.</span>
</label>
</div>
@@ -192,11 +207,11 @@
<label class="strong">Default visibility when creating a new repository</label>
<fieldset>
<label class="radio">
<input type="radio" name="isCreateRepoOptionPublic" value="true"@if(context.settings.isCreateRepoOptionPublic){ checked}>
<input type="radio" name="basicBehavior.isCreateRepoOptionPublic" value="true"@if(context.settings.basicBehavior.isCreateRepoOptionPublic){ checked}>
<span class="strong">Public</span> <span class="normal">- All users and guests can read the repository.</span>
</label>
<label class="radio">
<input type="radio" name="isCreateRepoOptionPublic" value="false"@if(!context.settings.isCreateRepoOptionPublic){ checked}>
<input type="radio" name="basicBehavior.isCreateRepoOptionPublic" value="false"@if(!context.settings.basicBehavior.isCreateRepoOptionPublic){ checked}>
<span class="strong">Private</span> <span class="normal">- Only collaborators can read the repository.</span>
</label>
</fieldset>
@@ -207,11 +222,11 @@
<label class="strong">Anonymous access</label>
<fieldset>
<label class="radio">
<input type="radio" name="allowAnonymousAccess" value="true"@if(context.settings.allowAnonymousAccess){ checked}>
<input type="radio" name="basicBehavior.allowAnonymousAccess" value="true"@if(context.settings.basicBehavior.allowAnonymousAccess){ checked}>
<span class="strong">Allow</span> <span class="normal">- Anyone can view public repositories and user/group profiles.</span>
</label>
<label class="radio">
<input type="radio" name="allowAnonymousAccess" value="false"@if(!context.settings.allowAnonymousAccess){ checked}>
<input type="radio" name="basicBehavior.allowAnonymousAccess" value="false"@if(!context.settings.basicBehavior.allowAnonymousAccess){ checked}>
<span class="strong">Deny</span> <span class="normal">- Users must authenticate before viewing any information.</span>
</label>
</fieldset>
@@ -272,11 +287,11 @@
<label><span class="strong">Show repositories in sidebar</span></label>
<fieldset>
<label class="radio">
<input type="radio" name="limitVisibleRepositories" value="false"@if(!context.settings.limitVisibleRepositories){ checked}>
<input type="radio" name="basicBehavior.limitVisibleRepositories" value="false"@if(!context.settings.basicBehavior.limitVisibleRepositories){ checked}>
<span class="strong">All</span> <span class="normal">- Show all visible repositories in sidebar.</span>
</label>
<label class="radio">
<input type="radio" name="limitVisibleRepositories" value="true"@if(context.settings.limitVisibleRepositories){ checked}>
<input type="radio" name="basicBehavior.limitVisibleRepositories" value="true"@if(context.settings.basicBehavior.limitVisibleRepositories){ checked}>
<span class="strong">Limited</span> <span class="normal">- Show only owned repositories in sidebar.</span>
</label>
</fieldset>

View File

@@ -104,7 +104,7 @@
<ul class="dropdown-menu pull-right" style="width: auto;">
<li>
<ul class="menu">
@if(context.settings.repositoryOperation.create || context.loginAccount.get.isAdmin){
@if(context.settings.basicBehavior.repositoryOperation.create || context.loginAccount.get.isAdmin){
<li><a href="@context.path/new">New repository</a></li>
}
<li><a href="@context.path/groups/new">New group</a></li>

View File

@@ -75,7 +75,7 @@
@gitbucket.core.plugin.PluginRegistry().getRepositoryHeaders.map { repositoryHeaderComponent =>
@repositoryHeaderComponent(repository, context)
}
@if(repository.repository.options.allowFork && (context.settings.repositoryOperation.fork || context.loginAccount.map(_.isAdmin).getOrElse(false))) {
@if(repository.repository.options.allowFork && (context.settings.basicBehavior.repositoryOperation.fork || context.loginAccount.map(_.isAdmin).getOrElse(false))) {
@if(context.loginAccount.isEmpty){
<a class="btn btn-default btn-sm" href="@context.path/signin?redirect=@helpers.urlEncode(s"${context.path}/${repository.owner}/${repository.name}")">
<span class="strong"><i class="octicon octicon-repo-forked"></i>Fork</span><span class="muted">: @repository.forkedCount</span>

View File

@@ -16,7 +16,7 @@
</div>
</fieldset>
</form>
@if(context.settings.repositoryOperation.rename || context.loginAccount.get.isAdmin){
@if(context.settings.basicBehavior.repositoryOperation.rename || context.loginAccount.get.isAdmin){
<form id="rename-form" method="post" action="@helpers.url(repository)/settings/rename" validate="true" autocomplete="off">
<fieldset class="border-top form-group">
<label class="strong">Rename repository</label>
@@ -33,7 +33,7 @@
</fieldset>
</form>
}
@if(context.settings.repositoryOperation.transfer || context.loginAccount.get.isAdmin){
@if(context.settings.basicBehavior.repositoryOperation.transfer || context.loginAccount.get.isAdmin){
<form id="transfer-form" method="post" action="@helpers.url(repository)/settings/transfer" validate="true" autocomplete="off">
<fieldset class="border-top form-group">
<label class="strong">Transfer Ownership</label>
@@ -50,7 +50,7 @@
</fieldset>
</form>
}
@if(context.settings.repositoryOperation.delete || context.loginAccount.get.isAdmin){
@if(context.settings.basicBehavior.repositoryOperation.delete || context.loginAccount.get.isAdmin){
<form id="delete-form" method="post" action="@helpers.url(repository)/settings/delete">
<fieldset class="border-top form-group">
<label class="strong">Delete repository</label>

View File

@@ -5,7 +5,7 @@
<div class="content-wrapper main-center">
<div class="content body">
<div class="signin-form">
@if(context.settings.allowAnonymousAccess){
@if(context.settings.basicBehavior.allowAnonymousAccess){
@context.settings.information.map { information =>
<div class="alert alert-info" style="background-color: white; color: #555; border-color: #4183c4; font-size: small; line-height: 120%;">
<button type="button" class="close" data-dismiss="alert">&times;</button>

View File

@@ -24,6 +24,9 @@
<label for="password">Password:</label>
<span id="error-password" class="error"></span>
<input type="password" name="password" id="password" class="form-control" value="@password"/>
@if(systemSettings.basicBehavior.allowResetPassword){
<a href="@context.path/reset">Forgot password?</a>
}
</div>
<input type="hidden" name="hash"/>
<div>
@@ -33,12 +36,12 @@
</li>
</ul>
</div>
@if(systemSettings.allowAccountRegistration){
@if(systemSettings.basicBehavior.allowAccountRegistration){
<div class="panel panel-default">
<ul class="list-group list-group-flush">
<li class="list-group-item text-center">
Don't have an account? <a href="@context.path/register">Create one.</a>
<a href="@context.path/register">Create account</a>
</li>
</ul>
</div>
}
}

View File

@@ -8,11 +8,18 @@ import liquibase.database.jvm.JdbcConnection
import gitbucket.core.model._
import gitbucket.core.model.Profile.profile.blockingApi._
import org.apache.commons.io.FileUtils
import java.sql.DriverManager
import java.io.File
import gitbucket.core.controller.Context
import gitbucket.core.service.SystemSettingsService.{RepositoryOperation, RepositoryViewerSettings, Ssh, SystemSettings}
import gitbucket.core.service.SystemSettingsService.{
BasicBehavior,
RepositoryOperation,
RepositoryViewerSettings,
Ssh,
SystemSettings
}
import javax.servlet.http.{HttpServletRequest, HttpSession}
import org.mockito.Mockito._
@@ -32,19 +39,22 @@ trait ServiceSpecBase {
SystemSettings(
baseUrl = None,
information = None,
allowAccountRegistration = false,
allowAnonymousAccess = true,
isCreateRepoOptionPublic = true,
repositoryOperation = RepositoryOperation(
create = true,
delete = true,
rename = true,
transfer = true,
fork = true
basicBehavior = BasicBehavior(
allowAccountRegistration = false,
allowResetPassword = false,
allowAnonymousAccess = true,
isCreateRepoOptionPublic = true,
repositoryOperation = RepositoryOperation(
create = true,
delete = true,
rename = true,
transfer = true,
fork = true
),
gravatar = false,
notification = false,
limitVisibleRepositories = false,
),
gravatar = false,
notification = false,
limitVisibleRepositories = false,
ssh = Ssh(
enabled = false,
bindAddress = None,

View File

@@ -2,12 +2,12 @@ package gitbucket.core.view
import java.text.SimpleDateFormat
import java.util.Date
import javax.servlet.http.{HttpServletRequest, HttpSession}
import gitbucket.core.controller.Context
import gitbucket.core.model.Account
import gitbucket.core.service.RequestCache
import gitbucket.core.service.SystemSettingsService.{
BasicBehavior,
RepositoryOperation,
RepositoryViewerSettings,
Ssh,
@@ -151,19 +151,22 @@ class AvatarImageProviderSpec extends AnyFunSpec {
SystemSettings(
baseUrl = None,
information = None,
allowAccountRegistration = false,
allowAnonymousAccess = true,
isCreateRepoOptionPublic = true,
repositoryOperation = RepositoryOperation(
create = true,
delete = true,
rename = true,
transfer = true,
fork = true
basicBehavior = BasicBehavior(
allowAccountRegistration = false,
allowResetPassword = false,
allowAnonymousAccess = true,
isCreateRepoOptionPublic = true,
repositoryOperation = RepositoryOperation(
create = true,
delete = true,
rename = true,
transfer = true,
fork = true
),
gravatar = useGravatar,
notification = false,
limitVisibleRepositories = false
),
gravatar = useGravatar,
notification = false,
limitVisibleRepositories = false,
ssh = Ssh(
enabled = false,
bindAddress = None,