diff --git a/src/main/resources/update/2_9.sql b/src/main/resources/update/2_9.sql new file mode 100644 index 0000000..7be56ab --- /dev/null +++ b/src/main/resources/update/2_9.sql @@ -0,0 +1,13 @@ +DROP TABLE IF EXISTS ACCESS_TOKEN; + +CREATE TABLE ACCESS_TOKEN ( + ACCESS_TOKEN_ID INT NOT NULL AUTO_INCREMENT, + TOKEN_HASH VARCHAR(40) NOT NULL, + USER_NAME VARCHAR(100) NOT NULL, + NOTE TEXT NOT NULL +); + +ALTER TABLE ACCESS_TOKEN ADD CONSTRAINT IDX_ACCESS_TOKEN_PK PRIMARY KEY (ACCESS_TOKEN_ID); +ALTER TABLE ACCESS_TOKEN ADD CONSTRAINT IDX_ACCESS_TOKEN_FK0 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME) + ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE ACCESS_TOKEN ADD CONSTRAINT IDX_ACCESS_TOKEN_TOKEN_HASH UNIQUE(TOKEN_HASH); diff --git a/src/main/scala/app/AccountController.scala b/src/main/scala/app/AccountController.scala index 8311654..62711b5 100644 --- a/src/main/scala/app/AccountController.scala +++ b/src/main/scala/app/AccountController.scala @@ -18,10 +18,12 @@ class AccountController extends AccountControllerBase with AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator + with AccessTokenService trait AccountControllerBase extends AccountManagementControllerBase { self: AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService - with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator => + with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator + with AccessTokenService => case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String, url: Option[String], fileId: Option[String]) @@ -31,6 +33,8 @@ case class SshKeyForm(title: String, publicKey: String) + case class PersonalTokenForm(note: String) + val newForm = mapping( "userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName))), "password" -> trim(label("Password" , text(required, maxlength(20)))), @@ -54,6 +58,10 @@ "publicKey" -> trim(label("Key" , text(required, validPublicKey))) )(SshKeyForm.apply) + val personalTokenForm = mapping( + "note" -> trim(label("Token", text(required, maxlength(100)))) + )(PersonalTokenForm.apply) + case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String) case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String, clearImage: Boolean) @@ -206,6 +214,40 @@ redirect(s"/${userName}/_ssh") }) + get("/:userName/_application")(oneselfOnly { + val userName = params("userName") + getAccountByUserName(userName).map { x => + var tokens = getAccessTokens(x.userName) + val generatedToken = flash.get("generatedToken") match { + case Some((tokenId:Int, token:String)) => { + val gt = tokens.find(_.accessTokenId == tokenId) + gt.map{ t => + tokens = tokens.filterNot(_ == t) + (t, token) + } + } + case _ => None + } + account.html.application(x, tokens, generatedToken) + } getOrElse NotFound + }) + + post("/:userName/_personalToken", personalTokenForm)(oneselfOnly { form => + val userName = params("userName") + getAccountByUserName(userName).map { x => + val (tokenId, token) = generateAccessToken(userName, form.note) + flash += "generatedToken" -> (tokenId, token) + } + redirect(s"/${userName}/_application") + }) + + get("/:userName/_personalToken/delete/:id")(oneselfOnly { + val userName = params("userName") + val tokenId = params("id").toInt + deleteAccessToken(userName, tokenId) + redirect(s"/${userName}/_application") + }) + get("/register"){ if(context.settings.allowAccountRegistration){ if(context.loginAccount.isDefined){ diff --git a/src/main/scala/model/AccessToken.scala b/src/main/scala/model/AccessToken.scala new file mode 100644 index 0000000..acaad56 --- /dev/null +++ b/src/main/scala/model/AccessToken.scala @@ -0,0 +1,20 @@ +package model + +trait AccessTokenComponent { self: Profile => + import profile.simple._ + lazy val AccessTokens = TableQuery[AccessTokens] + + class AccessTokens(tag: Tag) extends Table[AccessToken](tag, "ACCESS_TOKEN") { + val accessTokenId = column[Int]("ACCESS_TOKEN_ID", O AutoInc) + val userName = column[String]("USER_NAME") + val tokenHash = column[String]("TOKEN_HASH") + val note = column[String]("NOTE") + def * = (accessTokenId, userName, tokenHash, note) <> (AccessToken.tupled, AccessToken.unapply) + } +} +case class AccessToken( + accessTokenId: Int = 0, + userName: String, + tokenHash: String, + note: String +) diff --git a/src/main/scala/model/Profile.scala b/src/main/scala/model/Profile.scala index d7ff17f..4b65ad4 100644 --- a/src/main/scala/model/Profile.scala +++ b/src/main/scala/model/Profile.scala @@ -19,7 +19,8 @@ object Profile extends { val profile = slick.driver.H2Driver -} with AccountComponent +} with AccessTokenComponent + with AccountComponent with ActivityComponent with CollaboratorComponent with CommitCommentComponent diff --git a/src/main/scala/service/AccesTokenService.scala b/src/main/scala/service/AccesTokenService.scala new file mode 100644 index 0000000..916fde1 --- /dev/null +++ b/src/main/scala/service/AccesTokenService.scala @@ -0,0 +1,43 @@ +package service + +import model.Profile._ +import profile.simple._ +import model.AccessToken +import util.StringUtil +import scala.util.Random + +trait AccessTokenService { + + def makeAccessTokenString: String = { + val bytes = new Array[Byte](20) + Random.nextBytes(bytes) + bytes.map("%02x".format(_)).mkString + } + + def tokenToHash(token: String): String = StringUtil.sha1(token) + + /** + * @retuen (TokenId, Token) + */ + def generateAccessToken(userName: String, note: String)(implicit s: Session): (Int, String) = { + var token: String = null + var hash: String = null + do{ + token = makeAccessTokenString + hash = tokenToHash(token) + }while(AccessTokens.filter(_.tokenHash === hash.bind).exists.run) + val newToken = AccessToken( + userName = userName, + note = note, + tokenHash = hash) + val tokenId = (AccessTokens returning AccessTokens.map(_.accessTokenId)) += newToken + (tokenId, token) + } + + def getAccessTokens(userName: String)(implicit s: Session): List[AccessToken] = + AccessTokens.filter(_.userName === userName.bind).sortBy(_.accessTokenId.desc).list + + def deleteAccessToken(userName: String, accessTokenId: Int)(implicit s: Session): Unit = + AccessTokens filter (t => t.userName === userName.bind && t.accessTokenId === accessTokenId) delete + +} diff --git a/src/main/scala/servlet/InitializeListener.scala b/src/main/scala/servlet/InitializeListener.scala index fdc3c48..a4b1e1e 100644 --- a/src/main/scala/servlet/InitializeListener.scala +++ b/src/main/scala/servlet/InitializeListener.scala @@ -19,6 +19,7 @@ * The history of versions. A head of this sequence is the current BitBucket version. */ val versions = Seq( + new Version(2, 9), new Version(2, 8), new Version(2, 7) { override def update(conn: Connection, cl: ClassLoader): Unit = { diff --git a/src/main/twirl/account/application.scala.html b/src/main/twirl/account/application.scala.html new file mode 100644 index 0000000..11c6814 --- /dev/null +++ b/src/main/twirl/account/application.scala.html @@ -0,0 +1,55 @@ +@(account: model.Account, personalTokens: List[model.AccessToken], gneratedToken:Option[(model.AccessToken, String)])(implicit context: app.Context) +@import context._ +@import view.helpers._ +@html.main("Applications"){ +
+
+
+ @menu("applications", settings.ssh) +
+
+
+
Personal access tokens
+
+ @if(personalTokens.isEmpty && gneratedToken.isEmpty){ + No tokens. + }else{ + Tokens you have generated that can be used to access the GitBucket API.
+ } + @gneratedToken.map{ case (token, tokenString) => +
+ Make sure to copy your new personal access token now. You won't be able to see it again! +
+ @helper.html.copy("generated-token-copy", tokenString){ + + } + Delete +
+ } + @personalTokens.zipWithIndex.map { case (token, i) => + @if(i != 0){ +
+ } + @token.note + Delete + } +
+
+
+
+
Generate new token
+
+
+ +
+ +

What's this token for?

+
+ +
+
+
+
+
+
+} diff --git a/src/main/twirl/account/menu.scala.html b/src/main/twirl/account/menu.scala.html index a5d9139..914a92b 100644 --- a/src/main/twirl/account/menu.scala.html +++ b/src/main/twirl/account/menu.scala.html @@ -10,5 +10,8 @@ SSH Keys } + + Applications +