Browse Source

Feat: Zod Generic String (#1661)

* start improving zod translations

* update zod translations
pull/1663/head
Bernd Storath 6 months ago
committed by GitHub
parent
commit
41fcd8fcda
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 132
      src/i18n/locales/en.json
  2. 2
      src/server/api/client/[clientId]/configuration.get.ts
  3. 2
      src/server/api/client/[clientId]/disable.post.ts
  4. 2
      src/server/api/client/[clientId]/enable.post.ts
  5. 2
      src/server/api/client/[clientId]/generateOneTimeLink.post.ts
  6. 2
      src/server/api/client/[clientId]/index.delete.ts
  7. 2
      src/server/api/client/[clientId]/index.post.ts
  8. 2
      src/server/api/client/[clientId]/qrcode.svg.get.ts
  9. 2
      src/server/api/client/index.post.ts
  10. 7
      src/server/api/me/index.post.ts
  11. 7
      src/server/api/me/password.post.ts
  12. 2
      src/server/api/setup/4.post.ts
  13. 1
      src/server/api/setup/5.post.ts
  14. 22
      src/server/database/repositories/client/types.ts
  15. 10
      src/server/database/repositories/general/types.ts
  16. 2
      src/server/database/repositories/hooks/types.ts
  17. 8
      src/server/database/repositories/interface/types.ts
  18. 4
      src/server/database/repositories/oneTimeLink/types.ts
  19. 52
      src/server/database/repositories/user/types.ts
  20. 4
      src/server/database/repositories/userConfig/types.ts
  21. 2
      src/server/routes/cnf/[oneTimeLink].ts
  22. 111
      src/server/utils/types.ts

132
src/i18n/locales/en.json

@ -38,73 +38,6 @@
"portPlaceholder": "443",
"migration": "Restore the backup"
},
"zod": {
"client": {
"id": "Client ID must be a valid number",
"name": "Name must be a valid string",
"nameMin": "Name must be at least 1 Character",
"expireDate": "expiredDate must be a valid string",
"expireDateMin": "expiredDate must be at least 1 Character",
"address4": "IPv4 Address must be a valid string",
"address4Min": "IPv4 Address must be a be at least 1 Character",
"address6": "IPv6 Address must be a valid string",
"address6Min": "IPv6 Address must be a be at least 1 Character",
"serverAllowedIps": "Allowed IPs must be a valid array of strings"
},
"user": {
"username": "Username must be a valid string",
"usernameMin": "Username must be at least 8 Characters",
"password": "Password must be a valid string",
"passwordMin": "Password must be at least 12 Characters",
"passwordUppercase": "Password must have at least 1 uppercase letter",
"passwordLowercase": "Password must have at least 1 lowercase letter",
"passwordNumber": "Password must have at least 1 number",
"passwordSpecial": "Password must have at least 1 special character",
"remember": "Remember must be a valid boolean",
"accept": "Please accept the condition",
"name": "Name must be a valid string",
"nameMin": "Name must be at least 1 Character",
"email": "Email must be a valid string",
"emailMin": "Email must be at least 1 Character",
"emailInvalid": "Email must be a valid email",
"passwordMatch": "Passwords must match"
},
"userConfig": {
"host": "Host must be a valid string",
"hostMin": "Host must contain at least 1 character"
},
"general": {
"sessionTimeout": "Session Timeout must be a valid number"
},
"interface": {
"cidr": "CIDR must be a valid string",
"cidrMin": "CIDR must be at least 1 Character",
"device": "Device must be a valid string",
"deviceMin": "Device must be at least 1 Character"
},
"otl": {
"otl": "oneTimeLink must be a valid string",
"otlMin": "oneTimeLink must be at least 1 Character"
},
"stringMalformed": "String is malformed",
"body": "Body must be a valid object",
"hook": "Hook must be a valid string",
"mtu": "MTU must be a valid number",
"mtuMin": "MTU must be at least 1280",
"mtuMax": "MTU must be at most 9000",
"port": "Port must be a valid number",
"portMin": "Port must be at least 1",
"portMax": "Port must be at most 65535",
"persistentKeepalive": "Persistent Keepalive must be a valid number",
"persistentKeepaliveMin": "Persistent Keepalive must be at least 0",
"persistentKeepaliveMax": "Persistent Keepalive must be at most 65535",
"address": "IP Address must be a valid string",
"addressMin": "IP Address must be a be at least 1 Character",
"dns": "DNS must be a valid array of strings",
"dnsMin": "DNS must have at least 1 item",
"allowedIps": "Allowed IPs must be a valid array of strings",
"allowedIpsMin": "Allowed IPs must have at least 1 item"
},
"name": "Name",
"username": "Username",
"signIn": "Sign In",
@ -151,16 +84,67 @@
"clear": "Clear",
"login": "Log in error"
},
"general": {
"sessionTimeout": "Session Timeout",
"metrics": "Metrics",
"prometheus": "Prometheus",
"json": "JSON"
},
"form": {
"actions": "Actions",
"save": "Save",
"revert": "Revert"
},
"password": "Password"
"password": "Password",
"zod": {
"generic": {
"required": "{0} is required",
"validNumber": "{0} must be a valid number",
"validString": "{0} must be a valid string",
"validBoolean": "{0} must be a valid boolean",
"validArray": "{0} must be a valid array",
"stringMin": "{0} must be at least {1} Character",
"numberMin": "{0} must be at least {1}"
},
"client": {
"id": "Client ID",
"name": "Name",
"expiresAt": "Expires At",
"address4": "IPv4 Address",
"address6": "IPv6 Address",
"serverAllowedIps": "Server Allowed IPs"
},
"user": {
"username": "Username",
"password": "Password",
"passwordUppercase": "Password must have at least 1 uppercase letter",
"passwordLowercase": "Password must have at least 1 lowercase letter",
"passwordNumber": "Password must have at least 1 number",
"passwordSpecial": "Password must have at least 1 special character",
"remember": "Remember",
"accept": "Accept",
"acceptTrue": "Accept Conditions to continue",
"name": "Name",
"email": "Email",
"emailInvalid": "Email must be a valid email",
"passwordMatch": "Passwords must match"
},
"userConfig": {
"host": "Host"
},
"general": {
"sessionTimeout": "Session Timeout",
"metricsEnabled": "Metrics",
"metricsPassword": "Metrics Password"
},
"interface": {
"cidr": "CIDR",
"device": "Device"
},
"otl": "One Time link",
"stringMalformed": "String is malformed",
"body": "Body must be a valid object",
"hook": "Hook",
"enabled": "Enabled",
"mtu": "MTU",
"port": "Port",
"persistentKeepalive": "Persistent Keepalive",
"address": "IP Address",
"dns": "DNS",
"allowedIps": "Allowed IPs"
}
}

2
src/server/api/client/[clientId]/configuration.get.ts

@ -6,7 +6,7 @@ export default definePermissionEventHandler(
async ({ event, checkPermissions }) => {
const { clientId } = await getValidatedRouterParams(
event,
validateZod(ClientGetSchema)
validateZod(ClientGetSchema, event)
);
const client = await Database.clients.get(clientId);
checkPermissions(client);

2
src/server/api/client/[clientId]/disable.post.ts

@ -6,7 +6,7 @@ export default definePermissionEventHandler(
async ({ event, checkPermissions }) => {
const { clientId } = await getValidatedRouterParams(
event,
validateZod(ClientGetSchema)
validateZod(ClientGetSchema, event)
);
const client = await Database.clients.get(clientId);

2
src/server/api/client/[clientId]/enable.post.ts

@ -6,7 +6,7 @@ export default definePermissionEventHandler(
async ({ event, checkPermissions }) => {
const { clientId } = await getValidatedRouterParams(
event,
validateZod(ClientGetSchema)
validateZod(ClientGetSchema, event)
);
const client = await Database.clients.get(clientId);

2
src/server/api/client/[clientId]/generateOneTimeLink.post.ts

@ -6,7 +6,7 @@ export default definePermissionEventHandler(
async ({ event, checkPermissions }) => {
const { clientId } = await getValidatedRouterParams(
event,
validateZod(ClientGetSchema)
validateZod(ClientGetSchema, event)
);
const client = await Database.clients.get(clientId);

2
src/server/api/client/[clientId]/index.delete.ts

@ -6,7 +6,7 @@ export default definePermissionEventHandler(
async ({ event, checkPermissions }) => {
const { clientId } = await getValidatedRouterParams(
event,
validateZod(ClientGetSchema)
validateZod(ClientGetSchema, event)
);
const client = await Database.clients.get(clientId);

2
src/server/api/client/[clientId]/index.post.ts

@ -9,7 +9,7 @@ export default definePermissionEventHandler(
async ({ event, checkPermissions }) => {
const { clientId } = await getValidatedRouterParams(
event,
validateZod(ClientGetSchema)
validateZod(ClientGetSchema, event)
);
const data = await readValidatedBody(

2
src/server/api/client/[clientId]/qrcode.svg.get.ts

@ -6,7 +6,7 @@ export default definePermissionEventHandler(
async ({ event, checkPermissions }) => {
const { clientId } = await getValidatedRouterParams(
event,
validateZod(ClientGetSchema)
validateZod(ClientGetSchema, event)
);
const client = await Database.clients.get(clientId);

2
src/server/api/client/index.post.ts

@ -6,7 +6,7 @@ export default definePermissionEventHandler(
async ({ event }) => {
const { name, expiresAt } = await readValidatedBody(
event,
validateZod(ClientCreateSchema)
validateZod(ClientCreateSchema, event)
);
await Database.clients.create({ name, expiresAt });

7
src/server/api/me/index.post.ts

@ -3,11 +3,14 @@ import { UserUpdateSchema } from '#db/repositories/user/types';
export default definePermissionEventHandler(
'me',
'update',
async ({ event, user }) => {
async ({ event, user, checkPermissions }) => {
const { name, email } = await readValidatedBody(
event,
validateZod(UserUpdateSchema)
validateZod(UserUpdateSchema, event)
);
checkPermissions(user);
await Database.users.update(user.id, name, email);
return { success: true };
}

7
src/server/api/me/password.post.ts

@ -3,11 +3,14 @@ import { UserUpdatePasswordSchema } from '#db/repositories/user/types';
export default definePermissionEventHandler(
'me',
'update',
async ({ event, user }) => {
async ({ event, user, checkPermissions }) => {
const { newPassword, currentPassword } = await readValidatedBody(
event,
validateZod(UserUpdatePasswordSchema)
validateZod(UserUpdatePasswordSchema, event)
);
checkPermissions(user);
await Database.users.updatePassword(user.id, currentPassword, newPassword);
return { success: true };
}

2
src/server/api/setup/4.post.ts

@ -6,6 +6,8 @@ export default defineSetupEventHandler(async ({ event }) => {
validateZod(UserSetupSchema, event)
);
// TODO: validate setup step
await Database.users.create(username, password);
await Database.general.setSetupStep(5);
return { success: true };

1
src/server/api/setup/5.post.ts

@ -5,6 +5,7 @@ export default defineSetupEventHandler(async ({ event }) => {
event,
validateZod(UserConfigSetupSchema, event)
);
// TODO: validate setup step
await Database.userConfigs.updateHostPort(host, port);
await Database.general.setSetupStep(0);
return { success: true };

22
src/server/database/repositories/client/types.ts

@ -3,8 +3,6 @@ import z from 'zod';
import type { client } from './schema';
export type ID = string;
export type ClientType = InferSelectModel<typeof client>;
export type CreateClientType = Omit<
@ -18,28 +16,28 @@ export type UpdateClientType = Omit<
>;
const name = z
.string({ message: 'zod.client.name' })
.min(1, 'zod.client.nameMin')
.string({ message: t('zod.client.name') })
.min(1, t('zod.client.name'))
.pipe(safeStringRefine);
const expiresAt = z
.string({ message: 'zod.client.expireDate' })
.min(1, 'zod.client.expireDateMin')
.string({ message: t('zod.client.expiresAt') })
.min(1, t('zod.client.expiresAt'))
.pipe(safeStringRefine)
.nullable();
const address4 = z
.string({ message: 'zod.client.address4' })
.min(1, { message: 'zod.client.address4Min' })
.string({ message: t('zod.client.address4') })
.min(1, { message: t('zod.client.address4') })
.pipe(safeStringRefine);
const address6 = z
.string({ message: 'zod.client.address6' })
.min(1, { message: 'zod.client.address6Min' })
.string({ message: t('zod.client.address6') })
.min(1, { message: t('zod.client.address6') })
.pipe(safeStringRefine);
const serverAllowedIps = z.array(AddressSchema, {
message: 'zod.serverAllowedIps',
message: t('zod.client.serverAllowedIps'),
});
export const ClientCreateSchema = z.object({
@ -65,7 +63,7 @@ export const ClientUpdateSchema = schemaForType<UpdateClientType>()(
);
// TODO: investigate if coerce is bad
const clientId = z.number({ message: 'zod.client.id', coerce: true });
const clientId = z.number({ message: t('zod.client.id'), coerce: true });
export const ClientGetSchema = z.object({
clientId: clientId,

10
src/server/database/repositories/general/types.ts

@ -4,11 +4,13 @@ import z from 'zod';
export type GeneralType = InferSelectModel<typeof general>;
const sessionTimeout = z.number({ message: 'zod.general.sessionTimeout' });
const metricsEnabled = z.boolean({ message: 'zod.general.metricsEnabled' });
const sessionTimeout = z.number({ message: t('zod.general.sessionTimeout') });
const metricsEnabled = z.boolean({ message: t('zod.general.metricsEnabled') });
const metricsPassword = z
.string({ message: 'zod.general.metricsPassword' })
.min(1, { message: 'zod.general.metricsPasswordMin' })
.string({ message: t('zod.general.metricsPassword') })
.min(1, { message: t('zod.general.metricsPassword') })
// TODO: validate argon2 regex?
.nullable();

2
src/server/database/repositories/hooks/types.ts

@ -6,7 +6,7 @@ export type HooksType = InferSelectModel<typeof hooks>;
export type HooksUpdateType = Omit<HooksType, 'id' | 'createdAt' | 'updatedAt'>;
const hook = z.string({ message: 'zod.hook' }).pipe(safeStringRefine);
const hook = z.string({ message: t('zod.hook') }).pipe(safeStringRefine);
export const HooksUpdateSchema = schemaForType<HooksUpdateType>()(
z.object({

8
src/server/database/repositories/interface/types.ts

@ -15,13 +15,13 @@ export type InterfaceUpdateType = Omit<
>;
const device = z
.string({ message: 'zod.interface.device' })
.min(1, 'zod.interface.deviceMin')
.string({ message: t('zod.interface.device') })
.min(1, t('zod.interface.device'))
.pipe(safeStringRefine);
const cidr = z
.string({ message: 'zod.interface.cidr' })
.min(1, { message: 'zod.interface.cidrMin' })
.string({ message: t('zod.interface.cidr') })
.min(1, { message: t('zod.interface.cidr') })
.pipe(safeStringRefine);
export const InterfaceUpdateSchema = schemaForType<InterfaceUpdateType>()(

4
src/server/database/repositories/oneTimeLink/types.ts

@ -5,8 +5,8 @@ import { z } from 'zod';
export type OneTimeLinkType = InferSelectModel<typeof oneTimeLink>;
const oneTimeLinkType = z
.string({ message: 'zod.otl.otl' })
.min(1, 'zod.otl.otlMin')
.string({ message: t('zod.otl.otl') })
.min(1, t('zod.otl.otl'))
.pipe(safeStringRefine);
export const OneTimeLinkGetSchema = z.object(

52
src/server/database/repositories/user/types.ts

@ -5,32 +5,20 @@ import z from 'zod';
export type UserType = InferSelectModel<typeof user>;
const username = z
.string({ message: 'zod.user.username' })
.min(8, 'zod.user.usernameMin')
.string({ message: t('zod.user.username') })
.min(8, t('zod.user.username'))
.pipe(safeStringRefine);
const password = z
.string({ message: 'zod.user.password' })
.min(12, 'zod.user.passwordMin')
.regex(/[A-Z]/, 'zod.user.passwordUppercase')
.regex(/[a-z]/, 'zod.user.passwordLowercase')
.regex(/\d/, 'zod.user.passwordNumber')
.regex(/[!@#$%^&*(),.?":{}|<>]/, 'zod.user.passwordSpecial')
.string({ message: t('zod.user.password') })
.min(12, t('zod.user.password'))
.regex(/[A-Z]/, t('zod.user.passwordUppercase'))
.regex(/[a-z]/, t('zod.user.passwordLowercase'))
.regex(/\d/, t('zod.user.passwordNumber'))
.regex(/[!@#$%^&*(),.?":{}|<>]/, t('zod.user.passwordSpecial'))
.pipe(safeStringRefine);
const remember = z.boolean({ message: 'zod.user.remember' });
const name = z
.string({ message: 'zod.user.name' })
.min(1, 'zod.user.nameMin')
.pipe(safeStringRefine);
const email = z
.string({ message: 'zod.user.email' })
.min(5, 'zod.user.emailMin')
.email({ message: 'zod.user.emailInvalid' })
.pipe(safeStringRefine)
.nullable();
const remember = z.boolean({ message: t('zod.user.remember') });
export const UserLoginSchema = z.object(
{
@ -41,9 +29,11 @@ export const UserLoginSchema = z.object(
{ message: objectMessage }
);
const accept = z.boolean().refine((val) => val === true, {
message: 'zod.user.accept',
});
const accept = z
.boolean({ message: t('zod.user.accept') })
.refine((val) => val === true, {
message: t('zod.user.acceptTrue'),
});
export const UserSetupSchema = z.object(
{
@ -54,6 +44,18 @@ export const UserSetupSchema = z.object(
{ message: objectMessage }
);
const name = z
.string({ message: t('zod.user.name') })
.min(1, 'zod.user.name')
.pipe(safeStringRefine);
const email = z
.string({ message: t('zod.user.email') })
.min(5, t('zod.user.email'))
.email({ message: t('zod.user.emailInvalid') })
.pipe(safeStringRefine)
.nullable();
export const UserUpdateSchema = z.object(
{
name: name,
@ -72,5 +74,5 @@ export const UserUpdatePasswordSchema = z
{ message: objectMessage }
)
.refine((val) => val.newPassword === val.confirmPassword, {
message: 'zod.user.passwordMatch',
message: t('zod.user.passwordMatch'),
});

4
src/server/database/repositories/userConfig/types.ts

@ -5,8 +5,8 @@ import z from 'zod';
export type UserConfigType = InferSelectModel<typeof userConfig>;
const host = z
.string({ message: 'zod.userConfig.host' })
.min(1, 'zod.userConfig.hostMin')
.string({ message: t('zod.userConfig.host') })
.min(1, t('zod.userConfig.host'))
.pipe(safeStringRefine);
export const UserConfigSetupSchema = z.object({

2
src/server/routes/cnf/[oneTimeLink].ts

@ -3,7 +3,7 @@ import { OneTimeLinkGetSchema } from '#db/repositories/oneTimeLink/types';
export default defineEventHandler(async (event) => {
const { oneTimeLink } = await getValidatedRouterParams(
event,
validateZod(OneTimeLinkGetSchema)
validateZod(OneTimeLinkGetSchema, event)
);
const clients = await WireGuard.getAllClients();
const client = clients.find(

111
src/server/utils/types.ts

@ -2,46 +2,53 @@ import type { ZodSchema } from 'zod';
import z from 'zod';
import type { H3Event, EventHandlerRequest } from 'h3';
export const objectMessage = 'zod.body';
/**
* return the string as is
*
* used for i18n ally
*/
export const t = (v: string) => v;
export const objectMessage = t('zod.body');
export const safeStringRefine = z
.string()
.refine(
(v) => v !== '__proto__' && v !== 'constructor' && v !== 'prototype',
{ message: 'zod.stringMalformed' }
{ message: t('zod.stringMalformed') }
);
// TODO: create custom getValidatedRouterParams and readValidatedBody wrapper
export const EnabledSchema = z.boolean({ message: 'zod.enabled' });
export const EnabledSchema = z.boolean({ message: t('zod.enabled') });
export const MtuSchema = z
.number({ message: 'zod.mtu' })
.min(1280, { message: 'zod.mtuMin' })
.max(9000, { message: 'zod.mtuMax' });
.number({ message: t('zod.mtu') })
.min(1280, { message: t('zod.mtu') })
.max(9000, { message: t('zod.mtu') });
export const PortSchema = z
.number({ message: 'zod.port' })
.min(1, { message: 'zod.portMin' })
.max(65535, { message: 'zod.portMax' });
.number({ message: t('zod.port') })
.min(1, { message: t('zod.port') })
.max(65535, { message: t('zod.port') });
export const PersistentKeepaliveSchema = z
.number({ message: 'zod.persistentKeepalive' })
.min(0, 'zod.persistentKeepaliveMin')
.max(65535, 'zod.persistentKeepaliveMax');
.number({ message: t('zod.persistentKeepalive') })
.min(0, t('zod.persistentKeepalive'))
.max(65535, t('zod.persistentKeepalive'));
export const AddressSchema = z
.string({ message: 'zod.address' })
.min(1, { message: 'zod.addressMin' })
.string({ message: t('zod.address') })
.min(1, { message: t('zod.address') })
.pipe(safeStringRefine);
export const DnsSchema = z
.array(AddressSchema, { message: 'zod.dns' })
.min(1, 'zod.dnsMin');
.array(AddressSchema, { message: t('zod.dns') })
.min(1, t('zod.dns'));
export const AllowedIpsSchema = z
.array(AddressSchema, { message: 'zod.allowedIps' })
.min(1, { message: 'zod.allowedIpsMin' });
.array(AddressSchema, { message: t('zod.allowedIps') })
.min(1, { message: t('zod.allowedIps') });
export const schemaForType =
<T>() =>
@ -52,26 +59,78 @@ export const schemaForType =
export function validateZod<T>(
schema: ZodSchema<T>,
event?: H3Event<EventHandlerRequest>
event: H3Event<EventHandlerRequest>
) {
return async (data: unknown) => {
let t: null | ((key: string) => string) = null;
if (event) {
t = await useTranslation(event);
}
try {
return await schema.parseAsync(data);
} catch (error) {
let message = 'Unexpected Error';
if (error instanceof z.ZodError) {
const t = await useTranslation(event);
message = error.issues
.map((v) => {
let m = v.message;
if (t) {
m = t(m);
let newMessage = null;
if (v.message.startsWith('zod.')) {
switch (v.code) {
case 'too_small':
switch (v.type) {
case 'string':
newMessage = t('zod.generic.stringMin', [
t(v.message),
v.minimum,
]);
break;
case 'number':
newMessage = t('zod.generic.numberMin', [
t(v.message),
v.minimum,
]);
break;
}
break;
case 'invalid_type': {
if (v.received === 'null' || v.received === 'undefined') {
newMessage = t('zod.generic.required', [
v.path.join('.'),
]);
} else {
switch (v.expected) {
case 'string':
newMessage = t('zod.generic.validString', [
t(v.message),
]);
break;
case 'boolean':
newMessage = t('zod.generic.validBoolean', [
t(v.message),
]);
break;
case 'number':
newMessage = t('zod.generic.validNumber', [
t(v.message),
]);
break;
case 'array':
newMessage = t('zod.generic.validArray', [
t(v.message),
]);
break;
}
}
break;
}
}
}
if (newMessage) {
m = newMessage;
} else {
m = t(v.message);
}
}
return m;

Loading…
Cancel
Save