chore: ⬆️ Upgrade to Nuxt 4
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 1s
Deploy to GitHub Pages / build (push) Failing after 1s
Deploy to GitHub Pages / deploy (push) Has been skipped
Docker / build (push) Failing after 1s
Mirror to Codeberg / Mirror (push) Failing after 1s

This commit is contained in:
Jesse Wierzbinski 2025-07-16 07:48:39 +02:00
parent 8debe97f63
commit 7f7cf20311
386 changed files with 2376 additions and 2332 deletions

156
app/utils/auth.ts Normal file
View file

@ -0,0 +1,156 @@
import type { CredentialApplication } from "@versia/client/schemas";
import { nanoid } from "nanoid";
import { toast } from "vue-sonner";
import type { z } from "zod";
import { confirmModalService } from "~/components/modals/composable";
import pkg from "~~/package.json";
import * as m from "~~/paraglide/messages.js";
const getRedirectUri = () => new URL("/", useRequestURL().origin);
export const askForInstance = async (): Promise<URL> => {
const { confirmed, value } = await confirmModalService.confirm({
title: m.sharp_alive_anteater_fade(),
inputType: "url",
message: m.noble_misty_rook_slide(),
});
if (confirmed && value) {
return new URL(URL.canParse(value) ? value : `https://${value}`);
}
throw new Error("No instance provided");
};
export const signIn = async (
appData: Ref<z.infer<typeof CredentialApplication> | null>,
origin: URL,
) => {
const id = toast.loading(m.level_due_ox_greet());
const client = useClient(origin);
const redirectUri = getRedirectUri();
redirectUri.searchParams.append("origin", origin.toString());
const output = await client.value.createApp("Versia-FE", {
scopes: ["read", "write", "follow", "push"],
redirect_uris: redirectUri.toString(),
// @ts-expect-error Package.json types are missing this field
website: pkg.homepage ?? undefined,
});
if (!output?.data) {
toast.dismiss(id);
toast.error(m.silly_sour_fireant_fear());
return;
}
appData.value = output.data;
const url = await client.value.generateAuthUrl(
output.data.client_id,
output.data.client_secret,
{
scopes: ["read", "write", "follow", "push"],
redirect_uri: redirectUri.toString(),
},
);
if (!url) {
toast.dismiss(id);
toast.error(m.candid_frail_lion_value());
return;
}
// Add "instance_switch_uri" parameter to URL
const toRedirect = new URL(url);
toRedirect.searchParams.append("instance_switch_uri", useRequestURL().href);
window.location.href = toRedirect.toString();
};
export const signInWithCode = (
code: string,
appData: z.infer<typeof CredentialApplication>,
origin: URL,
) => {
const client = useClient(origin);
const redirectUri = getRedirectUri();
redirectUri.searchParams.append("origin", origin.toString());
client.value
?.fetchAccessToken(
appData.client_id,
appData.client_secret,
code,
redirectUri.toString(),
)
.then(async (res) => {
const tempClient = useClient(origin, res.data).value;
const [accountOutput, instanceOutput] = await Promise.all([
tempClient.verifyAccountCredentials(),
tempClient.getInstance(),
]);
// Get account data
if (
!identities.value.find(
(i) => i.account.id === accountOutput.data.id,
)
) {
identity.value = {
id: nanoid(),
tokens: res.data,
account: accountOutput.data,
instance: instanceOutput.data,
permissions: [],
emojis: [],
};
}
// Remove code from URL
window.history.replaceState(
{},
document.title,
window.location.pathname,
);
// Redirect to home
window.location.pathname = "/";
});
};
export const signOut = async (
appData: z.infer<typeof CredentialApplication> | null,
identityToRevoke: Identity,
) => {
const id = toast.loading("Signing out...");
if (!appData) {
toast.dismiss(id);
toast.error("No app or identity data to sign out");
return;
}
// Don't do anything on error, as Versia Server doesn't implement the revoke endpoint yet
await client.value
?.revokeToken(
appData.client_id,
identityToRevoke.tokens.access_token,
identityToRevoke.tokens.access_token,
)
.catch(() => {
// Do nothing
});
identities.value = identities.value.filter(
(i) => i.id !== identityToRevoke.id,
);
toast.dismiss(id);
toast.success("Signed out");
};

64
app/utils/emojis.ts Normal file
View file

@ -0,0 +1,64 @@
import { mkdir, writeFile } from "node:fs/promises";
import { getIconData, iconToHTML, iconToSVG, replaceIDs } from "@iconify/utils";
import * as fluentEmojiData from "@iconify-json/fluent-emoji";
import * as fluentFlatEmojiData from "@iconify-json/fluent-emoji-flat";
import * as notoEmojiData from "@iconify-json/noto";
import * as twemojiData from "@iconify-json/twemoji";
const emojiSets = {
twemoji: twemojiData,
noto: notoEmojiData,
fluent: fluentEmojiData,
"fluent-flat": fluentFlatEmojiData,
} as const;
const prerenderEmojis = (set: keyof typeof emojiSets) => {
const data = emojiSets[set];
// Outputs an object in the format { "emoji": "<svg ... />" }
const emojisToName = Object.entries(data.chars).map(([unicode, name]) => {
const emojiUnicode = String.fromCodePoint(
...unicode.split("-").map((c) => Number.parseInt(c, 16)),
);
return [emojiUnicode, name] as const;
});
// Get the SVG for each emoji
return Object.fromEntries(
emojisToName.map(([emoji, name]) => {
const iconData = getIconData(data.icons, name);
if (!iconData) {
throw new Error(`Icon not found: ${name}`);
}
const svg = iconToSVG(iconData, {
width: 64,
height: 64,
});
return [
emoji,
iconToHTML(replaceIDs(svg.body), svg.attributes),
] as const;
}),
);
};
// Pregenerates images for all sets and places them in public/emojis/<set>/<unicode_name>.svg
const pregenerateImages = async (set: keyof typeof emojiSets) => {
const emojis = prerenderEmojis(set);
const setDir = `public/emojis/${set}`;
await mkdir(setDir, { recursive: true });
for (const [emoji, svg] of Object.entries(emojis)) {
await writeFile(`${setDir}/${emoji}.svg`, svg);
}
};
for (const set of Object.keys(emojiSets) as (keyof typeof emojiSets)[]) {
pregenerateImages(set);
}

22
app/utils/passwords.ts Normal file
View file

@ -0,0 +1,22 @@
/**
* Get the strength of a password
* @param password The password to check
* @returns Number from 0 to Infinity representing the strength of the password
*/
export const passwordStrength = (password: string): number => {
const length = password.length;
const specialChars = password.match(/[^A-Za-z0-9]/g)?.length || 0;
const numbers = password.match(/[0-9]/g)?.length || 0;
const upperCase = password.match(/[A-Z]/g)?.length || 0;
const lowerCase = password.match(/[a-z]/g)?.length || 0;
// Calculate the strength of the password
return (
(length * 4 +
specialChars * 6 +
numbers * 4 +
upperCase * 2 +
lowerCase * 2) /
16
);
};

8
app/utils/urls.ts Normal file
View file

@ -0,0 +1,8 @@
export const wrapUrl = (path: string) => {
return new URL(
path,
identity.value
? `https://${identity.value.instance.domain}`
: window.location.origin,
).toString();
};

32
app/utils/validators.ts Normal file
View file

@ -0,0 +1,32 @@
import {
caseInsensitive,
char,
charIn,
createRegExp,
digit,
exactly,
global,
letter,
multiline,
oneOrMore,
} from "magic-regexp";
export const emojiValidator = createRegExp(
// A-Z a-z 0-9 _ -
oneOrMore(letter.or(digit).or(charIn("_-"))),
[caseInsensitive, global],
);
export const partiallyTypedEmojiValidator = createRegExp(
exactly(":"),
oneOrMore(letter.or(digit).or(exactly("_")).or(exactly("-"))).notBefore(
char,
),
[caseInsensitive, multiline],
);
export const partiallyTypedMentionValidator = createRegExp(
exactly("@"),
oneOrMore(letter.or(digit).or(exactly("_"))).notBefore(char),
[caseInsensitive, multiline],
);