chore: initialize Gitea v1.27.2 fork
giteabot backport / giteabot (push) Canceled after 0s
giteabot / giteabot (push) Canceled after 0s
release-nightly / nightly-binary (push) Canceled after 0s
release-nightly / nightly-container (push) Canceled after 0s
cache-seeder / gobuild (push) Canceled after 0s
cache-seeder / lint (bindata, lint-backend) (push) Canceled after 0s
release-nightly-snapcraft / build-and-publish (push) Canceled after 0s

Includes direct password setup links in registration emails.

Assisted-by: Codex:GPT-5
This commit is contained in:
2026-08-15 22:56:18 +08:00
commit e4afba3416
6210 changed files with 802505 additions and 0 deletions
+273
View File
@@ -0,0 +1,273 @@
import {encodeURLEncodedBase64, decodeURLEncodedBase64} from '../utils.ts';
import {hideElem, showElem} from '../utils/dom.ts';
import {errorMessage} from '../modules/errors.ts';
import {GET, POST} from '../modules/fetch.ts';
const {appSubUrl} = window.config;
/** One of the possible values for the `data-webauthn-error-msg` attribute on the webauthn error message element */
type ErrorType = 'general' | 'insecure' | 'browser' | 'unable-to-process' | 'duplicated' | 'unknown';
export async function initUserAuthWebAuthn() {
const elPrompt = document.querySelector('.user.signin.webauthn-prompt');
const elSignInPasskeyBtn = document.querySelector('.signin-passkey');
if (!elPrompt && !elSignInPasskeyBtn) {
return;
}
const errorType = detectWebAuthnSupport();
if (errorType) {
if (elSignInPasskeyBtn) hideElem(elSignInPasskeyBtn);
return;
}
if (elSignInPasskeyBtn) {
elSignInPasskeyBtn.addEventListener('click', loginPasskey);
}
if (elPrompt) {
login2FA();
}
}
async function loginPasskey() {
const res = await GET(`${appSubUrl}/user/webauthn/passkey/assertion`);
if (!res.ok) {
webAuthnError('unknown');
return;
}
const options = await res.json();
options.publicKey.challenge = decodeURLEncodedBase64(options.publicKey.challenge);
for (const cred of options.publicKey.allowCredentials ?? []) {
cred.id = decodeURLEncodedBase64(cred.id);
}
try {
const credential = await navigator.credentials.get({
publicKey: options.publicKey,
}) as PublicKeyCredential;
const credResp = credential.response as AuthenticatorAssertionResponse;
// Move data into Arrays in case it is super long
const authData = new Uint8Array(credResp.authenticatorData);
const clientDataJSON = new Uint8Array(credResp.clientDataJSON);
const rawId = new Uint8Array(credential.rawId);
const sig = new Uint8Array(credResp.signature);
const userHandle = new Uint8Array(credResp.userHandle ?? []);
const res = await POST(`${appSubUrl}/user/webauthn/passkey/login`, {
data: {
id: credential.id,
rawId: encodeURLEncodedBase64(rawId),
type: credential.type,
clientExtensionResults: credential.getClientExtensionResults(),
response: {
authenticatorData: encodeURLEncodedBase64(authData),
clientDataJSON: encodeURLEncodedBase64(clientDataJSON),
signature: encodeURLEncodedBase64(sig),
userHandle: encodeURLEncodedBase64(userHandle),
},
},
});
if (res.status === 500) {
webAuthnError('unknown');
return;
} else if (!res.ok) {
webAuthnError('unable-to-process');
return;
}
const reply = await res.json();
window.location.assign(reply?.redirect ?? `${appSubUrl}/`);
} catch (err) {
webAuthnError('general', errorMessage(err));
}
}
async function login2FA() {
const res = await GET(`${appSubUrl}/user/webauthn/assertion`);
if (!res.ok) {
webAuthnError('unknown');
return;
}
const options = await res.json();
options.publicKey.challenge = decodeURLEncodedBase64(options.publicKey.challenge);
for (const cred of options.publicKey.allowCredentials ?? []) {
cred.id = decodeURLEncodedBase64(cred.id);
}
try {
const credential = await navigator.credentials.get({
publicKey: options.publicKey,
});
await verifyAssertion(credential);
} catch (err) {
if (!options.publicKey.extensions?.appid) {
webAuthnError('general', errorMessage(err));
return;
}
delete options.publicKey.extensions.appid;
try {
const credential = await navigator.credentials.get({
publicKey: options.publicKey,
});
await verifyAssertion(credential);
} catch (err) {
webAuthnError('general', errorMessage(err));
}
}
}
async function verifyAssertion(assertedCredential: any) { // TODO: Credential type does not work
// Move data into Arrays in case it is super long
const authData = new Uint8Array(assertedCredential.response.authenticatorData);
const clientDataJSON = new Uint8Array(assertedCredential.response.clientDataJSON);
const rawId = new Uint8Array(assertedCredential.rawId);
const sig = new Uint8Array(assertedCredential.response.signature);
const userHandle = new Uint8Array(assertedCredential.response.userHandle);
const res = await POST(`${appSubUrl}/user/webauthn/assertion`, {
data: {
id: assertedCredential.id,
rawId: encodeURLEncodedBase64(rawId),
type: assertedCredential.type,
clientExtensionResults: assertedCredential.getClientExtensionResults(),
response: {
authenticatorData: encodeURLEncodedBase64(authData),
clientDataJSON: encodeURLEncodedBase64(clientDataJSON),
signature: encodeURLEncodedBase64(sig),
userHandle: encodeURLEncodedBase64(userHandle),
},
},
});
if (res.status === 500) {
webAuthnError('unknown');
return;
} else if (!res.ok) {
webAuthnError('unable-to-process');
return;
}
const reply = await res.json();
window.location.assign(reply?.redirect ?? `${appSubUrl}/`);
}
async function webauthnRegistered(newCredential: any) { // TODO: Credential type does not work
const attestationObject = new Uint8Array(newCredential.response.attestationObject);
const clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON);
const rawId = new Uint8Array(newCredential.rawId);
const res = await POST(`${appSubUrl}/user/settings/security/webauthn/register`, {
data: {
id: newCredential.id,
rawId: encodeURLEncodedBase64(rawId),
type: newCredential.type,
response: {
attestationObject: encodeURLEncodedBase64(attestationObject),
clientDataJSON: encodeURLEncodedBase64(clientDataJSON),
},
},
});
if (res.status === 409) {
webAuthnError('duplicated');
return;
} else if (res.status !== 201) {
webAuthnError('unknown');
return;
}
window.location.reload();
}
function webAuthnError(errorType: ErrorType, message:string = '') {
const elErrorMsg = document.querySelector(`#webauthn-error-msg`)!;
if (errorType === 'general') {
elErrorMsg.textContent = message || 'unknown error';
} else {
const elTypedError = document.querySelector(`#webauthn-error [data-webauthn-error-msg=${CSS.escape(errorType)}]`);
if (elTypedError) {
elErrorMsg.textContent = `${elTypedError.textContent}${message ? ` ${message}` : ''}`;
} else {
elErrorMsg.textContent = `unknown error type: ${errorType}${message ? ` ${message}` : ''}`;
}
}
showElem('#webauthn-error');
}
/** Returns the error type or `null` when there was no error. */
function detectWebAuthnSupport(): ErrorType | null {
if (!window.isSecureContext) {
return 'insecure';
}
if (typeof window.PublicKeyCredential !== 'function') {
return 'browser';
}
return null;
}
export function initUserAuthWebAuthnRegister() {
const elRegister = document.querySelector<HTMLInputElement>('#register-webauthn');
if (!elRegister) return;
const errorType = detectWebAuthnSupport();
if (errorType) {
webAuthnError(errorType);
elRegister.disabled = true;
return;
}
elRegister.addEventListener('click', async (e) => {
e.preventDefault();
await webAuthnRegisterRequest();
});
}
async function webAuthnRegisterRequest() {
const elNickname = document.querySelector<HTMLInputElement>('#nickname')!;
const formData = new FormData();
formData.append('name', elNickname.value);
const res = await POST(`${appSubUrl}/user/settings/security/webauthn/request_register`, {
data: formData,
});
if (res.status === 409) {
webAuthnError('duplicated');
return;
} else if (!res.ok) {
webAuthnError('unknown');
return;
}
const options = await res.json();
elNickname.closest('div.field')!.classList.remove('error');
options.publicKey.challenge = decodeURLEncodedBase64(options.publicKey.challenge);
options.publicKey.user.id = decodeURLEncodedBase64(options.publicKey.user.id);
if (options.publicKey.excludeCredentials) {
for (const cred of options.publicKey.excludeCredentials) {
cred.id = decodeURLEncodedBase64(cred.id);
}
}
try {
const credential = await navigator.credentials.create({
publicKey: options.publicKey,
});
await webauthnRegistered(credential);
} catch (err) {
// an already registered authenticator raises this
if (err instanceof DOMException && err.name === 'InvalidStateError') {
webAuthnError('duplicated');
return;
}
webAuthnError('unknown', errorMessage(err));
}
}