api key creation vue component

This commit is contained in:
Jake Potrebic 2020-10-07 15:52:02 -07:00 committed by MiniDigger
parent 6db5a2752b
commit b5ec1dbc69
11 changed files with 299 additions and 276 deletions

View File

@ -0,0 +1,112 @@
<template>
<div v-show="error" id="keyAlert" class="alert alert-danger" role="alert" v-text="error"></div>
<div class="row">
<div class="col-md-6">
<h2 v-text="$t('user.apiKeys.createNew')"></h2>
<div class="row">
<div v-for="(chunk, index) in possiblePermissions" :key="`perm-chunk-${index}`" class="col-md-6">
<div v-for="perm in chunk" :key="perm.value" class="checkbox">
<label> <input v-model="form.perms" type="checkbox" :value="perm.value" /> {{ perm.name }} </label>
</div>
</div>
</div>
<div class="input-group">
<div class="input-group-prepend">
<label for="new-key-name-input" class="input-group-text" v-text="$t('user.apiKeys.keyName')"></label>
</div>
<input v-model.trim="form.name" type="text" class="form-control" id="new-key-name-input" />
<div class="input-group-append">
<button
type="button"
class="btn input-group-btn btn-primary"
:disabled="!form.perms.length || !form.name"
v-text="$t('user.apiKeys.createKeyBtn')"
@click="createKey"
></button>
</div>
</div>
</div>
<div class="col-md-6">
<h2 v-text="$t('user.apiKeys.existingKeys')"></h2>
<table class="table">
<thead>
<tr>
<th v-text="$t('user.apiKeys.keyName')"></th>
<th v-text="$t('user.apiKeys.keyToken')"></th>
<th v-text="$t('user.apiKeys.keyIdentifier')"></th>
<th v-text="$t('user.apiKeys.keyPermissions')"></th>
<th v-text="$t('user.apiKeys.keyDeleteColumn')"></th>
</tr>
</thead>
<tbody>
<tr v-for="key in existingKeys" :key="key.id">
<th v-text="key.name"></th>
<th v-text="key.token"></th>
<th v-text="key.tokenIdentifier"></th>
<th v-text="key.namedRawPermissions.join(', ')"></th>
<th><button class="btn btn-danger" v-text="$t('user.apiKeys.keyDeleteButton')" @click="deleteKey(key)"></button></th>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script>
import { chunk, remove } from 'lodash-es';
import { API } from '@/api';
export default {
name: 'ApiKeyManagement',
data() {
return {
form: {
perms: [],
name: '',
},
error: null,
possiblePermissions: chunk(window.API_KEY_PERMISSIONS, window.API_KEY_PERMISSIONS.length / 2),
existingKeys: window.EXISTING_KEYS,
};
},
methods: {
createKey() {
this.error = null;
if (this.form.perms.length === 0) {
this.error = this.$t('user.apiKeys.error.noPermsSet');
return;
}
if (!this.form.name || !this.form.name.trim()) {
this.error = this.$t('user.apiKeys.error.noNameSet');
return;
}
if (this.form.name.length > 255) {
this.error = this.$t('user.apiKeys.error.tooLongName');
return;
}
if (this.existingKeys.find((key) => key.name === this.form.name)) {
this.error = this.$t('user.apiKeys.error.nameAlreadyUsed');
return;
}
API.request('keys', 'POST', {
permissions: this.form.perms,
name: this.form.name,
}).then((newKey) => {
this.error = null;
this.existingKeys.push({
name: this.form.name,
token: newKey.key,
namedRawPermissions: newKey.perms,
});
this.form.name = '';
this.form.perms = [];
});
},
deleteKey(key) {
API.request(`keys?name=${key.name}`, 'DELETE').then(() => {
remove(this.existingKeys, (k) => k.name === key.name);
});
},
},
};
</script>

View File

@ -81,7 +81,7 @@
@prev="$emit('prev-page')"
></Pagination>
</div>
<div v-else class="list-group-item empty-project-list">
<div v-else class="list-group-item empty-project-list d-flex align-items-center">
<i class="far fa-2x fa-sad-tear"></i>
<span>Oops! No projects found...</span>
</div>

View File

@ -0,0 +1,6 @@
import { createApp } from 'vue';
import ApiKeyManagement from '@/ApiKeyManagement';
import { setupI18n } from '@/plugins/i18n';
const i18n = setupI18n();
createApp(ApiKeyManagement).use(i18n).mount('#api-key-management');

View File

@ -1,95 +0,0 @@
import $ from 'jquery';
import { apiV2Request } from '@/js/apiRequests';
//=====> EXTERNAL CONSTANTS
var NO_PERMS_SET = window.NO_PERMS_SET;
var NO_NAME_SET = window.NO_NAME_SET;
var TOO_LONG_NAME = window.TOO_LONG_NAME;
var NAMED_USED = window.NAMED_USED;
var DELETE_KEY = window.DELETE_KEY;
//=====> HELPER FUNCTIONS
function deleteKey(name, row) {
return function () {
apiV2Request('keys?name=' + name, 'DELETE').then(function () {
row.remove();
});
};
}
function showError(error) {
var alert = $('#keyAlert');
alert.text(error);
alert.show();
}
//=====> DOCUMENT READY
$(function () {
$('.api-key-row').each(function () {
var row = $(this);
var name = row.find('.api-key-name').text();
row.find('.api-key-row-delete-button').click(deleteKey(name, row));
});
$('#button-create-new-key').click(function () {
var checked = [];
$('#api-create-key-form')
.find('input[type=checkbox]')
.filter("input[id^='perm.']")
.filter(':checked')
.each(function () {
checked.push($(this).attr('id').substr('perm.'.length));
});
var name = $('#keyName').val();
if (checked.length === 0) {
showError(NO_PERMS_SET);
} else {
var hasName = name.length !== 0;
if (!hasName) {
showError(NO_NAME_SET);
return;
}
if (name.length > 255) {
showError(TOO_LONG_NAME);
return;
}
var nameTaken = $('.api-key-name:contains(' + name + ')').length;
if (nameTaken !== 0) {
showError(NAMED_USED);
return;
}
var data = {
permissions: checked,
name: name,
};
apiV2Request('keys', 'POST', data).then(function (newKey) {
$('#keyAlert').hide();
var namedPerms = '';
for (let perm of checked) {
namedPerms += perm + ', ';
}
namedPerms.substr(0, namedPerms.length - 2);
var row = $('<tr>');
var token = newKey.key;
row.append($('<th>').addClass('api-key-name').text(name));
row.append($('<th>').text(token));
row.append($('<th>'));
row.append($('<th>').text(namedPerms));
row.append($('<th>').append($('<button>').addClass('btn btn-danger api-key-row-delete-button').text(DELETE_KEY).click(deleteKey(name, row))));
$('#api-key-rows:last-child').append(row);
});
}
});
});

View File

@ -39,21 +39,33 @@
span, select { margin-left: auto; }
}
.username {
svg {
font-size: 0.6em;
font-weight: normal;
padding: 3px;
.user-title {
display: flex;
align-items: baseline;
.user-actions {
float: left;
margin-left: 0.5em;
& > a:hover {
text-decoration: none;
}
svg {
font-size: 1.5em;
font-weight: normal;
padding: 3px;
}
.action-lock-account:hover, .action-api svg:hover, .user-settings svg:hover {
background-color: gray;
transition: all 0.5s ease;
}
svg { cursor: pointer; }
.action-api, .user-settings { color: #333; }
}
.action-lock-account:hover, .action-api svg:hover, .user-settings svg:hover {
background-color: gray;
transition: all 0.5s ease;
}
svg { cursor: pointer; }
.action-api, .user-settings { color: #333; }
}
.organization-avatar {
@ -90,7 +102,6 @@
.user-avatar { margin-right: 20px; }
}
.user-badge > h1 { float: left; }
.float-right { @include size(20%, 100%); }
.user-roles li { display: inline; }

View File

@ -1,11 +1,11 @@
package io.papermc.hangar.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.papermc.hangar.config.hangar.HangarConfig;
import io.papermc.hangar.db.customtypes.LoggedActionType;
import io.papermc.hangar.db.dao.HangarDao;
import io.papermc.hangar.db.dao.UserDao;
import io.papermc.hangar.db.model.NotificationsTable;
import io.papermc.hangar.db.model.OrganizationsTable;
import io.papermc.hangar.db.model.UserSessionsTable;
import io.papermc.hangar.db.model.UsersTable;
import io.papermc.hangar.exceptions.HangarException;
@ -14,6 +14,8 @@ import io.papermc.hangar.model.NamedPermission;
import io.papermc.hangar.model.NotificationFilter;
import io.papermc.hangar.model.Prompt;
import io.papermc.hangar.model.viewhelpers.InviteSubject;
import io.papermc.hangar.model.viewhelpers.OrganizationData;
import io.papermc.hangar.model.viewhelpers.ScopedOrganizationData;
import io.papermc.hangar.model.viewhelpers.UserData;
import io.papermc.hangar.model.viewhelpers.UserRole;
import io.papermc.hangar.security.annotations.GlobalPermission;
@ -54,13 +56,16 @@ import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@Controller
public class UsersController extends HangarController {
private final ObjectMapper mapper;
private final HangarConfig hangarConfig;
private final AuthenticationService authenticationService;
private final UserService userService;
@ -81,7 +86,8 @@ public class UsersController extends HangarController {
@Autowired
public UsersController(HangarConfig hangarConfig, AuthenticationService authenticationService, UserService userService, OrgService orgService, RoleService roleService, ApiKeyService apiKeyService, PermissionService permissionService, SessionService sessionService, NotificationService notificationService, SsoService ssoService, UserActionLogService userActionLogService, HangarDao<UserDao> userDao, SitemapService sitemapService, HttpServletRequest request, HttpServletResponse response, Supplier<UsersTable> usersTable) {
public UsersController(ObjectMapper mapper, HangarConfig hangarConfig, AuthenticationService authenticationService, UserService userService, OrgService orgService, RoleService roleService, ApiKeyService apiKeyService, PermissionService permissionService, SessionService sessionService, NotificationService notificationService, SsoService ssoService, UserActionLogService userActionLogService, HangarDao<UserDao> userDao, SitemapService sitemapService, HttpServletRequest request, HttpServletResponse response, Supplier<UsersTable> usersTable) {
this.mapper = mapper;
this.hangarConfig = hangarConfig;
this.authenticationService = authenticationService;
this.userService = userService;
@ -238,12 +244,12 @@ public class UsersController extends HangarController {
@GetMapping("/{user}")
public ModelAndView showProjects(@PathVariable String user) {
ModelAndView mav = new ModelAndView("users/projects");
OrganizationsTable organizationsTable = orgService.getOrganization(user);
UserData userData = userService.getUserData(user);
Optional<OrganizationData> orgData = Optional.ofNullable(orgService.getOrganizationData(userData.getUser()));
Optional<ScopedOrganizationData> scopedOrgData = orgData.map(organizationData -> orgService.getScopedOrganizationData(organizationData.getOrg()));
mav.addObject("u", userService.getUserData(user));
if (organizationsTable != null) {
mav.addObject("o", orgService.getOrganizationData(organizationsTable, null));
mav.addObject("so", orgService.getScopedOrganizationData(organizationsTable));
}
mav.addObject("o", orgData);
mav.addObject("so", scopedOrgData);
return fillModel(mav);
}
@ -253,10 +259,15 @@ public class UsersController extends HangarController {
public ModelAndView editApiKeys(@PathVariable String user) {
ModelAndView mav = new ModelAndView("users/apiKeys");
UserData userData = userService.getUserData(user);
Optional<OrganizationData> orgData = Optional.ofNullable(orgService.getOrganizationData(userData.getUser()));
Optional<ScopedOrganizationData> scopedOrgData = orgData.map(organizationData -> orgService.getScopedOrganizationData(organizationData.getOrg()));
long userId = userData.getUser().getId();
mav.addObject("u", userData);
mav.addObject("o", orgData);
mav.addObject("so", scopedOrgData);
mav.addObject("keys", apiKeyService.getKeys(userId));
mav.addObject("perms", permissionService.getPossibleOrganizationPermissions(userId).add(permissionService.getPossibleProjectPermissions(userId)).add(userData.getUserPerm()).toNamed());
List<NamedPermission> perms = permissionService.getPossibleOrganizationPermissions(userId).add(permissionService.getPossibleProjectPermissions(userId)).add(userData.getUserPerm()).toNamed();
mav.addObject("perms", perms.stream().map(perm -> mapper.createObjectNode().put("value", perm.toString()).put("name", perm.getFrontendName())).collect(Collectors.toList()));
return fillModel(mav);
}

View File

@ -26,8 +26,9 @@ public interface ApiKeyDao {
@SqlUpdate("DELETE FROM api_keys k WHERE k.name = :keyName AND k.owner_id = :ownerId")
int delete(String keyName, long ownerId);
// Frontend key request, only show non-private info
@RegisterBeanMapper(ApiKey.class)
@SqlQuery("SELECT *, raw_key_permissions::BIGINT perm_value FROM api_keys WHERE owner_id = :ownerId")
@SqlQuery("SELECT id, name, token_identifier, raw_key_permissions::BIGINT perm_value FROM api_keys WHERE owner_id = :ownerId")
List<ApiKey> getByOwner(long ownerId);
@SqlQuery("SELECT *, raw_key_permissions::BIGINT perm_value FROM api_keys WHERE name = :keyName AND owner_id = :ownerId")

View File

@ -1,18 +1,56 @@
package io.papermc.hangar.model.viewhelpers;
import io.papermc.hangar.db.model.ApiKeysTable;
import io.papermc.hangar.model.NamedPermission;
import io.papermc.hangar.model.Permission;
import org.jdbi.v3.core.mapper.Nested;
import java.util.Collection;
public class ApiKey extends ApiKeysTable {
public class ApiKey {
private long id;
private String name;
private String tokenIdentifier;
private Permission rawKeyPermissions;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getTokenIdentifier() {
return tokenIdentifier;
}
public void setTokenIdentifier(String tokenIdentifier) {
this.tokenIdentifier = tokenIdentifier;
}
public Permission getRawKeyPermissions() {
return rawKeyPermissions;
}
@Nested("perm")
public void setRawKeyPermissions(Permission rawKeyPermissions) {
this.rawKeyPermissions = rawKeyPermissions;
}
public boolean getIsSubKey(Permission perm) {
return this.getRawKeyPermissions().has(perm);
return rawKeyPermissions.has(perm);
}
public Collection<NamedPermission> getNamedRawPermissions() {
return this.getRawKeyPermissions().toNamed();
return rawKeyPermissions.toNamed();
}
}

View File

@ -2,73 +2,15 @@
<#import "*/utils/hangar.ftlh" as hangar />
<#import "*/users/view.ftlh" as userView />
<#--@(u: UserData, o: Option[(OrganizationData, ScopedOrganizationData)], keys: Seq[ApiKey], perms: Seq[NamedPermission])(implicit messages: Messages, flash: Flash, request: OreRequest[_], config: OreConfig, assetsFinder: AssetsFinder)-->
<#assign scriptsVar>
<script <#--@CSPNonce.attr-->>
window.NO_PERMS_SET = '<@spring.message "user.apiKeys.error.noPermsSet" />';
window.NO_NAME_SET = '<@spring.message "user.apiKeys.error.noNameSet" />';
window.TOO_LONG_NAME = '<@spring.message "user.apiKeys.error.tooLongName" />';
window.NAMED_USED = '<@spring.message "user.apiKeys.error.nameAlreadyUsed" />';
window.DELETE_KEY = '<@spring.message "user.apiKeys.keyDeleteButton" />';
<script nonce="${nonce}">
window.API_KEY_PERMISSIONS = ${mapper.valueToTree(perms)};
window.EXISTING_KEYS = ${mapper.valueToTree(keys)};
</script>
<script type="text/javascript" src="<@hangar.url "js/apiKeysManagement.js" />"></script>
<script type="text/javascript" src="<@hangar.url "js/api-key-management.js" />"></script>
</#assign>
<#assign NamedPermission=@helper["io.papermc.hangar.model.NamedPermission"] />
<@userView.view u=u o=o additionalScripts=scriptsVar>
<div id="keyAlert" class="alert alert-danger" role="alert" style="display: none;"></div>
<div class="row">
<div class="col-md-6">
<h2><@spring.message "user.apiKeys.createNew" /></h2>
<div id="api-create-key-form">
<div class="row">
<#list perms?chunk( (perms?size/2)?int ) as chunk>
<div class="col-md-6">
<#list chunk as perm>
<div class="checkbox">
<label>
<input type="checkbox" id="perm.${perm.name()}"> ${perm.name()}
</label>
</div>
</#list>
</div>
</#list>
</div>
<div class="form-group">
<label for="keyName"><@spring.message "user.apiKeys.keyName" />:</label>
<input type="text" class="form-control" id="keyName">
</div>
<button id="button-create-new-key" class="btn btn-default"><@spring.message "user.apiKeys.createKeyBtn" /></button>
</div>
</div>
<div class="col-md-6">
<h2><@spring.message "user.apiKeys.existingKeys" /></h2>
<table class="table">
<thead>
<tr>
<th><@spring.message "user.apiKeys.keyName" /></th>
<th><@spring.message "user.apiKeys.keyToken" /></th>
<th><@spring.message "user.apiKeys.keyIdentifier" /></th>
<th><@spring.message "user.apiKeys.keyPermissions" /></th>
<th><@spring.message "user.apiKeys.keyDeleteColumn" /></th>
</tr>
</thead>
<tbody id="api-key-rows">
<#list keys as key>
<tr class="api-key-row">
<th class="api-key-name">${key.name}</th>
<th></th>
<th>${key.tokenIdentifier}</th>
<th>${key.namedRawPermissions?map(np -> np.name())?join(", ")}</th>
<th><button class="btn btn-danger api-key-row-delete-button"><@spring.message "user.apiKeys.keyDeleteButton" /></button></th>
</tr>
</#list>
</tbody>
</table>
</div>
</div>
<@userView.view u=u o=o so=so additionalScripts=scriptsVar>
<div id="api-key-management"></div>
</@userView.view>

View File

@ -3,45 +3,36 @@
<#import "*/users/view.ftlh" as users />
<#import "*/users/memberList.ftlh" as memberList />
<#--@import controllers.Routes.IO_PAPERMC_HANGAR_MODEL__PERMISSION.getRouteUrl(.Requests.OreRequest
@import models.viewhelper.{OrganizationData, ScopedOrganizationData, UserData}
@import ore.OreConfig
@import ore.permission.Permission
@import ore.permission.role.Role
@import util.syntax._
@import views.html.utils.userAvatar
@(u: UserData, o: Option[(OrganizationData, ScopedOrganizationData)])(
implicit messages: Messages, flash: Flash, request: OreRequest[_], config: OreConfig, assetsFinder: AssetsFinder)-->
<#assign Permission=@helper["io.papermc.hangar.model.Permission"] />
<#assign Role=@helper["io.papermc.hangar.model.Role"] />
<#function canEditOrgMembers>
<#return u.isOrga() && o?? && so.permissions.has(Permission.ManageOrganizationMembers)>
<#return u.isOrga() && o.present && so.get().permissions.has(Permission.ManageOrganizationMembers)>
</#function>
<#assign scriptsVar>
<script type="text/javascript" src="<@hangar.url "js/user-profile.js" />"></script>
<#if u.isOrga() && o?? && canEditOrgMembers()>
<#if u.isOrga() && o.present && canEditOrgMembers()>
<script type="text/javascript" src="<@hangar.url "js/orgInvites.js" />"></script>
</#if>
<script type="text/javascript" src="<@hangar.url "js/userSearch.js" />"></script>
<script type="text/javascript" src="<@hangar.url "js/memberList.js" />"></script>
</#assign>
<@users.view u=u o=o additionalScripts=scriptsVar>
<@users.view u=u o=o so=so additionalScripts=scriptsVar>
<div class="row">
<div class="col-md-8">
<div id="user-profile"></div>
</div>
<div class="col-md-4">
<#if u.isOrga() && o?? && canEditOrgMembers()>
<#if u.isOrga() && o.present && canEditOrgMembers()>
<div class="card-user-info card">
<div class="card-header">
<h3 class="card-title"><@spring.message "project.manager" /></h3>
</div>
<table class="table card-body">
<tbody>
<#list o.projectRoles as role, project>
<#list o.get().projectRoles as role, project>
<tr>
<td>
<a href="${Routes.PROJECTS_SHOW.getRouteUrl(project.ownerName, project.slug)}">${project.ownerName}/${project.slug}</a>
@ -132,9 +123,9 @@
</div>
<#else>
<#assign orgData=o> <#-- todo fix scopeddata -->
<@memberList.memberList project=orgData editable=true perms=so.permissions
saveCall=Routes.ORG_UPDATE_MEMBERS.getRouteUrl(orgData.org.name)
removeCall=Routes.ORG_REMOVE_MEMBER.getRouteUrl(orgData.org.name) />
<@memberList.memberList project=orgData.get() editable=true perms=so.get().permissions
saveCall=Routes.ORG_UPDATE_MEMBERS.getRouteUrl(orgData.get().org.name)
removeCall=Routes.ORG_REMOVE_MEMBER.getRouteUrl(orgData.get().org.name) />
</#if>
</div>
</div>

View File

@ -8,28 +8,34 @@
<#import "*/utils/csrf.ftlh" as csrf>
<#import "*/utils/prompt.ftlh" as promptView>
<#-- @ftlvariable name="u" type="io.papermc.hangar.model.viewhelpers.UserData" -->
<#-- @ftlvariable name="Permission" type="io.papermc.hangar.model.Permission" -->
<#assign Permission=@helper["io.papermc.hangar.model.Permission"]>
<#function canEditOrgSettings u o={}>
<#return u.isOrga() && o?? && so.permissions.has(Permission.EditOrganizationSettings)>
<#function canEditOrgSettings u o so>
<#-- @ftlvariable name="u" type="io.papermc.hangar.model.viewhelpers.UserData" -->
<#-- @ftlvariable name="o" type="java.util.Optional<io.papermc.hangar.model.viewhelpers.OrganizationData>" -->
<#-- @ftlvariable name="so" type="java.util.Optional<io.papermc.hangar.model.viewhelpers.ScopedOrganizationData>" -->
<#-- @ftlvariable name="Permission" type="io.papermc.hangar.model.Permission" -->
<#assign Permission=@helper["io.papermc.hangar.model.Permission"]>
<#return u.isOrga() && o.present && so.get().permissions.has(Permission.EditOrganizationSettings)>
</#function>
<#macro view u o={} additionalScripts="">
<#-- @ftlvariable name="u" type="io.papermc.hangar.model.viewhelpers.UserData" -->
<#macro view u o so additionalScripts="">
<#-- @ftlvariable name="u" type="io.papermc.hangar.model.viewhelpers.UserData" -->
<#-- @ftlvariable name="o" type="java.util.Optional<io.papermc.hangar.model.viewhelpers.OrganizationData>" -->
<#-- @ftlvariable name="so" type="java.util.Optional<io.papermc.hangar.model.viewhelpers.ScopedOrganizationData>" -->
<#-- @ftlvariable name="Permission" type="io.papermc.hangar.model.Permission" -->
<#assign Permission=@helper["io.papermc.hangar.model.Permission"]>
<#assign scriptsVar>
<script <#-- @CSPNonce.attr -->>
window.USERNAME ='${u.user.name}';
window.NO_ACTION_MESSAGE = {};
window.CATEGORY_TITLE = {};
window.CATEGORY_ICON = {};
<#assign Category=@helper["io.papermc.hangar.model.Category"]>
<#list Category.values() as category>
window.CATEGORY_TITLE['${category.apiName}'] = '${category.title}';
window.CATEGORY_ICON['${category.apiName}'] = '${category.icon}';
</#list>
window.NO_ACTION_MESSAGE.starred = '<@spring.messageArgs code="user.noStars" args=[u.user.name] />';
window.NO_ACTION_MESSAGE.watching = '<@spring.messageArgs code="user.noWatching" args=[u.user.name] />';
window.USERNAME = '${u.user.name}';
window.NO_ACTION_MESSAGE = {};
window.CATEGORY_TITLE = {};
window.CATEGORY_ICON = {};
<#assign Category=@helper["io.papermc.hangar.model.Category"]>
<#list Category.values() as category>
window.CATEGORY_TITLE['${category.apiName}'] = '${category.title}';
window.CATEGORY_ICON['${category.apiName}'] = '${category.icon}';
</#list>
window.NO_ACTION_MESSAGE.starred = '<@spring.messageArgs code="user.noStars" args=[u.user.name] />';
window.NO_ACTION_MESSAGE.watching = '<@spring.messageArgs code="user.noWatching" args=[u.user.name] />';
</script>
<script type="text/javascript" src="<@hangar.url "js/userPage.js" />"></script>
${additionalScripts}
@ -44,81 +50,80 @@
<strong>Success!</strong> <span class="success"></span>
</div>
<!-- Header -->
<!-- Header -->
<div class="row user-header">
<div class="header-body">
<!-- Title -->
<!-- Title -->
<span class="user-badge">
<#assign avatarClass>
user-avatar-md <#if canEditOrgSettings(u, o)>organization-avatar</#if>
user-avatar-md <#if canEditOrgSettings(u, o, so)>organization-avatar</#if>
</#assign>
<@userAvatar.userAvatar userName=u.user.name avatarUrl=utils.avatarUrl(u.user.name) clazz=avatarClass />
<#if canEditOrgSettings(u, o)>
<#if canEditOrgSettings(u, o, so)>
<div class="edit-avatar" style="display: none;">
<a href="${Routes.ORG_UPDATE_AVATAR.getRouteUrl(u.user.name)}"><i class="fas fa-edit"></i> <@spring.message "user.editAvatar" /></a>
<a href="${Routes.ORG_UPDATE_AVATAR.getRouteUrl(u.user.name)}"><i
class="fas fa-edit"></i> <@spring.message "user.editAvatar" /></a>
</div>
<#assign Prompt=@helper["io.papermc.hangar.model.Prompt"] />
<#-- @ftlvariable name="Prompt" type="io.papermc.hangar.model.Prompt" -->
<#-- @ftlvariable name="Prompt" type="io.papermc.hangar.model.Prompt" -->
<#if !u.headerData.currentUser.readPrompts?seq_contains(Prompt.CHANGE_AVATAR.ordinal())>
<@promptView.prompt prompt=Prompt.CHANGE_AVATAR id="popover-avatar" />
</#if>
</#if>
<div>
<div class="user-title">
<h1 class="username float-left">
${u.user.name}
</h1>
<div class="user-actions">
<#if u.isCurrent() && !u.isOrga()>
<a class="user-settings" href="${config.getAuthUrl()}/accounts/settings">
<i class="fas fa-cog" data-toggle="tooltip" data-placement="top" title="Settings"></i>
</a>
<span data-toggle="modal" data-target="#modal-lock">
<i class="fas <#if u.user.isLocked()>fa-lock<#else>fa-unlock-alt</#if> action-lock-account"
data-toggle="tooltip"
data-placement="top"
title="<#if !u.user.isLocked()><@spring.message "user.lock" /><#else><@spring.message "user.unlock" /></#if>"></i>
</span>
<span class="user-title">
<h1 class="username">
${u.user.name}
<#if u.isCurrent() && !u.isOrga()>
<a class="user-settings" href="${config.getAuthUrl()}/accounts/settings">
<i class="fas fa-cog" data-toggle="tooltip"
data-placement="top" title="Settings"></i>
</a>
<span data-toggle="modal" data-target="#modal-lock">
<i class="fas <#if u.user.isLocked()>fa-lock<#else>fa-unlock-alt</#if> action-lock-account" data-toggle="tooltip"
data-placement="top" title="<#if !u.user.isLocked()><@spring.message "user.lock" /><#else><@spring.message "user.unlock" /></#if>"></i>
</span>
<a class="action-api" href="${Routes.USERS_EDIT_API_KEYS.getRouteUrl(u.user.name)}">
<i class="fas fa-key" data-toggle="tooltip" data-placement="top" title="API Keys"></i>
</a>
</#if>
<#if u.hasUser()>
<#if u.userPerm.has(Permission.ModNotesAndFlags) || u.userPerm.has(Permission.Reviewer)>
<a class="user-settings" href="${Routes.SHOW_ACTIVITIES.getRouteUrl(u.user.name)}">
<i class="fas fa-calendar" data-toggle="tooltip"
data-placement="top" title="Activity"></i>
<a class="action-api" href="${Routes.USERS_EDIT_API_KEYS.getRouteUrl(u.user.name)}">
<i class="fas fa-key" data-toggle="tooltip" data-placement="top" title="API Keys"></i>
</a>
</#if>
</#if>
<#if u.headerData.globalPerm(Permission.EditAllUserSettings)>
<a class="user-settings" href="${Routes.USER_ADMIN.getRouteUrl(u.user.name)}">
<i class="fas fa-wrench" data-toggle="tooltip"
data-placement="top" title="User Admin"></i>
</a>
</#if>
</h1>
<#if u.hasUser()>
<#if u.userPerm.has(Permission.ModNotesAndFlags) || u.userPerm.has(Permission.Reviewer)>
<a class="user-settings" href="${Routes.SHOW_ACTIVITIES.getRouteUrl(u.user.name)}">
<i class="fas fa-calendar" data-toggle="tooltip" data-placement="top" title="Activity"></i>
</a>
</#if>
</#if>
<#if u.headerData.globalPerm(Permission.EditAllUserSettings)>
<a class="user-settings" href="${Routes.USER_ADMIN.getRouteUrl(u.user.name)}">
<i class="fas fa-wrench" data-toggle="tooltip" data-placement="top" title="User Admin"></i>
</a>
</#if>
</div>
<div class="clearfix"></div>
</div>
<div class="user-tag">
<i class="minor">
<#if u.user.tagline??>
${u.user.tagline}
<#elseif u.isCurrent() || canEditOrgSettings(u, o)>
<#elseif u.isCurrent() || canEditOrgSettings(u, o, so)>
Add a tagline
</#if>
</i>
<#if u.isCurrent() || canEditOrgSettings(u, o)>
<#if u.isCurrent() || canEditOrgSettings(u, o, so)>
<a href="#" data-toggle="modal" data-target="#modal-tagline">
<i class="fas fa-edit"></i>
</a>
</#if>
</div>
</span>
</div>
</span>
<!-- Roles -->
@ -131,7 +136,7 @@
<div class="user-info">
<i class="minor">${u.projectCount}&nbsp;<#if u.projectCount == 1>project<#else>projects</#if></i><br/>
<i class="minor">
<@spring.messageArgs code="user.memberSince" args=[utils.prettifyDate(u.user.joinDate!u.user.createdAt)] />
<@spring.messageArgs code="user.memberSince" args=[utils.prettifyDate(u.user.joinDate!u.user.createdAt)] />
</i><br/>
<a href="https://papermc.io/forums/users/${u.user.name}">
<@spring.message "user.viewOnForums" /> <i class="fas fa-external-link-alt"></i>
@ -144,11 +149,11 @@
<#assign lockModalTitle>
<#compress>
<#if u.user.isLocked()>
user.unlock
<#else>
user.lock
</#if>
<#if u.user.isLocked()>
user.unlock
<#else>
user.lock
</#if>
</#compress>
</#assign>
<@modal.modal lockModalTitle "modal-lock" "label-lock">
@ -163,7 +168,8 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal"><@spring.message "general.close" /></button>
<button type="button" class="btn btn-default"
data-dismiss="modal"><@spring.message "general.close" /></button>
<@form.form method="POST" action=Routes.USERS_VERIFY.getRouteUrl(Routes.USERS_SET_LOCKED.getRouteUrl(u.user.name, (!u.user.isLocked())?string, "", "")) class="form-inline">
<@csrf.formField />
<button type="submit" class="btn btn-primary"><@spring.message "general.continue" /></button>
@ -181,7 +187,7 @@
<p><@spring.message "user.tagline.info" /></p>
</div>
<input class="form-control" type="text" value="${u.user.tagline!""}" id="tagline"
name="tagline" maxlength="${config.getUser().maxTaglineLen}" />
name="tagline" maxlength="${config.getUser().maxTaglineLen}"/>
</div>
<div class="clearfix"></div>
</div>
@ -189,7 +195,7 @@
<button type="button" class="btn btn-default" data-dismiss="modal">
<@spring.message "general.close" />
</button>
<input type="submit" value="<@spring.message "general.save" />" class="btn btn-primary" />
<input type="submit" value="<@spring.message "general.save" />" class="btn btn-primary"/>
</div>
</@form.form>
</@modal.modal>