mirror of
https://github.com/docmost/docmost.git
synced 2026-06-10 18:16:57 +08:00
feat(cloud): add find-workspace and email verification endpoints (#2020)
* feat: add find-workspace and email verification endpoints * sync
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
// MIT - https://github.com/typestack/class-validator/pull/2626
|
||||
import isISO6391Validator from 'validator/lib/isISO6391';
|
||||
import { buildMessage, ValidateBy, ValidationOptions } from 'class-validator';
|
||||
|
||||
export const IS_ISO6391 = 'isISO6391';
|
||||
|
||||
/**
|
||||
* Check if the string is a valid [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1) officially assigned language code.
|
||||
*/
|
||||
export function isISO6391(value: unknown): boolean {
|
||||
return typeof value === 'string' && isISO6391Validator(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the string is a valid [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1) officially assigned language code.
|
||||
*/
|
||||
export function IsISO6391(
|
||||
validationOptions?: ValidationOptions,
|
||||
): PropertyDecorator {
|
||||
return ValidateBy(
|
||||
{
|
||||
name: IS_ISO6391,
|
||||
validator: {
|
||||
validate: (value, args): boolean => isISO6391(value),
|
||||
defaultMessage: buildMessage(
|
||||
(eachPrefix) =>
|
||||
eachPrefix + '$property must be a valid ISO 639-1 language code',
|
||||
validationOptions,
|
||||
),
|
||||
},
|
||||
},
|
||||
validationOptions,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import { containsDomain } from './no-urls.validator';
|
||||
|
||||
// containsDomain returns true if value contains a domain-like pattern
|
||||
// The full NoUrls validator also checks for https:// URLs separately
|
||||
|
||||
describe('containsDomain', () => {
|
||||
describe('bare domains with real TLDs — should block', () => {
|
||||
it.each([
|
||||
'example.com',
|
||||
'example.net',
|
||||
'example.org',
|
||||
'example.io',
|
||||
'example.co',
|
||||
'example.dev',
|
||||
'example.app',
|
||||
'example.me',
|
||||
'example.info',
|
||||
'example.tech',
|
||||
'example.aero',
|
||||
'example.cloud',
|
||||
'example.museum',
|
||||
'example.abc',
|
||||
'example.uk',
|
||||
'example.de',
|
||||
'example.fr',
|
||||
'example.ru',
|
||||
])('blocks "%s"', (value) => {
|
||||
expect(containsDomain(value)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('domains with paths — should block', () => {
|
||||
it.each([
|
||||
'example.com/reset',
|
||||
'example.com/reset-password',
|
||||
'click example.com/page',
|
||||
'go to example.net/login',
|
||||
])('blocks "%s"', (value) => {
|
||||
expect(containsDomain(value)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('multi-part domains — should block', () => {
|
||||
it.each([
|
||||
'Foo.com.net',
|
||||
'Foo.com.',
|
||||
'Foo.mine.net',
|
||||
'Foo.mine.ne',
|
||||
'sub.example.com',
|
||||
'login.example.co.uk',
|
||||
])('blocks "%s"', (value) => {
|
||||
expect(containsDomain(value)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('domain in sentence — should block', () => {
|
||||
it.each([
|
||||
'Reset your password at example.com',
|
||||
'URGENT click example.com/reset',
|
||||
'Visit example.org for details',
|
||||
'go to mysite.io now',
|
||||
])('blocks "%s"', (value) => {
|
||||
expect(containsDomain(value)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('case insensitive — should block', () => {
|
||||
it.each(['EXAMPLE.COM', 'Example.Com', 'example.COM'])('blocks "%s"', (value) => {
|
||||
expect(containsDomain(value)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fake TLDs — should allow', () => {
|
||||
it.each([
|
||||
'Foo.mine',
|
||||
'Foo.blarg',
|
||||
'Foo.qqq',
|
||||
'Foo.zz',
|
||||
'Foo.abcd',
|
||||
'Foo.abcde',
|
||||
'Foo.abcdef',
|
||||
'Foo.abcdefg',
|
||||
])('allows "%s"', (value) => {
|
||||
expect(containsDomain(value)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('too short suffix — should allow', () => {
|
||||
it.each(['Foo.a', 'Foo.c', 'A.B'])('allows "%s"', (value) => {
|
||||
expect(containsDomain(value)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('multi-part with fake TLD — should allow', () => {
|
||||
it.each(['Foo.mine.', 'Foo.mine.n'])('allows "%s"', (value) => {
|
||||
expect(containsDomain(value)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('emails — should allow', () => {
|
||||
it.each([
|
||||
'user@example.com',
|
||||
'admin@company.org',
|
||||
'test@sub.domain.co.uk',
|
||||
])('allows "%s"', (value) => {
|
||||
expect(containsDomain(value)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('normal names — should allow', () => {
|
||||
it.each([
|
||||
'John Smith',
|
||||
'Dr. Smith',
|
||||
'A. B. Charlie',
|
||||
'John',
|
||||
'Mary Jane',
|
||||
"O'Brien",
|
||||
'Jean-Pierre',
|
||||
'José García',
|
||||
])('allows "%s"', (value) => {
|
||||
expect(containsDomain(value)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('IP addresses — should allow', () => {
|
||||
it.each(['192.168.1.1', '10.0.0.1', '127.0.0.1'])(
|
||||
'allows "%s"',
|
||||
(value) => {
|
||||
expect(containsDomain(value)).toBe(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('edge cases — should allow', () => {
|
||||
it.each(['', ' ', '.', '..', 'hello', '.com', 'a.b'])(
|
||||
'allows "%s"',
|
||||
(value) => {
|
||||
expect(containsDomain(value)).toBe(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { registerDecorator, ValidationOptions } from 'class-validator';
|
||||
import * as tlds from 'tlds';
|
||||
|
||||
const URL_PATTERN = /https?:\/\//i;
|
||||
const tldSet = new Set(tlds.map((t) => t.toLowerCase()));
|
||||
|
||||
export function containsDomain(value: string): boolean {
|
||||
const tokens = value.split(/\s+/);
|
||||
for (const token of tokens) {
|
||||
if (token.includes('@')) continue;
|
||||
const segments = token.split('.');
|
||||
for (let i = 1; i < segments.length; i++) {
|
||||
const suffix = segments[i].replace(/[^\w].*/g, '');
|
||||
if (segments[i - 1] && suffix && tldSet.has(suffix.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function NoUrls(validationOptions?: ValidationOptions) {
|
||||
return function (object: object, propertyName: string) {
|
||||
registerDecorator({
|
||||
name: 'noUrls',
|
||||
target: object.constructor,
|
||||
propertyName,
|
||||
options: {
|
||||
message: 'Must not contain URLs or domain names',
|
||||
...validationOptions,
|
||||
},
|
||||
validator: {
|
||||
validate(value: unknown) {
|
||||
if (typeof value !== 'string') return true;
|
||||
if (URL_PATTERN.test(value)) return false;
|
||||
if (containsDomain(value)) return false;
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user