Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ecc9b62842 |
@@ -295,6 +295,9 @@ User.listDetail = async function(){
|
|||||||
obj.onboardingRequired = obj.onboardingNeeds.length > 0 ? 'yes' : '';
|
obj.onboardingRequired = obj.onboardingNeeds.length > 0 ? 'yes' : '';
|
||||||
obj.isServiceAccount = serviceAccountDNs.has(String(obj.dn).toLowerCase()) ? 'yes' : '';
|
obj.isServiceAccount = serviceAccountDNs.has(String(obj.dn).toLowerCase()) ? 'yes' : '';
|
||||||
obj.managerUids = obj.manager.map(dn => dnToUid.get(String(dn).toLowerCase()) || dn);
|
obj.managerUids = obj.manager.map(dn => dnToUid.get(String(dn).toLowerCase()) || dn);
|
||||||
|
// hasSshKey is a boolean flag for the UI -- sshPublicKey may be an array,
|
||||||
|
// and Mustache's {{#sshPublicKey}}...{{/sshPublicKey}} iterates over each item.
|
||||||
|
obj.hasSshKey = obj.sshPublicKey ? 'yes' : '';
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -49,12 +49,9 @@
|
|||||||
<span class="badge bg-secondary">{{kind}}{{#metadata.subType}} ({{metadata.subType}}){{/metadata.subType}}</span>
|
<span class="badge bg-secondary">{{kind}}{{#metadata.subType}} ({{metadata.subType}}){{/metadata.subType}}</span>
|
||||||
{{#metadata.isProduction}}<span class="badge bg-danger">Prod</span>{{/metadata.isProduction}}
|
{{#metadata.isProduction}}<span class="badge bg-danger">Prod</span>{{/metadata.isProduction}}
|
||||||
{{^metadata.isProduction}}<span class="badge bg-info">Dev</span>{{/metadata.isProduction}}
|
{{^metadata.isProduction}}<span class="badge bg-info">Dev</span>{{/metadata.isProduction}}
|
||||||
<span class="badge bg-light text-dark border">{{hostName}}</span>
|
<a href="#" class="text-reset text-decoration-none ms-2" onclick="openEditModal('{{id}}'); return false;" title="View details">
|
||||||
<br>
|
|
||||||
<a href="#" class="text-reset text-decoration-none" onclick="openEditModal('{{id}}'); return false;" title="View details">
|
|
||||||
<strong>{{name}}</strong>
|
<strong>{{name}}</strong>
|
||||||
</a>
|
</a>
|
||||||
<small class="text-muted">{{slug}}</small>
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{#metadata.ip}}<div><small>IP:</small> {{metadata.ip}}</div>{{/metadata.ip}}
|
{{#metadata.ip}}<div><small>IP:</small> {{metadata.ip}}</div>{{/metadata.ip}}
|
||||||
|
|||||||
+63
-21
@@ -1,15 +1,6 @@
|
|||||||
<%- include('top') %>
|
<%- include('top') %>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.portal-banner {
|
|
||||||
background-color: var(--bs-primary);
|
|
||||||
color: white;
|
|
||||||
padding: 2.5rem 1rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
border-radius: .5rem;
|
|
||||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
.portal-banner h1 { font-weight: 700; }
|
|
||||||
.catalog-grid {
|
.catalog-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
@@ -23,7 +14,19 @@
|
|||||||
}
|
}
|
||||||
.service-card:hover { transform: translateY(-3px); box-shadow: 0 .5rem 1rem rgba(0,0,0,.15)!important; }
|
.service-card:hover { transform: translateY(-3px); box-shadow: 0 .5rem 1rem rgba(0,0,0,.15)!important; }
|
||||||
.service-card .card-body { flex: 1; }
|
.service-card .card-body { flex: 1; }
|
||||||
.card-icon { font-size: 1.4rem; width: 1.8rem; text-align: center; }
|
.card-icon {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
width: 1.8rem;
|
||||||
|
text-align: center;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.card-icon-img {
|
||||||
|
width: 1.8rem;
|
||||||
|
height: 1.8rem;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
.howto code {
|
.howto code {
|
||||||
display: block;
|
display: block;
|
||||||
background: var(--bs-tertiary-bg, #f1f3f5);
|
background: var(--bs-tertiary-bg, #f1f3f5);
|
||||||
@@ -37,12 +40,6 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
<div class="portal-banner text-center">
|
|
||||||
<h1><%- name %> Portal</h1>
|
|
||||||
<p class="lead mb-3">Everything the lab offers — what you can reach, and how to reach it.</p>
|
|
||||||
<a href="/profile" class="btn btn-light shadow-sm"><i class="fa-solid fa-user"></i> My Profile</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<input type="text" id="catalog-search" class="form-control shadow-sm"
|
<input type="text" id="catalog-search" class="form-control shadow-sm"
|
||||||
@@ -68,8 +65,36 @@
|
|||||||
<ul class="list-group mb-5 shadow-sm" id="approvals"></ul>
|
<ul class="list-group mb-5 shadow-sm" id="approvals"></ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="mb-3"><i class="fa-solid fa-layer-group text-success"></i> My Access</h3>
|
<!-- My Access section with tabs -->
|
||||||
<div class="catalog-grid mb-5" id="my-services"></div>
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<span><i class="fa-solid fa-layer-group text-success"></i> My Access</span>
|
||||||
|
</div>
|
||||||
|
<div class="px-3 pt-3 border-bottom">
|
||||||
|
<ul class="nav nav-tabs border-bottom-0" role="tablist">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#my-services-tab" type="button" role="tab">
|
||||||
|
<i class="fa-solid fa-cube"></i> Services
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#my-hosts-tab" type="button" role="tab">
|
||||||
|
<i class="fa-solid fa-server"></i> Hosts
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="tab-content">
|
||||||
|
<div class="tab-pane fade show active" id="my-services-tab" role="tabpanel">
|
||||||
|
<div class="catalog-grid" id="my-services"></div>
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane fade" id="my-hosts-tab" role="tabpanel">
|
||||||
|
<div class="catalog-grid" id="my-hosts"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3 class="mb-3"><i class="fa-solid fa-compass text-secondary"></i> Discover More</h3>
|
<h3 class="mb-3"><i class="fa-solid fa-compass text-secondary"></i> Discover More</h3>
|
||||||
<p class="text-muted small">Things you don't have access to yet. Request what you need.</p>
|
<p class="text-muted small">Things you don't have access to yet. Request what you need.</p>
|
||||||
@@ -99,6 +124,17 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render icon as either Font Awesome class or <img> for URL
|
||||||
|
function renderIcon(icon, kind) {
|
||||||
|
if (!icon) icon = KIND_ICONS[kind] || 'fa-solid fa-cube';
|
||||||
|
// If it starts with http, treat it as an image URL
|
||||||
|
if (/^https?:\/\//i.test(icon)) {
|
||||||
|
return '<img src="' + esc(icon) + '" alt="" class="card-icon-img">';
|
||||||
|
}
|
||||||
|
// Otherwise it's a Font Awesome class
|
||||||
|
return '<i class="' + esc(icon) + ' card-icon"></i>';
|
||||||
|
}
|
||||||
|
|
||||||
// "How do I actually use this?" — the question the directory exists to
|
// "How do I actually use this?" — the question the directory exists to
|
||||||
// answer and the one the old portal never did. Everything here is derived
|
// answer and the one the old portal never did. Everything here is derived
|
||||||
// from directory metadata; nothing is hardcoded per-service.
|
// from directory metadata; nothing is hardcoded per-service.
|
||||||
@@ -142,7 +178,6 @@
|
|||||||
|
|
||||||
function cardHtml(r, accessible) {
|
function cardHtml(r, accessible) {
|
||||||
var md = r.metadata || {};
|
var md = r.metadata || {};
|
||||||
var icon = md.icon || KIND_ICONS[r.kind] || 'fa-solid fa-cube';
|
|
||||||
var blurb = md.tagline || r.description || 'No description provided';
|
var blurb = md.tagline || r.description || 'No description provided';
|
||||||
var href = accessible ? linkFor(r) : null;
|
var href = accessible ? linkFor(r) : null;
|
||||||
var lines = accessible ? howTo(r) : [];
|
var lines = accessible ? howTo(r) : [];
|
||||||
@@ -166,10 +201,12 @@
|
|||||||
+ '<i class="fa-solid fa-hand"></i> Request access</button>';
|
+ '<i class="fa-solid fa-hand"></i> Request access</button>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var iconHtml = renderIcon(md.icon, r.kind);
|
||||||
|
|
||||||
return '<div class="card shadow-sm service-card ' + (accessible ? 'border-success' : '') + '">'
|
return '<div class="card shadow-sm service-card ' + (accessible ? 'border-success' : '') + '">'
|
||||||
+ '<div class="card-body">'
|
+ '<div class="card-body">'
|
||||||
+ '<h5 class="card-title d-flex align-items-start gap-2">'
|
+ '<h5 class="card-title d-flex align-items-start gap-2">'
|
||||||
+ '<i class="' + esc(icon) + ' card-icon ' + (accessible ? 'text-success' : 'text-secondary') + '"></i>'
|
+ iconHtml
|
||||||
+ '<span>' + esc(r.name) + '</span>'
|
+ '<span>' + esc(r.name) + '</span>'
|
||||||
+ '</h5>'
|
+ '</h5>'
|
||||||
+ '<div class="mb-2"><span class="badge bg-secondary">' + esc(r.kind)
|
+ '<div class="mb-2"><span class="badge bg-secondary">' + esc(r.kind)
|
||||||
@@ -235,7 +272,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderAll() {
|
function renderAll() {
|
||||||
renderGrid('my-services', state.mine, true);
|
// Split mine into services and hosts
|
||||||
|
var myServices = state.mine.filter(function(r) { return r.kind === 'service' || r.kind === 'oauth'; });
|
||||||
|
var myHosts = state.mine.filter(function(r) { return r.kind === 'host'; });
|
||||||
|
|
||||||
|
renderGrid('my-services', myServices, true);
|
||||||
|
renderGrid('my-hosts', myHosts, true);
|
||||||
renderGrid('other-services', state.others, false);
|
renderGrid('other-services', state.others, false);
|
||||||
renderRequests();
|
renderRequests();
|
||||||
renderApprovals();
|
renderApprovals();
|
||||||
|
|||||||
+237
-299
@@ -6,13 +6,10 @@
|
|||||||
var isOwnProfile = !location.pathname.includes('/users/');
|
var isOwnProfile = !location.pathname.includes('/users/');
|
||||||
|
|
||||||
function renderProfile(user){
|
function renderProfile(user){
|
||||||
// data.photo = unescape(encodeURIComponent(data.jpegPhoto));
|
|
||||||
user.createTimestamp = moment(user.createTimestamp, "YYYYMMDDHHmmssZ").fromNow();
|
user.createTimestamp = moment(user.createTimestamp, "YYYYMMDDHHmmssZ").fromNow();
|
||||||
user.modifyTimestamp = moment(user.modifyTimestamp, "YYYYMMDDHHmmssZ").fromNow();
|
user.modifyTimestamp = moment(user.modifyTimestamp, "YYYYMMDDHHmmssZ").fromNow();
|
||||||
user.managerUids = (user.manager || []).map(app.user.dnToUid);
|
user.managerUids = (user.manager || []).map(app.user.dnToUid);
|
||||||
|
|
||||||
$.scope.user.update(user);
|
$.scope.user.update(user);
|
||||||
$.scope.passwordReset.update(user);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
async function renderUserGroups(user){
|
async function renderUserGroups(user){
|
||||||
@@ -92,9 +89,6 @@
|
|||||||
async function renderMyServices(){
|
async function renderMyServices(){
|
||||||
try{
|
try{
|
||||||
let res = await app.api.get('discovery/me');
|
let res = await app.api.get('discovery/me');
|
||||||
// The server already resolves this by walking the graph, so a service
|
|
||||||
// with no address of its own inherits its host's. Only fall back
|
|
||||||
// locally when it genuinely resolved to nothing.
|
|
||||||
res.results.forEach(r => {
|
res.results.forEach(r => {
|
||||||
r.resolvedAddress = r.resolvedAddress
|
r.resolvedAddress = r.resolvedAddress
|
||||||
|| (r.metadata && r.metadata.address)
|
|| (r.metadata && r.metadata.address)
|
||||||
@@ -112,11 +106,11 @@
|
|||||||
try{
|
try{
|
||||||
let uid = currentUser.uid;
|
let uid = currentUser.uid;
|
||||||
let res = await app.api.get(`metrics/user/${uid}`);
|
let res = await app.api.get(`metrics/user/${uid}`);
|
||||||
if (res && res.results) {
|
if (res && res.results && res.results.services) {
|
||||||
const renderList = (items, id) => {
|
const el = document.getElementById('metrics-user-services');
|
||||||
const el = document.getElementById(id);
|
|
||||||
if(!el) return;
|
if(!el) return;
|
||||||
el.innerHTML = '';
|
el.innerHTML = '';
|
||||||
|
const items = res.results.services;
|
||||||
if (!items || items.length === 0) {
|
if (!items || items.length === 0) {
|
||||||
el.innerHTML = '<li class="list-group-item text-muted">No data available</li>';
|
el.innerHTML = '<li class="list-group-item text-muted">No data available</li>';
|
||||||
return;
|
return;
|
||||||
@@ -127,19 +121,17 @@
|
|||||||
<span class="badge bg-primary rounded-pill">${item.score}</span>
|
<span class="badge bg-primary rounded-pill">${item.score}</span>
|
||||||
</li>`;
|
</li>`;
|
||||||
});
|
});
|
||||||
};
|
|
||||||
renderList(res.results.services, 'metrics-user-services');
|
|
||||||
}
|
}
|
||||||
}catch(error){
|
}catch(error){
|
||||||
console.error('renderMyMetrics error:', error)
|
console.error('renderMyMetrics error:', error);
|
||||||
|
const el = document.getElementById('metrics-user-services');
|
||||||
|
if(el) el.innerHTML = '<li class="list-group-item text-danger">Failed to load metrics</li>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function determinUser(){
|
async function determinUser(){
|
||||||
if(location.pathname.includes('/users/')){
|
if(location.pathname.includes('/users/')){
|
||||||
let uid = location.pathname.replace('/users/', '');
|
let uid = location.pathname.replace('/users/', '');
|
||||||
|
|
||||||
return (await app.api.get('user/'+uid)).results;
|
return (await app.api.get('user/'+uid)).results;
|
||||||
}else{
|
}else{
|
||||||
return await app.auth.asyncUser;
|
return await app.auth.asyncUser;
|
||||||
@@ -148,16 +140,9 @@
|
|||||||
|
|
||||||
function editUser(user){
|
function editUser(user){
|
||||||
user = user || currentUser;
|
user = user || currentUser;
|
||||||
|
|
||||||
var $profileCard = $('#userProfile');
|
var $profileCard = $('#userProfile');
|
||||||
var $editCard = $('#editProfile');
|
var $editCard = $('#editProfile');
|
||||||
|
|
||||||
$.scope.editProfile.update(user);
|
$.scope.editProfile.update(user);
|
||||||
// jq-repeat's update() is trailing-edge throttled (~50ms) as of 2.1.0 --
|
|
||||||
// wait for the throttle tick to land before sliding the updated card
|
|
||||||
// into view, or it can briefly show stale/empty data. The manager
|
|
||||||
// picker is a JS widget, not a mustache-bound input, so it also has to
|
|
||||||
// wait for update() to (re-)render its empty mount div before attaching.
|
|
||||||
setTimeout(function(){
|
setTimeout(function(){
|
||||||
app.ui.userSelect('#edit-manager', {
|
app.ui.userSelect('#edit-manager', {
|
||||||
name: 'manager',
|
name: 'manager',
|
||||||
@@ -172,8 +157,6 @@
|
|||||||
function editUserSeccess(data){
|
function editUserSeccess(data){
|
||||||
currentUser = data.results;
|
currentUser = data.results;
|
||||||
renderProfile(currentUser);
|
renderProfile(currentUser);
|
||||||
// Same throttle-tick wait as editUser() above -- renderProfile() calls
|
|
||||||
// $.scope.user.update()/passwordReset.update() internally.
|
|
||||||
setTimeout(function(){
|
setTimeout(function(){
|
||||||
$('#editProfile').slideUp();
|
$('#editProfile').slideUp();
|
||||||
$('#userProfile').slideDown()
|
$('#userProfile').slideDown()
|
||||||
@@ -201,6 +184,29 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Password reset modal
|
||||||
|
function openPasswordResetModal(){
|
||||||
|
app.modal.open({
|
||||||
|
title: 'Reset Password for ' + currentUser.uid,
|
||||||
|
bodyHtml:
|
||||||
|
'<div class="actionMessage mb-3" style="display:none"></div>'
|
||||||
|
+ '<form id="passwordResetForm" action="user/' + currentUser.uid + '/password" method="put" onsubmit="formAJAX(this)" evalAJAX="app.modal.close(); app.messages.toast(\'Password updated\', \'success\');">'
|
||||||
|
+ '<div class="mb-3">'
|
||||||
|
+ '<label class="form-label">New Password</label>'
|
||||||
|
+ '<input type="password" class="form-control shadow" name="userPassword" placeholder="8+ chars; mix upper/lower/number/symbol" validate="password">'
|
||||||
|
+ '</div>'
|
||||||
|
+ '<div class="mb-3">'
|
||||||
|
+ '<label class="form-label">Confirm Password</label>'
|
||||||
|
+ '<input type="password" class="form-control shadow" name="password" placeholder="Retype password" validate="eq:userPassword">'
|
||||||
|
+ '</div>'
|
||||||
|
+ '</form>',
|
||||||
|
footer: {
|
||||||
|
buttonsHtml: '<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>'
|
||||||
|
+ '<button type="submit" form="passwordResetForm" class="btn btn-warning"><i class="fa-solid fa-key"></i> Reset Password</button>',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$(document).ready(async function(){
|
$(document).ready(async function(){
|
||||||
currentUser = await determinUser();
|
currentUser = await determinUser();
|
||||||
|
|
||||||
@@ -210,7 +216,7 @@
|
|||||||
renderMyServices();
|
renderMyServices();
|
||||||
if(!isOwnProfile) {
|
if(!isOwnProfile) {
|
||||||
renderMyMetrics();
|
renderMyMetrics();
|
||||||
$('#user-metrics-card').show();
|
$('#tab-metrics').parent().show();
|
||||||
}
|
}
|
||||||
$('#personal-group-uid-label').text(currentUser.uid);
|
$('#personal-group-uid-label').text(currentUser.uid);
|
||||||
|
|
||||||
@@ -221,8 +227,6 @@
|
|||||||
name: 'members', values: [], placeholder: 'Type a username…',
|
name: 'members', values: [], placeholder: 'Type a username…',
|
||||||
});
|
});
|
||||||
|
|
||||||
// API Tokens are self-service only — never shown when an admin is
|
|
||||||
// viewing someone else's profile via /users/:uid.
|
|
||||||
if(isOwnProfile){
|
if(isOwnProfile){
|
||||||
$('#own-api-tokens-section').show();
|
$('#own-api-tokens-section').show();
|
||||||
tableAJAX();
|
tableAJAX();
|
||||||
@@ -230,145 +234,248 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="row mb-3" style="display:none">
|
<div class="container mt-4">
|
||||||
<div class="col-md-4">
|
<div class="row" style="display:none">
|
||||||
<div class="shadow-lg card">
|
<div class="col-12">
|
||||||
<div class="card-header shadow">
|
<div id="userProfile" class="shadow-lg card card-default mb-4">
|
||||||
<i class="fa-solid fa-arrow-rotate-left"></i>
|
<div class="card-header shadow d-flex justify-content-between align-items-center">
|
||||||
Password Reset
|
<div>
|
||||||
<div class="float-end">
|
|
||||||
<i class="fa-solid fa-arrows-up-down"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-header shadow actionMessage" style="display:none">
|
|
||||||
</div>
|
|
||||||
<div jq-repeat="passwordReset" class="card-body">
|
|
||||||
<h3>
|
|
||||||
Reset Password for {{uid}}
|
|
||||||
</h3>
|
|
||||||
<form action="user/{{uid}}/password" method="put" onsubmit="formAJAX(this)" class="mb-3">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Password</label>
|
|
||||||
<div class="input-group shadow">
|
|
||||||
<span class="input-group-text" id="addon-wrapping"><i class="fa-solid fa-key"></i></span>
|
|
||||||
<input type="password" name="userPassword" class="form-control" placeholder="huunteR!23" aria-label="Username" aria-describedby="addon-wrapping">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Again</label>
|
|
||||||
<div class="input-group shadow">
|
|
||||||
<span class="input-group-text" id="addon-wrapping"><i class="fa-solid fa-key"></i></span>
|
|
||||||
<input type="password" name="password" class="form-control" placeholder="huunteR!23" aria-label="Username" aria-describedby="addon-wrapping">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-outline-secondary shadow">Change</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-8">
|
|
||||||
<div id="userProfile" class="shadow-lg card card-default mb-8">
|
|
||||||
<div class="card-header shadow">
|
|
||||||
<i class="fa-regular fa-id-card"></i>
|
<i class="fa-regular fa-id-card"></i>
|
||||||
Profile
|
Profile: <strong>{{user.uid}}</strong>
|
||||||
<div class="float-end">
|
</div>
|
||||||
<i class="fa-solid fa-arrows-up-down"></i>
|
<div class="d-flex gap-2">
|
||||||
|
<button type="button" onclick="openPasswordResetModal()" class="btn btn-outline-warning btn-sm">
|
||||||
|
<i class="fa-solid fa-key"></i> Reset Password
|
||||||
|
</button>
|
||||||
|
<button type="button" onclick="editUser()" class="btn btn-warning btn-sm">
|
||||||
|
<i class="fa-solid fa-user-pen"></i> Edit
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-header shadow actionMessage" style="display:none">
|
<div class="card-header shadow actionMessage" style="display:none"></div>
|
||||||
|
|
||||||
|
<div class="px-3 pt-3 border-bottom">
|
||||||
|
<ul class="nav nav-tabs border-bottom-0" role="tablist">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tab-profile" type="button" role="tab">
|
||||||
|
<i class="fa-solid fa-user"></i> Profile
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-groups" type="button" role="tab">
|
||||||
|
<i class="fa-solid fa-users"></i> My Groups
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-services" type="button" role="tab">
|
||||||
|
<i class="fa-solid fa-layer-group"></i> My Services
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation" id="tab-metrics-parent" style="display:none">
|
||||||
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-metrics" type="button" role="tab">
|
||||||
|
<i class="fa-solid fa-chart-line"></i> Security & Usage
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-members" type="button" role="tab">
|
||||||
|
<i class="fa-solid fa-people-group"></i> Members of {{user.uid}}'s Group
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="profile-body" jq-repeat="user">
|
|
||||||
<div class="card-body profile-body-{{uid}}">
|
<div class="card-body">
|
||||||
<h2><i>User Name:</i> <b>{{uid}}</b></h2>
|
<div class="tab-content">
|
||||||
{{^isServiceAccount}}<i>Name:</i> <b>{{givenName}} {{sn}}</b><br />{{/isServiceAccount}}
|
<!-- Profile Tab -->
|
||||||
<i>Email:</i> <b>{{mail}}</b>
|
<div class="tab-pane fade show active" id="tab-profile" role="tabpanel">
|
||||||
|
<div jq-repeat="user">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<p><i>Name:</i> <b>{{givenName}} {{sn}}</b></p>
|
||||||
|
<p><i>Email:</i> <b>{{mail}}</b>
|
||||||
{{#emailVerified}}<span class="badge bg-success ms-1"><i class="fa-solid fa-circle-check"></i> Verified</span>{{/emailVerified}}
|
{{#emailVerified}}<span class="badge bg-success ms-1"><i class="fa-solid fa-circle-check"></i> Verified</span>{{/emailVerified}}
|
||||||
<br />
|
</p>
|
||||||
<i>Phone:</i> <b>{{mobile}}</b>
|
<p><i>Phone:</i> <b>{{mobile}}</b>
|
||||||
{{#phoneVerified}}<span class="badge bg-success ms-1"><i class="fa-solid fa-circle-check"></i> Verified</span>{{/phoneVerified}}
|
{{#phoneVerified}}<span class="badge bg-success ms-1"><i class="fa-solid fa-circle-check"></i> Verified</span>{{/phoneVerified}}
|
||||||
<br />
|
</p>
|
||||||
<i>Location (Site):</i> <b>{{location}} </b><br />
|
<p><i>Location (Site):</i> <b>{{location}}</b></p>
|
||||||
<i>LDAP DN:</i> <b>{{dn}} </b><br />
|
<p><i>LDAP DN:</i> <b>{{dn}}</b></p>
|
||||||
<i>Home Directory:</i> <b>{{homeDirectory}} </b><br />
|
<p><i>Home Directory:</i> <b>{{homeDirectory}}</b></p>
|
||||||
<i>Login Shell:</i> <b>{{loginShell}} </b><br />
|
</div>
|
||||||
<i>Manager(s):</i>
|
<div class="col-md-6">
|
||||||
|
<p><i>Login Shell:</i> <b>{{loginShell}}</b></p>
|
||||||
|
<p><i>Manager(s):</i>
|
||||||
{{#managerUids}}<span class="badge bg-secondary me-1">{{.}}</span>{{/managerUids}}
|
{{#managerUids}}<span class="badge bg-secondary me-1">{{.}}</span>{{/managerUids}}
|
||||||
<br />
|
</p>
|
||||||
<i>Status:</i>
|
<p><i>Status:</i>
|
||||||
{{#isActive}}<span class="badge bg-success">Active</span>{{/isActive}}
|
{{#isActive}}<span class="badge bg-success">Active</span>{{/isActive}}
|
||||||
{{#isInactive}}<span class="badge bg-danger">Inactive</span>{{/isInactive}}
|
{{#isInactive}}<span class="badge bg-danger">Inactive</span>{{/isInactive}}
|
||||||
<br />
|
</p>
|
||||||
<i>Date of Birth:</i> <b>{{dateOfBirth}}</b><br />
|
<p><i>Date of Birth:</i> <b>{{dateOfBirth}}</b></p>
|
||||||
<i>TOS:</i>
|
<p><i>TOS:</i>
|
||||||
{{#tosAccepted}}<span class="badge bg-success">Accepted</span>{{/tosAccepted}}
|
{{#tosAccepted}}<span class="badge bg-success">Accepted</span>{{/tosAccepted}}
|
||||||
{{#tosNotAccepted}}<span class="badge bg-warning text-dark">Pending</span>{{/tosNotAccepted}}
|
{{#tosNotAccepted}}<span class="badge bg-warning text-dark">Pending</span>{{/tosNotAccepted}}
|
||||||
<br />
|
|
||||||
<i>SSH Public Key:</i> <b>{{sshPublicKey}}</b><br />
|
|
||||||
<i>Unix User ID:</i> <b>{{uidNumber}} </b><br />
|
|
||||||
<i>Unix Group ID:</i> <b>{{gidNumber}} </b><br />
|
|
||||||
<i>Description:</i><br>
|
|
||||||
<p>
|
|
||||||
{{description}}
|
|
||||||
</p>
|
</p>
|
||||||
<!-- <img id="profile_photo" /> -->
|
<p><i>SSH Public Key:</i> <b>{{sshPublicKey}}</b></p>
|
||||||
|
<p><i>Unix User ID:</i> <b>{{uidNumber}}</b></p>
|
||||||
|
<p><i>Unix Group ID:</i> <b>{{gidNumber}}</b></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer profile-body-{{uid}}">
|
</div>
|
||||||
<div class="float-end">
|
{{#description}}
|
||||||
<button type="button" onclick="editUser()" class="btn btn-warning btn shadow ">
|
<hr>
|
||||||
<i class="fa-solid fa-user-pen"></i>
|
<p><i>Description:</i><br>{{description}}</p>
|
||||||
</button>
|
{{/description}}
|
||||||
|
<hr>
|
||||||
|
<p class="text-muted small mb-0">
|
||||||
|
<i>Joined:</i> <b>{{createTimestamp}}</b> | <i>Edited:</i> <b>{{modifyTimestamp}}</b>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 border-top pt-3">
|
||||||
|
<h6 class="text-muted">Admin Actions</h6>
|
||||||
|
<div class="d-flex gap-2 flex-wrap group-required group-required-app_sso_admin">
|
||||||
{{#isActive}}
|
{{#isActive}}
|
||||||
<button type="button" class="btn btn-outline-warning shadow group-required group-required-app_sso_admin" title="Deactivate user" onclick="toggleActive('{{uid}}', false)">
|
<button type="button" class="btn btn-outline-warning" title="Deactivate user" onclick="toggleActive('{{uid}}', false)">
|
||||||
<i class="fa-solid fa-lock"></i>
|
<i class="fa-solid fa-lock"></i> Deactivate
|
||||||
</button>
|
</button>
|
||||||
{{/isActive}}
|
{{/isActive}}
|
||||||
{{#isInactive}}
|
{{#isInactive}}
|
||||||
<button type="button" class="btn btn-warning shadow group-required group-required-app_sso_admin" title="Activate user" onclick="toggleActive('{{uid}}', true)">
|
<button type="button" class="btn btn-warning" title="Activate user" onclick="toggleActive('{{uid}}', true)">
|
||||||
<i class="fa-solid fa-lock-open"></i>
|
<i class="fa-solid fa-lock-open"></i> Activate
|
||||||
</button>
|
</button>
|
||||||
{{/isInactive}}
|
{{/isInactive}}
|
||||||
<button type="button" class="btn btn-secondary shadow group-required group-required-app_sso_admin" title="Impersonate this user" onclick="startImpersonate('{{uid}}')">
|
<button type="button" class="btn btn-secondary" title="Impersonate this user" onclick="startImpersonate('{{uid}}')">
|
||||||
<i class="fa-solid fa-user-secret"></i>
|
<i class="fa-solid fa-user-secret"></i> Impersonate
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-danger shadow group-required group-required-app_sso_admin" onclick="deleteUser('{{uid}}', this)">
|
<button type="button" class="btn btn-danger" onclick="deleteUser('{{uid}}', this)">
|
||||||
<i class="fa-solid fa-user-slash"></i>
|
<i class="fa-solid fa-user-slash"></i> Delete User
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="float-left">
|
|
||||||
<i>Joined:</i> <b>{{createTimestamp}} </b> <br/>
|
|
||||||
<i>Edited:</i> <b>{{modifyTimestamp}} </b>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="editProfile" class="shadow-lg card card-default mb-8" style="display:none">
|
<!-- My Groups Tab -->
|
||||||
|
<div class="tab-pane fade" id="tab-groups" role="tabpanel">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th class="group-required group-required-app_sso_admin text-end">Actions</th>
|
||||||
|
</thead>
|
||||||
|
<tbody jq-repeat="mygroups">
|
||||||
|
<tr>
|
||||||
|
<td>{{cn}}</td>
|
||||||
|
<td>{{description}}</td>
|
||||||
|
<td class="text-end group-required group-required-app_sso_admin">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger" title="Remove from group" onclick="removeFromGroup('{{cn}}', this)">
|
||||||
|
<i class="fa-solid fa-xmark"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="group-required group-required-app_sso_admin mt-3">
|
||||||
|
<label class="form-label small">Add to group</label>
|
||||||
|
<div class="d-flex gap-2 align-items-start">
|
||||||
|
<div id="add-group-select" class="flex-grow-1"></div>
|
||||||
|
<button type="button" class="btn btn-outline-dark" onclick="addToGroups(this)">Add</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- My Services Tab -->
|
||||||
|
<div class="tab-pane fade" id="tab-services" role="tabpanel">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Address / IP</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Kind</th>
|
||||||
|
</thead>
|
||||||
|
<tbody jq-repeat="myservices">
|
||||||
|
<tr>
|
||||||
|
<td>{{name}} <small class="text-muted">({{slug}})</small></td>
|
||||||
|
<td><a href="{{resolvedAddress}}" target="_blank"><code>{{resolvedAddress}}</code></a></td>
|
||||||
|
<td>{{description}}</td>
|
||||||
|
<td>
|
||||||
|
{{#metadata.isProduction}}<span class="badge bg-danger mb-1">Prod</span><br>{{/metadata.isProduction}}
|
||||||
|
<span class="badge bg-secondary">{{kind}}{{#metadata.subType}} ({{metadata.subType}}){{/metadata.subType}}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Security & Usage Tab -->
|
||||||
|
<div class="tab-pane fade" id="tab-metrics" role="tabpanel">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h6 class="text-success"><i class="fa-solid fa-plug"></i> Top Services Used (Last 7 Days)</h6>
|
||||||
|
<ul class="list-group list-group-flush border rounded" id="metrics-user-services">
|
||||||
|
<li class="list-group-item text-muted">Loading...</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Members Tab -->
|
||||||
|
<div class="tab-pane fade" id="tab-members" role="tabpanel">
|
||||||
|
<p class="text-muted small">
|
||||||
|
Every account gets a personal Unix group (its primary GID) — add
|
||||||
|
other accounts here as supplementary members.
|
||||||
|
</p>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<th>Username</th>
|
||||||
|
<th class="text-end">Actions</th>
|
||||||
|
</thead>
|
||||||
|
<tbody jq-repeat="personalGroupMembers">
|
||||||
|
<tr>
|
||||||
|
<td>{{uid}}</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger" title="Remove from group" onclick="removePersonalGroupMember('{{uid}}', this)">
|
||||||
|
<i class="fa-solid fa-xmark"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<label class="form-label small">Add member</label>
|
||||||
|
<div class="d-flex gap-2 align-items-start">
|
||||||
|
<div id="add-personal-group-member-select" class="flex-grow-1"></div>
|
||||||
|
<button type="button" class="btn btn-outline-dark" onclick="addPersonalGroupMembers(this)">Add</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Profile Card (hidden by default) -->
|
||||||
|
<div id="editProfile" class="shadow-lg card card-default mb-4" style="display:none">
|
||||||
<div class="card-header shadow">
|
<div class="card-header shadow">
|
||||||
<i class="fad fa-id-card"></i>
|
<i class="fad fa-id-card"></i>
|
||||||
Edit Profile
|
Edit Profile
|
||||||
<div class="float-end">
|
|
||||||
<i class="fa-solid fa-arrows-up-down"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-header shadow actionMessage" style="display:none">
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card-header shadow actionMessage" style="display:none"></div>
|
||||||
<div class="card-body" jq-repeat="editProfile">
|
<div class="card-body" jq-repeat="editProfile">
|
||||||
<div id="tableAJAX">
|
|
||||||
<h3>Editing {{uid}}</h3>
|
<h3>Editing {{uid}}</h3>
|
||||||
<form action="user/{{uid}}" method="put" onsubmit="formAJAX(this)" evalAJAX="editUserSeccess(data)">
|
<form action="user/{{uid}}" method="put" onsubmit="formAJAX(this)" evalAJAX="editUserSeccess(data)">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Date of Birth</label>
|
<label class="form-label">Date of Birth</label>
|
||||||
<input type="date" class="form-control shadow" name="dateOfBirth" value="{{dateOfBirth}}" />
|
<input type="date" class="form-control shadow" name="dateOfBirth" value="{{dateOfBirth}}" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">SSH Public Key</label>
|
<label class="form-label">SSH Public Key</label>
|
||||||
<input type="text" class="form-control" name="sshPublicKey" placeholder="ssh-rsa AAAAB3NzaC1yc2EAAAADAQ..." value="{{sshPublicKey}}" />
|
<input type="text" class="form-control" name="sshPublicKey" placeholder="ssh-rsa AAAAB3NzaC1yc2EAAAADAQ..." value="{{sshPublicKey}}" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Mobile Phone</label>
|
<label class="form-label">Mobile Phone</label>
|
||||||
<input type="text" class="form-control" name="mobile" placeholder="9175551234" value="{{mobile}}" />
|
<input type="text" class="form-control" name="mobile" placeholder="9175551234" value="{{mobile}}" />
|
||||||
@@ -390,172 +497,22 @@
|
|||||||
<div id="edit-manager"></div>
|
<div id="edit-manager"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">User Description (Optional)</label>
|
<label class="form-label">User Description (optional)</label>
|
||||||
<textarea class="form-control" name="description" placeholder="Admin group for gitea app">{{description}}</textarea>
|
<textarea class="form-control" name="description" placeholder="Admin group for gitea app">{{description}}</textarea>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-outline-dark btn-warning">Change</button>
|
<button type="submit" class="btn btn-outline-dark btn-warning">Save Changes</button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="$('#editProfile').slideUp(); $('#userProfile').slideDown();">Cancel</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="shadow-lg card card-default mb-8">
|
|
||||||
<div class="card-header shadow">
|
|
||||||
<i class="fa-solid fa-users-viewfinder"></i>
|
|
||||||
My groups
|
|
||||||
<div class="float-end">
|
|
||||||
<a href="/docs/accounts" class="text-reset me-2" title="Help"><i class="fa-solid fa-circle-question"></i></a>
|
|
||||||
<i class="fa-solid fa-arrows-up-down"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-header shadow actionMessage" style="display:none">
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<th>
|
|
||||||
Name
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
Description
|
|
||||||
</th>
|
|
||||||
<th class="group-required group-required-app_sso_admin"></th>
|
|
||||||
</thead>
|
|
||||||
<tbody jq-repeat="mygroups">
|
|
||||||
<tr>
|
|
||||||
<td>{{cn}}</td>
|
|
||||||
<td>{{description}}</td>
|
|
||||||
<td class="text-end group-required group-required-app_sso_admin">
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger" title="Remove from group" onclick="removeFromGroup('{{cn}}', this)">
|
|
||||||
<i class="fa-solid fa-xmark"></i>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="group-required group-required-app_sso_admin">
|
|
||||||
<label class="form-label small">Add to group</label>
|
|
||||||
<div class="d-flex gap-2 align-items-start">
|
|
||||||
<div id="add-group-select" class="flex-grow-1"></div>
|
|
||||||
<button type="button" class="btn btn-outline-dark" onclick="addToGroups(this)">Add</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="shadow-lg card card-default mb-8">
|
|
||||||
<div class="card-header shadow">
|
|
||||||
<i class="fa-solid fa-layer-group"></i>
|
|
||||||
My Services
|
|
||||||
<div class="float-end">
|
|
||||||
<i class="fa-solid fa-arrows-up-down"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Address / IP</th>
|
|
||||||
<th>Description</th>
|
|
||||||
<th>Kind</th>
|
|
||||||
</thead>
|
|
||||||
<tbody jq-repeat="myservices">
|
|
||||||
<tr>
|
|
||||||
<td>{{name}} <small class="text-muted">({{slug}})</small></td>
|
|
||||||
<td><a href="{{resolvedAddress}}" target="_blank"><code>{{resolvedAddress}}</code></a></td>
|
|
||||||
<td>{{description}}</td>
|
|
||||||
<td>
|
|
||||||
{{#metadata.isProduction}}<span class="badge bg-danger mb-1">Prod</span><br>{{/metadata.isProduction}}
|
|
||||||
<span class="badge bg-secondary">{{kind}}{{#metadata.subType}} ({{metadata.subType}}){{/metadata.subType}}</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="shadow-lg card card-default mb-8 group-required group-required-app_sso_admin" id="user-metrics-card" style="display:none">
|
|
||||||
<div class="card-header shadow bg-success text-white">
|
|
||||||
<i class="fa-solid fa-chart-line"></i>
|
|
||||||
Security & Usage Stats (Last 7 Days)
|
|
||||||
<div class="float-end">
|
|
||||||
<i class="fa-solid fa-arrows-up-down"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body p-0">
|
|
||||||
<div class="row m-0">
|
|
||||||
<div class="col-12 p-3">
|
|
||||||
<h6 class="text-success"><i class="fa-solid fa-plug"></i> Top Services Used</h6>
|
|
||||||
<ul class="list-group list-group-flush border rounded" id="metrics-user-services">
|
|
||||||
<li class="list-group-item text-muted">Loading...</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="shadow-lg card card-default mb-8 group-required group-required-app_sso_admin">
|
|
||||||
<div class="card-header shadow">
|
|
||||||
<i class="fa-solid fa-people-group"></i>
|
|
||||||
Members of <span id="personal-group-uid-label"></span>'s group
|
|
||||||
<div class="float-end">
|
|
||||||
<a href="/docs/accounts" class="text-reset me-2" title="Help"><i class="fa-solid fa-circle-question"></i></a>
|
|
||||||
<i class="fa-solid fa-arrows-up-down"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-header shadow actionMessage" style="display:none">
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<p class="text-muted small">
|
|
||||||
Every account gets a personal Unix group (its primary GID) — add
|
|
||||||
other accounts here as supplementary members (e.g. to share write
|
|
||||||
access to files owned by this group).
|
|
||||||
</p>
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<th>
|
|
||||||
Username
|
|
||||||
</th>
|
|
||||||
<th class="text-end"></th>
|
|
||||||
</thead>
|
|
||||||
<tbody jq-repeat="personalGroupMembers">
|
|
||||||
<tr>
|
|
||||||
<td>{{uid}}</td>
|
|
||||||
<td class="text-end">
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger" title="Remove from group" onclick="removePersonalGroupMember('{{uid}}', this)">
|
|
||||||
<i class="fa-solid fa-xmark"></i>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<label class="form-label small">Add member</label>
|
|
||||||
<div class="d-flex gap-2 align-items-start">
|
|
||||||
<div id="add-personal-group-member-select" class="flex-grow-1"></div>
|
|
||||||
<button type="button" class="btn btn-outline-dark" onclick="addPersonalGroupMembers(this)">Add</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
// Any logged-in user can manage their own API tokens (self-service).
|
// API Tokens section (same as before)
|
||||||
// Section is only revealed (see $(document).ready above) when isOwnProfile.
|
|
||||||
|
|
||||||
var tokensById = {};
|
var tokensById = {};
|
||||||
|
|
||||||
// Shared "reveal secret once" display -- same pattern as jump-host's and
|
|
||||||
// proxy's showToken().
|
|
||||||
function showToken(title, token){
|
function showToken(title, token){
|
||||||
app.modal.open({title: title, bodyHtml:
|
app.modal.open({title: title, bodyHtml:
|
||||||
'<p class="text-danger"><i class="fa-solid fa-triangle-exclamation"></i> Save this token now — it will <strong>not</strong> be shown again.</p>'
|
'<p class="text-danger"><i class="fa-solid fa-triangle-exclamation"></i> Save this token now — it will <strong>not</strong> be shown again.</p>'
|
||||||
@@ -564,10 +521,7 @@
|
|||||||
+ '<p class="mt-3 mb-0 text-muted small">Use it as a bearer token:<br><code>Authorization: Bearer ' + app.util.escapeHtml(token) + '</code></p>'
|
+ '<p class="mt-3 mb-0 text-muted small">Use it as a bearer token:<br><code>Authorization: Bearer ' + app.util.escapeHtml(token) + '</code></p>'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Not the checkmark-flash technique this file used to use for its copy
|
|
||||||
// buttons -- FontAwesome replaces <i> icons with inline <svg>, so
|
|
||||||
// swapping the <i>'s class silently no-ops. A toast doesn't have that
|
|
||||||
// problem.
|
|
||||||
function copyFieldValue(sel){
|
function copyFieldValue(sel){
|
||||||
var $el = $(sel);
|
var $el = $(sel);
|
||||||
var text = $el.val();
|
var text = $el.val();
|
||||||
@@ -580,17 +534,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fmtTime(ms){
|
function fmtTime(ms){
|
||||||
// created_on/last_used_on come back from Redis as strings (model-redis
|
|
||||||
// only coerces fields with an explicit `type`); moment(value, "x") parses
|
|
||||||
// a numeric string-or-number as a Unix-ms timestamp, unlike new Date(str).
|
|
||||||
if(!ms || Number(ms) === 0) return '—';
|
if(!ms || Number(ms) === 0) return '—';
|
||||||
var t = moment(ms, "x");
|
var t = moment(ms, "x");
|
||||||
if(!t.isValid()) return '—';
|
if(!t.isValid()) return '—';
|
||||||
return t.fromNow() + ' <span class="text-muted">(' + t.format('YYYY-MM-DD HH:mm') + ')</span>';
|
return t.fromNow() + ' <span class="text-muted">(' + t.format('YYYY-MM-DD HH:mm') + ')</span>';
|
||||||
}
|
}
|
||||||
function fmtExpiry(token){
|
function fmtExpiry(token){
|
||||||
// expires_at is type:number (a real number); isExpired is a class getter
|
|
||||||
// that is NOT serialized to the client, so compute expiry here.
|
|
||||||
var exp = Number(token.expires_at);
|
var exp = Number(token.expires_at);
|
||||||
if(!exp) return '<span class="badge text-bg-secondary">never</span>';
|
if(!exp) return '<span class="badge text-bg-secondary">never</span>';
|
||||||
if(Date.now() > exp) return '<span class="badge text-bg-danger">expired</span>';
|
if(Date.now() > exp) return '<span class="badge text-bg-danger">expired</span>';
|
||||||
@@ -639,14 +588,6 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create is a native <form>+formAJAX submission (matching this app's own
|
|
||||||
// hostModal-style convention) rather than a JS-built payload. Deliberately
|
|
||||||
// does NOT call app.modal.close() before showToken() -- app.modal is a
|
|
||||||
// singleton, and close() immediately followed by open() in the same tick
|
|
||||||
// collides with Bootstrap's hide-transition guard (show() silently
|
|
||||||
// no-ops while _isTransitioning is still true from the just-started
|
|
||||||
// hide()). open() alone already overwrites the (already-visible) modal's
|
|
||||||
// content in place.
|
|
||||||
function createApiToken(){
|
function createApiToken(){
|
||||||
var $body = app.modal.open({
|
var $body = app.modal.open({
|
||||||
title: 'New API Token',
|
title: 'New API Token',
|
||||||
@@ -714,9 +655,6 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Not class="row": app-base.js's `$('div.row').fadeIn('slow')` page-reveal
|
|
||||||
runs before this page's own ready handler and would unhide any div.row
|
|
||||||
unconditionally, defeating the isOwnProfile check below. -->
|
|
||||||
<div id="own-api-tokens-section" style="display:none">
|
<div id="own-api-tokens-section" style="display:none">
|
||||||
<div class="row mt-3 justify-content-center">
|
<div class="row mt-3 justify-content-center">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
|
|||||||
@@ -301,7 +301,7 @@
|
|||||||
{{mail}}
|
{{mail}}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{#sshPublicKey}}<i class="fa-regular fa-circle-check text-success"></i>{{/sshPublicKey}}
|
{{#hasSshKey}}<i class="fa-regular fa-circle-check text-success"></i>{{/hasSshKey}}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{#isActive}}<i class="fa-regular fa-circle-check text-success"></i>{{/isActive}}
|
{{#isActive}}<i class="fa-regular fa-circle-check text-success"></i>{{/isActive}}
|
||||||
|
|||||||
Reference in New Issue
Block a user