├── .prettierignore ├── src ├── pages │ ├── index.tsx │ ├── _app.tsx │ ├── _document.tsx │ ├── friends │ │ ├── online.tsx │ │ ├── add.tsx │ │ ├── all.tsx │ │ └── pending.tsx │ ├── servers │ │ └── [serverId] │ │ │ ├── index.tsx │ │ │ └── channels │ │ │ └── [channelId].tsx │ └── user │ │ └── verify-email │ │ └── [token].tsx ├── components │ ├── feedback │ │ ├── toast │ │ │ ├── index.ts │ │ │ └── toast.tsx │ │ ├── tooltip │ │ │ ├── index.ts │ │ │ └── tooltip.tsx │ │ └── toast-manager │ │ │ ├── index.ts │ │ │ └── toast-manager.tsx │ ├── inputs │ │ ├── errors │ │ │ ├── index.ts │ │ │ └── errors.tsx │ │ ├── spinner │ │ │ ├── index.ts │ │ │ └── spinner.tsx │ │ ├── icon-button │ │ │ ├── index.ts │ │ │ └── icon-button.tsx │ │ └── text-field │ │ │ └── index.ts │ ├── utils │ │ └── modal │ │ │ ├── index.ts │ │ │ └── modal.tsx │ ├── layouts │ │ ├── app-layout │ │ │ ├── index.ts │ │ │ ├── header │ │ │ │ ├── index.ts │ │ │ │ └── header.tsx │ │ │ ├── message │ │ │ │ ├── index.ts │ │ │ │ └── message.tsx │ │ │ ├── servers │ │ │ │ ├── index.ts │ │ │ │ └── servers.tsx │ │ │ ├── sidebar │ │ │ │ ├── index.ts │ │ │ │ ├── sidebar.tsx │ │ │ │ └── sidebar-footer.tsx │ │ │ ├── friends │ │ │ │ ├── wumpus │ │ │ │ │ └── index.ts │ │ │ │ ├── add-friends │ │ │ │ │ └── index.ts │ │ │ │ ├── all-friends │ │ │ │ │ └── index.ts │ │ │ │ ├── friend-header │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── friend-button │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── friend-button.tsx │ │ │ │ │ └── friend-header.tsx │ │ │ │ ├── friend-sidebar │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── friend-sidebar-body │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── direct-messages-modal │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── friend-sidebar-header │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── friend-sidebar-header.tsx │ │ │ │ │ └── friend-sidebar.tsx │ │ │ │ ├── no-friends-found │ │ │ │ │ └── index.ts │ │ │ │ └── pending-friends │ │ │ │ │ └── index.ts │ │ │ ├── profile-image │ │ │ │ ├── index.ts │ │ │ │ └── profile-image.tsx │ │ │ ├── channels │ │ │ │ ├── server-body │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── server-body.tsx │ │ │ │ │ ├── create-channel-button.tsx │ │ │ │ │ ├── text-channels.tsx │ │ │ │ │ ├── voice-channels.tsx │ │ │ │ │ └── channel-button.tsx │ │ │ │ ├── server-header │ │ │ │ │ ├── index.ts │ │ │ │ │ └── channel-type-buttons.tsx │ │ │ │ ├── text-channel │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── channel-text-area │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── channel-text-area.tsx │ │ │ │ │ └── text-channel.tsx │ │ │ │ ├── voice-channel │ │ │ │ │ ├── index.ts │ │ │ │ │ └── voice-channel.tsx │ │ │ │ ├── welcome-text │ │ │ │ │ ├── index.ts │ │ │ │ │ └── welcome-text.tsx │ │ │ │ ├── channel-sidebar │ │ │ │ │ ├── index.ts │ │ │ │ │ └── channel-sidebar.tsx │ │ │ │ ├── invite-people-modal │ │ │ │ │ └── index.ts │ │ │ │ └── channel-header │ │ │ │ │ └── channel-header.tsx │ │ │ ├── direct-messages │ │ │ │ ├── index.ts │ │ │ │ └── direct-messages.tsx │ │ │ └── server-members │ │ │ │ ├── index.ts │ │ │ │ └── server-members.tsx │ │ └── global-layout │ │ │ ├── index.ts │ │ │ └── global-layout.tsx │ ├── routes │ │ └── auth-route │ │ │ ├── index.ts │ │ │ └── auth-route.tsx │ └── icons │ │ ├── chevron-right.tsx │ │ ├── plus.tsx │ │ ├── bars.tsx │ │ ├── close.tsx │ │ ├── channel.tsx │ │ ├── chevron-down.tsx │ │ ├── pound.tsx │ │ ├── search.tsx │ │ ├── check.tsx │ │ ├── pencil.tsx │ │ ├── exclamation-circle.tsx │ │ ├── microphone.tsx │ │ ├── volume-up.tsx │ │ ├── user-add.tsx │ │ ├── icon.tsx │ │ ├── eye.tsx │ │ ├── eye-off.tsx │ │ ├── camera.tsx │ │ ├── friend.tsx │ │ ├── settings.tsx │ │ ├── index.ts │ │ └── logo.tsx ├── server │ ├── routes │ │ ├── auth │ │ │ ├── index.ts │ │ │ └── auth.route.ts │ │ ├── user │ │ │ ├── index.ts │ │ │ ├── friend │ │ │ │ ├── index.ts │ │ │ │ └── friend.route.ts │ │ │ ├── direct-message │ │ │ │ └── direct-message.route.ts │ │ │ └── user.route.ts │ │ ├── message │ │ │ ├── index.ts │ │ │ └── message.route.ts │ │ ├── server │ │ │ ├── index.ts │ │ │ ├── user │ │ │ │ ├── index.ts │ │ │ │ └── user.route.ts │ │ │ ├── channel │ │ │ │ ├── index.ts │ │ │ │ └── channel.route.ts │ │ │ ├── invite │ │ │ │ ├── index.ts │ │ │ │ └── invite.route.ts │ │ │ └── server.route.ts │ │ └── index.ts │ ├── services │ │ ├── mail │ │ │ ├── index.ts │ │ │ └── mail.service.ts │ │ ├── server │ │ │ ├── index.ts │ │ │ ├── channel │ │ │ │ └── index.ts │ │ │ ├── invite │ │ │ │ └── index.ts │ │ │ └── user │ │ │ │ └── user.service.ts │ │ ├── user │ │ │ ├── index.ts │ │ │ └── friend │ │ │ │ └── index.ts │ │ └── message │ │ │ └── index.ts │ ├── validators │ │ ├── auth │ │ │ ├── index.ts │ │ │ └── auth.validator.ts │ │ ├── user │ │ │ ├── index.ts │ │ │ ├── friend │ │ │ │ ├── index.ts │ │ │ │ └── friend.validator.ts │ │ │ └── direct-message │ │ │ │ └── direct-message.validator.ts │ │ ├── server │ │ │ ├── index.ts │ │ │ ├── channel │ │ │ │ ├── index.ts │ │ │ │ └── channel.validator.ts │ │ │ ├── invite │ │ │ │ └── invite.validator.ts │ │ │ └── server.validator.ts │ │ └── message │ │ │ ├── index.ts │ │ │ └── message.validator.ts │ ├── controllers │ │ ├── auth │ │ │ ├── index.ts │ │ │ └── auth.controller.ts │ │ ├── user │ │ │ ├── index.ts │ │ │ ├── friend │ │ │ │ └── index.ts │ │ │ └── direct-message │ │ │ │ ├── index.ts │ │ │ │ └── direct-message.controller.ts │ │ ├── message │ │ │ ├── index.ts │ │ │ └── message.controller.ts │ │ └── server │ │ │ ├── index.ts │ │ │ ├── channel │ │ │ ├── index.ts │ │ │ └── channel.controller.ts │ │ │ ├── user │ │ │ └── user.controller.ts │ │ │ ├── invite │ │ │ └── invite.controller.ts │ │ │ └── server.controller.ts │ ├── db │ │ ├── models │ │ │ ├── friend.model.ts │ │ │ ├── server.model.ts │ │ │ ├── refresh-token.model.ts │ │ │ ├── server-invite.model.ts │ │ │ ├── user-role.model.ts │ │ │ ├── invitable.ts │ │ │ ├── channel.model.ts │ │ │ ├── direct-message.model.ts │ │ │ ├── direct-message-user.model.ts │ │ │ ├── server-user.model.ts │ │ │ ├── index.ts │ │ │ ├── message.model.ts │ │ │ └── user.model.ts │ │ ├── index.ts │ │ └── seeders │ │ │ ├── 20220605201156-add-direct-messages.js │ │ │ ├── 20220217160636-add-user-roles.js │ │ │ ├── 20220524035027-add-friends.js │ │ │ ├── 20220605201313-add-direct-message-users.js │ │ │ └── 20220217160404-add-users.js │ ├── middleware │ │ ├── catch-async.ts │ │ ├── error-handler.ts │ │ ├── authenticate.ts │ │ └── authorize.ts │ ├── index.ts │ └── app.ts ├── styles │ ├── variables.css │ ├── scrollbar.css │ ├── toasts.css │ ├── animations.css │ ├── spinners.css │ ├── transitions.css │ └── globals.css ├── utils │ ├── types │ │ ├── interfaces │ │ │ ├── error.ts │ │ │ ├── jwt-token.ts │ │ │ ├── toast-interface.ts │ │ │ └── system-error.ts │ │ ├── requests │ │ │ ├── auth │ │ │ │ └── login.ts │ │ │ ├── server │ │ │ │ ├── create-server.ts │ │ │ │ ├── invite │ │ │ │ │ └── create-server-invite.ts │ │ │ │ └── channel │ │ │ │ │ └── create-channel.ts │ │ │ ├── user │ │ │ │ ├── reset-password.ts │ │ │ │ ├── friend │ │ │ │ │ └── create-friend.ts │ │ │ │ ├── confirm-reset-password.ts │ │ │ │ ├── direct-message │ │ │ │ │ └── create-direct-message.ts │ │ │ │ └── create-user.ts │ │ │ ├── events │ │ │ │ ├── join-server-request.ts │ │ │ │ └── join-channel-request.ts │ │ │ └── message │ │ │ │ └── create-message.ts │ │ ├── dtos │ │ │ ├── friend-request.ts │ │ │ ├── friend.ts │ │ │ ├── server-invite.ts │ │ │ ├── channel.ts │ │ │ ├── server-user.ts │ │ │ ├── direct-message-user.ts │ │ │ ├── request-user.ts │ │ │ ├── server.ts │ │ │ ├── invitable.ts │ │ │ ├── direct-message.ts │ │ │ ├── user.ts │ │ │ └── message.ts │ │ ├── app-props-layout.ts │ │ ├── props │ │ │ └── input.ts │ │ ├── next-page-layout.ts │ │ ├── web-socket.ts │ │ └── environment.d.ts │ ├── enums │ │ ├── channel-type.ts │ │ ├── roles.ts │ │ ├── message-type.ts │ │ ├── server-roles.ts │ │ ├── home-state.ts │ │ ├── icon-size.ts │ │ ├── events.ts │ │ └── errors.ts │ ├── hooks │ │ ├── use-rtc.ts │ │ ├── use-auth.ts │ │ ├── use-user.ts │ │ ├── use-toasts.ts │ │ ├── use-server.ts │ │ ├── use-socket.ts │ │ ├── use-channel.ts │ │ ├── use-modal.ts │ │ ├── use-servers.ts │ │ ├── use-window-size.ts │ │ ├── use-direct-message.ts │ │ └── use-app.ts │ ├── set-utils.ts │ ├── constants │ │ └── errors.ts │ ├── date-utils.ts │ ├── services │ │ └── handle-service-error.ts │ └── contexts │ │ ├── app-context.tsx │ │ ├── window-context.tsx │ │ ├── socket-context.tsx │ │ ├── channel-context.tsx │ │ ├── servers-context.tsx │ │ └── toast-context.tsx ├── config │ ├── stun.config.ts │ ├── smtp.config.ts │ ├── api.config.ts │ └── db.config.ts └── services │ ├── api.ts │ ├── server-user-service.ts │ ├── auth-service.ts │ ├── server-invite-service.ts │ ├── message-service.ts │ ├── server-service.ts │ ├── channel-service.ts │ ├── direct-message-service.ts │ ├── friend-service.ts │ └── user-service.ts ├── public └── favicon.ico ├── postcss.config.js ├── tsconfig.test.json ├── .prettierrc.json ├── .vscode ├── settings.json └── launch.json ├── nodemon.json ├── .eslintrc.json ├── next-env.d.ts ├── .sequelizerc ├── tsconfig.server.json ├── next.config.js ├── .env.example ├── babel.test.config.js ├── test ├── index.test.ts ├── integration │ ├── auth │ │ └── logout.test.ts │ └── user │ │ └── verify-email.test.ts └── fixtures │ └── index.ts ├── .githooks ├── pre-commit └── pre-push ├── tsconfig.json ├── .gitignore ├── README.md ├── tailwind.config.js └── package.json /.prettierignore: -------------------------------------------------------------------------------- 1 | .githooks 2 | .prettierignore -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | export { default } from './friends/all'; 2 | -------------------------------------------------------------------------------- /src/components/feedback/toast/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './toast'; 2 | -------------------------------------------------------------------------------- /src/components/inputs/errors/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './errors'; 2 | -------------------------------------------------------------------------------- /src/components/utils/modal/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './modal'; 2 | -------------------------------------------------------------------------------- /src/server/routes/auth/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './auth.route'; 2 | -------------------------------------------------------------------------------- /src/server/routes/user/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './user.route'; 2 | -------------------------------------------------------------------------------- /src/components/feedback/tooltip/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './tooltip'; 2 | -------------------------------------------------------------------------------- /src/components/inputs/spinner/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './spinner'; 2 | -------------------------------------------------------------------------------- /src/server/routes/message/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './message.route'; 2 | -------------------------------------------------------------------------------- /src/server/routes/server/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './server.route'; 2 | -------------------------------------------------------------------------------- /src/server/routes/server/user/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './user.route'; 2 | -------------------------------------------------------------------------------- /src/server/services/mail/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './mail.service'; 2 | -------------------------------------------------------------------------------- /src/server/services/server/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './server.service'; 2 | -------------------------------------------------------------------------------- /src/server/services/user/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './user.service'; 2 | -------------------------------------------------------------------------------- /src/server/validators/auth/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './auth.validator'; 2 | -------------------------------------------------------------------------------- /src/server/validators/user/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './user.validator'; 2 | -------------------------------------------------------------------------------- /src/components/inputs/icon-button/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './icon-button'; 2 | -------------------------------------------------------------------------------- /src/components/inputs/text-field/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './text-field'; 2 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './app-layout'; 2 | -------------------------------------------------------------------------------- /src/components/routes/auth-route/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './auth-route'; 2 | -------------------------------------------------------------------------------- /src/server/controllers/auth/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './auth.controller'; 2 | -------------------------------------------------------------------------------- /src/server/controllers/user/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './user.controller'; 2 | -------------------------------------------------------------------------------- /src/server/routes/server/channel/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './channel.route'; 2 | -------------------------------------------------------------------------------- /src/server/routes/server/invite/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './invite.route'; 2 | -------------------------------------------------------------------------------- /src/server/routes/user/friend/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './friend.route'; 2 | -------------------------------------------------------------------------------- /src/server/services/message/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './message.service'; 2 | -------------------------------------------------------------------------------- /src/server/services/user/friend/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './friend.service'; 2 | -------------------------------------------------------------------------------- /src/server/validators/server/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './server.validator'; 2 | -------------------------------------------------------------------------------- /src/styles/variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --font-primary: 'Inter', sans-serif; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/feedback/toast-manager/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './toast-manager'; 2 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/header/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './header'; 2 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/message/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './message'; 2 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/servers/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './servers'; 2 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/sidebar/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './sidebar'; 2 | -------------------------------------------------------------------------------- /src/components/layouts/global-layout/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './global-layout'; 2 | -------------------------------------------------------------------------------- /src/server/controllers/message/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './message.controller'; 2 | -------------------------------------------------------------------------------- /src/server/controllers/server/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './server.controller'; 2 | -------------------------------------------------------------------------------- /src/server/services/server/channel/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './channel.service'; 2 | -------------------------------------------------------------------------------- /src/server/services/server/invite/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './invite.service'; 2 | -------------------------------------------------------------------------------- /src/server/validators/message/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './message.validator'; 2 | -------------------------------------------------------------------------------- /src/server/validators/user/friend/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './friend.validator'; 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noahskorner/discord-clone/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/components/layouts/app-layout/friends/wumpus/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './wumpus'; 2 | -------------------------------------------------------------------------------- /src/server/controllers/server/channel/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './channel.controller'; 2 | -------------------------------------------------------------------------------- /src/server/controllers/user/friend/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './friend.controller'; 2 | -------------------------------------------------------------------------------- /src/server/validators/server/channel/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './channel.validator'; 2 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/profile-image/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './profile-image'; 2 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/channels/server-body/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './server-body'; 2 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/direct-messages/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './direct-messages'; 2 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/friends/add-friends/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './add-friends'; 2 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/friends/all-friends/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './all-friends'; 2 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/server-members/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './server-members'; 2 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/channels/server-header/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './server-header'; 2 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/channels/text-channel/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './text-channel'; 2 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/channels/voice-channel/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './voice-channel'; 2 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/channels/welcome-text/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './welcome-text'; 2 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/friends/friend-header/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './friend-header'; 2 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/friends/friend-sidebar/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './friend-sidebar'; 2 | -------------------------------------------------------------------------------- /src/server/controllers/user/direct-message/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './direct-message.controller'; 2 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/channels/channel-sidebar/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './channel-sidebar'; 2 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/friends/no-friends-found/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './no-friends-found'; 2 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/friends/pending-friends/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './pending-friends'; 2 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/channels/invite-people-modal/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './invite-people-modal'; 2 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/friends/friend-header/friend-button/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './friend-button'; 2 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es6" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "endOfLine": "lf" 6 | } 7 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/channels/text-channel/channel-text-area/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './channel-text-area'; 2 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/friends/friend-sidebar/friend-sidebar-body/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './friend-sidebar-body'; 2 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/friends/friend-sidebar/direct-messages-modal/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './direct-messages-modal'; 2 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/friends/friend-sidebar/friend-sidebar-header/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './friend-sidebar-header'; 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.eol": "\n", 3 | "editor.formatOnSave": true, 4 | "typescript.tsdk": "./node_modules/typescript/lib" 5 | } 6 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src/server"], 3 | "ext": "ts", 4 | "exec": "ts-node --project tsconfig.server.json src/server/index.ts" 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/types/interfaces/error.ts: -------------------------------------------------------------------------------- 1 | interface ErrorInterface { 2 | field?: string; 3 | message: string; 4 | } 5 | 6 | export default ErrorInterface; 7 | -------------------------------------------------------------------------------- /src/utils/types/requests/auth/login.ts: -------------------------------------------------------------------------------- 1 | interface LoginRequest { 2 | email: string; 3 | password: string; 4 | } 5 | 6 | export default LoginRequest; 7 | -------------------------------------------------------------------------------- /src/utils/types/requests/server/create-server.ts: -------------------------------------------------------------------------------- 1 | interface CreateServerRequest { 2 | name: string; 3 | } 4 | 5 | export default CreateServerRequest; 6 | -------------------------------------------------------------------------------- /src/utils/types/requests/user/reset-password.ts: -------------------------------------------------------------------------------- 1 | interface ResetPasswordRequest { 2 | email: string; 3 | } 4 | 5 | export default ResetPasswordRequest; 6 | -------------------------------------------------------------------------------- /src/utils/types/requests/events/join-server-request.ts: -------------------------------------------------------------------------------- 1 | interface JoinServerRequest { 2 | serverId: number; 3 | } 4 | 5 | export default JoinServerRequest; 6 | -------------------------------------------------------------------------------- /src/utils/types/requests/events/join-channel-request.ts: -------------------------------------------------------------------------------- 1 | interface JoinChannelRequest { 2 | channelId: number; 3 | } 4 | 5 | export default JoinChannelRequest; 6 | -------------------------------------------------------------------------------- /src/utils/types/requests/user/friend/create-friend.ts: -------------------------------------------------------------------------------- 1 | interface CreateFriendRequest { 2 | addresseeEmail: string; 3 | } 4 | 5 | export default CreateFriendRequest; 6 | -------------------------------------------------------------------------------- /src/utils/enums/channel-type.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | enum ChannelType { 3 | VOICE = 'VOICE', 4 | TEXT = 'TEXT', 5 | } 6 | 7 | export default ChannelType; 8 | -------------------------------------------------------------------------------- /src/utils/enums/roles.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | enum RoleEnum { 3 | ADMIN = 'ADMIN', 4 | SUPERADMIN = 'SUPERADMIN', 5 | } 6 | 7 | export default RoleEnum; 8 | -------------------------------------------------------------------------------- /src/utils/types/dtos/friend-request.ts: -------------------------------------------------------------------------------- 1 | import InvitableDto from './invitable'; 2 | 3 | class FriendRequestDto extends InvitableDto {} 4 | 5 | export default FriendRequestDto; 6 | -------------------------------------------------------------------------------- /src/utils/enums/message-type.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | enum MessageType { 3 | DIRECT, 4 | CHANNEL, 5 | SERVER_INVITE, 6 | } 7 | 8 | export default MessageType; 9 | -------------------------------------------------------------------------------- /src/utils/types/requests/server/invite/create-server-invite.ts: -------------------------------------------------------------------------------- 1 | interface CreateServerInviteRequest { 2 | friendId: number; 3 | } 4 | 5 | export default CreateServerInviteRequest; 6 | -------------------------------------------------------------------------------- /src/utils/enums/server-roles.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | enum ServerRoleEnum { 3 | OWNER = 'OWNER', 4 | MEMBER = 'MEMBER', 5 | } 6 | 7 | export default ServerRoleEnum; 8 | -------------------------------------------------------------------------------- /src/utils/types/interfaces/jwt-token.ts: -------------------------------------------------------------------------------- 1 | import RequestUser from '../dtos/request-user'; 2 | 3 | interface JwtToken extends RequestUser { 4 | exp: number; 5 | } 6 | 7 | export default JwtToken; 8 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "eslint-config-prettier"], 3 | "env": { 4 | "jest": true 5 | }, 6 | "rules": { 7 | "no-unused-vars": "warn" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/enums/home-state.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | enum HomeState { 4 | ONLINE, 5 | ALL, 6 | PENDING, 7 | ADD_FRIEND, 8 | } 9 | 10 | export default HomeState; 11 | -------------------------------------------------------------------------------- /src/utils/types/requests/user/confirm-reset-password.ts: -------------------------------------------------------------------------------- 1 | interface ConfirmResetPasswordRequest { 2 | password: string; 3 | confirmPassword: string; 4 | } 5 | 6 | export default ConfirmResetPasswordRequest; 7 | -------------------------------------------------------------------------------- /src/utils/types/requests/user/direct-message/create-direct-message.ts: -------------------------------------------------------------------------------- 1 | interface CreateDirectMessageRequest { 2 | userId: number; 3 | friendIds: number[]; 4 | } 5 | 6 | export default CreateDirectMessageRequest; 7 | -------------------------------------------------------------------------------- /src/utils/types/app-props-layout.ts: -------------------------------------------------------------------------------- 1 | import { AppProps } from 'next/app'; 2 | import { NextPageLayout } from './next-page-layout'; 3 | 4 | export type AppPropsLayout = AppProps & { 5 | Component: NextPageLayout; 6 | }; 7 | -------------------------------------------------------------------------------- /src/utils/types/requests/user/create-user.ts: -------------------------------------------------------------------------------- 1 | interface CreateUserRequest { 2 | username: string; 3 | email: string; 4 | password: string; 5 | confirmPassword: string; 6 | } 7 | 8 | export default CreateUserRequest; 9 | -------------------------------------------------------------------------------- /src/utils/hooks/use-rtc.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { RTCContext } from '../contexts/rtc-context'; 3 | 4 | const useRTC = () => { 5 | return useContext(RTCContext); 6 | }; 7 | 8 | export default useRTC; 9 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /src/utils/hooks/use-auth.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { AuthContext } from '../contexts/auth-context'; 3 | 4 | const useAuth = () => { 5 | return useContext(AuthContext); 6 | }; 7 | 8 | export default useAuth; 9 | -------------------------------------------------------------------------------- /src/utils/hooks/use-user.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { UserContext } from '../contexts/user-context'; 3 | 4 | const useUser = () => { 5 | return useContext(UserContext); 6 | }; 7 | 8 | export default useUser; 9 | -------------------------------------------------------------------------------- /src/utils/hooks/use-toasts.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { ToastContext } from '../contexts/toast-context'; 3 | 4 | const useToasts = () => { 5 | return useContext(ToastContext); 6 | }; 7 | 8 | export default useToasts; 9 | -------------------------------------------------------------------------------- /src/utils/hooks/use-server.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { ServerContext } from '../contexts/server-context'; 3 | 4 | const useServer = () => { 5 | return useContext(ServerContext); 6 | }; 7 | 8 | export default useServer; 9 | -------------------------------------------------------------------------------- /src/utils/hooks/use-socket.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { SocketContext } from '../contexts/socket-context'; 3 | 4 | const useSocket = () => { 5 | return useContext(SocketContext); 6 | }; 7 | 8 | export default useSocket; 9 | -------------------------------------------------------------------------------- /src/utils/types/props/input.ts: -------------------------------------------------------------------------------- 1 | import ErrorInterface from '../interfaces/error'; 2 | 3 | interface InputProps { 4 | label?: string; 5 | placeholder?: string; 6 | errors?: ErrorInterface[]; 7 | } 8 | 9 | export default InputProps; 10 | -------------------------------------------------------------------------------- /src/server/db/models/friend.model.ts: -------------------------------------------------------------------------------- 1 | import { Table } from 'sequelize-typescript'; 2 | import Invitable from './invitable'; 3 | 4 | @Table({ tableName: 'friend', underscored: true }) 5 | class Friend extends Invitable {} 6 | 7 | export default Friend; 8 | -------------------------------------------------------------------------------- /src/utils/hooks/use-channel.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { ChannelContext } from '../contexts/channel-context'; 3 | 4 | const useChannel = () => { 5 | return useContext(ChannelContext); 6 | }; 7 | 8 | export default useChannel; 9 | -------------------------------------------------------------------------------- /src/utils/hooks/use-modal.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | const useModal = () => { 4 | const [showModal, setShowModal] = useState(false); 5 | 6 | return { showModal, setShowModal }; 7 | }; 8 | 9 | export default useModal; 10 | -------------------------------------------------------------------------------- /src/utils/hooks/use-servers.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { ServersContext } from '../contexts/servers-context'; 3 | 4 | const useServers = () => { 5 | return useContext(ServersContext); 6 | }; 7 | 8 | export default useServers; 9 | -------------------------------------------------------------------------------- /src/utils/types/requests/server/channel/create-channel.ts: -------------------------------------------------------------------------------- 1 | import ChannelType from '../../../../enums/channel-type'; 2 | 3 | interface CreateChannelRequest { 4 | type: ChannelType; 5 | name: string; 6 | } 7 | 8 | export default CreateChannelRequest; 9 | -------------------------------------------------------------------------------- /src/utils/hooks/use-window-size.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { WindowContext } from '../contexts/window-context'; 3 | 4 | const useWindowSize = () => { 5 | return useContext(WindowContext); 6 | }; 7 | 8 | export default useWindowSize; 9 | -------------------------------------------------------------------------------- /src/utils/enums/icon-size.ts: -------------------------------------------------------------------------------- 1 | enum IconSize { 2 | xs = 12, 3 | sm = 16, 4 | md = 20, 5 | lg = 24, 6 | xl = 28, 7 | '2xl' = 32, 8 | '3xl' = 36, 9 | '4xl' = 40, 10 | '5xl' = 44, 11 | '6xl' = 48, 12 | } 13 | 14 | export default IconSize; 15 | -------------------------------------------------------------------------------- /src/utils/types/interfaces/toast-interface.ts: -------------------------------------------------------------------------------- 1 | export type Color = 'success' | 'danger'; 2 | 3 | interface ToastInterface { 4 | id: string; 5 | color: Color; 6 | title: string; 7 | body?: string; 8 | } 9 | 10 | export default ToastInterface; 11 | -------------------------------------------------------------------------------- /src/config/stun.config.ts: -------------------------------------------------------------------------------- 1 | const stunConfig = { 2 | iceServers: [ 3 | { 4 | urls: ['stun:stun1.l.google.com:19302', 'stun:stun2.l.google.com:19302'], 5 | }, 6 | ], 7 | iceCandidatePoolSize: 10, 8 | }; 9 | 10 | export default stunConfig; 11 | -------------------------------------------------------------------------------- /src/utils/types/next-page-layout.ts: -------------------------------------------------------------------------------- 1 | import { NextPage } from 'next'; 2 | import { ReactElement, ReactNode } from 'react'; 3 | 4 | export type NextPageLayout = NextPage & { 5 | // eslint-disable-next-line no-unused-vars 6 | getLayout?: (page: ReactElement) => ReactNode; 7 | }; 8 | -------------------------------------------------------------------------------- /src/utils/hooks/use-direct-message.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { DirectMessageContext } from '../contexts/direct-message-context'; 3 | 4 | const useDirectMessage = () => { 5 | return { ...useContext(DirectMessageContext) }; 6 | }; 7 | 8 | export default useDirectMessage; 9 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/server-members/server-members.tsx: -------------------------------------------------------------------------------- 1 | const ServerMembers = () => { 2 | return ( 3 |
4 | Server Members 5 |
6 | ); 7 | }; 8 | 9 | export default ServerMembers; 10 | -------------------------------------------------------------------------------- /src/services/api.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export const BASE_URL = '/api/v1'; 4 | 5 | const API = axios.create({ 6 | baseURL: BASE_URL, 7 | headers: { 8 | Accept: 'application/json', 9 | 'Content-Type': 'application/json', 10 | }, 11 | }); 12 | 13 | export default API; 14 | -------------------------------------------------------------------------------- /src/server/middleware/catch-async.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | 3 | const catchAsync = (fn: Function) => { 4 | return (req: Request, res: Response, next: NextFunction) => { 5 | fn(req, res, next).catch(next); 6 | }; 7 | }; 8 | 9 | export default catchAsync; 10 | -------------------------------------------------------------------------------- /src/utils/types/requests/message/create-message.ts: -------------------------------------------------------------------------------- 1 | import MessageType from '../../../enums/message-type'; 2 | 3 | interface CreateMessageRequest { 4 | type: MessageType; 5 | body: string; 6 | directMessageId?: number; 7 | serverInviteId?: number; 8 | } 9 | 10 | export default CreateMessageRequest; 11 | -------------------------------------------------------------------------------- /.sequelizerc: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | config: path.resolve('src', 'server/db/config.json'), 5 | 'models-path': path.resolve('src', 'server/db/models'), 6 | 'seeders-path': path.resolve('src', 'server/db/seeders'), 7 | 'migrations-path': path.resolve('src', 'server/db/migrations'), 8 | }; 9 | -------------------------------------------------------------------------------- /tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": false, 5 | "module": "commonjs", 6 | "target": "es2017", 7 | "lib": ["dom", "es2017"], 8 | "outDir": ".next/server" 9 | }, 10 | "include": ["src/server/**/*.ts"], 11 | "exclude": [".next"] 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/types/web-socket.ts: -------------------------------------------------------------------------------- 1 | import { Server as BaseWebSocket } from 'socket.io'; 2 | 3 | class WebSocket extends BaseWebSocket { 4 | public async findByUserId(userId: number) { 5 | return Array.from(await this.fetchSockets()).find( 6 | (e: any) => e.user?.id === userId, 7 | ); 8 | } 9 | } 10 | 11 | export default WebSocket; 12 | -------------------------------------------------------------------------------- /src/config/smtp.config.ts: -------------------------------------------------------------------------------- 1 | import { createTransport } from 'nodemailer'; 2 | import env from './env.config'; 3 | 4 | const transporter = createTransport({ 5 | port: 465, 6 | host: env.SMTP_HOST, 7 | auth: { 8 | user: env.SMTP_USER, 9 | pass: env.SMTP_PASSWORD, 10 | }, 11 | secure: true, 12 | }); 13 | 14 | export default transporter; 15 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/channels/channel-sidebar/channel-sidebar.tsx: -------------------------------------------------------------------------------- 1 | import Sidebar from '../../sidebar'; 2 | import ServerBody from '../server-body'; 3 | import ServerHeader from '../server-header'; 4 | 5 | const ChannelSidebar = () => { 6 | return } body={} />; 7 | }; 8 | 9 | export default ChannelSidebar; 10 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: false, 4 | images: { domains: ['www.clipartmax.com'] }, 5 | webpack(config) { 6 | config.module.rules.push({ 7 | test: /\.svg$/, 8 | use: ['@svgr/webpack'], 9 | }); 10 | 11 | return config; 12 | }, 13 | }; 14 | 15 | module.exports = nextConfig; 16 | -------------------------------------------------------------------------------- /src/server/services/mail/mail.service.ts: -------------------------------------------------------------------------------- 1 | import Mail from 'nodemailer/lib/mailer'; 2 | import transporter from '../../../config/smtp.config'; 3 | 4 | class MailService { 5 | public sendMail = async (mailOptions: Mail.Options) => { 6 | if (process.env.NODE_ENV === 'production') 7 | transporter.sendMail(mailOptions); 8 | }; 9 | } 10 | 11 | export default MailService; 12 | -------------------------------------------------------------------------------- /src/styles/scrollbar.css: -------------------------------------------------------------------------------- 1 | /* Width */ 2 | ::-webkit-scrollbar { 3 | width: 8px; 4 | } 5 | 6 | /* Track */ 7 | ::-webkit-scrollbar-track { 8 | background-color: hsl(210, calc(var(--saturation-factor, 1) * 9.8%), 20%); 9 | border-radius: 8px; 10 | margin: 6px 0; 11 | } 12 | 13 | /* Handle */ 14 | ::-webkit-scrollbar-thumb { 15 | background: #202225; 16 | border-radius: 8px; 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Server", 6 | "type": "node-terminal", 7 | "request": "launch", 8 | "command": "npm run dev" 9 | }, 10 | { 11 | "name": "Client", 12 | "type": "pwa-chrome", 13 | "request": "launch", 14 | "url": "http://localhost:3000" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME= 2 | HOST= 3 | PORT= 4 | DATABASE_URL= 5 | DATABASE= 6 | DB_USER= 7 | DB_PASSWORD= 8 | DB_HOST= 9 | DB_PORT= 10 | ACCESS_TOKEN_SECRET= 11 | REFRESH_TOKEN_SECRET= 12 | VERIFY_EMAIL_SECRET= 13 | RESET_PASSWORD_SECRET= 14 | ACCESS_TOKEN_EXPIRATION= 15 | REFRESH_TOKEN_EXPIRATION= 16 | VERIFY_EMAIL_EXPIRATION= 17 | RESET_PASSWORD_EXPIRATION= 18 | SMTP_HOST= 19 | SMTP_USER= 20 | SMTP_PASSWORD= -------------------------------------------------------------------------------- /src/components/layouts/app-layout/channels/server-body/server-body.tsx: -------------------------------------------------------------------------------- 1 | import TextChannels from './text-channels'; 2 | import VoiceChannels from './voice-channels'; 3 | 4 | const ServerBody = () => { 5 | return ( 6 |
7 | 8 | 9 |
10 | ); 11 | }; 12 | 13 | export default ServerBody; 14 | -------------------------------------------------------------------------------- /src/utils/types/dtos/friend.ts: -------------------------------------------------------------------------------- 1 | class FriendDto { 2 | public friendId: number; 3 | public id: number; 4 | public username: string; 5 | public email: string; 6 | 7 | constructor(friendId: number, id: number, username: string, email: string) { 8 | this.friendId = friendId; 9 | this.id = id; 10 | this.username = username; 11 | this.email = email; 12 | } 13 | } 14 | 15 | export default FriendDto; 16 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/friends/friend-sidebar/friend-sidebar.tsx: -------------------------------------------------------------------------------- 1 | import Sidebar from '../../sidebar'; 2 | import FriendSidebarBody from './friend-sidebar-body'; 3 | import FriendSidebarHeader from './friend-sidebar-header'; 4 | 5 | const FriendSidebar = () => { 6 | return ( 7 | } body={} /> 8 | ); 9 | }; 10 | 11 | export default FriendSidebar; 12 | -------------------------------------------------------------------------------- /src/utils/types/dtos/server-invite.ts: -------------------------------------------------------------------------------- 1 | import ServerInvite from '../../../server/db/models/server-invite.model'; 2 | import InvitableDto from './invitable'; 3 | 4 | class ServerInviteDto extends InvitableDto { 5 | public serverId: number; 6 | 7 | constructor(serverInvite: ServerInvite) { 8 | super(serverInvite); 9 | this.serverId = serverInvite.serverId; 10 | } 11 | } 12 | 13 | export default ServerInviteDto; 14 | -------------------------------------------------------------------------------- /src/config/api.config.ts: -------------------------------------------------------------------------------- 1 | import rateLimit from 'express-rate-limit'; 2 | 3 | const apiLimiter = rateLimit({ 4 | windowMs: 15 * 60 * 1000, // 15 minutes 5 | max: 1000, // Limit each IP to 100 requests per `window` (here, per 15 minutes) 6 | standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers 7 | legacyHeaders: false, // Disable the `X-RateLimit-*` headers 8 | }); 9 | 10 | export default apiLimiter; 11 | -------------------------------------------------------------------------------- /src/utils/hooks/use-app.ts: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import { useContext } from 'react'; 3 | import { AppContext } from '../contexts/app-context'; 4 | 5 | const useApp = () => { 6 | const router = useRouter(); 7 | 8 | const isHomePage = 9 | router.pathname === '/' || router.pathname.includes('friend'); 10 | 11 | return { isHomePage, ...useContext(AppContext) }; 12 | }; 13 | 14 | export default useApp; 15 | -------------------------------------------------------------------------------- /src/utils/types/interfaces/system-error.ts: -------------------------------------------------------------------------------- 1 | import ErrorEnum from '../../enums/errors'; 2 | import ErrorInterface from './error'; 3 | 4 | class SystemError extends Error { 5 | public type: ErrorEnum; 6 | public errors: ErrorInterface[]; 7 | 8 | constructor(type: ErrorEnum, errors: ErrorInterface[]) { 9 | super(); 10 | this.type = type; 11 | this.errors = errors; 12 | } 13 | } 14 | 15 | export default SystemError; 16 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import GlobalLayout from '../components/layouts/global-layout'; 2 | import '../styles/globals.css'; 3 | import { AppPropsLayout } from '../utils/types/app-props-layout'; 4 | 5 | const App = ({ Component, pageProps }: AppPropsLayout) => { 6 | const getLayout = Component.getLayout ?? ((page) => page); 7 | 8 | return {getLayout()}; 9 | }; 10 | 11 | export default App; 12 | -------------------------------------------------------------------------------- /src/utils/set-utils.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | const some = (set: Set, predicate: (e: T) => boolean) => { 3 | return Array.from(set).some(predicate); 4 | }; 5 | 6 | // eslint-disable-next-line no-unused-vars 7 | const find = (set: Set, predicate: (e: T) => boolean) => { 8 | return Array.from(set).find(predicate); 9 | }; 10 | 11 | const SetUtils = { 12 | some, 13 | find, 14 | }; 15 | 16 | export default SetUtils; 17 | -------------------------------------------------------------------------------- /babel.test.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: 'es6' } }], 4 | '@babel/preset-typescript', 5 | ], 6 | plugins: [ 7 | ['@babel/plugin-proposal-decorators', { legacy: true }], 8 | ['@babel/plugin-transform-flow-strip-types'], 9 | ['@babel/plugin-proposal-class-properties', { loose: true }], 10 | ['@babel/plugin-proposal-private-property-in-object', { loose: true }], 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /src/server/routes/server/invite/invite.route.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import ServerInviteController from '../../../controllers/server/invite/invite.controller'; 3 | import authenticate from '../../../middleware/authenticate'; 4 | 5 | const inviteController = new ServerInviteController(); 6 | 7 | const inviteRouter = Router({ mergeParams: true }); 8 | inviteRouter.post('/', authenticate, inviteController.create); 9 | 10 | export default inviteRouter; 11 | -------------------------------------------------------------------------------- /src/server/db/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | import { exec } from 'child_process'; 3 | 4 | const seedDatabase = async () => { 5 | await new Promise((resolve, reject) => { 6 | exec( 7 | 'npx sequelize-cli db:seed:all --config src/server/db/config.json --seeders-path src/server/db/seeders', 8 | { env: process.env }, 9 | (err) => (err ? reject(err) : resolve(() => {})), 10 | ); 11 | }); 12 | }; 13 | 14 | export default seedDatabase; 15 | -------------------------------------------------------------------------------- /src/server/routes/message/message.route.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import MessageController from '../../controllers/message'; 3 | import authenticate from '../../middleware/authenticate'; 4 | 5 | const messageController = new MessageController(); 6 | 7 | const messageRouter = Router(); 8 | messageRouter.post('/', authenticate, messageController.create); 9 | messageRouter.get('/', authenticate, messageController.index); 10 | 11 | export default messageRouter; 12 | -------------------------------------------------------------------------------- /src/utils/types/dtos/channel.ts: -------------------------------------------------------------------------------- 1 | import Channel from '../../../server/db/models/channel.model'; 2 | import ChannelType from '../../enums/channel-type'; 3 | 4 | class ChannelDto { 5 | public id: number; 6 | public type: ChannelType; 7 | public name: string; 8 | 9 | constructor(channel: Channel) { 10 | this.id = channel.id; 11 | this.type = channel.type as ChannelType; 12 | this.name = channel.name; 13 | } 14 | } 15 | 16 | export default ChannelDto; 17 | -------------------------------------------------------------------------------- /src/server/routes/server/user/user.route.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import ServerUserController from '../../../controllers/server/user/user.controller'; 3 | import authenticate from '../../../middleware/authenticate'; 4 | 5 | const serverUserController = new ServerUserController(); 6 | 7 | const serverUserRouter = Router({ mergeParams: true }); 8 | serverUserRouter.post('/', authenticate, serverUserController.create); 9 | 10 | export default serverUserRouter; 11 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import app from '../src/server/app'; 3 | 4 | describe('root path should', () => { 5 | test('return unauthorized', async () => { 6 | const response = await request(app).get('/api/v1'); 7 | expect(response.statusCode).toBe(401); 8 | }); 9 | }); 10 | 11 | // logout 12 | // getUser 13 | // resetPassword 14 | // confirmResetPassword 15 | // server create 16 | // server index 17 | // server get 18 | // channel get 19 | -------------------------------------------------------------------------------- /src/server/routes/auth/auth.route.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import AuthController from '../../controllers/auth'; 3 | import authenticate from '../../middleware/authenticate'; 4 | 5 | const authController = new AuthController(); 6 | 7 | const authRouter = Router(); 8 | authRouter.get('/', authController.refreshToken); 9 | authRouter.post('/', authController.login); 10 | authRouter.delete('/', authenticate, authController.logout); 11 | 12 | export default authRouter; 13 | -------------------------------------------------------------------------------- /src/services/server-user-service.ts: -------------------------------------------------------------------------------- 1 | import { AxiosResponse } from 'axios'; 2 | import ServerUserDto from '../utils/types/dtos/server-user'; 3 | import API from './api'; 4 | 5 | const ServerUserService = { 6 | create: ( 7 | serverId: number, 8 | payload: { 9 | serverInviteId: number; 10 | }, 11 | ): Promise> => { 12 | return API.post(`/server/${serverId}/user`, payload); 13 | }, 14 | }; 15 | 16 | export default ServerUserService; 17 | -------------------------------------------------------------------------------- /src/utils/constants/errors.ts: -------------------------------------------------------------------------------- 1 | import ErrorInterface from '../types/interfaces/error'; 2 | 3 | export const ERROR_UNKOWN = { 4 | message: 'An unknown error has occurred. Please try again.', 5 | } as ErrorInterface; 6 | export const ERROR_INSUFFICIENT_PERMISSIONS = { 7 | message: 'You do not have permission to perform this action.', 8 | } as ErrorInterface; 9 | export const ERROR_UNAUTHORIZED = { 10 | message: 'You must be logged in to perform this action.', 11 | } as ErrorInterface; 12 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/friends/friend-sidebar/friend-sidebar-header/friend-sidebar-header.tsx: -------------------------------------------------------------------------------- 1 | const FriendSidebarHeader = () => { 2 | return ( 3 |
4 | 7 |
8 | ); 9 | }; 10 | 11 | export default FriendSidebarHeader; 12 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/direct-messages/direct-messages.tsx: -------------------------------------------------------------------------------- 1 | import useDirectMessage from '../../../../utils/hooks/use-direct-message'; 2 | import Message from '../message'; 3 | 4 | const DirectMessages = () => { 5 | const { messages } = useDirectMessage(); 6 | 7 | return ( 8 | <> 9 | {Array.from(messages).map((message) => { 10 | return ; 11 | })} 12 | 13 | ); 14 | }; 15 | 16 | export default DirectMessages; 17 | -------------------------------------------------------------------------------- /src/server/db/models/server.model.ts: -------------------------------------------------------------------------------- 1 | import { Table, Column, Model, DataType, HasMany } from 'sequelize-typescript'; 2 | import ServerUser from './server-user.model'; 3 | import Channel from './channel.model'; 4 | 5 | @Table({ tableName: 'server', underscored: true }) 6 | class Server extends Model { 7 | @Column(DataType.STRING) 8 | name!: string; 9 | 10 | @HasMany(() => ServerUser) 11 | users!: ServerUser[]; 12 | 13 | @HasMany(() => Channel) 14 | channels!: Channel[]; 15 | } 16 | 17 | export default Server; 18 | -------------------------------------------------------------------------------- /src/server/routes/server/channel/channel.route.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import ChannelController from '../../../controllers/server/channel'; 3 | import authenticate from '../../../middleware/authenticate'; 4 | 5 | const channelController = new ChannelController(); 6 | 7 | const channelRouter = Router({ mergeParams: true }); 8 | channelRouter.post('/', authenticate, channelController.create); 9 | channelRouter.get('/:channelId', authenticate, channelController.get); 10 | 11 | export default channelRouter; 12 | -------------------------------------------------------------------------------- /src/styles/toasts.css: -------------------------------------------------------------------------------- 1 | /* Tailwind */ 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | 6 | @layer components { 7 | .toast-success { 8 | @apply border-y border-r border-l-4 border-y-slate-900/10 border-r-slate-900/10 border-l-green-500 dark:border-y-slate-300/10 dark:border-r-slate-300/10; 9 | } 10 | .toast-danger { 11 | @apply border-y border-r border-l-4 border-y-slate-900/10 border-r-slate-900/10 border-l-red-500 dark:border-y-slate-300/10 dark:border-r-slate-300/10; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document'; 2 | 3 | const Document = () => { 4 | return ( 5 | 6 | 7 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default Document; 21 | -------------------------------------------------------------------------------- /src/server/db/models/refresh-token.model.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | Column, 4 | Model, 5 | DataType, 6 | ForeignKey, 7 | BelongsTo, 8 | } from 'sequelize-typescript'; 9 | import User from './user.model'; 10 | 11 | @Table({ tableName: 'refresh_token', underscored: true }) 12 | class RefreshToken extends Model { 13 | @Column(DataType.STRING) 14 | token!: string; 15 | 16 | @ForeignKey(() => User) 17 | userId!: number; 18 | 19 | @BelongsTo(() => User) 20 | user!: User; 21 | } 22 | 23 | export default RefreshToken; 24 | -------------------------------------------------------------------------------- /src/server/db/seeders/20220605201156-add-direct-messages.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up(queryInterface, Sequelize) { 5 | await queryInterface.bulkInsert( 6 | 'direct_message', 7 | [ 8 | { 9 | created_by_id: 1, 10 | created_at: new Date(), 11 | updated_at: new Date(), 12 | }, 13 | ], 14 | {}, 15 | ); 16 | }, 17 | 18 | async down(queryInterface, Sequelize) { 19 | await queryInterface.bulkDelete('direct_message', null, {}); 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /src/server/db/models/server-invite.model.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BelongsTo, 3 | Column, 4 | DataType, 5 | ForeignKey, 6 | Table, 7 | } from 'sequelize-typescript'; 8 | import Invitable from './invitable'; 9 | import Server from './server.model'; 10 | 11 | @Table({ tableName: 'server_invite', underscored: true }) 12 | class ServerInvite extends Invitable { 13 | @ForeignKey(() => Server) 14 | @Column(DataType.INTEGER) 15 | serverId!: number; 16 | 17 | @BelongsTo(() => Server) 18 | server!: Server; 19 | } 20 | 21 | export default ServerInvite; 22 | -------------------------------------------------------------------------------- /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | SCRIPT_PREFIX="\x1b[1;35m[.githooks/pre-commit]\x1b[0m" 4 | FILES=$(git diff --cached --name-only --diff-filter=ACMR | sed 's| |\\ |g') 5 | [ -z "$FILES" ] && exit 0 6 | 7 | echo -e "$SCRIPT_PREFIX Running Prettier on any modified files..." 8 | 9 | # Prettify all selected files 10 | echo "$FILES" | xargs ./node_modules/.bin/prettier --write 11 | 12 | echo -e "$SCRIPT_PREFIX Linting all files..."; 13 | 14 | npm run lint; 15 | 16 | # Add back the modified files to staging 17 | echo "$FILES" | xargs git add 18 | 19 | exit 0 -------------------------------------------------------------------------------- /src/server/middleware/error-handler.ts: -------------------------------------------------------------------------------- 1 | import { Errback, Response, NextFunction, Request } from 'express'; 2 | import { ERROR_UNKOWN } from '../../utils/constants/errors'; 3 | import ErrorInterface from '../../utils/types/interfaces/error'; 4 | 5 | const errorHandler = ( 6 | err: Errback, 7 | req: Request, 8 | res: Response, 9 | // eslint-disable-next-line no-unused-vars 10 | next: NextFunction, 11 | ) => { 12 | const errors: ErrorInterface[] = [ERROR_UNKOWN]; 13 | return res.status(500).json(errors); 14 | }; 15 | 16 | export default errorHandler; 17 | -------------------------------------------------------------------------------- /src/services/auth-service.ts: -------------------------------------------------------------------------------- 1 | import { AxiosResponse } from 'axios'; 2 | import LoginRequest from '../utils/types/requests/auth/login'; 3 | import API from './api'; 4 | 5 | const AuthService = { 6 | login: (payload: LoginRequest): Promise> => { 7 | return API.post('auth', payload); 8 | }, 9 | refreshToken: (): Promise> => { 10 | return API.get('auth'); 11 | }, 12 | logout: (): Promise> => { 13 | return API.delete('auth'); 14 | }, 15 | }; 16 | 17 | export default AuthService; 18 | -------------------------------------------------------------------------------- /src/components/icons/chevron-right.tsx: -------------------------------------------------------------------------------- 1 | import Icon, { IconProps } from './icon'; 2 | 3 | const ChevronRightIcon = (props: IconProps) => { 4 | return ( 5 | 14 | 15 | 16 | } 17 | /> 18 | ); 19 | }; 20 | 21 | export default ChevronRightIcon; 22 | -------------------------------------------------------------------------------- /src/components/inputs/icon-button/icon-button.tsx: -------------------------------------------------------------------------------- 1 | import { MouseEventHandler } from 'react'; 2 | 3 | interface IconButtonProps { 4 | children: JSX.Element; 5 | onClick?: MouseEventHandler; 6 | } 7 | 8 | const IconButton = ({ children, onClick = () => {} }: IconButtonProps) => { 9 | return ( 10 | 16 | ); 17 | }; 18 | 19 | export default IconButton; 20 | -------------------------------------------------------------------------------- /src/services/server-invite-service.ts: -------------------------------------------------------------------------------- 1 | import { AxiosResponse } from 'axios'; 2 | import ServerInviteDto from '../utils/types/dtos/server-invite'; 3 | import CreateServerInviteRequest from '../utils/types/requests/server/invite/create-server-invite'; 4 | import API from './api'; 5 | 6 | const ServerInviteService = { 7 | create: ( 8 | serverId: number, 9 | payload: CreateServerInviteRequest, 10 | ): Promise> => { 11 | return API.post(`/server/${serverId}/invite`, payload); 12 | }, 13 | }; 14 | 15 | export default ServerInviteService; 16 | -------------------------------------------------------------------------------- /src/components/inputs/errors/errors.tsx: -------------------------------------------------------------------------------- 1 | import ErrorInterface from '../../../utils/types/interfaces/error'; 2 | 3 | interface ErrorsProps { 4 | errors: ErrorInterface[]; 5 | } 6 | 7 | const Errors = ({ errors }: ErrorsProps) => { 8 | return errors.length ? ( 9 |
10 | {errors.map((error, index) => { 11 | return ( 12 |

13 | {error.message} 14 |

15 | ); 16 | })} 17 |
18 | ) : ( 19 | <> 20 | ); 21 | }; 22 | 23 | export default Errors; 24 | -------------------------------------------------------------------------------- /src/server/routes/user/friend/friend.route.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import FriendController from '../../../controllers/user/friend'; 3 | import authenticate from '../../../middleware/authenticate'; 4 | 5 | const friendController = new FriendController(); 6 | 7 | const friendRouter = Router({ mergeParams: true }); 8 | friendRouter.post('/', authenticate, friendController.create); 9 | friendRouter.put('/:friendId', authenticate, friendController.update); 10 | friendRouter.delete('/:friendId', authenticate, friendController.delete); 11 | 12 | export default friendRouter; 13 | -------------------------------------------------------------------------------- /src/services/message-service.ts: -------------------------------------------------------------------------------- 1 | import { AxiosResponse } from 'axios'; 2 | import MessageDto from '../utils/types/dtos/message'; 3 | import API from './api'; 4 | 5 | const MessageService = { 6 | list: ({ 7 | directMessageId, 8 | skip, 9 | take, 10 | }: { 11 | directMessageId: string | number; 12 | skip: number; 13 | take: number; 14 | }): Promise> => { 15 | return API.get( 16 | `/message/?directMessageId=${directMessageId}&skip=${skip}&take=${take}`, 17 | ); 18 | }, 19 | }; 20 | 21 | export default MessageService; 22 | -------------------------------------------------------------------------------- /src/utils/types/dtos/server-user.ts: -------------------------------------------------------------------------------- 1 | import ServerUser from '../../../server/db/models/server-user.model'; 2 | import ServerRoleEnum from '../../enums/server-roles'; 3 | 4 | class ServerUserDto { 5 | public id: number; 6 | public role: ServerRoleEnum; 7 | public username: string; 8 | public email: string; 9 | 10 | constructor(serverUser: ServerUser) { 11 | this.id = serverUser.user.id; 12 | this.role = serverUser.role as ServerRoleEnum; 13 | this.username = serverUser.user.username; 14 | this.email = serverUser.user.email; 15 | } 16 | } 17 | 18 | export default ServerUserDto; 19 | -------------------------------------------------------------------------------- /src/components/icons/plus.tsx: -------------------------------------------------------------------------------- 1 | import Icon, { IconProps } from './icon'; 2 | 3 | const PlusIcon = (props: IconProps) => { 4 | return ( 5 | 14 | 19 | 20 | } 21 | /> 22 | ); 23 | }; 24 | 25 | export default PlusIcon; 26 | -------------------------------------------------------------------------------- /src/components/icons/bars.tsx: -------------------------------------------------------------------------------- 1 | import Icon, { IconProps } from './icon'; 2 | 3 | const BarsIcon = (props: IconProps) => { 4 | return ( 5 | 14 | 19 | 20 | } 21 | /> 22 | ); 23 | }; 24 | 25 | export default BarsIcon; 26 | -------------------------------------------------------------------------------- /src/components/icons/close.tsx: -------------------------------------------------------------------------------- 1 | import Icon, { IconProps } from './icon'; 2 | 3 | const CloseIcon = (props: IconProps) => { 4 | return ( 5 | 14 | 19 | 20 | } 21 | /> 22 | ); 23 | }; 24 | 25 | export default CloseIcon; 26 | -------------------------------------------------------------------------------- /src/utils/types/dtos/direct-message-user.ts: -------------------------------------------------------------------------------- 1 | import DirectMessageUser from '../../../server/db/models/direct-message-user.model'; 2 | 3 | class DirectMessageUserDto { 4 | public userId: number; 5 | public email: string; 6 | public username: string; 7 | 8 | constructor(directMessageUser: DirectMessageUser) { 9 | this.userId = directMessageUser.userId; 10 | this.email = 11 | directMessageUser.user != null ? directMessageUser.user.email : ''; 12 | this.username = 13 | directMessageUser.user != null ? directMessageUser.user.username : ''; 14 | } 15 | } 16 | 17 | export default DirectMessageUserDto; 18 | -------------------------------------------------------------------------------- /src/components/icons/channel.tsx: -------------------------------------------------------------------------------- 1 | import ChannelType from '../../utils/enums/channel-type'; 2 | import { IconProps } from './icon'; 3 | import PoundIcon from './pound'; 4 | import VolumeUpIcon from './volume-up'; 5 | 6 | interface ChannelIconProps extends IconProps { 7 | channelType: ChannelType; 8 | } 9 | 10 | const ChannelIcon = ({ channelType, ...props }: ChannelIconProps) => { 11 | return channelType === ChannelType.TEXT ? ( 12 | 13 | ) : channelType === ChannelType.VOICE ? ( 14 | 15 | ) : ( 16 | <> 17 | ); 18 | }; 19 | 20 | export default ChannelIcon; 21 | -------------------------------------------------------------------------------- /src/components/icons/chevron-down.tsx: -------------------------------------------------------------------------------- 1 | import Icon, { IconProps } from './icon'; 2 | 3 | const ChevronDownIcon = (props: IconProps) => { 4 | return ( 5 | 14 | 19 | 20 | } 21 | /> 22 | ); 23 | }; 24 | 25 | export default ChevronDownIcon; 26 | -------------------------------------------------------------------------------- /src/components/icons/pound.tsx: -------------------------------------------------------------------------------- 1 | import Icon, { IconProps } from './icon'; 2 | 3 | const PoundIcon = (props: IconProps) => { 4 | return ( 5 | 14 | 19 | 20 | } 21 | /> 22 | ); 23 | }; 24 | 25 | export default PoundIcon; 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "target": "es5", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "incremental": true 18 | }, 19 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /src/components/icons/search.tsx: -------------------------------------------------------------------------------- 1 | import Icon, { IconProps } from './icon'; 2 | 3 | const SearchIcon = (props: IconProps) => { 4 | return ( 5 | 14 | 19 | 20 | } 21 | /> 22 | ); 23 | }; 24 | 25 | export default SearchIcon; 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # vercel 29 | .vercel 30 | 31 | # typescript 32 | *.tsbuildinfo 33 | 34 | # env files 35 | *.env 36 | *.env.* 37 | !.env.example 38 | 39 | # config files 40 | src/server/db/config.json 41 | src/server/db/config.production.json 42 | -------------------------------------------------------------------------------- /src/server/routes/user/direct-message/direct-message.route.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import DirectMessageController from '../../../controllers/user/direct-message/direct-message.controller'; 3 | import authenticate from '../../../middleware/authenticate'; 4 | 5 | const directMessageController = new DirectMessageController(); 6 | 7 | const directMessageRouter = Router({ mergeParams: true }); 8 | directMessageRouter.get( 9 | '/:directMessageId', 10 | authenticate, 11 | directMessageController.get, 12 | ); 13 | directMessageRouter.post('/', authenticate, directMessageController.create); 14 | 15 | export default directMessageRouter; 16 | -------------------------------------------------------------------------------- /src/config/db.config.ts: -------------------------------------------------------------------------------- 1 | import { Sequelize } from 'sequelize-typescript'; 2 | import env from './env.config'; 3 | 4 | const sequelize = 5 | env.DATABASE_URL.length > 0 6 | ? new Sequelize(env.DATABASE_URL, { 7 | dialect: 'postgres', 8 | dialectOptions: { 9 | ssl: { 10 | require: true, 11 | rejectUnauthorized: false, 12 | }, 13 | }, 14 | logging: false, 15 | }) 16 | : new Sequelize(env.DATABASE, env.DB_USER, env.DB_PASSWORD, { 17 | host: env.DB_HOST, 18 | dialect: 'postgres', 19 | logging: false, 20 | }); 21 | 22 | export default sequelize; 23 | -------------------------------------------------------------------------------- /src/server/validators/server/invite/invite.validator.ts: -------------------------------------------------------------------------------- 1 | import ErrorInterface from '../../../../utils/types/interfaces/error'; 2 | import CreateServerInviteRequest from '../../../../utils/types/requests/server/invite/create-server-invite'; 3 | 4 | const InviteValidator = { 5 | create: ({ friendId }: CreateServerInviteRequest) => { 6 | const validationErrors: ErrorInterface[] = []; 7 | if (friendId == null || isNaN(friendId)) 8 | validationErrors.push({ 9 | field: 'friendId', 10 | message: 'Must provide a valid friendId', 11 | }); 12 | return validationErrors; 13 | }, 14 | }; 15 | 16 | export default InviteValidator; 17 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/profile-image/profile-image.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | 3 | interface ProfileImageProps { 4 | width: string | number; 5 | height: string | number; 6 | } 7 | 8 | const ProfileImage = ({ width, height }: ProfileImageProps) => { 9 | const src = 10 | 'https://www.clipartmax.com/png/middle/364-3643767_about-brent-kovacs-user-profile-placeholder.png'; 11 | 12 | return ( 13 | Profile Image src} 17 | width={width} 18 | height={height} 19 | className="rounded-full" 20 | /> 21 | ); 22 | }; 23 | 24 | export default ProfileImage; 25 | -------------------------------------------------------------------------------- /src/utils/types/dtos/request-user.ts: -------------------------------------------------------------------------------- 1 | import User from '../../../server/db/models/user.model'; 2 | import RoleEnum from '../../enums/roles'; 3 | 4 | class RequestUser { 5 | public id: number; 6 | public email: string; 7 | public roles: RoleEnum[]; 8 | 9 | constructor({ id, email, roles }: User) { 10 | this.id = id; 11 | this.email = email; 12 | this.roles = 13 | roles == null ? [] : roles.map((userRole) => userRole.role as RoleEnum); 14 | } 15 | 16 | public toJSON = () => { 17 | return { 18 | id: this.id, 19 | email: this.email, 20 | roles: this.roles, 21 | }; 22 | }; 23 | } 24 | 25 | export default RequestUser; 26 | -------------------------------------------------------------------------------- /src/services/server-service.ts: -------------------------------------------------------------------------------- 1 | import { AxiosResponse } from 'axios'; 2 | import ServerDto from '../utils/types/dtos/server'; 3 | import CreateServerRequest from '../utils/types/requests/server/create-server'; 4 | import API from './api'; 5 | 6 | const ServerService = { 7 | create: (payload: CreateServerRequest): Promise> => { 8 | return API.post('/server', payload); 9 | }, 10 | list: (): Promise> => { 11 | return API.get('/server'); 12 | }, 13 | get: (id: string | number): Promise> => { 14 | return API.get(`/server/${id}`); 15 | }, 16 | }; 17 | 18 | export default ServerService; 19 | -------------------------------------------------------------------------------- /src/components/icons/check.tsx: -------------------------------------------------------------------------------- 1 | import Icon, { IconProps } from './icon'; 2 | 3 | const CheckIcon = (props: IconProps) => { 4 | return ( 5 | 16 | 21 | 22 | } 23 | /> 24 | ); 25 | }; 26 | 27 | export default CheckIcon; 28 | -------------------------------------------------------------------------------- /src/styles/animations.css: -------------------------------------------------------------------------------- 1 | .bounce-in { 2 | animation: bounce-in 0.5s; 3 | } 4 | 5 | @keyframes bounce-in { 6 | 0% { 7 | transform: translateY(-25px) scale(1.25); 8 | } 9 | 50% { 10 | transform: translateY(0); 11 | } 12 | 75% { 13 | transform: translateY(-5px); 14 | } 15 | 100% { 16 | transform: translateY(0) scale(1); 17 | } 18 | } 19 | @-webkit-keyframes bounce-in { 20 | 0% { 21 | -webkit-transform: translateY(-25px) scale(1.25); 22 | } 23 | 50% { 24 | -webkit-transform: translateY(0); 25 | } 26 | 75% { 27 | -webkit-transform: translateY(-5px); 28 | } 29 | 100% { 30 | -webkit-transform: translateY(0) scale(1); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/components/icons/pencil.tsx: -------------------------------------------------------------------------------- 1 | import Icon, { IconProps } from './icon'; 2 | 3 | const PencilIcon = (props: IconProps) => { 4 | return ( 5 | 14 | 19 | 20 | } 21 | /> 22 | ); 23 | }; 24 | 25 | export default PencilIcon; 26 | -------------------------------------------------------------------------------- /src/components/icons/exclamation-circle.tsx: -------------------------------------------------------------------------------- 1 | import Icon, { IconProps } from './icon'; 2 | 3 | const ExclamationCircleIcon = (props: IconProps) => { 4 | return ( 5 | 13 | 18 | 19 | } 20 | /> 21 | ); 22 | }; 23 | 24 | export default ExclamationCircleIcon; 25 | -------------------------------------------------------------------------------- /src/components/icons/microphone.tsx: -------------------------------------------------------------------------------- 1 | import Icon, { IconProps } from './icon'; 2 | 3 | const MicrophoneIcon = (props: IconProps) => { 4 | return ( 5 | 14 | 19 | 20 | } 21 | /> 22 | ); 23 | }; 24 | 25 | export default MicrophoneIcon; 26 | -------------------------------------------------------------------------------- /src/server/middleware/authenticate.ts: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import { Request, Response, NextFunction } from 'express'; 3 | import env from '../../config/env.config'; 4 | import RequestUser from '../../utils/types/dtos/request-user'; 5 | 6 | const authenticate = (req: Request, res: Response, next: NextFunction) => { 7 | const authHeader = req.headers['authorization']; 8 | const token = authHeader && authHeader.split(' ')[1]; 9 | 10 | if (!token) return res.sendStatus(401); 11 | 12 | try { 13 | req.user = jwt.verify(token, env.ACCESS_TOKEN_SECRET) as RequestUser; 14 | return next(); 15 | } catch { 16 | return res.sendStatus(401); 17 | } 18 | }; 19 | 20 | export default authenticate; 21 | -------------------------------------------------------------------------------- /src/server/db/seeders/20220217160636-add-user-roles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up(queryInterface, Sequelize) { 5 | await queryInterface.bulkInsert( 6 | 'user_role', 7 | [ 8 | { 9 | user_id: 1, 10 | role: 'ADMIN', 11 | created_at: new Date(), 12 | updated_at: new Date(), 13 | }, 14 | { 15 | user_id: 1, 16 | role: 'SUPERADMIN', 17 | created_at: new Date(), 18 | updated_at: new Date(), 19 | }, 20 | ], 21 | {}, 22 | ); 23 | }, 24 | 25 | async down(queryInterface, Sequelize) { 26 | await queryInterface.bulkDelete('user_role', null, {}); 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /src/server/validators/user/friend/friend.validator.ts: -------------------------------------------------------------------------------- 1 | import isEmail from 'validator/lib/isEmail'; 2 | import ErrorInterface from '../../../../utils/types/interfaces/error'; 3 | import CreateFriendRequest from '../../../../utils/types/requests/user/friend/create-friend'; 4 | 5 | const FriendValidator = { 6 | create: ({ addresseeEmail }: CreateFriendRequest): ErrorInterface[] => { 7 | const errors: ErrorInterface[] = []; 8 | 9 | if (addresseeEmail == null || !isEmail(addresseeEmail)) { 10 | errors.push({ 11 | field: 'addresseeEmail', 12 | message: 'Must provide a valid addressee email.', 13 | }); 14 | } 15 | 16 | return errors; 17 | }, 18 | }; 19 | 20 | export default FriendValidator; 21 | -------------------------------------------------------------------------------- /src/services/channel-service.ts: -------------------------------------------------------------------------------- 1 | import { AxiosResponse } from 'axios'; 2 | import ChannelDto from '../utils/types/dtos/channel'; 3 | import CreateChannelRequest from '../utils/types/requests/server/channel/create-channel'; 4 | import API from './api'; 5 | 6 | const ChannelService = { 7 | create: ( 8 | serverId: number, 9 | payload: CreateChannelRequest, 10 | ): Promise> => { 11 | return API.post(`/server/${serverId}/channel`, payload); 12 | }, 13 | get: ( 14 | serverId: number, 15 | channelId: number, 16 | ): Promise> => { 17 | return API.get(`/server/${serverId}/channel/${channelId}`); 18 | }, 19 | }; 20 | 21 | export default ChannelService; 22 | -------------------------------------------------------------------------------- /src/styles/spinners.css: -------------------------------------------------------------------------------- 1 | .spinner { 2 | display: inline-block; 3 | position: relative; 4 | } 5 | .spinner div { 6 | box-sizing: border-box; 7 | display: block; 8 | position: absolute; 9 | border-style: solid; 10 | border-radius: 50%; 11 | animation: spinner 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; 12 | border-color: #fff transparent transparent transparent; 13 | } 14 | .spinner div:nth-child(1) { 15 | animation-delay: -0.45s; 16 | } 17 | .spinner div:nth-child(2) { 18 | animation-delay: -0.3s; 19 | } 20 | .spinner div:nth-child(3) { 21 | animation-delay: -0.15s; 22 | } 23 | @keyframes spinner { 24 | 0% { 25 | transform: rotate(0deg); 26 | } 27 | 100% { 28 | transform: rotate(360deg); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/channels/channel-header/channel-header.tsx: -------------------------------------------------------------------------------- 1 | import useChannel from '../../../../../utils/hooks/use-channel'; 2 | import { ChannelIcon, IconSize } from '../../../../icons'; 3 | import Header from '../../header'; 4 | 5 | const ChannelHeader = () => { 6 | const { channel } = useChannel(); 7 | 8 | return ( 9 |
10 | {channel == null ? ( 11 | <> 12 | ) : ( 13 |
14 | 15 |

{channel.name}

16 |
17 | )} 18 |
19 | ); 20 | }; 21 | 22 | export default ChannelHeader; 23 | -------------------------------------------------------------------------------- /src/pages/friends/online.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | import AppLayout from '../../components/layouts/app-layout'; 3 | import FriendHeader from '../../components/layouts/app-layout/friends/friend-header/friend-header'; 4 | import FriendSidebar from '../../components/layouts/app-layout/friends/friend-sidebar/friend-sidebar'; 5 | import { NextPageLayout } from '../../utils/types/next-page-layout'; 6 | 7 | const OnlineFriendsPage: NextPageLayout = () => { 8 | return <>; 9 | }; 10 | 11 | OnlineFriendsPage.getLayout = (page: ReactElement) => { 12 | return ( 13 | } sidebar={}> 14 | {page} 15 | 16 | ); 17 | }; 18 | 19 | export default OnlineFriendsPage; 20 | -------------------------------------------------------------------------------- /src/server/db/models/user-role.model.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | Column, 4 | Model, 5 | DataType, 6 | ForeignKey, 7 | BelongsTo, 8 | PrimaryKey, 9 | } from 'sequelize-typescript'; 10 | import RoleEnum from '../../../utils/enums/roles'; 11 | import User from './user.model'; 12 | 13 | const ROLE_ENUM = Object.values(RoleEnum).map((role) => role.toString()); 14 | 15 | @Table({ tableName: 'user_role', underscored: true }) 16 | class UserRole extends Model { 17 | @PrimaryKey 18 | @Column(DataType.ENUM(...ROLE_ENUM)) 19 | role!: string; 20 | 21 | @PrimaryKey 22 | @ForeignKey(() => User) 23 | @Column(DataType.INTEGER) 24 | userId!: number; 25 | 26 | @BelongsTo(() => User) 27 | user!: User; 28 | } 29 | 30 | export default UserRole; 31 | -------------------------------------------------------------------------------- /src/server/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import RoleEnum from '../../utils/enums/roles'; 3 | import authenticate from '../middleware/authenticate'; 4 | import authorize from '../middleware/authorize'; 5 | import authRouter from './auth/auth.route'; 6 | import messageRouter from './message'; 7 | import serverRouter from './server/server.route'; 8 | import userRouter from './user/user.route'; 9 | 10 | const router = Router(); 11 | router.get('/', authenticate, authorize([RoleEnum.SUPERADMIN]), (req, res) => { 12 | return res.sendStatus(200); 13 | }); 14 | router.use('/user', userRouter); 15 | router.use('/auth', authRouter); 16 | router.use('/server', serverRouter); 17 | router.use('/message', messageRouter); 18 | 19 | export default router; 20 | -------------------------------------------------------------------------------- /src/server/db/models/invitable.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BelongsTo, 3 | Column, 4 | DataType, 5 | Default, 6 | ForeignKey, 7 | Model, 8 | } from 'sequelize-typescript'; 9 | import User from './user.model'; 10 | 11 | class Invitable extends Model { 12 | @ForeignKey(() => User) 13 | @Column(DataType.INTEGER) 14 | requesterId!: number; 15 | 16 | @BelongsTo(() => User, 'requester_id') 17 | requester!: User; 18 | 19 | @ForeignKey(() => User) 20 | @Column(DataType.INTEGER) 21 | addresseeId!: number; 22 | 23 | @BelongsTo(() => User, 'addressee_id') 24 | addressee!: User; 25 | 26 | @Default(false) 27 | @Column(DataType.BOOLEAN) 28 | accepted!: boolean; 29 | 30 | @Column(DataType.DATE) 31 | acceptedAt!: Date; 32 | } 33 | 34 | export default Invitable; 35 | -------------------------------------------------------------------------------- /src/server/db/models/channel.model.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | Column, 4 | Model, 5 | DataType, 6 | ForeignKey, 7 | BelongsTo, 8 | } from 'sequelize-typescript'; 9 | import ChannelType from '../../../utils/enums/channel-type'; 10 | import Server from './server.model'; 11 | 12 | const CHANNEL_TYPE = Object.values(ChannelType).map((type) => type.toString()); 13 | 14 | @Table({ tableName: 'channel', underscored: true }) 15 | class Channel extends Model { 16 | @Column(DataType.ENUM(...CHANNEL_TYPE)) 17 | type!: string; 18 | 19 | @Column(DataType.STRING) 20 | name!: string; 21 | 22 | @ForeignKey(() => Server) 23 | @Column(DataType.INTEGER) 24 | serverId!: number; 25 | 26 | @BelongsTo(() => Server) 27 | server!: Server; 28 | } 29 | 30 | export default Channel; 31 | -------------------------------------------------------------------------------- /src/components/icons/volume-up.tsx: -------------------------------------------------------------------------------- 1 | import Icon, { IconProps } from './icon'; 2 | 3 | const VolumeUpIcon = (props: IconProps) => { 4 | return ( 5 | 14 | 19 | 20 | } 21 | /> 22 | ); 23 | }; 24 | 25 | export default VolumeUpIcon; 26 | -------------------------------------------------------------------------------- /src/server/db/models/direct-message.model.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | Column, 4 | Model, 5 | DataType, 6 | ForeignKey, 7 | BelongsTo, 8 | HasMany, 9 | } from 'sequelize-typescript'; 10 | import DirectMessageUser from './direct-message-user.model'; 11 | import Message from './message.model'; 12 | import User from './user.model'; 13 | 14 | @Table({ tableName: 'direct_message', underscored: true }) 15 | class DirectMessage extends Model { 16 | @ForeignKey(() => User) 17 | @Column(DataType.INTEGER) 18 | createdById!: number; 19 | 20 | @BelongsTo(() => User) 21 | createdBy!: User; 22 | 23 | @HasMany(() => DirectMessageUser) 24 | users!: DirectMessageUser[]; 25 | 26 | @HasMany(() => Message) 27 | messages!: Message[]; 28 | } 29 | 30 | export default DirectMessage; 31 | -------------------------------------------------------------------------------- /src/components/icons/user-add.tsx: -------------------------------------------------------------------------------- 1 | import Icon, { IconProps } from './icon'; 2 | 3 | const UserAddIcon = (props: IconProps) => { 4 | return ( 5 | 17 | 18 | 19 | 20 | 21 | 22 | } 23 | /> 24 | ); 25 | }; 26 | 27 | export default UserAddIcon; 28 | -------------------------------------------------------------------------------- /src/components/icons/icon.tsx: -------------------------------------------------------------------------------- 1 | import { cloneElement } from 'react'; 2 | import IconSize from '../../utils/enums/icon-size'; 3 | export interface IconProps { 4 | className?: string; 5 | size?: IconSize; 6 | width?: number | string; 7 | height?: number | string; 8 | strokeWidth?: number | string; 9 | } 10 | 11 | interface IconWrapperProps extends IconProps { 12 | icon: JSX.Element; 13 | } 14 | 15 | const Icon = ({ 16 | size = IconSize.md, 17 | strokeWidth = 2, 18 | icon, 19 | width, 20 | height, 21 | className, 22 | }: IconWrapperProps) => { 23 | const iconStyle = { 24 | width: width ?? size, 25 | height: height ?? size, 26 | strokeWidth, 27 | }; 28 | 29 | return <>{cloneElement(icon, { style: iconStyle, className: className })}; 30 | }; 31 | 32 | export default Icon; 33 | -------------------------------------------------------------------------------- /src/utils/date-utils.ts: -------------------------------------------------------------------------------- 1 | const DateUtils = { 2 | UTC: () => { 3 | const date = new Date(); 4 | return Date.UTC( 5 | date.getUTCFullYear(), 6 | date.getUTCMonth(), 7 | date.getUTCDate(), 8 | date.getUTCHours(), 9 | date.getUTCMinutes(), 10 | date.getUTCSeconds(), 11 | ); 12 | }, 13 | getFormattedDate: (date: Date | string) => { 14 | const formattedDate = new Date(date); 15 | const year = formattedDate.getFullYear(); 16 | 17 | let month = (1 + formattedDate.getMonth()).toString(); 18 | month = month.length > 1 ? month : '0' + month; 19 | 20 | let day = formattedDate.getDate().toString(); 21 | day = day.length > 1 ? day : '0' + day; 22 | 23 | return month + '/' + day + '/' + year; 24 | }, 25 | }; 26 | 27 | export default DateUtils; 28 | -------------------------------------------------------------------------------- /src/utils/enums/events.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | enum EventEnum { 3 | CONNECTION = 'connection', 4 | DISCONNECT = 'disconnect', 5 | JOIN_SERVER = 'join-server', 6 | JOIN_CHANNEL = 'join-channel', 7 | SEND_OFFER = 'send-offer', 8 | RECEIVE_OFFER = 'receive-offer', 9 | SEND_ANSWER = 'send-answer', 10 | RECEIVE_ANSWER = 'receive-answer', 11 | SEND_ICE_CANDIDATE = 'send-ice-candidate', 12 | RECEIVE_ICE_CANDIDATE = 'receive-ice-candidate', 13 | USER_CONNECTED = 'user-connected', 14 | USER_DISCONNECTED = 'user-disconnected', 15 | JOIN_DIRECT_MESSAGE = 'join-direct-message', 16 | SEND_DIRECT_MESSAGE = 'send-direct-message', 17 | RECEIVE_DIRECT_MESSAGE = 'receive-direct-message', 18 | DIRECT_MESSAGE_CREATED = 'direct-message-created', 19 | } 20 | 21 | export default EventEnum; 22 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/channels/server-body/create-channel-button.tsx: -------------------------------------------------------------------------------- 1 | import useApp from '../../../../../utils/hooks/use-app'; 2 | import Tooltip from '../../../../feedback/tooltip'; 3 | import { IconSize, PlusIcon } from '../../../../icons'; 4 | 5 | const CreateChannelButton = () => { 6 | const { setShowCreateChannelModal } = useApp(); 7 | 8 | const handleCreateChannelBtnClick = () => { 9 | setShowCreateChannelModal(true); 10 | }; 11 | return ( 12 | 13 | 19 | 20 | ); 21 | }; 22 | 23 | export default CreateChannelButton; 24 | -------------------------------------------------------------------------------- /src/server/routes/server/server.route.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import ServerController from '../../controllers/server'; 3 | import authenticate from '../../middleware/authenticate'; 4 | import userRouter from './user'; 5 | import channelRouter from './channel'; 6 | import inviteRouter from './invite'; 7 | 8 | const serverController = new ServerController(); 9 | 10 | const serverRouter = Router(); 11 | serverRouter.post('/', authenticate, serverController.create); 12 | serverRouter.get('/', authenticate, serverController.index); 13 | serverRouter.get('/:serverId', authenticate, serverController.get); 14 | serverRouter.use('/:serverId/channel', channelRouter); 15 | serverRouter.use('/:serverId/invite', inviteRouter); 16 | serverRouter.use('/:serverId/user', userRouter); 17 | 18 | export default serverRouter; 19 | -------------------------------------------------------------------------------- /src/server/validators/auth/auth.validator.ts: -------------------------------------------------------------------------------- 1 | import isEmail from 'validator/lib/isEmail'; 2 | import isLength from 'validator/lib/isLength'; 3 | import ErrorInterface from '../../../utils/types/interfaces/error'; 4 | import LoginRequest from '../../../utils/types/requests/auth/login'; 5 | 6 | const AuthValidator = { 7 | login: ({ email, password }: LoginRequest) => { 8 | const errors: ErrorInterface[] = []; 9 | 10 | if (email == null || !isEmail(email)) 11 | errors.push({ field: 'email', message: 'Must provide a valid email.' }); 12 | if (password == null || !isLength(password, { min: 8 })) 13 | errors.push({ 14 | field: 'password', 15 | message: 'Must provide a valid password.', 16 | }); 17 | 18 | return errors; 19 | }, 20 | }; 21 | 22 | export default AuthValidator; 23 | -------------------------------------------------------------------------------- /src/server/db/models/direct-message-user.model.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BelongsTo, 3 | Column, 4 | DataType, 5 | ForeignKey, 6 | Model, 7 | PrimaryKey, 8 | Table, 9 | } from 'sequelize-typescript'; 10 | import DirectMessage from './direct-message.model'; 11 | import User from './user.model'; 12 | 13 | @Table({ tableName: 'direct_message_user', underscored: true }) 14 | class DirectMessageUser extends Model { 15 | @PrimaryKey 16 | @ForeignKey(() => DirectMessage) 17 | @Column(DataType.INTEGER) 18 | directMessageId!: number; 19 | 20 | @BelongsTo(() => DirectMessage) 21 | directMessage!: DirectMessage; 22 | 23 | @PrimaryKey 24 | @ForeignKey(() => User) 25 | @Column(DataType.INTEGER) 26 | userId!: number; 27 | 28 | @BelongsTo(() => User) 29 | user!: User; 30 | } 31 | 32 | export default DirectMessageUser; 33 | -------------------------------------------------------------------------------- /src/server/index.ts: -------------------------------------------------------------------------------- 1 | import createNext from 'next'; 2 | import { Response, Request } from 'express'; 3 | 4 | const dev = process.env.NODE_ENV !== 'production'; 5 | const next = createNext({ dev }); 6 | const handle = next.getRequestHandler(); 7 | 8 | next 9 | .prepare() 10 | .then(() => { 11 | const env = require('../config/env.config').default; 12 | const app = require('./app').default; 13 | const server = require('./server').default; 14 | 15 | // NEXT PAGES 16 | app.get('*', (req: Request, res: Response) => { 17 | return handle(req, res); 18 | }); 19 | 20 | // START APP 21 | server.listen(env.PORT, () => { 22 | console.log(`Server now listening on port ${env.PORT}`); 23 | }); 24 | }) 25 | .catch((error) => { 26 | console.error(error.stack); 27 | process.exit(1); 28 | }); 29 | -------------------------------------------------------------------------------- /src/server/middleware/authorize.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import RoleEnum from '../../utils/enums/roles'; 3 | import UserRole from '../db/models/user-role.model'; 4 | 5 | const authorize = (permittedRoles: RoleEnum[]) => { 6 | return async (req: Request, res: Response, next: NextFunction) => { 7 | if (req.user == null) return res.sendStatus(401); 8 | const userId = req.user.id; 9 | 10 | const userRoles = await UserRole.findAll({ 11 | where: { 12 | userId, 13 | }, 14 | }); 15 | 16 | const roles = userRoles.map((userRole) => userRole.role as RoleEnum); 17 | 18 | if (permittedRoles.some((permittedRole) => roles.includes(permittedRole))) 19 | return next(); 20 | else return res.sendStatus(403); 21 | }; 22 | }; 23 | 24 | export default authorize; 25 | -------------------------------------------------------------------------------- /src/server/validators/user/direct-message/direct-message.validator.ts: -------------------------------------------------------------------------------- 1 | import ErrorInterface from '../../../../utils/types/interfaces/error'; 2 | import CreateDirectMessageRequest from '../../../../utils/types/requests/user/direct-message/create-direct-message'; 3 | 4 | const DirectMessageValidator = { 5 | create: ({ friendIds }: CreateDirectMessageRequest) => { 6 | const errors: ErrorInterface[] = []; 7 | 8 | if (friendIds == null) 9 | errors.push({ 10 | field: 'friendIds', 11 | message: 'Must provide a list of friendIds.', 12 | }); 13 | else if (friendIds.length < 1) 14 | errors.push({ 15 | field: 'friendIds', 16 | message: 'Must provide at least one friendId.', 17 | }); 18 | 19 | return errors; 20 | }, 21 | }; 22 | 23 | export default DirectMessageValidator; 24 | -------------------------------------------------------------------------------- /src/pages/friends/add.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | import AppLayout from '../../components/layouts/app-layout'; 3 | import AddFriends from '../../components/layouts/app-layout/friends/add-friends/add-friends'; 4 | import FriendHeader from '../../components/layouts/app-layout/friends/friend-header/friend-header'; 5 | import FriendSidebar from '../../components/layouts/app-layout/friends/friend-sidebar/friend-sidebar'; 6 | import { NextPageLayout } from '../../utils/types/next-page-layout'; 7 | 8 | const AddFriendsPage: NextPageLayout = () => { 9 | return ; 10 | }; 11 | 12 | AddFriendsPage.getLayout = (page: ReactElement) => { 13 | return ( 14 | } sidebar={}> 15 | {page} 16 | 17 | ); 18 | }; 19 | 20 | export default AddFriendsPage; 21 | -------------------------------------------------------------------------------- /src/pages/friends/all.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | import AppLayout from '../../components/layouts/app-layout'; 3 | import AllFriends from '../../components/layouts/app-layout/friends/all-friends/all-friends'; 4 | import FriendHeader from '../../components/layouts/app-layout/friends/friend-header/friend-header'; 5 | import FriendSidebar from '../../components/layouts/app-layout/friends/friend-sidebar/friend-sidebar'; 6 | import { NextPageLayout } from '../../utils/types/next-page-layout'; 7 | 8 | const AllFriendsPage: NextPageLayout = () => { 9 | return ; 10 | }; 11 | 12 | AllFriendsPage.getLayout = (page: ReactElement) => { 13 | return ( 14 | } sidebar={}> 15 | {page} 16 | 17 | ); 18 | }; 19 | 20 | export default AllFriendsPage; 21 | -------------------------------------------------------------------------------- /src/services/direct-message-service.ts: -------------------------------------------------------------------------------- 1 | import { AxiosResponse } from 'axios'; 2 | import DirectMessageDto from '../utils/types/dtos/direct-message'; 3 | import CreateDirectMessageRequest from '../utils/types/requests/user/direct-message/create-direct-message'; 4 | import API from './api'; 5 | 6 | const DirectMessageService = { 7 | create: ( 8 | payload: CreateDirectMessageRequest, 9 | ): Promise> => { 10 | return API.post(`/user/${payload.userId}/direct-message`, payload); 11 | }, 12 | get: ({ 13 | userId, 14 | directMessageId, 15 | }: { 16 | userId: number; 17 | directMessageId: string | number; 18 | }): Promise> => { 19 | return API.get(`/user/${userId}/direct-message/${directMessageId}`); 20 | }, 21 | }; 22 | 23 | export default DirectMessageService; 24 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/friends/friend-header/friend-button/friend-button.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { useRouter } from 'next/router'; 3 | 4 | interface FriendButtonProps { 5 | href: string; 6 | children: JSX.Element | string; 7 | } 8 | 9 | const FriendButton = ({ href, children }: FriendButtonProps) => { 10 | const router = useRouter(); 11 | 12 | return ( 13 | 14 | 21 | {children} 22 | 23 | 24 | ); 25 | }; 26 | 27 | export default FriendButton; 28 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/header/header.tsx: -------------------------------------------------------------------------------- 1 | import useApp from '../../../../utils/hooks/use-app'; 2 | import { BarsIcon, IconSize } from '../../../icons'; 3 | 4 | interface HeaderProps { 5 | children: JSX.Element; 6 | } 7 | 8 | const Header = ({ children }: HeaderProps) => { 9 | const { setShowSidebar } = useApp(); 10 | 11 | const handleSidebarBtnClick = () => { 12 | setShowSidebar((prev) => !prev); 13 | }; 14 | 15 | return ( 16 |
17 | 23 | {children} 24 |
25 | ); 26 | }; 27 | 28 | export default Header; 29 | -------------------------------------------------------------------------------- /src/pages/friends/pending.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | import AppLayout from '../../components/layouts/app-layout'; 3 | import PendingFriends from '../../components/layouts/app-layout/friends/pending-friends/pending-friends'; 4 | import FriendHeader from '../../components/layouts/app-layout/friends/friend-header/friend-header'; 5 | import { NextPageLayout } from '../../utils/types/next-page-layout'; 6 | import FriendSidebar from '../../components/layouts/app-layout/friends/friend-sidebar/friend-sidebar'; 7 | 8 | const PendingFriendsPage: NextPageLayout = () => { 9 | return ; 10 | }; 11 | 12 | PendingFriendsPage.getLayout = (page: ReactElement) => { 13 | return ( 14 | } sidebar={}> 15 | {page} 16 | 17 | ); 18 | }; 19 | 20 | export default PendingFriendsPage; 21 | -------------------------------------------------------------------------------- /src/components/icons/eye.tsx: -------------------------------------------------------------------------------- 1 | import Icon, { IconProps } from './icon'; 2 | 3 | const EyeIcon = (props: IconProps) => { 4 | return ( 5 | 14 | 19 | 24 | 25 | } 26 | /> 27 | ); 28 | }; 29 | 30 | export default EyeIcon; 31 | -------------------------------------------------------------------------------- /src/components/icons/eye-off.tsx: -------------------------------------------------------------------------------- 1 | import Icon, { IconProps } from './icon'; 2 | 3 | const EyeOffIcon = (props: IconProps) => { 4 | return ( 5 | 14 | 19 | 20 | } 21 | /> 22 | ); 23 | }; 24 | 25 | export default EyeOffIcon; 26 | -------------------------------------------------------------------------------- /src/server/db/seeders/20220524035027-add-friends.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up(queryInterface, Sequelize) { 5 | await queryInterface.bulkInsert( 6 | 'friend', 7 | [ 8 | { 9 | id: 1, 10 | requester_id: 1, 11 | addressee_id: 2, 12 | accepted: true, 13 | accepted_at: new Date(), 14 | created_at: new Date(), 15 | updated_at: new Date(), 16 | }, 17 | { 18 | id: 2, 19 | requester_id: 1, 20 | addressee_id: 3, 21 | accepted: true, 22 | accepted_at: new Date(), 23 | created_at: new Date(), 24 | updated_at: new Date(), 25 | }, 26 | ], 27 | {}, 28 | ); 29 | }, 30 | 31 | async down(queryInterface, Sequelize) { 32 | await queryInterface.bulkDelete('user', null, {}); 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/channels/text-channel/text-channel.tsx: -------------------------------------------------------------------------------- 1 | import useChannel from '../../../../../utils/hooks/use-channel'; 2 | import WelcomeText from '../welcome-text'; 3 | import ChannelTextArea from './channel-text-area'; 4 | 5 | const TextChannel = () => { 6 | const { channel } = useChannel(); 7 | 8 | return ( 9 |
10 |
11 | 15 |
16 | {}} 19 | onKeyDown={() => {}} 20 | placeholder={`Message #${channel?.name}`} 21 | /> 22 |
23 | ); 24 | }; 25 | 26 | export default TextChannel; 27 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/sidebar/sidebar.tsx: -------------------------------------------------------------------------------- 1 | import useWindowSize from '../../../../utils/hooks/use-window-size'; 2 | import InviteFriendModal from '../channels/invite-people-modal'; 3 | import SidebarFooter from './sidebar-footer'; 4 | 5 | interface SidebarProps { 6 | header: JSX.Element; 7 | body: JSX.Element; 8 | } 9 | 10 | const Sidebar = ({ header, body }: SidebarProps) => { 11 | const { heightStyle } = useWindowSize(); 12 | 13 | return ( 14 | <> 15 |
19 |
{header}
20 |
{body}
21 | 22 |
23 | 24 | 25 | ); 26 | }; 27 | 28 | export default Sidebar; 29 | -------------------------------------------------------------------------------- /src/server/routes/user/user.route.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import UserController from '../../controllers/user'; 3 | import authenticate from '../../middleware/authenticate'; 4 | import directMessageRouter from './direct-message/direct-message.route'; 5 | import friendRouter from './friend'; 6 | 7 | const userController = new UserController(); 8 | 9 | const userRouter = Router(); 10 | userRouter.post('/', userController.register); 11 | userRouter.put('/verify-email/:token', userController.verifyEmail); 12 | userRouter.get('/:id', authenticate, userController.getUser); 13 | userRouter.put('/reset-password', userController.resetPassword); 14 | userRouter.put( 15 | '/reset-password/confirm/:token', 16 | userController.confirmResetPassword, 17 | ); 18 | userRouter.use('/:id/friend', friendRouter); 19 | userRouter.use('/:id/direct-message', directMessageRouter); 20 | 21 | export default userRouter; 22 | -------------------------------------------------------------------------------- /src/components/layouts/app-layout/channels/welcome-text/welcome-text.tsx: -------------------------------------------------------------------------------- 1 | import ChannelType from '../../../../../utils/enums/channel-type'; 2 | import { ChannelIcon, IconSize } from '../../../../icons'; 3 | 4 | interface WelcomeTextProps { 5 | channelName: string; 6 | channelType: ChannelType; 7 | } 8 | 9 | const WelcomeText = ({ channelName, channelType }: WelcomeTextProps) => { 10 | return ( 11 | 12 | 13 | 14 | 15 |

Welcome to #{channelName}!

16 |

17 | This is the start of the #{channelName} channel. 18 |

19 |
20 | ); 21 | }; 22 | 23 | export default WelcomeText; 24 | -------------------------------------------------------------------------------- /src/components/routes/auth-route/auth-route.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import { useEffect } from 'react'; 3 | import { UserProvder } from '../../../utils/contexts/user-context'; 4 | import useAuth from '../../../utils/hooks/use-auth'; 5 | 6 | interface AuthRouteProps { 7 | element: JSX.Element; 8 | } 9 | 10 | const AuthRoute = ({ element }: AuthRouteProps) => { 11 | const { loading, isAuthenticated, refreshAccessToken } = useAuth(); 12 | const router = useRouter(); 13 | 14 | useEffect(() => { 15 | refreshAccessToken(); 16 | // eslint-disable-next-line react-hooks/exhaustive-deps 17 | }, []); 18 | 19 | if (loading) { 20 | return <>; 21 | } else { 22 | if (isAuthenticated) { 23 | return {element}; 24 | } else { 25 | router.push('/login'); 26 | return <>; 27 | } 28 | } 29 | }; 30 | 31 | export default AuthRoute; 32 | -------------------------------------------------------------------------------- /src/services/friend-service.ts: -------------------------------------------------------------------------------- 1 | import { AxiosResponse } from 'axios'; 2 | import FriendRequestDto from '../utils/types/dtos/friend-request'; 3 | import CreateFriendRequest from '../utils/types/requests/user/friend/create-friend'; 4 | import API from './api'; 5 | 6 | const FriendService = { 7 | create: ( 8 | userId: number, 9 | payload: CreateFriendRequest, 10 | ): Promise> => { 11 | return API.post(`/user/${userId}/friend`, payload); 12 | }, 13 | update: ( 14 | userId: number, 15 | friendId: number, 16 | ): Promise> => { 17 | return API.put(`/user/${userId}/friend/${friendId}`); 18 | }, 19 | delete: ( 20 | userId: number, 21 | friendId: number, 22 | ): Promise> => { 23 | return API.delete(`/user/${userId}/friend/${friendId}`); 24 | }, 25 | }; 26 | 27 | export default FriendService; 28 | -------------------------------------------------------------------------------- /src/components/icons/camera.tsx: -------------------------------------------------------------------------------- 1 | import Icon, { IconProps } from './icon'; 2 | 3 | const CameraIcon = (props: IconProps) => { 4 | return ( 5 | 14 | 19 | 24 | 25 | } 26 | /> 27 | ); 28 | }; 29 | 30 | export default CameraIcon; 31 | -------------------------------------------------------------------------------- /src/utils/types/dtos/server.ts: -------------------------------------------------------------------------------- 1 | import Server from '../../../server/db/models/server.model'; 2 | import ChannelDto from './channel'; 3 | import ServerUserDto from './server-user'; 4 | 5 | class ServerDto { 6 | public id: number; 7 | public name: string; 8 | public createdAt: string; 9 | public updatedAt: string; 10 | public users: ServerUserDto[]; 11 | public channels: ChannelDto[]; 12 | 13 | constructor(server: Server) { 14 | this.id = server.id; 15 | this.name = server.name; 16 | this.createdAt = server.createdAt; 17 | this.updatedAt = server.updatedAt; 18 | this.users = 19 | server.users == null 20 | ? [] 21 | : server.users.map((serverUser) => new ServerUserDto(serverUser)); 22 | this.channels = 23 | server.channels == null 24 | ? [] 25 | : server.channels.map((channel) => new ChannelDto(channel)); 26 | } 27 | } 28 | 29 | export default ServerDto; 30 | -------------------------------------------------------------------------------- /src/server/db/seeders/20220605201313-add-direct-message-users.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | async up(queryInterface, Sequelize) { 5 | await queryInterface.bulkInsert( 6 | 'direct_message_user', 7 | [ 8 | { 9 | direct_message_id: 1, 10 | user_id: 1, 11 | created_at: new Date(), 12 | updated_at: new Date(), 13 | }, 14 | { 15 | direct_message_id: 1, 16 | user_id: 2, 17 | created_at: new Date(), 18 | updated_at: new Date(), 19 | }, 20 | { 21 | direct_message_id: 1, 22 | user_id: 3, 23 | created_at: new Date(), 24 | updated_at: new Date(), 25 | }, 26 | ], 27 | {}, 28 | ); 29 | }, 30 | 31 | async down(queryInterface, Sequelize) { 32 | await queryInterface.bulkDelete('direct_message_user', null, {}); 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /src/utils/types/environment.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | declare global { 3 | namespace NodeJS { 4 | interface ProcessEnv { 5 | NODE_ENV: 'test' | 'development' | 'production'; 6 | APP_NAME?: string; 7 | HOST?: string; 8 | PORT?: string; 9 | DATABASE_URL?: string; 10 | DATABASE?: string; 11 | DB_USER?: string; 12 | DB_PASSWORD?: string; 13 | DB_HOST?: string; 14 | DB_PORT?: string; 15 | ACCESS_TOKEN_SECRET?: string; 16 | REFRESH_TOKEN_SECRET?: string; 17 | VERIFY_EMAIL_SECRET?: string; 18 | RESET_PASSWORD_SECRET?: string; 19 | ACCESS_TOKEN_EXPIRATION: string; 20 | REFRESH_TOKEN_EXPIRATION: string; 21 | VERIFY_EMAIL_EXPIRATION: string; 22 | RESET_PASSWORD_EXPIRATION: string; 23 | SMTP_HOST?: string; 24 | SMTP_USER?: string; 25 | SMTP_PASSWORD?: string; 26 | } 27 | } 28 | } 29 | 30 | export {}; 31 | -------------------------------------------------------------------------------- /src/server/validators/server/server.validator.ts: -------------------------------------------------------------------------------- 1 | import isLength from 'validator/lib/isLength'; 2 | import ErrorInterface from '../../../utils/types/interfaces/error'; 3 | import CreateServerRequest from '../../../utils/types/requests/server/create-server'; 4 | 5 | const ERROR_MUST_PROVIDE_SEVER_NAME = { 6 | field: 'name', 7 | message: 'Must provide a server name.', 8 | }; 9 | 10 | const ERROR_INVALID_SEVER_NAME = { 11 | field: 'name', 12 | message: 'Server name must be between 4 and 25 characters.', 13 | }; 14 | 15 | const ServerValidator = { 16 | create: ({ name }: CreateServerRequest) => { 17 | const errors: ErrorInterface[] = []; 18 | 19 | if (name == null) { 20 | errors.push(ERROR_MUST_PROVIDE_SEVER_NAME); 21 | } else if (!isLength(name, { min: 4, max: 25 })) { 22 | errors.push(ERROR_INVALID_SEVER_NAME); 23 | } 24 | 25 | return errors; 26 | }, 27 | }; 28 | 29 | export default ServerValidator; 30 | -------------------------------------------------------------------------------- /src/utils/types/dtos/invitable.ts: -------------------------------------------------------------------------------- 1 | import Invitable from '../../../server/db/models/invitable'; 2 | 3 | class InvitableDto { 4 | public id: number; 5 | public requester: { 6 | id: number; 7 | email: string; 8 | username: string; 9 | }; 10 | public addressee: { 11 | id: number; 12 | email: string; 13 | username: string; 14 | }; 15 | public accepted: boolean; 16 | public createdAt: Date; 17 | public acceptedAt: Date; 18 | 19 | constructor(invite: Invitable) { 20 | this.id = invite.id; 21 | this.requester = { 22 | id: invite.requesterId, 23 | email: invite.requester?.email ?? '', 24 | username: invite.requester?.username ?? '', 25 | }; 26 | this.addressee = { 27 | id: invite.addresseeId, 28 | email: invite.addressee?.email ?? '', 29 | username: invite.addressee?.username ?? '', 30 | }; 31 | this.accepted = invite.accepted; 32 | this.createdAt = invite.createdAt; 33 | this.acceptedAt = invite.acceptedAt; 34 | } 35 | } 36 | 37 | export default InvitableDto; 38 | -------------------------------------------------------------------------------- /src/components/inputs/spinner/spinner.tsx: -------------------------------------------------------------------------------- 1 | interface SpinnerProps { 2 | size: 'sm' | 'md' | 'lg'; 3 | className?: string; 4 | } 5 | 6 | const Spinner = ({ size, className }: SpinnerProps) => { 7 | const getSpinnerSize = () => { 8 | switch (size) { 9 | case 'sm': 10 | return 'w-4 h-4'; 11 | case 'md': 12 | return 'w-5 h-5'; 13 | case 'lg': 14 | return 'w-7 h-7'; 15 | } 16 | }; 17 | 18 | const getSpinnerDivSize = () => { 19 | switch (size) { 20 | case 'sm': 21 | return 'w-4 h-4 border-2 margin-2 border-white'; 22 | case 'md': 23 | return 'w-5 h-5 border-2 margin-2 border-white'; 24 | case 'lg': 25 | return 'w-7 h-7 border-2 margin-2 border-white'; 26 | } 27 | }; 28 | 29 | return ( 30 |
31 |
32 |
33 |
34 |
35 | ); 36 | }; 37 | 38 | export default Spinner; 39 | -------------------------------------------------------------------------------- /src/components/icons/friend.tsx: -------------------------------------------------------------------------------- 1 | import Icon, { IconProps } from './icon'; 2 | 3 | const FriendIcon = (props: IconProps) => { 4 | return ( 5 |