diff --git a/api/bun.lock b/api/bun.lock index 6dd19d982..e41c5a34f 100644 --- a/api/bun.lock +++ b/api/bun.lock @@ -4,6 +4,7 @@ "": { "name": "api", "dependencies": { + "@2toad/profanity": "^3.1.1", "@elysiajs/cors": "^1.2.0", "@elysiajs/jwt": "^1.2.0", "@sinclair/typebox": "^0.34.14", @@ -13,6 +14,7 @@ "ky": "^1.7.4", "luxon": "^3.5.0", "pg": "^8.13.1", + "the-big-username-blacklist": "^1.5.2", }, "devDependencies": { "@eslint/js": "^9.19.0", @@ -31,6 +33,8 @@ }, }, "packages": { + "@2toad/profanity": ["@2toad/profanity@3.1.1", "", {}, "sha512-07ny4pCSa4gDrcJ4vZ/WWmiM90+8kv/clXfnDvThf9IJq0GldpjRVdzHCfMwGDs2Y/8eClmTGzKb5tEfUWy/uA=="], + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, ""], "@elysiajs/cors": ["@elysiajs/cors@1.2.0", "", { "peerDependencies": { "elysia": ">= 1.2.0" } }, ""], @@ -331,6 +335,8 @@ "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "the-big-username-blacklist": ["the-big-username-blacklist@1.5.2", "", {}, "sha512-bKRIZbu3AoDhEkjNcErodWLpR18vZQQqg9DEab/zELgGw++M1x0KBeTGdoEPHPw0ghmx1jf/B6kZKuwDDPhGBQ=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "ts-api-utils": ["ts-api-utils@2.0.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ=="], diff --git a/api/index.d.ts b/api/index.d.ts new file mode 100644 index 000000000..1304d27aa --- /dev/null +++ b/api/index.d.ts @@ -0,0 +1 @@ +declare module 'the-big-username-blacklist' diff --git a/api/package.json b/api/package.json index 124430a80..c70632209 100644 --- a/api/package.json +++ b/api/package.json @@ -30,6 +30,7 @@ "typescript": "^5.7.3" }, "dependencies": { + "@2toad/profanity": "^3.1.1", "@elysiajs/cors": "^1.2.0", "@elysiajs/jwt": "^1.2.0", "@sinclair/typebox": "^0.34.14", @@ -38,6 +39,7 @@ "elysia": "^1.2.10", "ky": "^1.7.4", "luxon": "^3.5.0", - "pg": "^8.13.1" + "pg": "^8.13.1", + "the-big-username-blacklist": "^1.5.2" } } diff --git a/api/src/routes/auth.ts b/api/src/routes/auth.ts index 076672a9a..41a5dcd6f 100644 --- a/api/src/routes/auth.ts +++ b/api/src/routes/auth.ts @@ -6,7 +6,8 @@ import { signinResponse, signUpBody, signinBody, - podProviderEndpoint + podProviderEndpoint, + ApiSignUpErrors } from '../types' import { eq } from 'drizzle-orm' import Elysia, { t } from 'elysia' @@ -87,20 +88,29 @@ const authPlugin = new Elysia({ name: 'auth' }) ) .post( '/signup', - async ({ body, error, headers: { auth }, jwt }) => { + async ({ body, error, headers: { auth }, jwt, profanity }) => { // check if user is already logged in if (auth && (await jwt.verify(auth))) { return "You're already logged in" } const { username, password, email, providerName } = body + // check if username or email is profane + if ((profanity.exists(username), profanity.exists(email))) { + return error(400, ApiSignUpErrors.UsernameOrEmailContainsProfanity) + } + // check if username contains unwanted characters + const unwantedChars = new RegExp(/[@#/\\$%^&*!?<>+~=]/g) + if (unwantedChars.test(username)) { + return error(400, ApiSignUpErrors.UsernameInvalid) + } // try to sign up the user with the current provider try { const providerResponse = await ActivityPod.signup(podProviderEndpoint[providerName], username, password, email) let userResponse: SelectUsers[] = [] if (providerResponse.token === undefined) { - return error(400, 'Provider did not return a token') + return error(400, ApiSignUpErrors.ProviderToken) } else { // the provider created a new user, so we need to create a new user in the database try { @@ -121,7 +131,7 @@ const authPlugin = new Elysia({ name: 'auth' }) } } catch (e) { console.error('Error while checking if user is in the database: ', e) - return error(500, 'Error while checking user') + return error(500, ApiSignUpErrors.DBError) } const tokenObject = getTokenObject(new User(userResponse[0], providerResponse.token)) const authToken = await jwt.sign(tokenObject) @@ -138,7 +148,7 @@ const authPlugin = new Elysia({ name: 'auth' }) return error(errorJson.code, errorJson.message) } console.error('Error while signing up the user', e) - return error(400, 'Error with the provider') + return error(400, ApiSignUpErrors.ProviderDefault) } }, { @@ -146,7 +156,7 @@ const authPlugin = new Elysia({ name: 'auth' }) body: signUpBody, response: { 200: signinResponse, - 400: t.String(), + 400: t.Enum(ApiSignUpErrors), 500: t.String() } } diff --git a/api/src/routes/setup.ts b/api/src/routes/setup.ts index e6377bc32..bbac186f4 100644 --- a/api/src/routes/setup.ts +++ b/api/src/routes/setup.ts @@ -1,7 +1,9 @@ +import { Profanity } from '@2toad/profanity' import User from '../decorater/User' import cors from '@elysiajs/cors' import jwt from '@elysiajs/jwt' import Elysia from 'elysia' +import { list } from 'the-big-username-blacklist' const setupPlugin = new Elysia({ name: 'setup' }) .use( @@ -12,6 +14,14 @@ const setupPlugin = new Elysia({ name: 'setup' }) ) .use(cors()) .decorate('user', new User()) + .decorate('profanity', () => { + const profanity = new Profanity({ + languages: ['en', 'de', 'fr', 'ja', 'pt', 'es', 'ru', 'ar', 'ko'], + wholeWord: false + }) + profanity.addWords(list) + return profanity + }) .macro({ isSignedIn: enabled => { if (!enabled) return diff --git a/api/src/routes/users.ts b/api/src/routes/users.ts index 8b2184321..3b0c51ab7 100644 --- a/api/src/routes/users.ts +++ b/api/src/routes/users.ts @@ -116,7 +116,6 @@ export default new Elysia({ name: 'user', prefix: '/user' }) } return returnObject } catch (_) { - console.log(_) return error(400, FollowErrors.NotValidProvider) } }, diff --git a/api/src/test/profanity.test.ts b/api/src/test/profanity.test.ts new file mode 100644 index 000000000..ce679a779 --- /dev/null +++ b/api/src/test/profanity.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'bun:test' +import { getProfanity } from '@/util' + +describe('Check profanity module', () => { + it('should return false for strings without profanity', () => { + const notProfane = [ + 'Taiwan', + 'Taiwanese', + 'Taiwanesepeople', + 'Protestant', + 'Protestantism', + 'Transgender', + 'Hongkong', + 'HongKong', + 'HongKongpeople', + 'Taiwanisafreecountry' + ] + + const profanity = getProfanity() + + notProfane.forEach(word => { + expect(profanity.exists(word)).toBe(false) + }) + }) +}) diff --git a/api/src/types/errors.ts b/api/src/types/errors.ts index 47ab290e7..d6850149a 100644 --- a/api/src/types/errors.ts +++ b/api/src/types/errors.ts @@ -6,6 +6,14 @@ export enum FollowErrors { IsSelf = 'Cannot follow yourself.' } +export enum ApiSignUpErrors { + UsernameInvalid = 'Username is invalid', + UsernameOrEmailContainsProfanity = 'Username or email contains profanity', + ProviderToken = 'Provider did not return a token', + DBError = 'Error while checking user', + ProviderDefault = 'Error with the provider' +} + // Errors that are thrown by the pod provider export enum ProviderSignUpErrors { providerSignUpDefault = 'Error while signing up the user', diff --git a/api/src/util/index.ts b/api/src/util/index.ts new file mode 100644 index 000000000..4d8e08037 --- /dev/null +++ b/api/src/util/index.ts @@ -0,0 +1,17 @@ +export * from './user' +import { Profanity } from '@2toad/profanity' +import { List } from '@2toad/profanity/dist/models' +import { list } from 'the-big-username-blacklist' + +export function getProfanity() { + const whiteList = new List(() => true) + whiteList.addWords(['taiwan']) + const profanity = new Profanity({ + languages: ['en', 'de', 'fr', 'ja', 'pt', 'es', 'ru', 'ar', 'ko'], + wholeWord: true + }) + profanity.addWords(list) + profanity.whitelist = whiteList + + return profanity +} diff --git a/frontend/bun.lockb b/frontend/bun.lockb index 3da09f7bf..fb24196d7 100755 Binary files a/frontend/bun.lockb and b/frontend/bun.lockb differ diff --git a/frontend/env.d.ts b/frontend/env.d.ts index fdd16900c..54e850141 100644 --- a/frontend/env.d.ts +++ b/frontend/env.d.ts @@ -1,6 +1,7 @@ /// declare module 'vue-iconsax' +declare module 'the-big-username-blacklist' interface ImportMetaEnv { readonly VITE_API_URL: string diff --git a/frontend/package.json b/frontend/package.json index 84e2d7d15..0f07bfbd5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,6 +19,7 @@ "open:ios": "cap open ios" }, "dependencies": { + "@2toad/profanity": "^3.1.1", "@capacitor/android": "^7.0.1", "@capacitor/core": "^7.0.1", "@capacitor/ios": "^7.0.1", @@ -28,6 +29,7 @@ "luxon": "^3.5.0", "pinia": "^2.3.1", "tailwindcss": "^4.0.0", + "the-big-username-blacklist": "^1.5.2", "vue": "^3.5.13", "vue-iconsax": "^2.0.0", "vue-router": "^4.5.0", diff --git a/frontend/src/components/SignupForm.vue b/frontend/src/components/SignupForm.vue index 2ac95e9ee..033325fc3 100644 --- a/frontend/src/components/SignupForm.vue +++ b/frontend/src/components/SignupForm.vue @@ -4,8 +4,7 @@ import { useAuthStore } from '@/stores/authStore' import MemoryButton from '@/components/MemoryButton.vue' import MemoryInput from '@/components/MemoryInput.vue' import { ref } from 'vue' -import { ProviderSignUpErrors } from '@/types' -import { ViablePodProvider } from '#api/types' +import { ViablePodProvider, ProviderSignUpErrors, ApiSignUpErrors } from '#api/types' // Store const authStore = useAuthStore() @@ -31,7 +30,6 @@ async function submitForm() { const authResponse = await authStore.signup(email.value, username.value, password.value, endpoint.value) // if the response is a string, it means that there was an error if (authResponse) { - console.log(authResponse) switch (authResponse) { case ProviderSignUpErrors['username.already.exists']: case ProviderSignUpErrors['username.invalid']: @@ -41,6 +39,10 @@ async function submitForm() { case ProviderSignUpErrors['email.invalid']: errorMessages.value.email = authResponse break + case ApiSignUpErrors.UsernameOrEmailContainsProfanity: + case ApiSignUpErrors.UsernameInvalid: + errorMessages.value.username = authResponse + break default: errorMessages.value.default = authResponse break diff --git a/frontend/src/controller/api.ts b/frontend/src/controller/api.ts index 6e9372855..fbdd898d7 100644 --- a/frontend/src/controller/api.ts +++ b/frontend/src/controller/api.ts @@ -86,8 +86,8 @@ export class ApiClient { } } } - console.log('error when requesting api: ', e) - console.log(await e.response.text()) + console.error('error when requesting api: ', e) + console.error(await e.response.text()) return { data: ApiErrorsGeneral.default, status: e.response.status diff --git a/frontend/src/controller/formValidation.ts b/frontend/src/controller/formValidation.ts index 192423a86..e3a7f57cb 100644 --- a/frontend/src/controller/formValidation.ts +++ b/frontend/src/controller/formValidation.ts @@ -1,3 +1,7 @@ +import { getProfanity } from '#api/util' + +const profanity = getProfanity() + export function validateUsername(username: string, required = true): string | undefined { if (required && username === '') { return 'Username is Required' @@ -8,7 +12,13 @@ export function validateUsername(username: string, required = true): string | un if (username.includes(' ')) { return 'Username cannot contain spaces' } - // TODO: check if username includes bad words + if (profanity.exists(username)) { + return 'Username is blacklisted' + } + const unwantedChars = new RegExp(/[@#/\\$%^&*!?<>+~=]/g) + if (unwantedChars.test(username)) { + return 'Username cannot contain the following characters: @ # \\ $ % ^ & * ! ? < > + ~ =' + } return undefined } @@ -25,7 +35,7 @@ export function validateEmail(email: string, required = true): string | undefine if (required && email === '') { return 'Email is required' } - if (emailRegex.exec(email) === null) { + if (!emailRegex.test(email)) { return 'Invalid Email' } return undefined diff --git a/frontend/src/stores/authStore.ts b/frontend/src/stores/authStore.ts index 6b6c11cb9..fbba5ce04 100644 --- a/frontend/src/stores/authStore.ts +++ b/frontend/src/stores/authStore.ts @@ -1,9 +1,10 @@ -import type { SignUpBody, SignInResponse, ViablePodProvider, SelectUsers } from '#api/types' +import type { SignUpBody, ViablePodProvider, SelectUsers } from '#api/types' import { ApiClient } from '@/controller/api' import type { ApiErrors } from '@/types' import { defineStore } from 'pinia' import { ref } from 'vue' import { useRouter } from 'vue-router' +import { VsNotification } from 'vuesax-alpha' export const useAuthStore = defineStore('auth', () => { // Default state @@ -66,17 +67,22 @@ export const useAuthStore = defineStore('auth', () => { try { const { data: response, status } = await client.signin({ username, password, providerName }) if (status === 200) { - const signInResponse = response as SignInResponse + const signInResponse = response setLoggedIn(true) setToken(signInResponse.token) setUser(signInResponse.user) router.push({ name: 'home' }) - } else if (status === 401) { - return response as ApiErrors + } else if (status === 401 || status === 400) { + VsNotification({ + title: 'Wrong Credentials', + content: 'Please check your credentials', + color: 'danger' + }) + return response } } catch (error) { - console.log('error when trying to signIn: ', error) + console.error('error when trying to signIn: ', error) } } /** @@ -95,13 +101,18 @@ export const useAuthStore = defineStore('auth', () => { const body: SignUpBody = { username, password, email, providerName } const { data: response, status } = await client.signup(body) if (status === 200) { - const signupResponse = response as SignInResponse + const signupResponse = response setLoggedIn(true) setToken(signupResponse.token) setUser(signupResponse.user) router.push({ name: 'home' }) } else if (status === 500) { - return response as ApiErrors + VsNotification({ + title: 'Error', + content: 'Something went wrong', + color: 'danger' + }) + return response } } diff --git a/frontend/src/types/enums.ts b/frontend/src/types/enums.ts index f787eec57..debc476d2 100644 --- a/frontend/src/types/enums.ts +++ b/frontend/src/types/enums.ts @@ -1,6 +1,6 @@ -import type { FollowErrors, ProviderSignInErrors, ProviderSignUpErrors } from '#api/types' +import type { ApiSignUpErrors, FollowErrors, ProviderSignInErrors, ProviderSignUpErrors } from '#api/types' -export type ApiErrors = ProviderSignUpErrors | ApiErrorsGeneral | ProviderSignInErrors | FollowErrors +export type ApiErrors = ProviderSignUpErrors | ApiErrorsGeneral | ProviderSignInErrors | FollowErrors | ApiSignUpErrors export enum ApiErrorsGeneral { default = 'Something went wrong',