mirror of
https://github.com/versia-pub/frontend.git
synced 2026-03-13 03:29:16 +01:00
chore: ⬆️ Upgrade to Nuxt 4
Some checks failed
Some checks failed
This commit is contained in:
parent
8debe97f63
commit
7f7cf20311
386 changed files with 2376 additions and 2332 deletions
156
app/utils/auth.ts
Normal file
156
app/utils/auth.ts
Normal 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
64
app/utils/emojis.ts
Normal 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
22
app/utils/passwords.ts
Normal 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
8
app/utils/urls.ts
Normal 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
32
app/utils/validators.ts
Normal 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],
|
||||
);
|
||||
Loading…
Add table
Add a link
Reference in a new issue