diff --git a/dashboard/src/api.js b/dashboard/src/api.js index 6227ce0..e38377f 100644 --- a/dashboard/src/api.js +++ b/dashboard/src/api.js @@ -97,6 +97,18 @@ function getTokens() { }); } +function updateToken(opts) { + return m.request({ + method: "POST", + url: baseUrl() + "/auth/token", + background: true, + body: opts, + headers: { + authorization: storage.getHeaderToken() + } + }); +} + function deleteToken(tokenId) { return m.request({ method: "DELETE", @@ -244,5 +256,6 @@ export { getUserTags, getUserProjects, getLeaderboards, - getCommitLog + getCommitLog, + updateToken }; diff --git a/dashboard/src/modals/TokenList.js b/dashboard/src/modals/TokenList.js index d7bc56b..a96b543 100644 --- a/dashboard/src/modals/TokenList.js +++ b/dashboard/src/modals/TokenList.js @@ -1,5 +1,6 @@ import m from "mithril"; import $ from "jquery"; +import _ from "lodash"; import * as api from "../api"; import utils from "../utils"; @@ -34,7 +35,7 @@ function renderModal(tokens) { document.body.appendChild(modal); } - availableTokens = tokens; + availableTokens = _.orderBy(tokens, ["tknId"]); m.render(modal, m(Modal)); @@ -122,6 +123,7 @@ const Modal = { "thead", m("tr", [ m("th", { scope: "col" }, "ID"), + m("th", { scope: "col" }, "Name"), m("th", { scope: "col" }, "Last usage"), m("th", { scope: "col" }, "") ]) @@ -129,8 +131,62 @@ const Modal = { m( "tbody", availableTokens.map(t => { + const defaultName = "-"; + return m("tr", [ m("td", { scope: "row" }, t.tknId.substring(0, 6)), + m( + "td", + { + scope: "row", + onclick: function (e) { + const el = e.target; + const input = document.createElement("input"); + input.maxLength = 42; + input.value = el.innerHTML; + + input.onkeydown = function (event) { + if (event.key == "Enter") { + this.blur(); + } + }; + + input.onblur = function () { + el.innerHTML = input.value + ? input.value + : defaultName; + input.replaceWith(el); + + // Update the token name. + if (input.value && input.value !== t.tknId) { + api + .updateToken({ + tokenId: t.tknId, + tokenName: input.value + }) + .then(api.getTokens) + .then(function (tokens) { + renderModal(tokens); + }) + .catch(function (e) { + console.log("Failed to update the token"); + if (e && e.response) { + console.log(e.response); + return; + } + utils.showError( + "Failed to update the token" + ); + }); + } + }; + + el.replaceWith(input); + input.focus(); + } + }, + t.tknName ? t.tknName : defaultName + ), m( "td", t.lastUsage ? utils.formatDate(t.lastUsage) : "Not used" diff --git a/migrations/00002-add-token-name.sql b/migrations/00002-add-token-name.sql new file mode 100644 index 0000000..ea9c288 --- /dev/null +++ b/migrations/00002-add-token-name.sql @@ -0,0 +1,2 @@ +ALTER TABLE auth_tokens ADD token_name TEXT; +ALTER TABLE auth_tokens ADD token_description TEXT; diff --git a/shell.nix b/shell.nix index 133d258..d883e93 100644 --- a/shell.nix +++ b/shell.nix @@ -11,6 +11,7 @@ pkgs.stdenv.mkDerivation { pkgs.hlint pkgs.pgcli pkgs.cabal2nix + pkgs.ormolu ]; shellHook = '' diff --git a/src/Haka/Database.hs b/src/Haka/Database.hs index 5758fa5..a873964 100644 --- a/src/Haka/Database.hs +++ b/src/Haka/Database.hs @@ -41,6 +41,7 @@ import Haka.Types StoredUser (..), TimelineRow (..), TokenData (..), + TokenMetadata (..), ) import qualified Haka.Utils as Utils import qualified Hasql.Pool as HqPool @@ -128,6 +129,9 @@ class (Monad m, MonadThrow m) => Db m where -- | Get total time between the given time ranges. getTotalTimeBetween :: HqPool.Pool -> V.Vector (Text, Text, UTCTime, UTCTime) -> m [Int64] + -- | Update token metadata set by the user. + updateTokenMetadata :: HqPool.Pool -> Text -> TokenMetadata -> m () + instance Db IO where getUser pool token = do res <- HqPool.use pool (Sessions.getUser token) @@ -225,6 +229,9 @@ instance Db IO where -- We return in reverse order because we insert with descending but we sort in ascending. res <- HqPool.use pool (Sessions.getTotalTimeBetween ranges) either (throw . SessionException) (pure . reverse) res + updateTokenMetadata pool user metadata = do + res <- HqPool.use pool (Sessions.updateTokenMetadata user metadata) + either (throw . SessionException) pure res mkTokenData :: Text -> IO TokenData mkTokenData user = do diff --git a/src/Haka/Db/Sessions.hs b/src/Haka/Db/Sessions.hs index cff0bc1..a4f1d87 100644 --- a/src/Haka/Db/Sessions.hs +++ b/src/Haka/Db/Sessions.hs @@ -21,6 +21,7 @@ import Haka.Types StoredUser (..), TimelineRow (..), TokenData (..), + TokenMetadata (..), ) import qualified Haka.Utils as Utils import Hasql.Session (Session, statement) @@ -31,6 +32,9 @@ import PostgreSQL.Binary.Data (UUID) updateTokenUsage :: Text -> Session () updateTokenUsage tkn = statement tkn Statements.updateTokenUsage +updateTokenMetadata :: Text -> TokenMetadata -> Session () +updateTokenMetadata user metadata = statement (tokenId metadata, user, tokenName metadata) Statements.updateTokenMetadata + listApiTokens :: Text -> Session [StoredApiToken] listApiTokens usr = statement usr Statements.listApiTokens diff --git a/src/Haka/Db/Statements.hs b/src/Haka/Db/Statements.hs index 5bfdda8..67d3984 100644 --- a/src/Haka/Db/Statements.hs +++ b/src/Haka/Db/Statements.hs @@ -51,6 +51,19 @@ updateTokenUsage = Statement query params D.noResult True params :: E.Params Text params = E.param (E.nonNullable E.text) +updateTokenMetadata :: Statement (Text, Text, Text) () +updateTokenMetadata = Statement query params D.noResult True + where + query :: ByteString + query = [r| UPDATE auth_tokens SET token_name = $3 WHERE token = $1 AND owner = $2; |] + + params :: E.Params (Text, Text, Text) + params = + contrazip3 + (E.param (E.nonNullable E.text)) + (E.param (E.nonNullable E.text)) + (E.param (E.nonNullable E.text)) + listApiTokens :: Statement Text [StoredApiToken] listApiTokens = Statement query params result True where @@ -58,7 +71,10 @@ listApiTokens = Statement query params result True query = [r| select - token, last_usage::timestamp + token, + last_usage::timestamp, + token_name, + token_description from auth_tokens where @@ -74,6 +90,8 @@ listApiTokens = Statement query params result True StoredApiToken <$> (D.column . D.nonNullable) D.text <*> (D.column . D.nullable) D.timestamptz + <*> (D.column . D.nullable) D.text + <*> (D.column . D.nullable) D.text result :: D.Result [StoredApiToken] result = D.rowList storedApiToken diff --git a/src/Haka/Handlers/Authentication.hs b/src/Haka/Handlers/Authentication.hs index 35dbaf2..c5180d9 100644 --- a/src/Haka/Handlers/Authentication.hs +++ b/src/Haka/Handlers/Authentication.hs @@ -21,6 +21,7 @@ import Haka.Types ( ApiToken, StoredApiToken, TokenData (..), + TokenMetadata (..), ) import Haka.Utils (getRefreshToken) import Katip @@ -104,6 +105,13 @@ type DeleteToken = :> Header "Authorization" ApiToken :> DeleteNoContent +type UpdateToken = + "auth" + :> "token" + :> Header "Authorization" ApiToken + :> ReqBody '[JSON] TokenMetadata + :> PostNoContent + type API = Login :<|> RefreshToken @@ -112,6 +120,7 @@ type API = :<|> DeleteToken :<|> Logout :<|> Register + :<|> UpdateToken getStoredApiTokensHandler :: Maybe ApiToken -> AppM [StoredApiToken] getStoredApiTokensHandler Nothing = throw Err.missingAuthError @@ -235,14 +244,32 @@ createAPITokenHandler (Just tkn) = deleteTokenHandler :: Text -> Maybe ApiToken -> AppM NoContent deleteTokenHandler _ Nothing = throw Err.missingAuthError -deleteTokenHandler tokenId (Just tkn) = do +deleteTokenHandler tknId (Just tkn) = do dbPool <- asks pool - res <- try $ liftIO $ Db.deleteApiToken dbPool tkn tokenId + res <- try $ liftIO $ Db.deleteApiToken dbPool tkn tknId _ <- either Err.logError pure res return NoContent +updateTokenHandler :: Maybe ApiToken -> TokenMetadata -> AppM NoContent +updateTokenHandler Nothing _ = throw Err.missingAuthError +updateTokenHandler (Just apiTkn) metadata = do + dbPool <- asks pool + + userRes <- try $ liftIO $ Db.getUser dbPool apiTkn + + user <- either Err.logError pure userRes + + case user of + Nothing -> throw Err.invalidTokenError + Just user' -> do + res <- try $ liftIO $ Db.updateTokenMetadata dbPool user' metadata + + _ <- either Err.logError pure res + + return NoContent + server settings = loginHandler :<|> refreshTokenHandler @@ -251,3 +278,4 @@ server settings = :<|> deleteTokenHandler :<|> logoutHandler :<|> registerHandler settings + :<|> updateTokenHandler diff --git a/src/Haka/Types.hs b/src/Haka/Types.hs index fb0ea2a..516b0fa 100644 --- a/src/Haka/Types.hs +++ b/src/Haka/Types.hs @@ -21,6 +21,7 @@ module Haka.Types ProjectStatRow (..), TokenData (..), LeaderboardRow (..), + TokenMetadata (..), ) where @@ -43,7 +44,11 @@ data StoredApiToken = StoredApiToken { -- Some characters to identify a token. tknId :: Text, -- When the token was used. - lastUsage :: Maybe UTCTime + lastUsage :: Maybe UTCTime, + -- An optional name given to the token. + tknName :: Maybe Text, + -- An optiona description given to the token. + tknDesc :: Maybe Text } deriving (Show, Generic) @@ -266,3 +271,13 @@ data LeaderboardRow = LeaderboardRow leadTotalSeconds :: Int64 } deriving (Eq, Show, Generic) + +data TokenMetadata = TokenMetadata + { tokenName :: Text, + tokenId :: Text + } + deriving (Show, Generic) + +instance ToJSON TokenMetadata + +instance FromJSON TokenMetadata