diff --git a/fda-authentication-service/rest-service/src/main/java/at/tuwien/endpoints/TokenEndpoint.java b/fda-authentication-service/rest-service/src/main/java/at/tuwien/endpoints/TokenEndpoint.java index 7a7bea971b29ef7dab1303f03db4da485c74ec81..a9218dd66a5b08ac1016264163663a75d0024eb5 100644 --- a/fda-authentication-service/rest-service/src/main/java/at/tuwien/endpoints/TokenEndpoint.java +++ b/fda-authentication-service/rest-service/src/main/java/at/tuwien/endpoints/TokenEndpoint.java @@ -84,16 +84,17 @@ public class TokenEndpoint { .body(dto); } - @DeleteMapping("/{hash}") + @DeleteMapping("/{id}") @Transactional @Timed(value = "token.delete", description = "Time needed to delete the developer tokens") @Operation(summary = "Delete developer token", security = @SecurityRequirement(name = "bearerAuth")) - public void delete(@NotNull @PathVariable("hash") String hash, + public void delete(@NotNull @PathVariable("id") Long id, @NotNull Principal principal) throws TokenNotFoundException, UserNotFoundException { - log.debug("endpoint delete developer token, hash={}, principal={}", hash, principal); - final Token token = tokenService.findOne(hash); + log.debug("endpoint delete developer token, id={}, principal={}", id, principal); + final Token token = tokenService.findOne(id); log.trace("found token {}", token); tokenService.delete(token.getTokenHash(), principal); + log.info("Deleted token with id {}", id); } } \ No newline at end of file diff --git a/fda-authentication-service/services/src/main/java/at/tuwien/service/TokenService.java b/fda-authentication-service/services/src/main/java/at/tuwien/service/TokenService.java index 4915baaf36e2a7e3b2d1845ad94ccf7047d91483..caeeb8d41f9532c40b167a6758c8f5e542355d12 100644 --- a/fda-authentication-service/services/src/main/java/at/tuwien/service/TokenService.java +++ b/fda-authentication-service/services/src/main/java/at/tuwien/service/TokenService.java @@ -42,6 +42,15 @@ public interface TokenService { */ Token findOne(String tokenHash) throws TokenNotFoundException; + /** + * Finds a token by id. + * + * @param id The token id. + * @return The token, if successful. + * @throws TokenNotFoundException The token was not found in the metadata database. + */ + Token findOne(Long id) throws TokenNotFoundException; + /** * Deletes a developer token in the metadata database by hash and user principal. * diff --git a/fda-authentication-service/services/src/main/java/at/tuwien/service/impl/TokenServiceImpl.java b/fda-authentication-service/services/src/main/java/at/tuwien/service/impl/TokenServiceImpl.java index 33456b3299ffb8c6967251c8ff2a233f14b1669d..534e3727dcf3fc97810770710fd9451956dd7a4f 100644 --- a/fda-authentication-service/services/src/main/java/at/tuwien/service/impl/TokenServiceImpl.java +++ b/fda-authentication-service/services/src/main/java/at/tuwien/service/impl/TokenServiceImpl.java @@ -81,6 +81,17 @@ public class TokenServiceImpl implements TokenService { return optional.get(); } + @Override + @Transactional(readOnly = true) + public Token findOne(Long id) throws TokenNotFoundException { + final Optional<Token> optional = tokenRepository.findById(id); + if (optional.isEmpty()) { + log.error("Failed to find token with id {}", id); + throw new TokenNotFoundException("Failed to find token"); + } + return optional.get(); + } + @Override @Transactional public void delete(String tokenHash, Principal principal) throws TokenNotFoundException, UserNotFoundException { diff --git a/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/Token.java b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/Token.java index 17811e2efec89e48b290977818db22cb51212c59..44ee9ba94412e6ef1405af32223414f028d3d0f7 100644 --- a/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/Token.java +++ b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/Token.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import org.hibernate.annotations.GenericGenerator; import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; diff --git a/fda-ui/layouts/default.vue b/fda-ui/layouts/default.vue index 63c821f9eed411adbe629b3af998f1f07b0c4cd0..919a3359c3cc76f21e02575667be36cb98e5ac7c 100644 --- a/fda-ui/layouts/default.vue +++ b/fda-ui/layouts/default.vue @@ -263,8 +263,8 @@ export default { } }, login () { - let redirect = ![undefined ,'/', '/login'].includes(this.$router.currentRoute.path) - this.$router.push({ path: '/login', query: redirect ? { redirect: this.$router.currentRoute.path } : {}}) + const redirect = ![undefined, '/', '/login'].includes(this.$router.currentRoute.path) + this.$router.push({ path: '/login', query: redirect ? { redirect: this.$router.currentRoute.path } : {} }) }, navigate (item) { this.$router.push(this.metadata(item).link) diff --git a/fda-ui/nuxt.config.js b/fda-ui/nuxt.config.js index 08afe5b2e157211bc6a6352dd5343badea42db39..902ab4c355e844468c25af4a737178b917ab9560 100644 --- a/fda-ui/nuxt.config.js +++ b/fda-ui/nuxt.config.js @@ -77,7 +77,8 @@ export default { sharedFilesystem: process.env.SHARED_FILESYSTEM || '/tmp', version: process.env.VERSION || 'latest', logo: process.env.LOGO || '/logo.png', - mailVerify: process.env.MAIL_VERIFY || false + mailVerify: process.env.MAIL_VERIFY || false, + tokenMax: process.env.TOKEN_MAX || 5 }, proxy: { diff --git a/fda-ui/pages/login.vue b/fda-ui/pages/login.vue index b9b82425b216eb84ac34d9bedfbdef1765e72778..caf8bf208eb8768656cd7a62bccc8f5ea3af974d 100644 --- a/fda-ui/pages/login.vue +++ b/fda-ui/pages/login.vue @@ -96,7 +96,7 @@ export default { delete user.token this.$store.commit('SET_USER', user) this.$toast.success('Welcome back!') - this.$router.push(this.$route.query.redirect ? this.$route.query.redirect : '/container') + this.$router.push(this.$route.query.redirect ? this.$route.query.redirect : '/container') } catch (err) { if (err.response !== undefined && err.response.status !== undefined) { if (err.response.status === 418) { diff --git a/fda-ui/pages/user/developer.vue b/fda-ui/pages/user/developer.vue index bdc5f38bfe235be6489c3348793df1be5390645f..9dc9bf35fade6bf5784ef4116e9b62a69c0646e8 100644 --- a/fda-ui/pages/user/developer.vue +++ b/fda-ui/pages/user/developer.vue @@ -9,9 +9,10 @@ <v-card-text> <v-list-item v-for="(item, i) in tokens" :key="i" three-line> <v-list-item-content> - <v-list-item-title>sha256:{{ item.token_hash }}</v-list-item-title> - <v-list-item-subtitle v-if="!item.token"> - Created on {{ format(item.created) }}, Valid until: {{ format(item.expires) }}</v-list-item-subtitle> + <v-list-item-title :class="tokenClass(item)">sha256:{{ item.token_hash }}</v-list-item-title> + <v-list-item-subtitle v-if="!item.token" :class="tokenClass(item)"> + Last used: <span v-if="item.last_used">{{ format(item.last_used) }}</span><span v-if="!item.last_used">Never</span> — valid until: {{ format(item.expires) }} + </v-list-item-subtitle> <v-list-item-subtitle v-if="item.token"> <v-text-field v-model="item.token" @@ -23,12 +24,12 @@ @click:append-outer="copy(item)" /> </v-list-item-subtitle> <v-list-item-subtitle v-if="!item.token"> - <a @click="revokeToken(item.token_hash)">Revoke Token</a> + <a @click="revokeToken(item.id)">Revoke Token</a> </v-list-item-subtitle> </v-list-item-content> </v-list-item> - <v-btn class="mt-4" x-small @click="mintToken"> - Mint Token + <v-btn :disabled="tokens.length >= tokenMax" class="mt-4" color="secondary" small @click="mintToken"> + Create Token </v-btn> </v-card-text> </v-card> @@ -59,6 +60,9 @@ export default { return { headers: { Authorization: `Bearer ${this.token}` } } + }, + tokenMax () { + return this.$config.tokenMax } }, mounted () { @@ -74,6 +78,9 @@ export default { format (timestamp) { return formatTimestamp(timestamp) }, + tokenClass (token) { + return token.last_used ? '' : 'token-not_used' + }, async loadTokens () { this.loading = true try { @@ -102,10 +109,10 @@ export default { } this.loading = false }, - async revokeToken (hash) { + async revokeToken (id) { this.loading = true try { - await this.$axios.delete(`/api/user/token/${hash}`, this.config) + await this.$axios.delete(`/api/user/token/${id}`, this.config) await this.loadTokens() } catch (err) { this.$toast.error('Could not delete token') @@ -115,3 +122,8 @@ export default { } } </script> +<style> +.token-not_used { + opacity: 0.4; +} +</style>