mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-10 04:22:09 +10:00
use arktype for clientside validation
This commit is contained in:
13
.vscode/settings.json
vendored
13
.vscode/settings.json
vendored
@ -1,8 +1,5 @@
|
||||
{
|
||||
"spellchecker.ignoreWordsList": [
|
||||
"mTLS",
|
||||
"Wireguard"
|
||||
],
|
||||
"spellchecker.ignoreWordsList": ["mTLS", "Wireguard"],
|
||||
"sqltools.connections": [
|
||||
{
|
||||
"previewLimit": 50,
|
||||
@ -14,5 +11,11 @@
|
||||
"username": "drop",
|
||||
"password": "drop"
|
||||
}
|
||||
]
|
||||
],
|
||||
// allow autocomplete for ArkType expressions like "string | num"
|
||||
"editor.quickSuggestions": {
|
||||
"strings": "on"
|
||||
},
|
||||
// prioritize ArkType's "type" for autoimports
|
||||
"typescript.preferences.autoImportSpecifierExcludeRegexes": ["^(node:)?os$"]
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
"@prisma/client": "^6.1.0",
|
||||
"@tailwindcss/vite": "^4.0.6",
|
||||
"argon2": "^0.41.1",
|
||||
"arktype": "^2.1.10",
|
||||
"axios": "^1.7.7",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cookie-es": "^1.2.2",
|
||||
|
||||
@ -188,6 +188,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { XCircleIcon } from "@heroicons/vue/24/solid";
|
||||
import { type } from "arktype";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@ -208,14 +209,20 @@ const username = ref(invitation.data.value?.username);
|
||||
const password = ref("");
|
||||
const confirmPassword = ref(undefined);
|
||||
|
||||
const mailRegex = /^\S+@\S+\.\S+$/;
|
||||
const validEmail = computed(() => mailRegex.test(email.value ?? ""));
|
||||
const validUsername = computed(
|
||||
() =>
|
||||
(username.value?.length ?? 0) >= 5 &&
|
||||
username.value?.toLowerCase() == username.value
|
||||
const emailValidator = type("string.email");
|
||||
const validEmail = computed(
|
||||
() => !(emailValidator(email.value) instanceof type.errors)
|
||||
);
|
||||
|
||||
const usernameValidator = type("string.lower.preformatted >= 5");
|
||||
const validUsername = computed(
|
||||
() => !(usernameValidator(username.value) instanceof type.errors)
|
||||
);
|
||||
|
||||
const passwordValidator = type("string >= 14");
|
||||
const validPassword = computed(
|
||||
() => !(passwordValidator(password.value) instanceof type.errors)
|
||||
);
|
||||
const validPassword = computed(() => (password.value?.length ?? 0) >= 14);
|
||||
const validConfirmPassword = computed(
|
||||
() => password.value == confirmPassword.value
|
||||
);
|
||||
|
||||
@ -4,9 +4,15 @@ import { createHashArgon2 } from "~/server/internal/security/simple";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import * as jdenticon from "jdenticon";
|
||||
import objectHandler from "~/server/internal/objects";
|
||||
import { type } from "arktype";
|
||||
import { writeNonLiteralDefaultMessage } from "arktype/internal/parser/shift/operator/default.ts";
|
||||
|
||||
// Only really a simple test, in case people mistype their emails
|
||||
const mailRegex = /^\S+@\S+\.\S+$/;
|
||||
const userValidator = type({
|
||||
username: "string >= 5",
|
||||
email: "string.email",
|
||||
password: "string >= 14",
|
||||
"displayName?": "string | undefined",
|
||||
});
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const body = await readBody(h3);
|
||||
@ -27,59 +33,24 @@ export default defineEventHandler(async (h3) => {
|
||||
statusMessage: "Invalid or expired invitation.",
|
||||
});
|
||||
|
||||
const useInvitationOrBodyRequirement = (
|
||||
field: keyof Invitation,
|
||||
check: (v: string) => boolean
|
||||
) => {
|
||||
if (invitation[field]) {
|
||||
return invitation[field].toString();
|
||||
const user = userValidator(body);
|
||||
if (user instanceof type.errors) {
|
||||
// hover out.summary to see validation errors
|
||||
console.error(user.summary);
|
||||
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: user.summary,
|
||||
});
|
||||
}
|
||||
|
||||
const v: string = body[field]?.toString();
|
||||
const valid = check(v);
|
||||
return valid ? v : undefined;
|
||||
};
|
||||
// reuse items from invite
|
||||
if (invitation.username !== null) user.username = invitation.username;
|
||||
if (invitation.email !== null) user.email = invitation.email;
|
||||
|
||||
const username = useInvitationOrBodyRequirement(
|
||||
"username",
|
||||
(e) => e.length >= 5
|
||||
);
|
||||
const email = useInvitationOrBodyRequirement("email", (e) =>
|
||||
mailRegex.test(e)
|
||||
);
|
||||
const password = body.password;
|
||||
const displayName = body.displayName || username;
|
||||
|
||||
if (username === undefined)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Username is invalid. Must be more than 5 characters.",
|
||||
const existing = await prisma.user.count({
|
||||
where: { username: user.username },
|
||||
});
|
||||
if (username.toLowerCase() != username)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Username must be all lowercase",
|
||||
});
|
||||
|
||||
if (email === undefined)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Invalid email. Must follow the format you@example.com",
|
||||
});
|
||||
|
||||
if (!password)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Password empty or missing.",
|
||||
});
|
||||
|
||||
if (password.length < 14)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Password must be 14 or more characters.",
|
||||
});
|
||||
|
||||
const existing = await prisma.user.count({ where: { username: username } });
|
||||
if (existing > 0)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
@ -91,12 +62,12 @@ export default defineEventHandler(async (h3) => {
|
||||
const profilePictureId = uuidv4();
|
||||
await objectHandler.createFromSource(
|
||||
profilePictureId,
|
||||
async () => jdenticon.toPng(username, 256),
|
||||
async () => jdenticon.toPng(user.username, 256),
|
||||
{},
|
||||
[`internal:read`, `${userId}:write`]
|
||||
);
|
||||
|
||||
const hash = await createHashArgon2(password);
|
||||
const hash = await createHashArgon2(user.password);
|
||||
const [linkMec] = await prisma.$transaction([
|
||||
prisma.linkedAuthMec.create({
|
||||
data: {
|
||||
@ -106,9 +77,9 @@ export default defineEventHandler(async (h3) => {
|
||||
user: {
|
||||
create: {
|
||||
id: userId,
|
||||
username,
|
||||
displayName,
|
||||
email,
|
||||
username: user.username,
|
||||
displayName: user.displayName ?? user.username,
|
||||
email: user.email,
|
||||
profilePicture: profilePictureId,
|
||||
admin: invitation.isAdmin,
|
||||
},
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
{
|
||||
"extends": "../.nuxt/tsconfig.server.json"
|
||||
"extends": "../.nuxt/tsconfig.server.json",
|
||||
"compilerOptions": {
|
||||
"exactOptionalPropertyTypes": true
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
"extends": "./.nuxt/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"exactOptionalPropertyTypes": true
|
||||
}
|
||||
}
|
||||
|
||||
20
yarn.lock
20
yarn.lock
@ -20,6 +20,18 @@
|
||||
resolved "https://registry.yarnpkg.com/@antfu/utils/-/utils-0.7.10.tgz#ae829f170158e297a9b6a28f161a8e487d00814d"
|
||||
integrity sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==
|
||||
|
||||
"@ark/schema@0.45.0":
|
||||
version "0.45.0"
|
||||
resolved "https://registry.yarnpkg.com/@ark/schema/-/schema-0.45.0.tgz#5d5da5dfe94ca45d36d54513fe0c53566483a1e7"
|
||||
integrity sha512-3XlMWkZbEjh0YsF92vnnRNCWNRNhRKDTf6XhugyCXH0YRFuM+w1vFLDbB2JLfZloEd7i5cbqsLaDLzyBZbPrSg==
|
||||
dependencies:
|
||||
"@ark/util" "0.45.0"
|
||||
|
||||
"@ark/util@0.45.0":
|
||||
version "0.45.0"
|
||||
resolved "https://registry.yarnpkg.com/@ark/util/-/util-0.45.0.tgz#2c55394a6af7865aeeb22924f301e28084aea4c0"
|
||||
integrity sha512-Z1gHEGbpPzLtPmYb932t2B++6YonlUi1Fa14IQ4vhsGMWhd81Mi1miUmdZXW4fNI/wg1saT7H2/5cAuONgTXhg==
|
||||
|
||||
"@babel/code-frame@^7.12.13", "@babel/code-frame@^7.22.13", "@babel/code-frame@^7.24.7", "@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0", "@babel/code-frame@^7.26.2":
|
||||
version "7.26.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85"
|
||||
@ -2106,6 +2118,14 @@ argparse@^2.0.1:
|
||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
|
||||
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
|
||||
|
||||
arktype@^2.1.10:
|
||||
version "2.1.10"
|
||||
resolved "https://registry.yarnpkg.com/arktype/-/arktype-2.1.10.tgz#7a2fb85d1e8fbb22077134993a12e9b12d34ef6e"
|
||||
integrity sha512-KqbrzI9qIGrQUClifyS1HpUp/oTSRtGDvnMKzwg2TAvxRpynY1mn/ubXaxAAdGPOM8V3pBqwb01Z6TcXqhBxzQ==
|
||||
dependencies:
|
||||
"@ark/schema" "0.45.0"
|
||||
"@ark/util" "0.45.0"
|
||||
|
||||
ast-kit@^1.0.1, ast-kit@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/ast-kit/-/ast-kit-1.3.0.tgz#37c8b7418b6c59b1e593d7790dc6c2b1c0814761"
|
||||
|
||||
Reference in New Issue
Block a user