From 0d82819cba9694bc9fb5a3fa53e2dbeda05d1242 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Tue, 29 Apr 2025 17:03:15 +0530 Subject: [PATCH] chore: release latest code (#1075) --- .pnp.cjs | 1 + ...t-darwin-arm64-npm-2.1.1-7f6025512f-10.zip | Bin 8406 -> 0 bytes .yarn/cache/fsevents-patch-19706e7e35-10.zip | Bin 23750 -> 0 bytes .../Email/daily-analytics-report.html.ts | 4 +- packages/auth/package.json | 2 +- ...offline-subscription-token-created.html.ts | 4 +- ...ed-subscription-invitation-created.html.ts | 4 +- .../Domain/Email/user-email-changed.html.ts | 4 +- .../user-invited-to-shared-vault.html.ts | 4 +- .../src/Domain/Email/user-signed-in.html.ts | 4 +- .../src/Domain/Setting/SettingCrypter.spec.ts | 33 +++- .../auth/src/Domain/Setting/SettingCrypter.ts | 13 ++ .../ActivatePremiumFeatures.ts | 2 +- .../GetUserKeyParams/GetUserKeyParams.ts | 2 +- packages/auth/src/Domain/UseCase/SignIn.ts | 3 +- packages/auth/src/Domain/UseCase/VerifyMFA.ts | 2 +- .../Base/BaseUsersController.ts | 2 +- .../Middleware/LockMiddleware.ts | 2 +- .../common/src/Domain/Html/SafeHtml.spec.ts | 16 ++ packages/common/src/Domain/Html/SafeHtml.ts | 32 ++++ packages/common/src/Domain/index.ts | 1 + .../src/Domain/Common/Username.spec.ts | 174 ++++++++++++++++++ .../domain-core/src/Domain/Common/Username.ts | 40 +++- packages/scheduler/package.json | 1 + .../Email/encourage-email-backups.html.ts | 4 +- .../encourage-subscription-purchasing.html.ts | 4 +- .../src/Domain/Email/exit-interview.html.ts | 4 +- .../Email/dropbox-backup-failed.html.ts | 4 +- .../email-backup-attachment-created.html.ts | 4 +- .../Email/google-drive-backup-failed.html.ts | 4 +- .../Email/one-drive-backup-failed.html.ts | 4 +- yarn.lock | 1 + 32 files changed, 350 insertions(+), 29 deletions(-) delete mode 100644 .yarn/cache/@cbor-extract-cbor-extract-darwin-arm64-npm-2.1.1-7f6025512f-10.zip delete mode 100644 .yarn/cache/fsevents-patch-19706e7e35-10.zip create mode 100644 packages/common/src/Domain/Html/SafeHtml.spec.ts create mode 100644 packages/common/src/Domain/Html/SafeHtml.ts diff --git a/.pnp.cjs b/.pnp.cjs index 57b64b29e..1448be776 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -6817,6 +6817,7 @@ const RAW_RUNTIME_STATE = ["@standardnotes/scheduler-server", "workspace:packages/scheduler"],\ ["@aws-sdk/client-sns", "npm:3.484.0"],\ ["@aws-sdk/client-sqs", "npm:3.484.0"],\ + ["@standardnotes/common", "workspace:packages/common"],\ ["@standardnotes/domain-core", "workspace:packages/domain-core"],\ ["@standardnotes/domain-events", "workspace:packages/domain-events"],\ ["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\ diff --git a/.yarn/cache/@cbor-extract-cbor-extract-darwin-arm64-npm-2.1.1-7f6025512f-10.zip b/.yarn/cache/@cbor-extract-cbor-extract-darwin-arm64-npm-2.1.1-7f6025512f-10.zip deleted file mode 100644 index ed30bfc94888de208e76f8e95c9e324c2b16d135..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8406 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 N85|5uKH1N`{{tdTJSYGF diff --git a/.yarn/cache/fsevents-patch-19706e7e35-10.zip b/.yarn/cache/fsevents-patch-19706e7e35-10.zip deleted file mode 100644 index aff1ab12ce57f312cc3bc58c78597020f7980e62..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23750 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 diff --git a/packages/analytics/src/Domain/Email/daily-analytics-report.html.ts b/packages/analytics/src/Domain/Email/daily-analytics-report.html.ts index 1f63dd30a..4348ceffb 100644 --- a/packages/analytics/src/Domain/Email/daily-analytics-report.html.ts +++ b/packages/analytics/src/Domain/Email/daily-analytics-report.html.ts @@ -5,6 +5,8 @@ import { AnalyticsActivity } from '../Analytics/AnalyticsActivity' import { StatisticMeasureName } from '../Statistics/StatisticMeasureName' import { Period } from '../Time/Period' +import { safeHtml } from '@standardnotes/common' + const countActiveUsers = (measureName: string, data: any): { yesterday: number; last30Days: number } => { const totalActiveUsersLast30DaysIncludingToday = data.statisticsOverTime.find( (a: { name: string; period: number }) => a.name === measureName && a.period === 27, @@ -567,7 +569,7 @@ export const html = (data: any, timer: TimerInterface) => { const totalActivePlusUsers = countActiveUsers(StatisticMeasureName.NAMES.ActivePlusUsers, data) const totalActiveProUsers = countActiveUsers(StatisticMeasureName.NAMES.ActiveProUsers, data) - return `
+ return safeHtml`

Hello,

Here are some statistics from yesterday: diff --git a/packages/auth/package.json b/packages/auth/package.json index 60467098c..a9960161d 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -4,7 +4,7 @@ "engines": { "node": ">=18.0.0 <21.0.0" }, - "description": "Auth Server", + "description": "Auth Server for SN", "main": "dist/src/index.js", "typings": "dist/src/index.d.ts", "author": "Karol Sójko ", diff --git a/packages/auth/src/Domain/Email/offline-subscription-token-created.html.ts b/packages/auth/src/Domain/Email/offline-subscription-token-created.html.ts index 6e7497ba4..99a250461 100644 --- a/packages/auth/src/Domain/Email/offline-subscription-token-created.html.ts +++ b/packages/auth/src/Domain/Email/offline-subscription-token-created.html.ts @@ -1,4 +1,6 @@ -export const html = (userEmail: string, offlineSubscriptionDashboardUrl: string) => `

+import { safeHtml } from '@standardnotes/common' + +export const html = (userEmail: string, offlineSubscriptionDashboardUrl: string) => safeHtml`
diff --git a/packages/auth/src/Domain/Email/shared-subscription-invitation-created.html.ts b/packages/auth/src/Domain/Email/shared-subscription-invitation-created.html.ts index 2ed1bacb8..422a0a5e8 100644 --- a/packages/auth/src/Domain/Email/shared-subscription-invitation-created.html.ts +++ b/packages/auth/src/Domain/Email/shared-subscription-invitation-created.html.ts @@ -1,4 +1,6 @@ -export const html = (inviterIdentifier: string, inviteUuid: string) => `

Hello,

+import { safeHtml } from '@standardnotes/common' + +export const html = (inviterIdentifier: string, inviteUuid: string) => safeHtml`

Hello,

You've been invited to join a Standard Notes premium subscription at no cost. ${inviterIdentifier} has invited you to share the benefits of their subscription plan.

Accept Invite diff --git a/packages/auth/src/Domain/Email/user-email-changed.html.ts b/packages/auth/src/Domain/Email/user-email-changed.html.ts index c8f7d2d35..c275bc780 100644 --- a/packages/auth/src/Domain/Email/user-email-changed.html.ts +++ b/packages/auth/src/Domain/Email/user-email-changed.html.ts @@ -1,4 +1,6 @@ -export const html = (newEmail: string) => ` +import { safeHtml } from '@standardnotes/common' + +export const html = (newEmail: string) => safeHtml`

Hello,

We are writing to inform you that your request to update your email address has been successfully processed. The email address associated with your Standard Notes account has now been changed to the following:

diff --git a/packages/auth/src/Domain/Email/user-invited-to-shared-vault.html.ts b/packages/auth/src/Domain/Email/user-invited-to-shared-vault.html.ts index bcd1be005..8bca278bc 100644 --- a/packages/auth/src/Domain/Email/user-invited-to-shared-vault.html.ts +++ b/packages/auth/src/Domain/Email/user-invited-to-shared-vault.html.ts @@ -1,4 +1,6 @@ -export const html = () => ` +import { safeHtml } from '@standardnotes/common' + +export const html = () => safeHtml`

Hello,

You've been invited to join a shared vault. This shared workspace will help you collaborate and securely manage notes and files.

diff --git a/packages/auth/src/Domain/Email/user-signed-in.html.ts b/packages/auth/src/Domain/Email/user-signed-in.html.ts index c72b9210e..a2be181d7 100644 --- a/packages/auth/src/Domain/Email/user-signed-in.html.ts +++ b/packages/auth/src/Domain/Email/user-signed-in.html.ts @@ -1,4 +1,6 @@ -export const html = (email: string, device: string, browser: string, timeAndDate: string) => ` +import { safeHtml } from '@standardnotes/common' + +export const html = (email: string, device: string, browser: string, timeAndDate: string) => safeHtml`

Hello,

We've detected a new sign-in to your account ${email}

diff --git a/packages/auth/src/Domain/Setting/SettingCrypter.spec.ts b/packages/auth/src/Domain/Setting/SettingCrypter.spec.ts index 7fb7d4451..1abed0a4f 100644 --- a/packages/auth/src/Domain/Setting/SettingCrypter.spec.ts +++ b/packages/auth/src/Domain/Setting/SettingCrypter.spec.ts @@ -13,6 +13,8 @@ describe('SettingCrypter', () => { let userRepository: UserRepositoryInterface let crypter: CrypterInterface let user: User + const encryptedValue = + '{"version":1,"encrypted":{"iv":"foobar","tag":"foobar","aad":"","ciphertext":"foobar","encoding":"utf-8"}}' const createDecrypter = () => new SettingCrypter(userRepository, crypter) @@ -32,14 +34,14 @@ describe('SettingCrypter', () => { it('should encrypt a string value', async () => { const string = 'decrypted' - crypter.encryptForUser = jest.fn().mockReturnValue('encrypted') + crypter.encryptForUser = jest.fn().mockReturnValue(encryptedValue) const encrypted = await createDecrypter().encryptValue( string, Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), ) - expect(encrypted).toEqual('encrypted') + expect(encrypted).toEqual(encryptedValue) }) it('should return null when trying to encrypt a null value', async () => { @@ -67,7 +69,7 @@ describe('SettingCrypter', () => { it('should decrypt an encrypted value of a setting', async () => { const setting = Setting.create({ name: SettingName.NAMES.ListedAuthorSecrets, - value: 'encrypted', + value: encryptedValue, serverEncryptionVersion: EncryptionVersion.Default, userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), sensitive: false, @@ -107,10 +109,25 @@ describe('SettingCrypter', () => { ) }) + it('should return unencrypted value if the setting has unencrypted value but the encryption version indicates otherwise', async () => { + const setting = Setting.create({ + name: SettingName.NAMES.ListedAuthorSecrets, + value: 'test', + serverEncryptionVersion: EncryptionVersion.Default, + userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), + sensitive: false, + timestamps: Timestamps.create(123, 123).getValue(), + }).getValue() + + expect(await createDecrypter().decryptSettingValue(setting, '00000000-0000-0000-0000-000000000000')).toEqual( + 'test', + ) + }) + it('should throw if the user could not be found', async () => { const setting = Setting.create({ name: SettingName.NAMES.ListedAuthorSecrets, - value: 'encrypted', + value: encryptedValue, serverEncryptionVersion: EncryptionVersion.Default, userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), sensitive: false, @@ -131,7 +148,7 @@ describe('SettingCrypter', () => { it('should throw if the user uuid is invalid', async () => { const setting = Setting.create({ name: SettingName.NAMES.ListedAuthorSecrets, - value: 'encrypted', + value: encryptedValue, serverEncryptionVersion: EncryptionVersion.Default, userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), sensitive: false, @@ -153,7 +170,7 @@ describe('SettingCrypter', () => { it('should decrypt an encrypted value of a setting', async () => { const setting = SubscriptionSetting.create({ name: SettingName.NAMES.ExtensionKey, - value: 'encrypted', + value: encryptedValue, sensitive: true, serverEncryptionVersion: EncryptionVersion.Default, userSubscriptionUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), @@ -198,7 +215,7 @@ describe('SettingCrypter', () => { it('should throw if the user could not be found', async () => { const setting = SubscriptionSetting.create({ name: SettingName.NAMES.ExtensionKey, - value: 'encrypted', + value: encryptedValue, sensitive: true, serverEncryptionVersion: EncryptionVersion.Default, userSubscriptionUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), @@ -219,7 +236,7 @@ describe('SettingCrypter', () => { it('should throw if the user uuid is invalid', async () => { const setting = SubscriptionSetting.create({ name: SettingName.NAMES.ExtensionKey, - value: 'encrypted', + value: encryptedValue, sensitive: true, serverEncryptionVersion: EncryptionVersion.Default, userSubscriptionUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), diff --git a/packages/auth/src/Domain/Setting/SettingCrypter.ts b/packages/auth/src/Domain/Setting/SettingCrypter.ts index 66ed243b2..02a5385c0 100644 --- a/packages/auth/src/Domain/Setting/SettingCrypter.ts +++ b/packages/auth/src/Domain/Setting/SettingCrypter.ts @@ -52,9 +52,22 @@ export class SettingCrypter implements SettingCrypterInterface { throw new Error(`Could not find user with uuid: ${userUuid.value}`) } + if (!this.isValidJSONSubjectForDecryption(value)) { + return value + } + return this.crypter.decryptForUser(value, user) } return value } + + private isValidJSONSubjectForDecryption(value: string): boolean { + try { + JSON.parse(value) + return true + } catch (error) { + return false + } + } } diff --git a/packages/auth/src/Domain/UseCase/ActivatePremiumFeatures/ActivatePremiumFeatures.ts b/packages/auth/src/Domain/UseCase/ActivatePremiumFeatures/ActivatePremiumFeatures.ts index 4efdd54b6..556f51a71 100644 --- a/packages/auth/src/Domain/UseCase/ActivatePremiumFeatures/ActivatePremiumFeatures.ts +++ b/packages/auth/src/Domain/UseCase/ActivatePremiumFeatures/ActivatePremiumFeatures.ts @@ -19,7 +19,7 @@ export class ActivatePremiumFeatures implements UseCaseInterface { ) {} async execute(dto: ActivatePremiumFeaturesDTO): Promise> { - const usernameOrError = Username.create(dto.username) + const usernameOrError = Username.create(dto.username, { skipValidation: true }) if (usernameOrError.isFailed()) { return Result.fail(usernameOrError.getError()) } diff --git a/packages/auth/src/Domain/UseCase/GetUserKeyParams/GetUserKeyParams.ts b/packages/auth/src/Domain/UseCase/GetUserKeyParams/GetUserKeyParams.ts index 17818f6bf..81f708857 100644 --- a/packages/auth/src/Domain/UseCase/GetUserKeyParams/GetUserKeyParams.ts +++ b/packages/auth/src/Domain/UseCase/GetUserKeyParams/GetUserKeyParams.ts @@ -22,7 +22,7 @@ export class GetUserKeyParams implements UseCaseInterface { async execute(dto: GetUserKeyParamsDTO): Promise { let user: User | null = null if (dto.email !== undefined) { - const usernameOrError = Username.create(dto.email) + const usernameOrError = Username.create(dto.email, { skipValidation: true }) if (usernameOrError.isFailed()) { throw Error(usernameOrError.getError()) } diff --git a/packages/auth/src/Domain/UseCase/SignIn.ts b/packages/auth/src/Domain/UseCase/SignIn.ts index 80c3f19e2..1d7ab1fa5 100644 --- a/packages/auth/src/Domain/UseCase/SignIn.ts +++ b/packages/auth/src/Domain/UseCase/SignIn.ts @@ -62,7 +62,8 @@ export class SignIn implements UseCaseInterface { } const apiVersion = apiVersionOrError.getValue() - const usernameOrError = Username.create(dto.email) + /** Skip validation which was newly added in 2025, to allow existing users to continue to sign in */ + const usernameOrError = Username.create(dto.email, { skipValidation: true }) if (usernameOrError.isFailed()) { return { success: false, diff --git a/packages/auth/src/Domain/UseCase/VerifyMFA.ts b/packages/auth/src/Domain/UseCase/VerifyMFA.ts index 3afb076ba..2ef4cc1f8 100644 --- a/packages/auth/src/Domain/UseCase/VerifyMFA.ts +++ b/packages/auth/src/Domain/UseCase/VerifyMFA.ts @@ -32,7 +32,7 @@ export class VerifyMFA implements UseCaseInterface { async execute(dto: VerifyMFADTO): Promise { try { - const usernameOrError = Username.create(dto.email) + const usernameOrError = Username.create(dto.email, { skipValidation: true }) if (usernameOrError.isFailed()) { return { success: false, diff --git a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseUsersController.ts b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseUsersController.ts index f78a7e76b..7834e3f9f 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseUsersController.ts +++ b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseUsersController.ts @@ -135,7 +135,7 @@ export class BaseUsersController extends BaseHttpController { 400, ) } - const usernameOrError = Username.create(locals.user.email) + const usernameOrError = Username.create(locals.user.email, { skipValidation: true }) if (usernameOrError.isFailed()) { return this.json( { diff --git a/packages/auth/src/Infra/InversifyExpressUtils/Middleware/LockMiddleware.ts b/packages/auth/src/Infra/InversifyExpressUtils/Middleware/LockMiddleware.ts index 6bf265749..2474c5e04 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/Middleware/LockMiddleware.ts +++ b/packages/auth/src/Infra/InversifyExpressUtils/Middleware/LockMiddleware.ts @@ -19,7 +19,7 @@ export class LockMiddleware extends BaseMiddleware { async handler(request: Request, response: Response, next: NextFunction): Promise { try { let identifier = request.body.email ?? request.body.username - const usernameOrError = Username.create(identifier) + const usernameOrError = Username.create(identifier, { skipValidation: true }) if (usernameOrError.isFailed()) { response.status(400).send({ error: { diff --git a/packages/common/src/Domain/Html/SafeHtml.spec.ts b/packages/common/src/Domain/Html/SafeHtml.spec.ts new file mode 100644 index 000000000..5d65a56aa --- /dev/null +++ b/packages/common/src/Domain/Html/SafeHtml.spec.ts @@ -0,0 +1,16 @@ +import { safeHtml } from './SafeHtml' + +describe('html', () => { + test('Should escape html from user input', () => { + const basicStringInput = '

User

' + const numberValue = 10 + expect(safeHtml`

Hello world, ${basicStringInput} ${numberValue}

`).toBe( + '

Hello world, <h1>User</h1> 10

', + ) + }) + + test('Should join arrays and escape', () => { + const arrayOfStrings = ['

User

', '

Test

'] + expect(safeHtml`

${arrayOfStrings}

`).toBe('

<h1>User</h1><p>Test</p>

') + }) +}) diff --git a/packages/common/src/Domain/Html/SafeHtml.ts b/packages/common/src/Domain/Html/SafeHtml.ts new file mode 100644 index 000000000..e49bd13c8 --- /dev/null +++ b/packages/common/src/Domain/Html/SafeHtml.ts @@ -0,0 +1,32 @@ +function escapeHTML(str: string) { + return str + .replace(/&/g, '&') + .replace(/>/g, '>') + .replace(/) { + const raw = literals.raw + + let result = raw[0] + + for (let index = 1; index < raw.length; index++) { + const literal = raw[index] + let substitution = substitutions[index - 1] + if (Array.isArray(substitution)) { + substitution = substitution.join('') + } else if (typeof substitution === 'number') { + substitution = substitution.toString() + } + substitution = escapeHTML(substitution) + result += substitution + literal + } + + return result +} diff --git a/packages/common/src/Domain/index.ts b/packages/common/src/Domain/index.ts index d788e6484..c6fe229d0 100644 --- a/packages/common/src/Domain/index.ts +++ b/packages/common/src/Domain/index.ts @@ -18,3 +18,4 @@ export * from './Subscription/SubscriptionName' export * from './Type/Either' export * from './Type/Only' export * from './User/UserRequestType' +export * from './Html/SafeHtml' diff --git a/packages/domain-core/src/Domain/Common/Username.spec.ts b/packages/domain-core/src/Domain/Common/Username.spec.ts index bfe3fdeeb..936fa1559 100644 --- a/packages/domain-core/src/Domain/Common/Username.spec.ts +++ b/packages/domain-core/src/Domain/Common/Username.spec.ts @@ -31,4 +31,178 @@ describe('Username', () => { expect(value.isPotentiallyAPrivateUsernameAccount()).toBeFalsy() }) + + describe('username validation', () => { + describe('valid usernames', () => { + const validUsernames = [ + 'johndoe', + 'john_doe', + 'john.doe', + 'john-doe', + 'john@doe', + 'john123', + 'j0hn.d0e', + 'user+name', + 'username_with_single_underscore', + // Maximum length + 'a'.repeat(100), + // Minimum length + 'abc', + // Email variants + 'user@example.com', + 'user.name@example.com', + 'user+test@example.com', + 'user-name@example.com', + 'user_name@example.com', + 'user123@example.com', + 'u@example.com', + 'user@sub.example.com', + 'user@example-site.com', + 'user@example.co.uk', + 'user+test+extra@example.com', + 'user-name-extra@example.com', + 'user.name.extra@example.com', + 'user.name+test-extra@example.com', + 'user-name.test+extra@example.com', + ] + + test.each(validUsernames)('should accept valid username: %s', (username) => { + const result = Username.create(username) + expect(result.isFailed()).toBeFalsy() + expect(result.getValue().value).toBe(username.toLowerCase()) + }) + }) + + describe('invalid usernames', () => { + const invalidUsernames = [ + // Length violations + ['ab', 'Username must be at least 3 characters long'], + ['a'.repeat(101), 'Username cannot be longer than 100 characters'], + + // Empty or whitespace + ['', 'Username cannot be empty'], + [' ', 'Username cannot be empty'], + [' ', 'Username cannot be empty'], + + // Whitespace in username + ['user name', 'Username can only contain letters, numbers, and the following special characters: . _ - @ +'], + ['user\tname', 'Username can only contain letters, numbers, and the following special characters: . _ - @ +'], + ['user\nname', 'Username can only contain letters, numbers, and the following special characters: . _ - @ +'], + + // Starting/ending with special characters + [ + '_username', + 'Username cannot start or end with special characters, and cannot have consecutive special characters', + ], + [ + 'username_', + 'Username cannot start or end with special characters, and cannot have consecutive special characters', + ], + [ + '.username', + 'Username cannot start or end with special characters, and cannot have consecutive special characters', + ], + [ + 'username.', + 'Username cannot start or end with special characters, and cannot have consecutive special characters', + ], + + // Consecutive special characters + [ + 'user__name', + 'Username cannot start or end with special characters, and cannot have consecutive special characters', + ], + [ + 'user..name', + 'Username cannot start or end with special characters, and cannot have consecutive special characters', + ], + [ + 'user.-name', + 'Username cannot start or end with special characters, and cannot have consecutive special characters', + ], + + // Invalid special characters + ['user{name}', 'Username can only contain letters, numbers, and the following special characters: . _ - @ +'], + ['user#name', 'Username can only contain letters, numbers, and the following special characters: . _ - @ +'], + ['user$name', 'Username can only contain letters, numbers, and the following special characters: . _ - @ +'], + ['user&name', 'Username can only contain letters, numbers, and the following special characters: . _ - @ +'], + ['user*name', 'Username can only contain letters, numbers, and the following special characters: . _ - @ +'], + ['user!name', 'Username can only contain letters, numbers, and the following special characters: . _ - @ +'], + ['user/name', 'Username can only contain letters, numbers, and the following special characters: . _ - @ +'], + ['user\\name', 'Username can only contain letters, numbers, and the following special characters: . _ - @ +'], + ['user"name', 'Username can only contain letters, numbers, and the following special characters: . _ - @ +'], + ["user'name", 'Username can only contain letters, numbers, and the following special characters: . _ - @ +'], + ['user:name', 'Username can only contain letters, numbers, and the following special characters: . _ - @ +'], + ['user=name', 'Username can only contain letters, numbers, and the following special characters: . _ - @ +'], + + // HTML-like patterns + ['