├── .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 | 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 |
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 |
16 |
17 |
23 |
24 |
25 |
26 | }
27 | />
28 | );
29 | };
30 |
31 | export default FriendIcon;
32 |
--------------------------------------------------------------------------------
/src/components/utils/modal/modal.tsx:
--------------------------------------------------------------------------------
1 | import { Dispatch, SetStateAction } from 'react';
2 | import { CSSTransition } from 'react-transition-group';
3 | import useWindowSize from '../../../utils/hooks/use-window-size';
4 |
5 | interface ModalProps {
6 | showModal: boolean;
7 | setShowModal: Dispatch>;
8 | children: JSX.Element;
9 | }
10 |
11 | const Modal = ({ showModal, setShowModal, children }: ModalProps) => {
12 | const { widthStyle, heightStyle } = useWindowSize();
13 |
14 | const handleModalBackgroundClick = () => {
15 | setShowModal(false);
16 | };
17 |
18 | return (
19 |
25 |
30 | {children}
31 |
32 |
33 | );
34 | };
35 |
36 | export default Modal;
37 |
--------------------------------------------------------------------------------
/src/server/db/models/server-user.model.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BelongsTo,
3 | Column,
4 | DataType,
5 | DefaultScope,
6 | ForeignKey,
7 | Model,
8 | PrimaryKey,
9 | Table,
10 | } from 'sequelize-typescript';
11 | import ServerRoleEnum from '../../../utils/enums/server-roles';
12 | import Server from './server.model';
13 | import User from './user.model';
14 |
15 | const SERVER_ROLE_ENUM = Object.values(ServerRoleEnum).map((role) =>
16 | role.toString(),
17 | );
18 |
19 | @DefaultScope(() => ({
20 | include: [
21 | {
22 | model: User,
23 | },
24 | ],
25 | }))
26 | @Table({ tableName: 'server_user', underscored: true })
27 | class ServerUser extends Model {
28 | @PrimaryKey
29 | @ForeignKey(() => Server)
30 | @Column(DataType.INTEGER)
31 | serverId!: number;
32 |
33 | @BelongsTo(() => Server)
34 | server!: Server;
35 |
36 | @PrimaryKey
37 | @ForeignKey(() => User)
38 | @Column(DataType.INTEGER)
39 | userId!: number;
40 |
41 | @BelongsTo(() => User)
42 | user!: User;
43 |
44 | @Column(DataType.ENUM(...SERVER_ROLE_ENUM))
45 | role!: string;
46 | }
47 |
48 | export default ServerUser;
49 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Discord Clone
2 |
3 | Built with NextJS, Typescript, TailwindCSS, ExpressJS, Postgres, Socket.IO, and WebRTC
4 |
5 | [Check it out here!](https://discord-clone-server-0.herokuapp.com/)
6 |
7 | ## Installation and Setup Instructions
8 |
9 | Clone down this repository. You will need node and npm installed globally on your machine.
10 |
11 | `cd discord-clone`
12 |
13 | Install Dependencies:
14 |
15 | `npm install`
16 |
17 | Create the .env file in the root directory, checkout [.env.example](/.env.example) for an example:
18 |
19 | `touch .env.local`
20 |
21 | To Start Server:
22 |
23 | `npm run dev`
24 |
25 | To Visit App:
26 |
27 | `localhost:3000`
28 |
29 | ## Things I wish I would've done differently
30 |
31 | - friend/friend_request to user_relationship
32 | - created request/response objects
33 | - throw vs return
34 | - organization of files (interfaces, dtos, etc.)
35 | - naming of direct_message vs message
36 | - message_group?
37 | - completed testing suite
38 | - my janky error handling
39 | - message architecture
40 | - uneccessary coupling in my current architecture
41 | - typescript classes vs interface
42 |
--------------------------------------------------------------------------------
/src/components/feedback/toast-manager/toast-manager.tsx:
--------------------------------------------------------------------------------
1 | import { CSSTransition, TransitionGroup } from 'react-transition-group';
2 | import useToasts from '../../../utils/hooks/use-toasts';
3 | import useWindowSize from '../../../utils/hooks/use-window-size';
4 | import Toast from '../toast';
5 |
6 | const ToastManager = () => {
7 | const { toasts } = useToasts();
8 | const { heightStyle } = useWindowSize();
9 |
10 | return (
11 |
15 |
16 | {toasts.reverse().map((toast) => {
17 | return (
18 |
24 |
25 |
26 | );
27 | })}
28 |
29 |
30 | );
31 | };
32 |
33 | export default ToastManager;
34 |
--------------------------------------------------------------------------------
/src/components/layouts/app-layout/channels/text-channel/channel-text-area/channel-text-area.tsx:
--------------------------------------------------------------------------------
1 | import { KeyboardEventHandler } from 'react';
2 |
3 | interface ChannelTextAreaProps {
4 | placeholder: string;
5 | value: string;
6 | // eslint-disable-next-line no-unused-vars
7 | onInput: (newValue: string) => void;
8 | onKeyDown: KeyboardEventHandler;
9 | }
10 |
11 | const ChannelTextArea = ({
12 | placeholder,
13 | value,
14 | onInput,
15 | onKeyDown,
16 | }: ChannelTextAreaProps) => {
17 | return (
18 |
19 |
20 | onInput((e.target as HTMLInputElement).value)}
24 | type="text"
25 | className="h-full w-full bg-slate-channel-text-area text-sm"
26 | placeholder={placeholder}
27 | name=""
28 | id=""
29 | />
30 |
31 |
32 | );
33 | };
34 |
35 | export default ChannelTextArea;
36 |
--------------------------------------------------------------------------------
/src/server/db/models/index.ts:
--------------------------------------------------------------------------------
1 | import sequelize from '../../../config/db.config';
2 | import Sequelize from 'sequelize';
3 | import User from './user.model';
4 | import RefreshToken from './refresh-token.model';
5 | import UserRole from './user-role.model';
6 | import Server from './server.model';
7 | import ServerUser from './server-user.model';
8 | import Channel from './channel.model';
9 | import Friend from './friend.model';
10 | import ServerInvite from './server-invite.model';
11 | import DirectMessage from './direct-message.model';
12 | import DirectMessageUser from './direct-message-user.model';
13 | import Message from './message.model';
14 |
15 | sequelize.addModels([
16 | User,
17 | RefreshToken,
18 | UserRole,
19 | Server,
20 | ServerInvite,
21 | ServerUser,
22 | Channel,
23 | Friend,
24 | DirectMessage,
25 | DirectMessageUser,
26 | Message,
27 | ]);
28 |
29 | const db = {
30 | Sequelize,
31 | sequelize,
32 | User,
33 | RefreshToken,
34 | UserRole,
35 | Server,
36 | ServerInvite,
37 | ServerUser,
38 | Channel,
39 | Friend,
40 | DirectMessage,
41 | DirectMessageUser,
42 | Message,
43 | };
44 |
45 | export default db;
46 |
--------------------------------------------------------------------------------
/src/components/feedback/toast/toast.tsx:
--------------------------------------------------------------------------------
1 | import useToasts from '../../../utils/hooks/use-toasts';
2 | import ToastInterface from '../../../utils/types/interfaces/toast-interface';
3 | import { CloseIcon } from '../../icons';
4 | import IconButton from '../../inputs/icon-button';
5 |
6 | const TOAST_CLASSES = {
7 | success: 'toast-success',
8 | danger: 'toast-danger',
9 | };
10 |
11 | const Toast = ({ id, color, title, body }: ToastInterface) => {
12 | const { removeToast } = useToasts();
13 |
14 | const handleToastClick = () => {
15 | removeToast(id);
16 | };
17 |
18 | return (
19 |
22 |
23 |
{title}
24 | {body &&
{body}
}
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | );
33 | };
34 |
35 | export default Toast;
36 |
--------------------------------------------------------------------------------
/src/server/db/models/message.model.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AllowNull,
3 | BelongsTo,
4 | Column,
5 | DataType,
6 | ForeignKey,
7 | Model,
8 | Table,
9 | } from 'sequelize-typescript';
10 | import MessageType from '../../../utils/enums/message-type';
11 | import DirectMessage from './direct-message.model';
12 | import ServerInvite from './server-invite.model';
13 | import User from './user.model';
14 |
15 | @Table({ tableName: 'message', underscored: true })
16 | class Message extends Model {
17 | @Column(DataType.INTEGER)
18 | type!: MessageType;
19 |
20 | @ForeignKey(() => User)
21 | @Column(DataType.INTEGER)
22 | senderId!: number;
23 |
24 | @BelongsTo(() => User)
25 | sender!: User;
26 |
27 | @Column(DataType.STRING)
28 | body!: string;
29 |
30 | @ForeignKey(() => DirectMessage)
31 | @AllowNull
32 | @Column(DataType.INTEGER)
33 | directMessageId!: number;
34 |
35 | @BelongsTo(() => DirectMessage)
36 | directMessage!: DirectMessage;
37 |
38 | @ForeignKey(() => ServerInvite)
39 | @AllowNull
40 | @Column(DataType.INTEGER)
41 | serverInviteId!: number;
42 |
43 | @BelongsTo(() => ServerInvite)
44 | serverInvite!: ServerInvite;
45 | }
46 |
47 | export default Message;
48 |
--------------------------------------------------------------------------------
/src/utils/services/handle-service-error.ts:
--------------------------------------------------------------------------------
1 | import axios, { AxiosError } from 'axios';
2 | import {
3 | ERROR_INSUFFICIENT_PERMISSIONS,
4 | ERROR_UNAUTHORIZED,
5 | ERROR_UNKOWN,
6 | } from '../constants/errors';
7 | import ErrorInterface from '../types/interfaces/error';
8 |
9 | const handleServiceError = (
10 | error: any,
11 | ): { status: number; errors: ErrorInterface[] } => {
12 | let status = 500;
13 | let errors = [ERROR_UNKOWN];
14 |
15 | const isAxiosError = axios.isAxiosError(error) && error.response;
16 | if (isAxiosError) {
17 | const axiosError = error as AxiosError;
18 | const { data } = axiosError.response!;
19 | status = axiosError.response!.status ?? 500;
20 |
21 | const isValidErrorArray =
22 | Array.isArray(data) &&
23 | data.length > 0 &&
24 | (data as ErrorInterface[])[0].message != null;
25 |
26 | if (isValidErrorArray) {
27 | errors = data as ErrorInterface[];
28 | } else if (status === 403) {
29 | errors = [ERROR_INSUFFICIENT_PERMISSIONS];
30 | } else if (status === 401) {
31 | errors = [ERROR_UNAUTHORIZED];
32 | }
33 | }
34 |
35 | return { status, errors };
36 | };
37 |
38 | export default handleServiceError;
39 |
--------------------------------------------------------------------------------
/src/server/controllers/server/user/user.controller.ts:
--------------------------------------------------------------------------------
1 | import catchAsync from '../../../middleware/catch-async';
2 | import { Request, Response } from 'express';
3 | import { ERROR_UNKOWN } from '../../../../utils/constants/errors';
4 | import ServerUserService from '../../../services/server/user/user.service';
5 | import ErrorEnum from '../../../../utils/enums/errors';
6 |
7 | class ServerUserController {
8 | private _serverUserService: ServerUserService;
9 |
10 | constructor() {
11 | this._serverUserService = new ServerUserService();
12 | }
13 |
14 | public create = catchAsync(async (req: Request, res: Response) => {
15 | const userId = req.user.id;
16 | const { serverInviteId } = req.body;
17 |
18 | try {
19 | const serverUser = await this._serverUserService.create({
20 | userId,
21 | serverInviteId,
22 | });
23 |
24 | return res.status(201).json(serverUser);
25 | } catch (e: any) {
26 | switch (e.type) {
27 | case ErrorEnum.NOT_INVITED:
28 | return res.status(403).json(e.errors);
29 | default:
30 | return res.status(500).json([ERROR_UNKOWN]);
31 | }
32 | }
33 | });
34 | }
35 | export default ServerUserController;
36 |
--------------------------------------------------------------------------------
/src/pages/servers/[serverId]/index.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 | import { ReactElement, useEffect } from 'react';
3 | import Spinner from '../../../components/inputs/spinner';
4 | import AppLayout from '../../../components/layouts/app-layout';
5 | import ChannelHeader from '../../../components/layouts/app-layout/channels/channel-header/channel-header';
6 | import ChannelSidebar from '../../../components/layouts/app-layout/channels/channel-sidebar';
7 | import useServer from '../../../utils/hooks/use-server';
8 |
9 | const ServerHomePage = () => {
10 | const router = useRouter();
11 | const { serverId } = router.query as {
12 | serverId: string;
13 | };
14 | const { server, loadServer } = useServer();
15 |
16 | useEffect(() => {
17 | loadServer(parseInt(serverId));
18 | // eslint-disable-next-line react-hooks/exhaustive-deps
19 | }, [serverId]);
20 |
21 | return server == null ? : <>{server.name}s Home page>;
22 | };
23 |
24 | ServerHomePage.getLayout = (page: ReactElement) => {
25 | return (
26 | } sidebar={}>
27 | {page}
28 |
29 | );
30 | };
31 |
32 | export default ServerHomePage;
33 |
--------------------------------------------------------------------------------
/src/components/icons/settings.tsx:
--------------------------------------------------------------------------------
1 | import Icon, { IconProps } from './icon';
2 |
3 | const SettingsIcon = (props: IconProps) => {
4 | return (
5 |
14 |
19 |
24 |
25 | }
26 | />
27 | );
28 | };
29 |
30 | export default SettingsIcon;
31 |
--------------------------------------------------------------------------------
/src/services/user-service.ts:
--------------------------------------------------------------------------------
1 | import { AxiosResponse } from 'axios';
2 | import UserDto from '../utils/types/dtos/user';
3 | import ConfirmResetPasswordRequest from '../utils/types/requests/user/confirm-reset-password';
4 | import CreateUserRequest from '../utils/types/requests/user/create-user';
5 | import ResetPasswordRequest from '../utils/types/requests/user/reset-password';
6 | import API from './api';
7 |
8 | const UserService = {
9 | create: (payload: CreateUserRequest): Promise> => {
10 | return API.post('/user', payload);
11 | },
12 | get: (userId: number): Promise> => {
13 | return API.get(`/user/${userId}`);
14 | },
15 | verifyEmail: (token: string): Promise> => {
16 | return API.put(`/user/verify-email/${token}`);
17 | },
18 | resetPassword: (
19 | payload: ResetPasswordRequest,
20 | ): Promise> => {
21 | return API.put('/user/reset-password', payload);
22 | },
23 | confirmResetPassword: (
24 | token: string,
25 | payload: ConfirmResetPasswordRequest,
26 | ): Promise> => {
27 | return API.put(`/user/reset-password/confirm/${token}`, payload);
28 | },
29 | };
30 |
31 | export default UserService;
32 |
--------------------------------------------------------------------------------
/src/utils/types/dtos/direct-message.ts:
--------------------------------------------------------------------------------
1 | import DirectMessage from '../../../server/db/models/direct-message.model';
2 | import DirectMessageUserDto from './direct-message-user';
3 |
4 | export const getLabel = (
5 | currentUserId: number,
6 | directMessage: DirectMessageDto,
7 | ) => {
8 | return directMessage?.users != null && directMessage.users.length > 0
9 | ? directMessage.users.length === 1
10 | ? directMessage.users[0].username
11 | : directMessage.users
12 | .filter((e) => e.userId !== currentUserId)
13 | .reduce((a, b, index) => {
14 | return index !==
15 | directMessage.users.filter((e) => e.userId !== currentUserId)
16 | .length -
17 | 1
18 | ? a + `${b.username}, `
19 | : a + b.username;
20 | }, '')
21 | : [];
22 | };
23 |
24 | class DirectMessageDto {
25 | id: number;
26 | users: DirectMessageUserDto[];
27 |
28 | constructor(directMessage: DirectMessage) {
29 | this.id = directMessage.id;
30 | this.users =
31 | directMessage.users == null
32 | ? []
33 | : directMessage.users.map((dmu) => new DirectMessageUserDto(dmu));
34 | }
35 | }
36 |
37 | export default DirectMessageDto;
38 |
--------------------------------------------------------------------------------
/src/server/app.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import errorHandler from './middleware/error-handler';
3 | import cookieParser from 'cookie-parser';
4 | import apiLimiter from '../config/api.config';
5 | import cors from 'cors';
6 | import env from '../config/env.config';
7 | import router from './routes';
8 | import db from './db/models';
9 | import RequestUser from '../utils/types/dtos/request-user';
10 |
11 | declare global {
12 | // eslint-disable-next-line no-unused-vars
13 | namespace Express {
14 | // eslint-disable-next-line no-unused-vars
15 | interface Request {
16 | user: RequestUser;
17 | }
18 | }
19 | }
20 |
21 | const app = express();
22 |
23 | // MIDDLEWARE
24 | app.use(express.json());
25 | app.use('/api', apiLimiter);
26 | app.use(cookieParser());
27 | app.use(
28 | cors({
29 | origin: [env.HOST],
30 | }),
31 | );
32 | app.use('/api/v1', router);
33 | app.use(errorHandler);
34 |
35 | // DATABASE
36 | if (process.env.NODE_ENV !== 'test') db.sequelize.sync();
37 | // if (process.env.NODE_ENV !== 'production') {
38 | // db.sequelize.sync({ force: true }).then(() => {
39 | // console.log('Dropping, re-syncing, and seeding database');
40 | // require('./db').default();
41 | // });
42 | // }
43 |
44 | export default app;
45 |
--------------------------------------------------------------------------------
/src/pages/user/verify-email/[token].tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next';
2 | import { useRouter } from 'next/router';
3 | import { useEffect } from 'react';
4 | import UserService from '../../../services/user-service';
5 | import useToasts from '../../../utils/hooks/use-toasts';
6 | import handleServiceError from '../../../utils/services/handle-service-error';
7 |
8 | const VerifyEmailPage: NextPage = () => {
9 | const router = useRouter();
10 | const { token } = router.query as { token: string };
11 | const { success, danger } = useToasts();
12 |
13 | useEffect(() => {
14 | const verifyEmail = async () => {
15 | if (token == null) return;
16 |
17 | try {
18 | await UserService.verifyEmail(token);
19 | success('Email verified!', 'You can login now, champ.');
20 | return router.push('/login');
21 | } catch (error) {
22 | const response = handleServiceError(error);
23 | response.errors.forEach((error) => {
24 | danger('Uh oh!', error.message);
25 | });
26 | return router.push('/login');
27 | }
28 | };
29 |
30 | verifyEmail();
31 |
32 | // eslint-disable-next-line react-hooks/exhaustive-deps
33 | }, [token]);
34 |
35 | return <>>;
36 | };
37 |
38 | export default VerifyEmailPage;
39 |
--------------------------------------------------------------------------------
/src/components/layouts/app-layout/sidebar/sidebar-footer.tsx:
--------------------------------------------------------------------------------
1 | import useAuth from '../../../../utils/hooks/use-auth';
2 | import useUser from '../../../../utils/hooks/use-user';
3 | import { MicrophoneIcon, SettingsIcon } from '../../../icons';
4 | import IconButton from '../../../inputs/icon-button';
5 | import ProfileImage from '../profile-image/profile-image';
6 |
7 | const SidebarFooter = () => {
8 | const { user } = useUser();
9 | const { logout } = useAuth();
10 |
11 | const handleLogoutBtnClick = async () => {
12 | await logout();
13 | };
14 | return (
15 |
16 |
17 |
20 |
21 | {user?.username}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | );
34 | };
35 |
36 | export default SidebarFooter;
37 |
--------------------------------------------------------------------------------
/src/components/layouts/global-layout/global-layout.tsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head';
2 | import { ReactNode } from 'react';
3 | import { AuthProvider } from '../../../utils/contexts/auth-context';
4 | import { ToastProvider } from '../../../utils/contexts/toast-context';
5 | import { WindowProvider } from '../../../utils/contexts/window-context';
6 | import useWindowSize from '../../../utils/hooks/use-window-size';
7 |
8 | interface GlobalLayoutProps {
9 | children: ReactNode;
10 | }
11 |
12 | const Layout = ({ children }: GlobalLayoutProps) => {
13 | const { heightStyle, widthStyle } = useWindowSize();
14 | return (
15 |
19 | {children}
20 |
21 | );
22 | };
23 |
24 | const GlobalLayout = (props: GlobalLayoutProps) => {
25 | return (
26 |
27 |
28 |
29 | <>
30 |
31 |
35 |
36 |
37 | >
38 |
39 |
40 |
41 | );
42 | };
43 |
44 | export default GlobalLayout;
45 |
--------------------------------------------------------------------------------
/src/styles/transitions.css:
--------------------------------------------------------------------------------
1 | .slide-in-enter {
2 | transform: translateX(-100%);
3 | --webkit-transform: translateX(-100%);
4 | }
5 | .slide-in-enter-active {
6 | transform: translateX(0%);
7 | transition: 200ms;
8 | --webkit-transform: translateX(0%);
9 | --webkit-transition: 200ms;
10 | }
11 | .slide-in-enter-done {
12 | transform: translateX(0%);
13 | --webkit-transform: translateX(0%);
14 | }
15 | .slide-in-appear {
16 | transform: none;
17 | }
18 | .slide-in-appear-active {
19 | transform: none;
20 | }
21 | .slide-in-appear-done {
22 | transform: none;
23 | }
24 | .slide-in-exit {
25 | transform: translateX(0%);
26 | --webkit-transform: translateX(0%);
27 | }
28 | .slide-in-exit-active {
29 | transform: translateX(-100%);
30 | transition: 200ms;
31 | --webkit-transform: translateX(-100%);
32 | --webkit-transition: 200ms;
33 | }
34 | .slide-in-exit-done {
35 | transform: translateX(-100%);
36 | --webkit-transform: translateX(-100%);
37 | }
38 |
39 | .fade-in-enter {
40 | opacity: 0;
41 | }
42 | .fade-in-enter-active {
43 | opacity: 1;
44 | transition: 200ms;
45 | --webkit-transition: 200ms;
46 | }
47 | .fade-in-enter-done {
48 | opacity: 1;
49 | }
50 | .fade-in-exit {
51 | opacity: 1;
52 | }
53 | .fade-in-exit-active {
54 | opacity: 0;
55 | transition: 200ms;
56 | --webkit-transition: 200ms;
57 | }
58 | .fade-in-exit-done {
59 | opacity: 0;
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/layouts/app-layout/message/message.tsx:
--------------------------------------------------------------------------------
1 | import DateUtils from '../../../../utils/date-utils';
2 | import MessageType from '../../../../utils/enums/message-type';
3 | import MessageDto from '../../../../utils/types/dtos/message';
4 | import ProfileImage from '../profile-image';
5 | import ServerInviteMessage from './server-invite-message';
6 |
7 | interface MessageProps {
8 | message: MessageDto;
9 | }
10 |
11 | const Message = ({ message }: MessageProps) => {
12 | return message.type == MessageType.DIRECT ? (
13 |
14 |
17 |
18 |
19 | {message.sender.username}
20 |
21 | {DateUtils.getFormattedDate(message.createdAt)}
22 |
23 |
24 |
{message.body}
25 |
26 |
27 | ) : message.type == MessageType.SERVER_INVITE ? (
28 |
29 | ) : (
30 | <>Message type not configured.>
31 | );
32 | };
33 |
34 | export default Message;
35 |
--------------------------------------------------------------------------------
/src/server/validators/server/channel/channel.validator.ts:
--------------------------------------------------------------------------------
1 | import isLength from 'validator/lib/isLength';
2 | import ChannelType from '../../../../utils/enums/channel-type';
3 | import ErrorInterface from '../../../../utils/types/interfaces/error';
4 | import CreateChannelRequest from '../../../../utils/types/requests/server/channel/create-channel';
5 |
6 | const ERROR_MUST_PROVIDE_TYPE: ErrorInterface = {
7 | field: 'type',
8 | message: 'Must provide a channel type.',
9 | };
10 | const ERROR_INVALID_TYPE: ErrorInterface = {
11 | field: 'type',
12 | message: 'Must provide a valid channel type.',
13 | };
14 | const ERROR_MUST_PROVIDE_NAME: ErrorInterface = {
15 | field: 'name',
16 | message: 'Must provide a channel name.',
17 | };
18 | const ERROR_INVALID_NAME: ErrorInterface = {
19 | field: 'name',
20 | message: 'Name must be at least 4 characters.',
21 | };
22 |
23 | const ChannelValidator = {
24 | create: ({ type, name }: CreateChannelRequest): ErrorInterface[] => {
25 | const errors = [];
26 | if (type == null) errors.push(ERROR_MUST_PROVIDE_TYPE);
27 | else if (!Object.values(ChannelType).includes(type))
28 | errors.push(ERROR_INVALID_TYPE);
29 |
30 | if (name == null) errors.push(ERROR_MUST_PROVIDE_NAME);
31 | else if (!isLength(name, { min: 4 })) errors.push(ERROR_INVALID_NAME);
32 |
33 | return errors;
34 | },
35 | };
36 |
37 | export default ChannelValidator;
38 |
--------------------------------------------------------------------------------
/src/utils/types/dtos/user.ts:
--------------------------------------------------------------------------------
1 | import User from '../../../server/db/models/user.model';
2 | import RoleEnum from '../../enums/roles';
3 | import DirectMessageDto from './direct-message';
4 | import FriendRequestDto from './friend-request';
5 | import ServerInviteDto from './server-invite';
6 |
7 | class UserDto {
8 | public id: number;
9 | public username: string;
10 | public email: string;
11 | public updatedAt: string;
12 | public createdAt: string;
13 | public roles: RoleEnum[];
14 | public friendRequests: FriendRequestDto[];
15 | public directMessages: DirectMessageDto[];
16 | public serverInvites: ServerInviteDto[] = [];
17 |
18 | constructor(user: User) {
19 | this.id = user.id;
20 | this.username = user.username;
21 | this.email = user.email;
22 | this.updatedAt = user.updatedAt;
23 | this.createdAt = user.updatedAt;
24 | this.roles =
25 | user.roles == null
26 | ? []
27 | : user.roles.map((userRole) => userRole.role as RoleEnum);
28 | this.friendRequests =
29 | user.friends == null
30 | ? []
31 | : user.friends.map((friend) => new FriendRequestDto(friend));
32 | this.directMessages =
33 | user.directMessages == null
34 | ? []
35 | : user.directMessages.map(
36 | (directMessage) => new DirectMessageDto(directMessage),
37 | );
38 | }
39 | }
40 |
41 | export default UserDto;
42 |
--------------------------------------------------------------------------------
/src/components/icons/index.ts:
--------------------------------------------------------------------------------
1 | import IconSize from '../../utils/enums/icon-size';
2 | import BarsIcon from './bars';
3 | import CameraIcon from './camera';
4 | import ChannelIcon from './channel';
5 | import CheckIcon from './check';
6 | import ChevronDownIcon from './chevron-down';
7 | import ChevronRightIcon from './chevron-right';
8 | import CloseIcon from './close';
9 | import ExclamationCircleIcon from './exclamation-circle';
10 | import EyeIcon from './eye';
11 | import EyeOffIcon from './eye-off';
12 | import FriendIcon from './friend';
13 | import LogoIcon from './logo';
14 | import MicrophoneIcon from './microphone';
15 | import PencilIcon from './pencil';
16 | import PlusIcon from './plus';
17 | import PoundIcon from './pound';
18 | import SearchIcon from './search';
19 | import SettingsIcon from './settings';
20 | import UserAddIcon from './user-add';
21 | import VolumeUpIcon from './volume-up';
22 |
23 | export { default } from './icon';
24 | export type { IconProps } from './icon';
25 | export { IconSize };
26 | export {
27 | BarsIcon,
28 | CameraIcon,
29 | ChannelIcon,
30 | CheckIcon,
31 | ChevronDownIcon,
32 | ChevronRightIcon,
33 | CloseIcon,
34 | ExclamationCircleIcon,
35 | EyeOffIcon,
36 | EyeIcon,
37 | FriendIcon,
38 | LogoIcon,
39 | MicrophoneIcon,
40 | PencilIcon,
41 | PlusIcon,
42 | PoundIcon,
43 | SearchIcon,
44 | SettingsIcon,
45 | UserAddIcon,
46 | VolumeUpIcon,
47 | };
48 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: [
3 | './src/pages/**/*.{js,ts,jsx,tsx}',
4 | './src/components/**/*.{js,ts,jsx,tsx}',
5 | ],
6 | theme: {
7 | extend: {
8 | colors: {
9 | slate: {
10 | 300: '#96989D',
11 | 400: '#a3a6aa',
12 | 500: '#4f545c',
13 | 600: 'rgba(79,84,92,0.4)',
14 | 700: '#36393f',
15 | 800: '#2f3136',
16 | 900: '#202225',
17 | 1000: '#292b2f',
18 | 1100: '#18191c',
19 | 'channel-text-area': '#40444b',
20 | },
21 | },
22 | fontFamily: {
23 | sans: '"Inter", sans-serif',
24 | },
25 | borderWidth: {
26 | 6: '6px',
27 | },
28 | width: {
29 | servers: '4.5rem',
30 | sidebar: '15rem',
31 | },
32 | height: {
33 | header: '3rem',
34 | body: 'calc(100% - 3rem)',
35 | 13: '3.5rem',
36 | },
37 | maxHeight: {
38 | body: 'calc(100% - 3rem)',
39 | },
40 | maxWidth: {
41 | 80: '80%',
42 | 'server-members': '15rem',
43 | },
44 | boxShadow: {
45 | header:
46 | '0 1px 0 rgba(4,4,5,0.2),0 1.5px 0 rgba(6,6,7,0.05),0 2px 0 rgba(4,4,5,0.05)',
47 | },
48 | backdropBlur: {
49 | xs: '2px',
50 | },
51 | fontSize: {
52 | xxs: '0.75rem',
53 | },
54 | },
55 | },
56 | plugins: [],
57 | };
58 |
--------------------------------------------------------------------------------
/src/server/validators/message/message.validator.ts:
--------------------------------------------------------------------------------
1 | import isLength from 'validator/lib/isLength';
2 | import MessageType from '../../../utils/enums/message-type';
3 | import ErrorInterface from '../../../utils/types/interfaces/error';
4 | import CreateMessageRequest from '../../../utils/types/requests/message/create-message';
5 |
6 | const validateType = (
7 | type: MessageType,
8 | directMessageId?: number,
9 | ): ErrorInterface[] => {
10 | const errors: ErrorInterface[] = [];
11 |
12 | if (!(type in MessageType))
13 | errors.push({
14 | field: 'type',
15 | message: 'Must provide a messsage type.',
16 | });
17 | else if (
18 | (type === MessageType.DIRECT || type === MessageType.SERVER_INVITE) &&
19 | directMessageId == null
20 | ) {
21 | errors.push({
22 | field: 'type',
23 | message: 'Must provide a directMessageId',
24 | });
25 | }
26 |
27 | return errors;
28 | };
29 |
30 | const MessageValidator = {
31 | create: ({
32 | type,
33 | body,
34 | directMessageId,
35 | }: CreateMessageRequest): ErrorInterface[] => {
36 | const errors: ErrorInterface[] = validateType(type, directMessageId);
37 |
38 | if (!isLength(body, { min: 1, max: 250 })) {
39 | errors.push({
40 | field: 'body',
41 | message: 'Body must be between 1 and 250 characters.',
42 | });
43 | }
44 |
45 | return errors;
46 | },
47 | };
48 |
49 | export default MessageValidator;
50 |
--------------------------------------------------------------------------------
/src/server/db/models/user.model.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Table,
3 | Column,
4 | Model,
5 | DataType,
6 | HasMany,
7 | DefaultScope,
8 | } from 'sequelize-typescript';
9 | import DirectMessage from './direct-message.model';
10 | import Friend from './friend.model';
11 | import RefreshToken from './refresh-token.model';
12 | import ServerUser from './server-user.model';
13 | import UserRole from './user-role.model';
14 |
15 | @DefaultScope(() => ({
16 | include: [
17 | {
18 | model: UserRole,
19 | },
20 | ],
21 | }))
22 | @Table({ tableName: 'user', underscored: true })
23 | class User extends Model {
24 | @Column(DataType.STRING)
25 | username!: string;
26 |
27 | @Column(DataType.STRING)
28 | email!: string;
29 |
30 | @Column(DataType.STRING)
31 | password!: string;
32 |
33 | @Column(DataType.BOOLEAN)
34 | isVerified!: boolean;
35 |
36 | @Column(DataType.STRING)
37 | verificationToken!: string;
38 |
39 | @Column(DataType.STRING)
40 | passwordResetToken!: string;
41 |
42 | @HasMany(() => RefreshToken, {
43 | onDelete: 'CASCADE',
44 | })
45 | refreshTokens!: RefreshToken[];
46 |
47 | @HasMany(() => UserRole, {
48 | onDelete: 'CASCADE',
49 | })
50 | roles!: UserRole[];
51 |
52 | @HasMany(() => ServerUser)
53 | servers!: ServerUser[];
54 |
55 | @HasMany(() => Friend)
56 | friends!: Friend[];
57 |
58 | @HasMany(() => DirectMessage)
59 | directMessages!: DirectMessage[];
60 | }
61 |
62 | export default User;
63 |
--------------------------------------------------------------------------------
/src/components/icons/logo.tsx:
--------------------------------------------------------------------------------
1 | import Icon, { IconProps } from './icon';
2 |
3 | const LogoIcon = (props: IconProps) => {
4 | return (
5 |
13 |
14 |
15 | }
16 | />
17 | );
18 | };
19 |
20 | export default LogoIcon;
21 |
--------------------------------------------------------------------------------
/src/utils/types/dtos/message.ts:
--------------------------------------------------------------------------------
1 | import Message from '../../../server/db/models/message.model';
2 | import MessageType from '../../enums/message-type';
3 | import ServerDto from './server';
4 | import ServerInviteDto from './server-invite';
5 |
6 | class MessageSender {
7 | public userId: number;
8 | public email: string;
9 | public username: string;
10 |
11 | constructor(userId: number, email: string, username: string) {
12 | this.userId = userId;
13 | this.email = email;
14 | this.username = username;
15 | }
16 | }
17 |
18 | class MessageDto {
19 | public id: number;
20 | public createdAt: string;
21 | public type: MessageType;
22 | public sender: MessageSender;
23 | public body: string;
24 | public directMessageId?: number;
25 | public serverInvite?: ServerInviteDto;
26 | public server?: ServerDto;
27 |
28 | constructor(message: Message) {
29 | this.id = message.id;
30 | this.createdAt = message.createdAt;
31 | this.type = message.type;
32 | this.sender = new MessageSender(
33 | message.sender?.id ?? 0,
34 | message.sender?.email ?? '',
35 | message.sender?.username ?? '',
36 | );
37 | this.body = message.body;
38 | this.directMessageId = message.directMessageId;
39 | this.serverInvite =
40 | message.serverInvite != null
41 | ? new ServerInviteDto(message.serverInvite)
42 | : undefined;
43 | this.server =
44 | message.serverInvite?.server != null
45 | ? new ServerDto(message.serverInvite.server)
46 | : undefined;
47 | }
48 | }
49 |
50 | export default MessageDto;
51 |
--------------------------------------------------------------------------------
/src/server/controllers/server/invite/invite.controller.ts:
--------------------------------------------------------------------------------
1 | import catchAsync from '../../../middleware/catch-async';
2 | import { Request, Response } from 'express';
3 | import { ERROR_UNKOWN } from '../../../../utils/constants/errors';
4 | import ServerInviteService from '../../../services/server/invite/invite.service';
5 | import ErrorEnum from '../../../../utils/enums/errors';
6 |
7 | class ServerInviteController {
8 | private _serverInviteService: ServerInviteService;
9 |
10 | constructor() {
11 | this._serverInviteService = new ServerInviteService();
12 | }
13 |
14 | public create = catchAsync(async (req: Request, res: Response) => {
15 | const userId = req.user.id;
16 | const serverId = parseInt(req.params.serverId);
17 | const { friendId } = req.body;
18 |
19 | try {
20 | const { errors, serverInvite } = await this._serverInviteService.create({
21 | userId,
22 | serverId,
23 | friendId,
24 | });
25 | if (errors != null) return res.status(400).json(errors);
26 |
27 | return res.status(201).json(serverInvite);
28 | } catch (e: any) {
29 | switch (e.type) {
30 | case ErrorEnum.INVITE_SERVER_USER_ALREADY_EXISTS:
31 | case ErrorEnum.FRIEND_REQUEST_NOT_FOUND:
32 | return res.status(400).json(e.errors);
33 | case ErrorEnum.INVITE_SERVER_USER_INSUFFICIENT_PERMISSIONS:
34 | return res.status(403).json(e.errors);
35 | default:
36 | return res.status(500).json([ERROR_UNKOWN]);
37 | }
38 | }
39 | });
40 | }
41 |
42 | export default ServerInviteController;
43 |
--------------------------------------------------------------------------------
/src/components/layouts/app-layout/channels/server-body/text-channels.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import useServer from '../../../../../utils/hooks/use-server';
3 | import ChannelButton from './channel-button';
4 | import CreateChannelButton from './create-channel-button';
5 | import { ChevronDownIcon, ChevronRightIcon, IconSize } from '../../../../icons';
6 |
7 | const TextChannels = () => {
8 | const [showTextChannels, setShowTextChannels] = useState(false);
9 |
10 | const { textChannels } = useServer();
11 |
12 | const handleTextChannelBtnClick = () => {
13 | setShowTextChannels((prev) => !prev);
14 | };
15 |
16 | return (
17 |
18 |
19 |
30 |
31 |
32 | {showTextChannels && (
33 |
34 | {textChannels.map((channel) => {
35 | return ;
36 | })}
37 |
38 | )}
39 |
40 | );
41 | };
42 |
43 | export default TextChannels;
44 |
--------------------------------------------------------------------------------
/src/components/layouts/app-layout/channels/server-body/voice-channels.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import useServer from '../../../../../utils/hooks/use-server';
3 | import ChannelButton from './channel-button';
4 | import CreateChannelButton from './create-channel-button';
5 | import { ChevronDownIcon, ChevronRightIcon, IconSize } from '../../../../icons';
6 |
7 | const VoiceChannels = () => {
8 | const [showVoiceChannels, setShowVoiceChannels] = useState(false);
9 | const { voiceChannels } = useServer();
10 |
11 | const handleVoiceChannelBtnClick = () => {
12 | setShowVoiceChannels((prev) => !prev);
13 | };
14 |
15 | return (
16 |
17 |
18 |
29 |
30 |
31 | {showVoiceChannels && (
32 |
33 | {voiceChannels.map((channel) => {
34 | return ;
35 | })}
36 |
37 | )}
38 |
39 | );
40 | };
41 |
42 | export default VoiceChannels;
43 |
--------------------------------------------------------------------------------
/src/server/db/seeders/20220217160404-add-users.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | async up(queryInterface, Sequelize) {
5 | await queryInterface.bulkInsert(
6 | 'user',
7 | [
8 | {
9 | id: 1,
10 | username: 'noah-got-hacked',
11 | email: 'noahskorner@gmail.com',
12 | password:
13 | '$2b$10$NAuxXaLjfkhINbeN1KD9EONfRfQZZH60.VqOxHNWyDjkxYd3N6hSO',
14 | is_verified: true,
15 | verification_token: '',
16 | password_reset_token: '',
17 | created_at: new Date(),
18 | updated_at: new Date(),
19 | },
20 | {
21 | id: 2,
22 | username: 'test',
23 | email: 'test@test.com',
24 | password:
25 | '$2b$10$NAuxXaLjfkhINbeN1KD9EONfRfQZZH60.VqOxHNWyDjkxYd3N6hSO',
26 | is_verified: true,
27 | verification_token: '',
28 | password_reset_token: '',
29 | created_at: new Date(),
30 | updated_at: new Date(),
31 | },
32 | {
33 | id: 3,
34 | username: 'test2',
35 | email: 'test2@test.com',
36 | password:
37 | '$2b$10$NAuxXaLjfkhINbeN1KD9EONfRfQZZH60.VqOxHNWyDjkxYd3N6hSO',
38 | is_verified: true,
39 | verification_token: '',
40 | password_reset_token: '',
41 | created_at: new Date(),
42 | updated_at: new Date(),
43 | },
44 | ],
45 | {},
46 | );
47 | },
48 |
49 | async down(queryInterface, Sequelize) {
50 | await queryInterface.bulkDelete('user', null, {});
51 | },
52 | };
53 |
--------------------------------------------------------------------------------
/src/utils/contexts/app-context.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, Dispatch, SetStateAction, useState } from 'react';
2 |
3 | interface AppContextInterface {
4 | showSidebar: boolean;
5 | showCreateChannelModal: boolean;
6 | showInvitePeopleModal: boolean;
7 | setShowSidebar: Dispatch>;
8 | setShowCreateChannelModal: Dispatch>;
9 | setShowInvitePeopleModal: Dispatch>;
10 | }
11 |
12 | const defaultValues = {
13 | showSidebar: false,
14 | showCreateChannelModal: false,
15 | showInvitePeopleModal: false,
16 | setShowSidebar: () => {},
17 | setShowCreateChannelModal: () => {},
18 | setShowInvitePeopleModal: () => {},
19 | };
20 |
21 | export const AppContext = createContext(defaultValues);
22 |
23 | interface AppProviderInterface {
24 | children: JSX.Element;
25 | }
26 |
27 | export const AppProvider = ({ children }: AppProviderInterface) => {
28 | const [showSidebar, setShowSidebar] = useState(defaultValues.showSidebar);
29 | const [showCreateChannelModal, setShowCreateChannelModal] = useState(
30 | defaultValues.showCreateChannelModal,
31 | );
32 | const [showInvitePeopleModal, setShowInvitePeopleModal] = useState(
33 | defaultValues.showInvitePeopleModal,
34 | );
35 |
36 | return (
37 |
47 | {children}
48 |
49 | );
50 | };
51 |
--------------------------------------------------------------------------------
/src/server/services/server/user/user.service.ts:
--------------------------------------------------------------------------------
1 | import DateUtils from '../../../../utils/date-utils';
2 | import ErrorEnum from '../../../../utils/enums/errors';
3 | import ServerRoleEnum from '../../../../utils/enums/server-roles';
4 | import ServerUserDto from '../../../../utils/types/dtos/server-user';
5 | import SystemError from '../../../../utils/types/interfaces/system-error';
6 | import ServerInvite from '../../../db/models/server-invite.model';
7 | import ServerUser from '../../../db/models/server-user.model';
8 | import User from '../../../db/models/user.model';
9 |
10 | const ERROR_NOT_INVITED = new SystemError(ErrorEnum.NOT_INVITED, [
11 | {
12 | message: 'You are not invited to this server, pal.',
13 | },
14 | ]);
15 |
16 | class ServerUserService {
17 | public async create({
18 | userId,
19 | serverInviteId,
20 | }: {
21 | userId: number;
22 | serverInviteId: number;
23 | }): Promise {
24 | const serverInvite = await ServerInvite.findOne({
25 | where: {
26 | addresseeId: userId,
27 | id: serverInviteId,
28 | },
29 | include: [{ model: User, as: 'addressee' }],
30 | });
31 |
32 | if (serverInvite == null) throw ERROR_NOT_INVITED;
33 |
34 | serverInvite.update({
35 | accepted: true,
36 | acceptedAt: DateUtils.UTC(),
37 | });
38 |
39 | const serverUser = await ServerUser.create({
40 | serverId: serverInvite.serverId,
41 | userId: userId,
42 | role: ServerRoleEnum.MEMBER,
43 | });
44 | serverUser.user = serverInvite.addressee;
45 |
46 | return new ServerUserDto(serverUser);
47 | }
48 | }
49 |
50 | export default ServerUserService;
51 |
--------------------------------------------------------------------------------
/src/components/layouts/app-layout/channels/voice-channel/voice-channel.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react';
2 | import useRTC from '../../../../../utils/hooks/use-rtc';
3 |
4 | const VoiceChannel = () => {
5 | const localVideoRef = useRef(null);
6 | const remoteVideoRef = useRef(null);
7 | const { localStream, remoteStream, startCall } = useRTC();
8 |
9 | useEffect(() => {
10 | if (localVideoRef.current == null) return;
11 | localVideoRef.current.srcObject = localStream;
12 | }, [localStream]);
13 |
14 | useEffect(() => {
15 | if (remoteVideoRef.current == null) return;
16 | remoteVideoRef.current.srcObject = remoteStream;
17 | }, [remoteStream]);
18 |
19 | return (
20 |
21 |
27 |
28 |
29 |
36 |
37 |
38 |
45 |
46 |
47 |
48 | );
49 | };
50 |
51 | export default VoiceChannel;
52 |
--------------------------------------------------------------------------------
/src/utils/enums/errors.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | enum ErrorEnum {
3 | SERVER_NOT_FOUND = 'ServerService.findById.ServerNotFound',
4 | INSUFFICIENT_PERMISIONS = 'ServerService.findById.InsufficientPermissions',
5 | CHANNEL_NOT_FOUND = 'ChannelService.findById.ChannelNotFound',
6 | ADDRESSEE_NOT_FOUND = 'FriendService.createFriendRequest.AddresseeNotFound',
7 | FRIEND_REQUEST_ALREADY_EXISTS = 'FriendService.createFriendRequest.FriendRequestAlreadyExists.',
8 | FRIENDS_WITH_SELF = 'FriendService.createFriendRequest.FriendsWithSelf',
9 | FRIEND_REQUEST_NOT_FOUND = 'FriendService.acceptFriendRequest.FriendRequestNotFound',
10 | FRIEND_REQUEST_INSUFFICIENT_PERMISSIONS_UPDATE = 'FriendService.acceptFriendRequest.FriendRequestInsufficientPermissionsUpdate',
11 | FRIEND_REQUEST_INSUFFICIENT_PERMISSIONS_DELETE = 'FriendService.acceptFriendRequest.FriendRequestInsufficientPermissionsDelete',
12 | ADD_SERVER_USER_INSUFFICIENT_PERMISSIONS = 'ServerUserService.addUserToServer.AddServerUserInsufficientPermissions',
13 | USER_NOT_FOUND = 'ServerUserService.addUserToServer.UserNotFound',
14 | INVITE_SERVER_USER_INSUFFICIENT_PERMISSIONS = 'ServerInviteService.inviteUserToServer.InviteServerUserInsufficientPermissions',
15 | INVITE_SERVER_USER_ALREADY_EXISTS = 'ServerInviteService.inviteUserToServer.InviteServerUserAlreadyExists',
16 | CREATE_DIRECT_MESSAGE_NOT_FRIENDS_WITH_ENTIRE_GROUP = 'DirectMessageService.createDirectMessage.CreateDirectMessageNotFriendsWithEntireGroup',
17 | DIRECT_MESSAGE_USER_NOT_FOUND = 'DirectMessageService.findById.DirectMessageUserNotFound',
18 | DIRECT_MESSAGE_NOT_FOUND = 'DirectMessageService.findById.DirectMessageNotFound',
19 | NOT_INVITED = 'InviteServer.accept.NotInvited',
20 | }
21 |
22 | export default ErrorEnum;
23 |
--------------------------------------------------------------------------------
/src/components/layouts/app-layout/channels/server-body/channel-button.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 | import ChannelType from '../../../../../utils/enums/channel-type';
3 | import useApp from '../../../../../utils/hooks/use-app';
4 | import useChannel from '../../../../../utils/hooks/use-channel';
5 | import ChannelDto from '../../../../../utils/types/dtos/channel';
6 | import { IconSize, PoundIcon, VolumeUpIcon } from '../../../../icons';
7 |
8 | interface ChannelButtonProps {
9 | channel: ChannelDto;
10 | }
11 |
12 | const ChannelButton = ({ channel }: ChannelButtonProps) => {
13 | const { setShowSidebar } = useApp();
14 | const router = useRouter();
15 | const { serverId } = router.query as { serverId: string };
16 | const { channel: currentChannel } = useChannel();
17 |
18 | const handleChannelBtnClick = async (channelId: number) => {
19 | router.push(`/servers/${serverId}/channels/${channelId}`);
20 | setShowSidebar(false);
21 | };
22 |
23 | return (
24 |
43 | );
44 | };
45 |
46 | export default ChannelButton;
47 |
--------------------------------------------------------------------------------
/.githooks/pre-push:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # A custom git hook that runs the build & all tests via `npm run all-tests` on push to the trunk.
4 | # If any part of the build or test run fails, the commit(s) will not be pushed.
5 |
6 | # This script was modified from the .git/hooks/pre-push.sample file, so some of its commentary
7 | # and implementation will remain.
8 |
9 | # An example hook script to verify what is about to be pushed. Called by "git
10 | # push" after it has checked the remote status, but before anything has been
11 | # pushed. If this script exits with a non-zero status nothing will be pushed.
12 | #
13 | # This hook is called with the following parameters:
14 | #
15 | # $1 -- Name of the remote to which the push is being done
16 | # $2 -- URL to which the push is being done
17 | #
18 | # If pushing without using a named remote those arguments will be equal.
19 | #
20 | # Information about the commits which are being pushed is supplied as lines to
21 | # the standard input in the form:
22 | #
23 | #
24 |
25 | REMOTE="$1"
26 | SCRIPT_PREFIX="\x1b[1;35m[.githooks/pre-push]\x1b[0m"
27 | MAIN_REF="refs/heads/main"
28 | URL="$2"
29 |
30 | while read LOCAL_REF LOCAL_SHA REMOTE_REF REMOTE_SHA
31 | do
32 | # Fetch updates from the remote branch & verify we're not behind before attempting to run tests.
33 | BEHIND_REMOTE=`git fetch && git status -uno | grep behind | wc -l`
34 | if [ $BEHIND_REMOTE -ne 0 ]
35 | then
36 | echo -e "$SCRIPT_PREFIX Tried pushing to $MAIN_REF, but it looks like your branch might be behind.";
37 | echo -e "$SCRIPT_PREFIX Try running git pull first.";
38 | exit 1
39 | fi
40 |
41 | echo -e "$SCRIPT_PREFIX Running 'npm run test' due to push to $MAIN_REF";
42 | npm run test
43 | if [ $? -ne 0 ]
44 | then
45 | exit 1
46 | fi
47 | done
48 |
49 | exit 0
--------------------------------------------------------------------------------
/src/utils/contexts/window-context.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useEffect, useState } from 'react';
2 |
3 | interface WindowContextInterface {
4 | height?: number;
5 | width?: number;
6 | heightStyle: string;
7 | widthStyle: string;
8 | isMobileWidth: boolean;
9 | }
10 |
11 | const defaultValues = {
12 | height: undefined,
13 | width: undefined,
14 | heightStyle: '',
15 | widthStyle: '',
16 | isMobileWidth: false,
17 | };
18 |
19 | export const WindowContext =
20 | createContext(defaultValues);
21 |
22 | interface WindowProviderInterface {
23 | children: JSX.Element;
24 | }
25 |
26 | export const WindowProvider = ({ children }: WindowProviderInterface) => {
27 | const [windowSize, setWindowSize] = useState<{
28 | width: number | undefined;
29 | height: number | undefined;
30 | }>({
31 | width: undefined,
32 | height: undefined,
33 | });
34 | const heightStyle =
35 | windowSize.height != null ? `${windowSize.height}px` : '100%';
36 | const widthStyle =
37 | windowSize.width != null ? `${windowSize.width}px` : '100%';
38 | const isMobileWidth = windowSize.width != null && windowSize.width <= 768;
39 |
40 | useEffect(() => {
41 | const handleResize = () => {
42 | setWindowSize({
43 | width: window.innerWidth,
44 | height: window.innerHeight,
45 | });
46 | };
47 | window.addEventListener('resize', handleResize);
48 | handleResize();
49 | return () => window.removeEventListener('resize', handleResize);
50 | }, []);
51 |
52 | return (
53 |
62 | {children}
63 |
64 | );
65 | };
66 |
--------------------------------------------------------------------------------
/src/components/feedback/tooltip/tooltip.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { CSSTransition } from 'react-transition-group';
3 |
4 | interface TooltipProps {
5 | text: string | JSX.Element;
6 | children: JSX.Element;
7 | direction: 'top' | 'left' | 'bottom' | 'right';
8 | size?: 'sm' | 'md';
9 | }
10 |
11 | const TOOLTIP_DIRECTION_CLASSES = {
12 | top: 'bottom-full flex-col-reverse mb-1',
13 | left: 'ml-4 left-full flex-row',
14 | bottom: 'top-full flex-col',
15 | right: 'right-full flex-row-reverse',
16 | };
17 |
18 | const TOOLTIP_SIZE_CLASSES = {
19 | sm: 'text-xs',
20 | md: 'text-sm',
21 | };
22 |
23 | const Tooltip = ({ text, children, direction, size = 'md' }: TooltipProps) => {
24 | const [showTooltip, setShowTooltip] = useState(false);
25 |
26 | const handleTooltipMouseOver = () => {
27 | setShowTooltip(true);
28 | };
29 |
30 | const handleTooltopMouseLeave = () => {
31 | setShowTooltip(false);
32 | };
33 |
34 | return (
35 |
40 | {children}
41 |
47 |
50 |
51 |
54 | {text}
55 |
56 |
57 |
58 |
59 | );
60 | };
61 |
62 | export default Tooltip;
63 |
--------------------------------------------------------------------------------
/src/components/layouts/app-layout/friends/friend-header/friend-header.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import useUser from '../../../../../utils/hooks/use-user';
3 | import { FriendIcon, IconSize } from '../../../../icons';
4 | import Header from '../../header';
5 | import FriendButton from './friend-button/friend-button';
6 |
7 | const FriendHeader = () => {
8 | const { numIncomingPendingFriendRequests } = useUser();
9 |
10 | return (
11 |
40 | );
41 | };
42 |
43 | export default FriendHeader;
44 |
--------------------------------------------------------------------------------
/src/utils/contexts/socket-context.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, MutableRefObject, useEffect, useRef } from 'react';
2 | import { io, Socket } from 'socket.io-client';
3 | import { DefaultEventsMap } from 'socket.io/dist/typed-events';
4 | import EventEnum from '../enums/events';
5 | import useAuth from '../hooks/use-auth';
6 | import useUser from '../hooks/use-user';
7 | import DirectMessageDto from '../types/dtos/direct-message';
8 |
9 | const BASE_URL = process.env.HOST ?? '';
10 |
11 | interface SocketContextInterface {
12 | socket: null | MutableRefObject>;
13 | }
14 |
15 | const defaultValues = {
16 | socket: null,
17 | };
18 |
19 | export const SocketContext =
20 | createContext(defaultValues);
21 |
22 | interface SocketProviderInterface {
23 | children: JSX.Element;
24 | }
25 |
26 | export const SocketProvider = ({ children }: SocketProviderInterface) => {
27 | const { accessToken } = useAuth();
28 | const { addDirectMessage } = useUser();
29 | const socket = useRef(
30 | io(BASE_URL, {
31 | query: { accessToken },
32 | }),
33 | );
34 |
35 | useEffect(() => {
36 | socket.current.connect();
37 |
38 | () => {
39 | socket.current.disconnect();
40 | };
41 | }, []);
42 |
43 | useEffect(() => {
44 | if (
45 | socket == null ||
46 | socket.current.hasListeners(EventEnum.DIRECT_MESSAGE_CREATED)
47 | )
48 | return;
49 |
50 | const handler = (directMessage: DirectMessageDto) => {
51 | addDirectMessage(directMessage);
52 | };
53 | socket.current.on(EventEnum.DIRECT_MESSAGE_CREATED, handler);
54 |
55 | () => {
56 | socket.current.off(EventEnum.DIRECT_MESSAGE_CREATED, handler);
57 | };
58 | // eslint-disable-next-line react-hooks/exhaustive-deps
59 | }, [socket]);
60 |
61 | return (
62 |
63 | {children}
64 |
65 | );
66 | };
67 |
--------------------------------------------------------------------------------
/src/server/controllers/message/message.controller.ts:
--------------------------------------------------------------------------------
1 | import catchAsync from '../../middleware/catch-async';
2 | import { Request, Response } from 'express';
3 | import MessageService from '../../services/message';
4 | import { ERROR_UNKOWN } from '../../../utils/constants/errors';
5 | import ErrorEnum from '../../../utils/enums/errors';
6 |
7 | class MessageController {
8 | private _messageService: MessageService;
9 |
10 | constructor() {
11 | this._messageService = new MessageService();
12 | }
13 |
14 | public create = catchAsync(async (req: Request, res: Response) => {
15 | try {
16 | const { message, errors } = await this._messageService.create(
17 | req.user.id,
18 | { ...req.body },
19 | );
20 | if (errors != null) return res.status(400).json(errors);
21 |
22 | return res.status(201).json(message);
23 | } catch (e: any) {
24 | switch (e.type) {
25 | case ErrorEnum.DIRECT_MESSAGE_USER_NOT_FOUND:
26 | return res.status(403).json(e.errors);
27 | default: {
28 | return res.status(500).json([ERROR_UNKOWN]);
29 | }
30 | }
31 | }
32 | });
33 |
34 | public index = catchAsync(async (req: Request, res: Response) => {
35 | try {
36 | const directMessageId = parseInt(req.query.directMessageId as string);
37 | const skip = parseInt(req.query.skip as string);
38 | const take = parseInt(req.query.take as string);
39 |
40 | const messages = await this._messageService.findAllByDirectMessageId(
41 | req.user.id,
42 | directMessageId,
43 | skip,
44 | take,
45 | );
46 |
47 | res.status(200).json(messages);
48 | } catch (e: any) {
49 | switch (e.type) {
50 | case ErrorEnum.DIRECT_MESSAGE_USER_NOT_FOUND:
51 | return res.status(403).json(e.errors);
52 | default:
53 | return res.status(500).json([ERROR_UNKOWN]);
54 | }
55 | }
56 | });
57 | }
58 |
59 | export default MessageController;
60 |
--------------------------------------------------------------------------------
/src/utils/contexts/channel-context.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, Dispatch, SetStateAction, useState } from 'react';
2 | import ChannelService from '../../services/channel-service';
3 | import useToasts from '../hooks/use-toasts';
4 | import handleServiceError from '../services/handle-service-error';
5 | import ChannelDto from '../types/dtos/channel';
6 |
7 | interface ChannelContextInterface {
8 | loading: boolean;
9 | channel: ChannelDto | null;
10 | setChannel: Dispatch>;
11 | // eslint-disable-next-line no-unused-vars
12 | loadChannel: (serverId: number, channelId: number) => Promise;
13 | }
14 |
15 | const defaultValues = {
16 | loading: false,
17 | channel: null,
18 | setChannel: () => {},
19 | loadChannel: async () => {},
20 | };
21 |
22 | export const ChannelContext =
23 | createContext(defaultValues);
24 |
25 | interface ChannelProviderInterface {
26 | children: JSX.Element;
27 | }
28 |
29 | export const ChannelProvider = ({ children }: ChannelProviderInterface) => {
30 | const [loading, setLoading] = useState(defaultValues.loading);
31 | const [channel, setChannel] = useState(
32 | defaultValues.channel,
33 | );
34 |
35 | const { danger } = useToasts();
36 |
37 | const loadChannel = async (serverId: number, channelId: number) => {
38 | setLoading(true);
39 | try {
40 | const response = await ChannelService.get(serverId, channelId);
41 | setChannel(response.data);
42 | } catch (error) {
43 | setChannel(null);
44 | const { errors } = handleServiceError(error);
45 | errors.forEach((error) => {
46 | danger(error.message);
47 | });
48 | } finally {
49 | setLoading(false);
50 | }
51 | };
52 |
53 | return (
54 |
62 | {children}
63 |
64 | );
65 | };
66 |
--------------------------------------------------------------------------------
/test/integration/auth/logout.test.ts:
--------------------------------------------------------------------------------
1 | import request from 'supertest';
2 | import app from '../../../src/server/app';
3 | import db from '../../../src/server/db/models';
4 | import User from '../../../src/server/db/models/user.model';
5 | import UserDto from '../../../src/utils/types/dtos/user';
6 | import CreateUserRequest from '../../../src/utils/types/requests/user/create-user';
7 |
8 | const baseURL = '/api/v1/auth';
9 | const username = 'test';
10 | const email = 'test@test.com';
11 | const password = 'password';
12 | let accessToken = '';
13 |
14 | const loginUser = async () => {
15 | const response = await request(app).post('/api/v1/auth').send({
16 | email,
17 | password,
18 | });
19 | accessToken = response.body;
20 | };
21 |
22 | describe('login should', () => {
23 | beforeAll(async () => {
24 | // Drop database
25 | await db.sequelize.sync({ force: true });
26 |
27 | // Register the user
28 | const response = await request(app)
29 | .post('/api/v1/user')
30 | .send({
31 | username,
32 | email,
33 | password,
34 | confirmPassword: password,
35 | } as CreateUserRequest);
36 |
37 | // Mock the verification email
38 | const userDto: UserDto = response.body;
39 | const user = await User.findByPk(userDto.id);
40 | const verificationToken = user!.verificationToken!;
41 |
42 | // Verify the user
43 | await request(app).put(`/api/v1/user/verify-email/${verificationToken}`);
44 | });
45 | test('return unauthorized when user is not logged in', async () => {
46 | // Arrange && Act
47 | const response = await request(app).delete(baseURL);
48 |
49 | // Assert
50 | expect(response.statusCode).toBe(401);
51 | });
52 | test('return unauthorized when token cookie not present', async () => {
53 | // Arrange
54 | await loginUser();
55 |
56 | // Act
57 | const response = await request(app).delete(baseURL);
58 |
59 | // Assert
60 | expect(response.statusCode).toBe(401);
61 | });
62 | afterAll(async () => {
63 | await db.sequelize.close();
64 | });
65 | });
66 |
--------------------------------------------------------------------------------
/src/server/controllers/server/server.controller.ts:
--------------------------------------------------------------------------------
1 | import catchAsync from '../../middleware/catch-async';
2 | import ServerValidator from '../../validators/server';
3 | import { Request, Response } from 'express';
4 | import { ERROR_UNKOWN } from '../../../utils/constants/errors';
5 | import ServerService from '../../services/server';
6 | import ErrorEnum from '../../../utils/enums/errors';
7 |
8 | class ServerController {
9 | private _serverService;
10 |
11 | constructor() {
12 | this._serverService = new ServerService();
13 | }
14 |
15 | public create = catchAsync(async (req: Request, res: Response) => {
16 | const validationErrors = ServerValidator.create({ ...req.body });
17 | if (validationErrors.length > 0) {
18 | return res.status(400).json(validationErrors);
19 | }
20 |
21 | const { name } = req.body;
22 | const { errors, server } = await this._serverService.create(
23 | name,
24 | req.user.id,
25 | );
26 |
27 | if (errors != null) return res.status(400).json(errors);
28 | if (server == null) return res.sendStatus(500);
29 |
30 | return res.status(201).json(server);
31 | });
32 |
33 | public index = catchAsync(async (req: Request, res: Response) => {
34 | const userId = req.user.id;
35 |
36 | const servers = await this._serverService.findAllByUserId(userId);
37 |
38 | return res.status(200).json(servers);
39 | });
40 |
41 | public get = catchAsync(async (req: Request, res: Response) => {
42 | try {
43 | const userId = req.user.id;
44 | const id = parseInt(req.params.serverId);
45 |
46 | const server = await this._serverService.findById(id, userId);
47 |
48 | return res.status(200).json(server);
49 | } catch (error: any) {
50 | switch (error.type) {
51 | case ErrorEnum.SERVER_NOT_FOUND:
52 | return res.status(400).json(error.errors);
53 | case ErrorEnum.INSUFFICIENT_PERMISIONS:
54 | return res.status(403).json(error.errors);
55 | default:
56 | return res.status(500).json([ERROR_UNKOWN]);
57 | }
58 | }
59 | });
60 | }
61 |
62 | export default ServerController;
63 |
--------------------------------------------------------------------------------
/src/pages/servers/[serverId]/channels/[channelId].tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 | import { ReactElement, useEffect } from 'react';
3 | import AppLayout from '../../../../components/layouts/app-layout';
4 | import ChannelHeader from '../../../../components/layouts/app-layout/channels/channel-header/channel-header';
5 | import ChannelSidebar from '../../../../components/layouts/app-layout/channels/channel-sidebar/channel-sidebar';
6 | import TextChannel from '../../../../components/layouts/app-layout/channels/text-channel';
7 | import VoiceChannel from '../../../../components/layouts/app-layout/channels/voice-channel';
8 | import ChannelType from '../../../../utils/enums/channel-type';
9 | import useChannel from '../../../../utils/hooks/use-channel';
10 | import useServer from '../../../../utils/hooks/use-server';
11 | import { NextPageLayout } from '../../../../utils/types/next-page-layout';
12 |
13 | const Channel = () => {
14 | const { channel } = useChannel();
15 | return channel == null ? (
16 | <>>
17 | ) : channel.type === ChannelType.TEXT ? (
18 |
19 | ) : channel.type === ChannelType.VOICE ? (
20 |
21 | ) : (
22 | <>>
23 | );
24 | };
25 |
26 | const ChannelPage: NextPageLayout = () => {
27 | const router = useRouter();
28 | const { serverId, channelId } = router.query as {
29 | serverId: string;
30 | channelId: string;
31 | };
32 | const { loadServer } = useServer();
33 | const { loadChannel } = useChannel();
34 |
35 | useEffect(() => {
36 | const newServerId = parseInt(serverId);
37 | const newChannelId = parseInt(channelId);
38 |
39 | if (isNaN(newServerId) || isNaN(newChannelId)) return;
40 |
41 | loadServer(newServerId);
42 | loadChannel(newServerId, newChannelId);
43 | // eslint-disable-next-line react-hooks/exhaustive-deps
44 | }, [channelId, serverId]);
45 |
46 | return ;
47 | };
48 |
49 | ChannelPage.getLayout = (page: ReactElement) => {
50 | return (
51 | } sidebar={}>
52 | {page}
53 |
54 | );
55 | };
56 |
57 | export default ChannelPage;
58 |
--------------------------------------------------------------------------------
/test/fixtures/index.ts:
--------------------------------------------------------------------------------
1 | import request from 'supertest';
2 | import app from '../../src/server/app';
3 | import CreateUserRequest from '../../src/utils/types/requests/user/create-user';
4 | import { faker } from '@faker-js/faker';
5 | import UserDto from '../../src/utils/types/dtos/user';
6 | import User from '../../src/server/db/models/user.model';
7 | import LoginRequest from '../../src/utils/types/requests/auth/login';
8 | import ServerDto from '../../src/utils/types/dtos/server';
9 | import CreateServerRequest from '../../src/utils/types/requests/server/create-server';
10 |
11 | export const registerUser = async () => {
12 | const username = faker.random.alphaNumeric(4);
13 | const email = faker.internet.email();
14 | const password = faker.random.alphaNumeric(8);
15 |
16 | const response = await request(app)
17 | .post('/api/v1/user')
18 | .send({
19 | username,
20 | email,
21 | password,
22 | confirmPassword: password,
23 | } as CreateUserRequest);
24 |
25 | // Mock verification email
26 | const userDto: UserDto = response.body;
27 | const user = await User.findByPk(userDto.id);
28 |
29 | return {
30 | userId: userDto.id,
31 | email,
32 | password,
33 | verificationToken: user!.verificationToken!,
34 | };
35 | };
36 |
37 | export const verifyEmail = async (verificationToken: string) => {
38 | await request(app).put(`/api/v1/user/verify-email/${verificationToken}`);
39 | };
40 |
41 | export const createAndLoginUser = async () => {
42 | const { email, password, verificationToken } = await registerUser();
43 | await verifyEmail(verificationToken);
44 |
45 | const response = await request(app)
46 | .post('/api/v1/auth')
47 | .send({
48 | email,
49 | password,
50 | } as LoginRequest);
51 |
52 | return response.body;
53 | };
54 |
55 | export const createServer = async (accessToken: string) => {
56 | // Create server
57 | const createServerResponse = await request(app)
58 | .post('/api/v1/server')
59 | .send({
60 | name: 'test server',
61 | } as CreateServerRequest)
62 | .set('Authorization', `Bearer ${accessToken}`);
63 |
64 | return createServerResponse.body as ServerDto;
65 | };
66 |
--------------------------------------------------------------------------------
/src/components/layouts/app-layout/servers/servers.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import useApp from '../../../../utils/hooks/use-app';
3 | import useServers from '../../../../utils/hooks/use-servers';
4 | import useWindowSize from '../../../../utils/hooks/use-window-size';
5 | import Tooltip from '../../../feedback/tooltip';
6 | import { IconSize, LogoIcon } from '../../../icons';
7 | import CreateServerModal from './create-server-modal';
8 |
9 | const Servers = () => {
10 | const { isHomePage } = useApp();
11 | const { heightStyle } = useWindowSize();
12 | const { servers } = useServers();
13 |
14 | return (
15 |
19 |
20 | {isHomePage && (
21 |
22 | )}
23 |
24 |
25 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | {servers.map((server) => {
39 | return (
40 |
41 |
45 |
46 |
47 |
48 | );
49 | })}
50 |
51 |
52 | );
53 | };
54 |
55 | export default Servers;
56 |
--------------------------------------------------------------------------------
/src/server/controllers/user/direct-message/direct-message.controller.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from 'express';
2 | import { ERROR_UNKOWN } from '../../../../utils/constants/errors';
3 | import ErrorEnum from '../../../../utils/enums/errors';
4 | import catchAsync from '../../../middleware/catch-async';
5 | import DirectMessageService from '../../../services/user/direct-message/direct-message.service';
6 |
7 | class DirectMessageController {
8 | private _directMessageService;
9 |
10 | constructor() {
11 | this._directMessageService = new DirectMessageService();
12 | }
13 |
14 | public get = catchAsync(async (req: Request, res: Response) => {
15 | try {
16 | const directMessageId = parseInt(req.params.directMessageId);
17 | const directMessage = await this._directMessageService.findById(
18 | directMessageId,
19 | req.user.id,
20 | );
21 |
22 | res.status(200).json(directMessage);
23 | } catch (e: any) {
24 | switch (e.type) {
25 | case ErrorEnum.DIRECT_MESSAGE_USER_NOT_FOUND:
26 | return res.status(403).json(e.errors);
27 | case ErrorEnum.DIRECT_MESSAGE_NOT_FOUND:
28 | return res.status(404).json(e.errors);
29 | default:
30 | return res.status(500).json([ERROR_UNKOWN]);
31 | }
32 | }
33 | });
34 |
35 | public create = catchAsync(async (req: Request, res: Response) => {
36 | try {
37 | const createDirectMessageRequest = {
38 | userId: req.user.id,
39 | friendIds: req.body.friendIds,
40 | };
41 | const { errors, directMessage } =
42 | await this._directMessageService.createDirectMessage(
43 | createDirectMessageRequest,
44 | );
45 |
46 | if (errors != null && errors.length > 0)
47 | return res.status(400).json(errors);
48 | if (directMessage == null) return res.status(500).json([ERROR_UNKOWN]);
49 |
50 | res.status(201).json(directMessage);
51 | } catch (e: any) {
52 | switch (e.type) {
53 | case ErrorEnum.CREATE_DIRECT_MESSAGE_NOT_FRIENDS_WITH_ENTIRE_GROUP:
54 | return res.status(403).json(e.errors);
55 | default:
56 | return res.status(500).json([ERROR_UNKOWN]);
57 | }
58 | }
59 | });
60 | }
61 | export default DirectMessageController;
62 |
--------------------------------------------------------------------------------
/src/server/controllers/server/channel/channel.controller.ts:
--------------------------------------------------------------------------------
1 | import catchAsync from '../../../middleware/catch-async';
2 | import ChannelService from '../../../services/server/channel';
3 | import { Request, Response } from 'express';
4 | import { ERROR_UNKOWN } from '../../../../utils/constants/errors';
5 | import ErrorEnum from '../../../../utils/enums/errors';
6 |
7 | class ChannelController {
8 | private _channelService;
9 |
10 | constructor() {
11 | this._channelService = new ChannelService();
12 | }
13 |
14 | public create = catchAsync(async (req: Request, res: Response) => {
15 | try {
16 | const serverId = req.params.serverId;
17 | const userId = req.user.id;
18 | const { type, name } = req.body;
19 | const { errors, channel } = await this._channelService.create({
20 | serverId,
21 | userId,
22 | type,
23 | name,
24 | });
25 |
26 | if (errors != null) return res.status(400).json(errors);
27 | if (channel == null) return res.status(500).json([ERROR_UNKOWN]);
28 |
29 | return res.status(201).json(channel);
30 | } catch (error: any) {
31 | switch (error.type) {
32 | case ErrorEnum.SERVER_NOT_FOUND:
33 | return res.status(400).json(error.errors);
34 | case ErrorEnum.INSUFFICIENT_PERMISIONS:
35 | return res.status(403).json(error.errors);
36 | default:
37 | return res.status(500).json([ERROR_UNKOWN]);
38 | }
39 | }
40 | });
41 |
42 | public get = catchAsync(async (req: Request, res: Response) => {
43 | try {
44 | const channelId = parseInt(req.params.channelId);
45 | const userId = req.user.id;
46 |
47 | const channel = await this._channelService.findById({
48 | channelId,
49 | userId,
50 | });
51 |
52 | if (channel == null) return res.status(500).json([ERROR_UNKOWN]);
53 |
54 | return res.status(200).json(channel);
55 | } catch (error: any) {
56 | switch (error.type) {
57 | case ErrorEnum.CHANNEL_NOT_FOUND:
58 | return res.status(400).json(error.errors);
59 | case ErrorEnum.INSUFFICIENT_PERMISIONS:
60 | return res.status(403).json(error.errors);
61 | default:
62 | return res.status(500).json([ERROR_UNKOWN]);
63 | }
64 | }
65 | });
66 | }
67 |
68 | export default ChannelController;
69 |
--------------------------------------------------------------------------------
/src/utils/contexts/servers-context.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | createContext,
3 | Dispatch,
4 | SetStateAction,
5 | useEffect,
6 | useState,
7 | } from 'react';
8 | import ServerService from '../../services/server-service';
9 | import useAuth from '../hooks/use-auth';
10 | import useToasts from '../hooks/use-toasts';
11 | import handleServiceError from '../services/handle-service-error';
12 | import ServerDto from '../types/dtos/server';
13 |
14 | interface ServersContextInterface {
15 | servers: ServerDto[];
16 | loading: boolean;
17 | setServers: Dispatch>;
18 | // eslint-disable-next-line no-unused-vars
19 | addServer: (server: ServerDto) => void;
20 | }
21 |
22 | const defaultValues = {
23 | servers: [],
24 | setServers: () => {},
25 | loading: false,
26 | addServer: () => {},
27 | };
28 |
29 | export const ServersContext =
30 | createContext(defaultValues);
31 |
32 | interface ServersProviderInterface {
33 | children: JSX.Element;
34 | }
35 |
36 | export const ServersProvider = ({ children }: ServersProviderInterface) => {
37 | const { loading: loadingAuth } = useAuth();
38 | const { danger } = useToasts();
39 | const [servers, setServers] = useState(defaultValues.servers);
40 | const [loading, setLoading] = useState(defaultValues.loading);
41 |
42 | const addServer = (server: ServerDto) => {
43 | setServers((prev) => {
44 | return prev.some((e) => e.id === server.id) ? prev : [...prev, server];
45 | });
46 | };
47 |
48 | useEffect(() => {
49 | if (loadingAuth) return;
50 |
51 | const loadServers = async () => {
52 | setLoading(true);
53 | try {
54 | const response = await ServerService.list();
55 | setServers(response.data);
56 | } catch (error) {
57 | const { errors } = handleServiceError(error);
58 | if (errors != null) {
59 | errors.forEach((error) => {
60 | danger(error.message);
61 | });
62 | }
63 | } finally {
64 | setLoading(false);
65 | }
66 | };
67 |
68 | loadServers();
69 | // eslint-disable-next-line react-hooks/exhaustive-deps
70 | }, [loadingAuth]);
71 |
72 | return (
73 |
76 | {children}
77 |
78 | );
79 | };
80 |
--------------------------------------------------------------------------------
/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 | @import './variables.css';
5 | @import './spinners.css';
6 | @import './transitions.css';
7 | @import './toasts.css';
8 | @import './backgrounds.css';
9 | @import './animations.css';
10 | @import './scrollbar.css';
11 |
12 | /* Global Styles */
13 | * {
14 | -webkit-font-smoothing: antialiased;
15 | -moz-osx-font-smoothing: grayscale;
16 | }
17 |
18 | :focus:not(:focus-visible) {
19 | outline: none;
20 | box-shadow: none;
21 | }
22 |
23 | textarea:focus,
24 | input:focus {
25 | outline: none;
26 | }
27 |
28 | *:focus {
29 | outline: none;
30 | }
31 |
32 | /* Fonts */
33 | .font-primary {
34 | font-family: var(--font-primary);
35 | }
36 |
37 | /* Utils */
38 | .scrollbar-none::-webkit-scrollbar {
39 | width: 0;
40 | height: 0;
41 | background: transparent;
42 | }
43 |
44 | .center {
45 | display: flex;
46 | justify-content: center;
47 | align-items: center;
48 | }
49 |
50 | /* Arrows */
51 | .arrow-up {
52 | width: 0;
53 | height: 0;
54 | border-left: 5px solid transparent;
55 | border-right: 5px solid transparent;
56 | border-bottom-width: 5px;
57 | border-bottom-style: solid;
58 | }
59 |
60 | /* Arrows */
61 | .arrow-top {
62 | width: 0;
63 | height: 0;
64 | border-left: 5px solid transparent;
65 | border-right: 5px solid transparent;
66 | border-top-width: 5px;
67 | border-top-style: solid;
68 | }
69 | .arrow-left {
70 | width: 0;
71 | height: 0;
72 | border-top: 5px solid transparent;
73 | border-bottom: 5px solid transparent;
74 | border-right-width: 5px;
75 | border-right-style: solid;
76 | }
77 | .arrow-right {
78 | width: 0;
79 | height: 0;
80 | border-left: 5px solid transparent;
81 | border-right: 5px solid transparent;
82 | border-bottom-width: 5px;
83 | border-bottom-style: solid;
84 | }
85 | .arrow-bottom {
86 | width: 0;
87 | height: 0;
88 | border-left: 5px solid transparent;
89 | border-right: 5px solid transparent;
90 | border-bottom-width: 5px;
91 | border-bottom-style: solid;
92 | }
93 |
94 | /* Buttons */
95 | .invite-btn:hover button {
96 | background-color: theme('colors.green.600');
97 | }
98 |
99 | /* Inputs */
100 | input::-webkit-input-placeholder {
101 | font-weight: 300;
102 | }
103 | input::-moz-placeholder {
104 | font-weight: 300;
105 | }
106 | input::-ms-input-placeholder {
107 | font-weight: 300;
108 | }
109 |
--------------------------------------------------------------------------------
/test/integration/user/verify-email.test.ts:
--------------------------------------------------------------------------------
1 | import request from 'supertest';
2 | import app from '../../../src/server/app';
3 | import db from '../../../src/server/db/models';
4 | import User from '../../../src/server/db/models/user.model';
5 | import UserDto from '../../../src/utils/types/dtos/user';
6 | import CreateUserRequest from '../../../src/utils/types/requests/user/create-user';
7 |
8 | const username = 'test';
9 | const email = 'test@test.com';
10 | const password = 'password';
11 | const baseURL = '/api/v1/user/verify-email';
12 | let verificationToken = '';
13 | let user: User | null = null;
14 |
15 | const dropDatabaseAndRegisterUser = async () => {
16 | // Drop database
17 | await db.sequelize.sync({ force: true });
18 |
19 | // Register user
20 | const registerResponse = await request(app)
21 | .post('/api/v1/user')
22 | .send({
23 | username,
24 | email,
25 | password,
26 | confirmPassword: password,
27 | } as CreateUserRequest);
28 |
29 | // Mock verification email
30 | const userDto: UserDto = registerResponse.body;
31 | user = await User.findByPk(userDto.id);
32 | verificationToken = user!.verificationToken!;
33 | };
34 |
35 | describe('verify email should', () => {
36 | test('return bad request when token is invalid', async () => {
37 | // Arrange && Act
38 | const response = await request(app).put(`${baseURL}/invalid`);
39 |
40 | // Assert
41 | expect(response.statusCode).toBe(400);
42 | });
43 | test('return ok', async () => {
44 | // Arrange
45 | await dropDatabaseAndRegisterUser();
46 |
47 | // Act
48 | const response = await request(app).put(`${baseURL}/${verificationToken}`);
49 |
50 | // Assert
51 | expect(response.statusCode).toBe(200);
52 | });
53 | test('return bad request when email already verified', async () => {
54 | // Arrange
55 | await dropDatabaseAndRegisterUser();
56 |
57 | // Act
58 | await request(app).put(`${baseURL}/${verificationToken}`);
59 | const response = await request(app).put(`${baseURL}/${verificationToken}`);
60 |
61 | // Assert
62 | expect(response.statusCode).toBe(400);
63 | });
64 | test('Should verify users email', async () => {
65 | // Arrange
66 | await dropDatabaseAndRegisterUser();
67 |
68 | // Act
69 | await request(app).put(`${baseURL}/${verificationToken}`);
70 | user = await User.findByPk(user?.id);
71 |
72 | // Assert
73 | expect(user?.isVerified).toBeTruthy();
74 | });
75 | afterAll(async () => {
76 | await db.sequelize.close();
77 | });
78 | });
79 |
--------------------------------------------------------------------------------
/src/utils/contexts/toast-context.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useState } from 'react';
2 | import ToastInterface, { Color } from '../types/interfaces/toast-interface';
3 | import { v4 as uuid } from 'uuid';
4 | import ToastManager from '../../components/feedback/toast-manager';
5 | import ErrorInterface from '../types/interfaces/error';
6 |
7 | interface ToastContextInterface {
8 | toasts: ToastInterface[];
9 | // eslint-disable-next-line no-unused-vars
10 | removeToast: (id: string) => void;
11 | // eslint-disable-next-line no-unused-vars
12 | success: (title: string, body?: string) => void;
13 | // eslint-disable-next-line no-unused-vars
14 | danger: (title: string, body?: string) => void;
15 | // eslint-disable-next-line no-unused-vars
16 | errorListToToasts: (errors: ErrorInterface[]) => void;
17 | }
18 |
19 | const defaultValues = {
20 | toasts: [],
21 | // eslint-disable-next-line no-unused-vars
22 | removeToast: (id: string) => {},
23 | // eslint-disable-next-line no-unused-vars
24 | success: (title: string, body?: string) => {},
25 | // eslint-disable-next-line no-unused-vars
26 | danger: (title: string, body?: string) => {},
27 | errorListToToasts: () => {},
28 | };
29 |
30 | export const ToastContext = createContext(defaultValues);
31 |
32 | interface ToastProviderInterface {
33 | children: JSX.Element;
34 | }
35 |
36 | const DEFAULT_TOAST_DURATION = 5000;
37 |
38 | export const ToastProvider = ({ children }: ToastProviderInterface) => {
39 | const [toasts, setToasts] = useState(defaultValues.toasts);
40 |
41 | const removeToast = (id: string) => {
42 | setToasts((prev) => prev.filter((toast) => toast.id !== id));
43 | };
44 |
45 | const addToast = (color: Color, title: string, body?: string) => {
46 | const toast: ToastInterface = {
47 | id: uuid(),
48 | title,
49 | color,
50 | body,
51 | };
52 | setToasts((prev) => [toast, ...prev]);
53 | setTimeout(() => removeToast(toast.id), DEFAULT_TOAST_DURATION);
54 | };
55 |
56 | const success = (title: string, body?: string) => {
57 | addToast('success', title, body);
58 | };
59 |
60 | const danger = (title: string, body?: string) => {
61 | addToast('danger', title, body);
62 | };
63 |
64 | const errorListToToasts = (errors: ErrorInterface[]) => {
65 | errors.forEach((e) => {
66 | danger(e.message);
67 | });
68 | };
69 |
70 | return (
71 |
80 | {children}
81 |
82 |
83 | );
84 | };
85 |
--------------------------------------------------------------------------------
/src/components/layouts/app-layout/channels/server-header/channel-type-buttons.tsx:
--------------------------------------------------------------------------------
1 | import { Dispatch, SetStateAction } from 'react';
2 | import ChannelType from '../../../../../utils/enums/channel-type';
3 | import { PoundIcon, VolumeUpIcon } from '../../../../icons';
4 |
5 | interface ChannelTypeButtonsProps {
6 | channelType: ChannelType;
7 | setChannelType: Dispatch>;
8 | }
9 |
10 | const ChannelTypeButtons = ({
11 | channelType,
12 | setChannelType,
13 | }: ChannelTypeButtonsProps) => {
14 | const handleTextBtnClick = () => {
15 | setChannelType(ChannelType.TEXT);
16 | };
17 | const handleVoiceBtnClick = () => {
18 | setChannelType(ChannelType.VOICE);
19 | };
20 | return (
21 | <>
22 |
45 |
68 | >
69 | );
70 | };
71 |
72 | export default ChannelTypeButtons;
73 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "discord-clone",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "NODE_ENV=development nodemon src/server/index.ts",
7 | "build": "next build && tsc && tsc --project tsconfig.server.json",
8 | "start": "NODE_ENV=production node .next/server/server/index.js",
9 | "test": "NODE_ENV=test jest --detectOpenHandles",
10 | "lint": "next lint",
11 | "format": "npx prettier --write ."
12 | },
13 | "dependencies": {
14 | "@svgr/webpack": "^6.2.1",
15 | "axios": "^0.26.1",
16 | "bcrypt": "^5.0.1",
17 | "cookie-parser": "^1.4.6",
18 | "cors": "^2.8.5",
19 | "dotenv": "^16.0.0",
20 | "express": "^4.17.3",
21 | "express-rate-limit": "^6.3.0",
22 | "jsonwebtoken": "^8.5.1",
23 | "jwt-decode": "^3.1.2",
24 | "next": "12.1.4",
25 | "nodemailer": "^6.7.3",
26 | "pg": "^8.7.3",
27 | "react": "18.0.0",
28 | "react-dom": "18.0.0",
29 | "react-transition-group": "^4.4.2",
30 | "reflect-metadata": "^0.1.13",
31 | "sequelize": "^6.17.0",
32 | "sequelize-typescript": "^2.1.3",
33 | "socket.io": "^4.4.1",
34 | "socket.io-client": "^4.4.1",
35 | "ts-node": "^10.7.0",
36 | "uuid": "^8.3.2",
37 | "validator": "^13.7.0",
38 | "webrtc-adapter": "^8.1.1"
39 | },
40 | "devDependencies": {
41 | "@babel/plugin-proposal-class-properties": "^7.16.7",
42 | "@babel/plugin-proposal-decorators": "^7.17.8",
43 | "@babel/plugin-proposal-private-property-in-object": "^7.16.7",
44 | "@babel/plugin-transform-flow-strip-types": "^7.16.7",
45 | "@babel/preset-env": "^7.16.11",
46 | "@babel/preset-typescript": "^7.16.7",
47 | "@faker-js/faker": "^6.2.0",
48 | "@testing-library/jest-dom": "^5.16.3",
49 | "@testing-library/react": "^13.0.0",
50 | "@types/bcrypt": "^5.0.0",
51 | "@types/cookie-parser": "^1.4.2",
52 | "@types/cors": "^2.8.12",
53 | "@types/express": "^4.17.13",
54 | "@types/jsonwebtoken": "^8.5.8",
55 | "@types/node": "17.0.23",
56 | "@types/nodemailer": "^6.4.4",
57 | "@types/react": "17.0.43",
58 | "@types/react-dom": "17.0.14",
59 | "@types/react-transition-group": "^4.4.4",
60 | "@types/supertest": "^2.0.12",
61 | "@types/uuid": "^8.3.4",
62 | "autoprefixer": "^10.4.4",
63 | "babel-jest": "^27.5.1",
64 | "eslint": "8.12.0",
65 | "eslint-config-next": "12.1.4",
66 | "eslint-config-prettier": "^8.5.0",
67 | "jest": "^27.5.1",
68 | "nodemon": "^2.0.15",
69 | "postcss": "^8.4.12",
70 | "prettier": "^2.6.2",
71 | "prettier-plugin-tailwindcss": "^0.1.8",
72 | "sequelize-cli": "^6.4.1",
73 | "supertest": "^6.2.2",
74 | "tailwindcss": "^3.0.23",
75 | "ts-jest": "^27.1.4",
76 | "typescript": "4.6.3"
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/server/controllers/auth/auth.controller.ts:
--------------------------------------------------------------------------------
1 | import catchAsync from '../../middleware/catch-async';
2 | import { Request, Response } from 'express';
3 | import AuthValidator from '../../validators/auth';
4 | import UserService from '../../services/user/user.service';
5 | import jwtDecode from 'jwt-decode';
6 | import JwtToken from '../../../utils/types/interfaces/jwt-token';
7 | import { env } from 'process';
8 |
9 | const REFRESH_TOKEN_COOKIE = 'token';
10 |
11 | class AuthController {
12 | private _userService;
13 |
14 | constructor() {
15 | this._userService = new UserService();
16 | }
17 |
18 | public login = catchAsync(async (req: Request, res: Response) => {
19 | const validationErrors = AuthValidator.login({ ...req.body });
20 | if (validationErrors.length > 0) {
21 | return res.status(400).json(validationErrors);
22 | }
23 |
24 | const { email, password } = req.body;
25 | const { errors, accessToken, refreshToken } =
26 | await this._userService.loginUser(email, password);
27 |
28 | if (errors != null) return res.status(400).json(errors);
29 | if (accessToken == null) return res.sendStatus(500);
30 | if (refreshToken == null) return res.sendStatus(500);
31 |
32 | res.cookie(REFRESH_TOKEN_COOKIE, refreshToken, {
33 | secure: process.env.NODE_ENV === 'production',
34 | httpOnly: true,
35 | expires: this.getTokenExpirationDate(refreshToken),
36 | });
37 | return res.status(201).json(accessToken);
38 | });
39 |
40 | public refreshToken = catchAsync(async (req: Request, res: Response) => {
41 | const { token } = req.cookies as { token?: string };
42 | if (token == null) return res.sendStatus(401);
43 |
44 | const result = await this._userService.refreshToken(token);
45 | if (result == null) return res.sendStatus(401);
46 |
47 | const { accessToken, refreshToken } = result;
48 | res.cookie(REFRESH_TOKEN_COOKIE, refreshToken, {
49 | secure: process.env.NODE_ENV === 'production',
50 | httpOnly: true,
51 | expires: this.getTokenExpirationDate(refreshToken),
52 | });
53 | return res.status(200).json(accessToken);
54 | });
55 |
56 | public logout = catchAsync(async (req: Request, res: Response) => {
57 | const { token } = req.cookies as { token?: string };
58 | if (token == null) return res.sendStatus(401);
59 |
60 | await this._userService.logoutUser(token);
61 |
62 | res.clearCookie(REFRESH_TOKEN_COOKIE);
63 | return res.sendStatus(200);
64 | });
65 |
66 | private getTokenExpirationDate = (token: string) => {
67 | const { exp } = jwtDecode(token);
68 | const expirationDate = new Date(0);
69 | expirationDate.setUTCSeconds(exp);
70 |
71 | return expirationDate;
72 | };
73 | }
74 |
75 | export default AuthController;
76 |
--------------------------------------------------------------------------------