20c0a48199
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
182 lines
7.6 KiB
Plaintext
182 lines
7.6 KiB
Plaintext
<script>
|
|
function renderUsernameSuggestions(suggestions) {
|
|
const container = document.getElementById('usernameOptions');
|
|
const hidden = document.getElementById('selectedUid');
|
|
const selector = document.getElementById('usernameSelector');
|
|
container.innerHTML = '';
|
|
if (!suggestions || !suggestions.length) return;
|
|
selector.style.display = '';
|
|
suggestions.forEach(function(uid, i) {
|
|
const btn = document.createElement('button');
|
|
btn.type = 'button';
|
|
btn.dataset.uid = uid;
|
|
btn.textContent = uid;
|
|
btn.className = 'btn ' + (i === 0 ? 'btn-secondary' : 'btn-outline-secondary');
|
|
btn.onclick = function() {
|
|
container.querySelectorAll('.btn').forEach(function(b) {
|
|
b.className = 'btn btn-outline-secondary';
|
|
});
|
|
btn.className = 'btn btn-secondary';
|
|
hidden.value = uid;
|
|
const override = document.getElementById('adminUidOverride');
|
|
if (override) override.value = '';
|
|
};
|
|
container.appendChild(btn);
|
|
});
|
|
hidden.value = suggestions[0];
|
|
}
|
|
|
|
async function fetchUsernameSuggestions() {
|
|
const gnEl = document.querySelector('[name="givenName"]');
|
|
const snEl = document.querySelector('[name="sn"]');
|
|
const dobEl = document.querySelector('[name="dob"]');
|
|
if (!gnEl || !snEl) return;
|
|
const gn = gnEl.value.trim();
|
|
const sn = snEl.value.trim();
|
|
const dob = dobEl ? dobEl.value.trim() : '';
|
|
if (gn.length < 2 || sn.length < 2) return;
|
|
try {
|
|
const params = new URLSearchParams({ givenName: gn, sn, dob });
|
|
const resp = await fetch('/api/auth/username-suggestions?' + params);
|
|
const data = await resp.json();
|
|
renderUsernameSuggestions(data.suggestions);
|
|
} catch(e) {}
|
|
}
|
|
</script>
|
|
<% if (locals.adminMode) { %>
|
|
<script>
|
|
// A service account (a Unix-style account something runs as -- a media
|
|
// manager, a torrent client, ...) isn't a person: no birthday, nothing to
|
|
// agree to, and it gets one name (a username), not a first/last name.
|
|
// Toggling this swaps the person-shaped fields for a single account-name
|
|
// field instead of asking for throwaway values.
|
|
function toggleServiceAccountFields(el){
|
|
var checked = el.checked;
|
|
var $form = $(el).closest('form');
|
|
|
|
$form.find('[name=dob]').prop('required', !checked).closest('.mb-3').toggle(!checked);
|
|
$form.find('#tosAgree').prop('required', !checked).closest('.mb-3').toggle(!checked);
|
|
$form.find('#personNameFields').toggle(!checked);
|
|
$form.find('#serviceAccountNameField').toggle(checked);
|
|
|
|
// Service accounts aren't a person with a mailbox, and a blank
|
|
// password is fine (no userPassword attribute set -- the account
|
|
// simply can't bind). Disabling (not just hiding) keeps disabled
|
|
// fields out of both form serialization and validation.
|
|
$form.find('[name=mail]').prop('disabled', checked).closest('.mb-3').toggle(!checked);
|
|
|
|
$form.find('[name=userPassword]').prop('disabled', checked).closest('.mb-3').toggle(!checked);
|
|
$form.find('[name=passwordMatch]').prop('disabled', checked).closest('.mb-3').toggle(!checked);
|
|
|
|
if(checked){
|
|
// Filler values so the LDAP schema (inetOrgPerson requires sn) is
|
|
// satisfied; not shown anywhere, the account name is what matters.
|
|
$form.find('[name=givenName]').val('Service');
|
|
$form.find('[name=sn]').val('Account');
|
|
document.getElementById('selectedUid').value = '';
|
|
}else{
|
|
$form.find('[name=givenName]').val('');
|
|
$form.find('[name=sn]').val('');
|
|
document.getElementById('usernameSelector').style.display = 'none';
|
|
document.getElementById('serviceAccountName').value = '';
|
|
}
|
|
}
|
|
</script>
|
|
<% } %>
|
|
<form action="user/" method="post" onsubmit="formAJAX(this)">
|
|
<input type="hidden" class="form-control" name="delete" value="false" />
|
|
<% if (locals.adminMode) { %>
|
|
<div class="mb-3 form-check">
|
|
<input type="checkbox" class="form-check-input" id="isServiceAccount" name="isServiceAccount" onchange="toggleServiceAccountFields(this)">
|
|
<label class="form-check-label" for="isServiceAccount">
|
|
This is a service account <small class="text-muted">(not a person — e.g. a Unix account an app/service runs as)</small>
|
|
</label>
|
|
</div>
|
|
<div class="mb-3" id="serviceAccountNameField" style="display:none">
|
|
<label class="form-label">Account name</label>
|
|
<input type="text" class="form-control shadow" id="serviceAccountName" placeholder="stuff_manager"
|
|
oninput="document.getElementById('selectedUid').value = this.value.trim()" />
|
|
</div>
|
|
<% } %>
|
|
<div id="personNameFields">
|
|
<div class="mb-3">
|
|
<label class="form-label">First name</label>
|
|
<input type="text" class="form-control shadow" name="givenName" placeholder="John" validate=":3" onblur="fetchUsernameSuggestions()" />
|
|
<div class="invalid-feedback"></div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Last name</label>
|
|
<input type="text" class="form-control shadow" name="sn" placeholder="smith" validate=":3" onblur="fetchUsernameSuggestions()" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Date of Birth</label>
|
|
<input type="date" class="form-control shadow" name="dob" required onchange="fetchUsernameSuggestions()" />
|
|
</div>
|
|
|
|
<div class="mb-3" id="usernameSelector" style="display:none">
|
|
<label class="form-label">Username</label>
|
|
<div id="usernameOptions" class="d-flex flex-wrap gap-2 mb-2"></div>
|
|
<input type="hidden" name="uid" id="selectedUid" />
|
|
<% if (locals.adminMode) { %>
|
|
<input type="text" class="form-control shadow mt-2" id="adminUidOverride"
|
|
placeholder="Custom username (admin only)"
|
|
oninput="document.getElementById('selectedUid').value = this.value.trim() || (document.querySelector('#usernameOptions .btn-secondary') && document.querySelector('#usernameOptions .btn-secondary').dataset.uid) || ''" />
|
|
<% } %>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Email</label>
|
|
<input type="text" class="form-control shadow" name="mail" placeholder="jsmith@gmail.com" validate="email:3" />
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">SSH Public Key</label>
|
|
<input type="text" class="form-control shadow" name="sshPublicKey" placeholder="ssh-rsa AAAAB3NzaC1yc2EAAAADAQ..." />
|
|
</div>
|
|
|
|
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Location (Site) <small class="text-muted">(optional)</small></label>
|
|
<input type="text" class="form-control shadow" name="location" placeholder="Site One" />
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Password</label>
|
|
<input type="password" class="form-control shadow" name="userPassword" placeholder="Atleast 5 char. long" validate="password:5"/>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Again</label>
|
|
<input type="password" class="form-control shadow" name="passwordMatch" placeholder="Retype password" validate="eq:userPassword"/>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">User Description (Optional)</label>
|
|
<textarea class="form-control shadow" name="description" placeholder="Admin group for gitea app"></textarea>
|
|
</div>
|
|
<div class="mb-3 form-check">
|
|
<input type="checkbox" class="form-check-input" id="tosAgree" name="tosAgree" required>
|
|
<label class="form-check-label" for="tosAgree">
|
|
I have read and agree to the <a href="/tos" target="_blank">Terms of Service</a>
|
|
</label>
|
|
</div>
|
|
<button type="submit" class="btn btn-outline-dark">Add</button>
|
|
</form>
|
|
<script>
|
|
$(document).ready(async function() {
|
|
const $loc = $('[name="location"]');
|
|
if (!$loc.val()) {
|
|
try {
|
|
const dirRes = await app.api.get('directory-admin/resources');
|
|
const resources = dirRes.results || [];
|
|
const site = resources.find(r => r.kind === 'site' && r.metadata && r.metadata.isCurrentSite);
|
|
if (site) {
|
|
$loc.val(site.name);
|
|
}
|
|
} catch(e) {}
|
|
}
|
|
});
|
|
</script>
|