diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml deleted file mode 100644 index 2a2725e2..00000000 --- a/.github/workflows/cd.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Continuous Deployment - -on: - push: - branches: - - main - -jobs: - build-vue: - name: Build Vue.js app - runs-on: ubuntu-latest - - defaults: - run: - working-directory: ./client - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version: 14 - - name: Install packages - run: yarn - - name: Build Vue.js app - run: yarn run build - - name: Upload build artifacts - uses: actions/upload-artifact@v2 - with: - name: vue-build - path: ${GITHUB_WORKSPACE}/public - - name: Deploy to GitHub Pages - uses: actions/github-script@v5 - with: - folder: ${GITHUB_WORKSPACE}/public diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7935b478..0ebda4d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,15 +20,11 @@ on: - develop jobs: - code-style: + code-style-express: name: Run style checks for Express server runs-on: ${{ matrix.os }} -# defaults: -# run: -# working-directory: ./ - strategy: matrix: os: [ ubuntu-latest ] @@ -94,10 +90,6 @@ jobs: runs-on: ${{ matrix.os }} -# defaults: -# run: -# working-directory: ./ - strategy: matrix: os: [ ubuntu-latest ] @@ -134,3 +126,26 @@ jobs: run: yarn run start:test env: MONGODB_URI: mongodb://localhost:27017/handshake + + build-vue: + name: Build Vue.js app + runs-on: ubuntu-latest + + defaults: + run: + working-directory: ./client + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: 14 + + - name: Install packages + run: yarn + + - name: Build Vue.js app + run: yarn run build \ No newline at end of file diff --git a/.gitignore b/.gitignore index e90af7f5..0b6926b0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ # Express public folder is automatically generated by Vue /public +/media/*/* +!/media/*/.gitkeep /bin/act diff --git a/app.js b/app.js index b45fdcfe..b96594c1 100644 --- a/app.js +++ b/app.js @@ -2,7 +2,6 @@ const express = require('express'); const path = require('path'); const logger = require('morgan'); -const multer = require('multer'); const cookieParser = require('cookie-parser'); const { authenticate } = require('./middlewares/authentication.middleware'); @@ -13,22 +12,25 @@ const app = express(); app.use(logger('dev')); app.use(express.urlencoded({ extended: false })); // parse application/x-www.js-form-urlencoded app.use(express.json({ limit: '4MB' })); // parse application/json -app.use(multer().none()); // parse multipart/form-data app.use(cookieParser()); app.use( express.static(path.join(__dirname, 'public'), { index: 'index.html' }) ); +app.use('/media', express.static(path.join(__dirname, 'media'))); app.set('view engine', 'html'); // TODO - controllers app.use('/auth', require('./routes/auth')); app.use('/api', authenticate, require('./routes/chat')); +app.use('/upload', authenticate, require('./routes/upload')); // serve Vue app if no matching route is found app.get('*', (req, res) => { res.sendFile(path.join(__dirname, 'public/index.html')); }); +app.locals.onlineUsers = new Set(); + module.exports = app; diff --git a/bin/www.js b/bin/www.js index 3051961a..8ba7b5da 100644 --- a/bin/www.js +++ b/bin/www.js @@ -10,6 +10,6 @@ server.on('listening', function () { console.log('Express server listening on port ' + server.address().port); }); -serverSocket.init(server); +serverSocket.init(server, app.locals.onlineUsers); initDB(); server.listen(app.get('port')); diff --git a/client/.prettierrc b/client/.prettierrc index c485e16e..da47b6c9 100644 --- a/client/.prettierrc +++ b/client/.prettierrc @@ -6,3 +6,4 @@ useTabs: false semi: true arrowParens: always bracketSpacing: true +bracketSameLine: true diff --git a/client/package.json b/client/package.json index 1f7f90dc..a750ec71 100644 --- a/client/package.json +++ b/client/package.json @@ -11,13 +11,19 @@ "style:prettier:fix": "prettier --write ." }, "dependencies": { + "@lottiefiles/lottie-interactivity": "^1.6.1", + "@lottiefiles/lottie-player": "^1.6.2", "axios": "^1.2.0", "core-js": "^3.8.3", + "localforage": "^1.10.0", "socket.io-client": "^4.5.4", "vue": "^2.6.14", "vue-axios": "^3.5.2", + "vue-custom-scrollbar": "^1.4.4", "vue-router": "^3.5.1", - "vuex": "^3.6.2" + "vuetify": "^2.6.0", + "vuex": "^3.6.2", + "vuex-persist": "^3.1.3" }, "devDependencies": { "@babel/core": "^7.12.16", @@ -34,6 +40,10 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^5.1.0", "eslint-plugin-vue": "^8.0.3", - "vue-template-compiler": "^2.6.14" + "sass": "~1.32.0", + "sass-loader": "^10.0.0", + "vue-cli-plugin-vuetify": "~2.5.8", + "vue-template-compiler": "^2.6.14", + "vuetify-loader": "^1.7.0" } } diff --git a/client/public/about.html b/client/public/about.html deleted file mode 100644 index 63d92649..00000000 --- a/client/public/about.html +++ /dev/null @@ -1,276 +0,0 @@ - - - - HandShake - - - - - - - - - - - - - - - - - - - - -
-

HandShake

-

Log in

-
-
-
-

Hand
Shake

-

Connecting the world, one Shake at a time.

-
-
-

- HandShake is a brand-new messaging service for everyone who is tired - of the usual old chat apps.
Scroll down to learn more about us. -

-
- -
-
- - - - -
- -
- - - - -
- -
-

- The messaging app for creative people, for fun people, for people who - love sharing pictures of their pets. -

-
- -
- - - - -
-
-

- Connect with people around the world and start messaging now! -

-

- With HandShake, keeping in contact is as easy as ever. -

-

- Text messages, pictures, videos, audio and documents: we got you! -

-

- Share the things you love with your family, friends and even with your - enemies (we hope you don't have any). -

-

- We'll make sure the texts reach them, and that no one else will ever - access them. -

-

- Voice calls and video calls will be supported in a future update. -

-
-
-

- A new generation of chat, for a new generation of users. -

-

Texting has never been more fun.

-

- Play games directly in the chat and challenge your friends to see who - the real MVP is. -

-
-
- - - - -
- -
-

made with β™‘ at USI by yours truly

-
-
- - - diff --git a/client/public/css/utils.css b/client/public/css/utils.css new file mode 100644 index 00000000..8ba371a6 --- /dev/null +++ b/client/public/css/utils.css @@ -0,0 +1,32 @@ +.h-100 { + height: 100%; +} + +.w-100 { + width: 100% !important; +} + +.gap-1 { + gap: 4px; +} +.gap-2 { + gap: 8px; +} +.gap-3 { + gap: 12px; +} +.gap-4 { + gap: 16px; +} +.gap-5 { + gap: 20px; +} +.gap-6 { + gap: 24px; +} +.gap-7 { + gap: 28px; +} +.gap-8 { + gap: 32px; +} diff --git a/client/public/index.html b/client/public/index.html index dd6758c3..aed68b56 100644 --- a/client/public/index.html +++ b/client/public/index.html @@ -8,26 +8,21 @@ + rel="stylesheet" /> + + + + - - - - - + href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" + rel="stylesheet" /> + diff --git a/client/src/App.vue b/client/src/App.vue index fc36b9d0..dbd0e3b7 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -1,7 +1,152 @@ - + + + + + diff --git a/client/src/assets/vuetifyThemes.js b/client/src/assets/vuetifyThemes.js new file mode 100644 index 00000000..cd2576b6 --- /dev/null +++ b/client/src/assets/vuetifyThemes.js @@ -0,0 +1,59 @@ +export default { + // light: { + // primary: '#1976D2', + // secondary: '#424242', + // accent: '#82B1FF', + // error: '#FF5252', + // info: '#2196F3', + // success: '#4CAF50', + // warning: '#FFC107', + // surface: '#c5c5c5', + // background: '#ffffff', + // textPrimary: '#ffffff', + // textSecondary: '#d9d9d9', + // }, + blue: { + // generate a theme using the following main color:#006cb7 + primary: '#00528c', + secondary: '#0d3f62', + accent: '#82B1FF', + error: '#FF5252', + info: '#2196F3', + success: '#4CAF50', + warning: '#FFC107', + surface: '#162d3d', + background: '#0f1d27', + textPrimary: '#d9e8f8', + textAccent: '#000000', + }, + green: { + // generate a theme using the following main color: #00a996 + primary: '#00806f', + secondary: '#00594a', + accent: '#82B1FF', + error: '#FF5252', + info: '#2196F3', + success: '#4CAF50', + warning: '#FFC107', + surface: '#0f2d2d', + background: '#0f1d1d', + textPrimary: '#e9fffe', + textAccent: '#000000', + }, + orange: { + primary: '#a66321', + secondary: '#663809', + surface: '#412b15', + background: '#271809', + textPrimary: '#ffebd6', + textAccent: '#000000', + }, + pink: { + primary: '#7e2b44', + secondary: '#8d1439', + surface: '#461826', + background: '#3a0e1b', + textPrimary: '#ffe6ee', + textAccent: '#000000', + }, +}; diff --git a/client/src/classes/user.js b/client/src/classes/user.js index b65589d3..396639ee 100644 --- a/client/src/classes/user.js +++ b/client/src/classes/user.js @@ -20,12 +20,23 @@ class User { name = null; email = null; chats = null; + online = false; + typing = false; - constructor({ _id = null, name = null, email = null, chats = null }) { + constructor({ + _id = null, + name = null, + email = null, + chats = null, + online = false, + typing = false, + }) { this._id = _id; this.name = name; this.email = email; this.chats = chats; + this.online = online; + this.typing = typing; } } diff --git a/client/src/components/AppMenu.vue b/client/src/components/AppMenu.vue index e70c0742..ca4f8690 100644 --- a/client/src/components/AppMenu.vue +++ b/client/src/components/AppMenu.vue @@ -1,59 +1,79 @@ diff --git a/client/src/components/AppSettings.vue b/client/src/components/AppSettings.vue new file mode 100644 index 00000000..bd617f46 --- /dev/null +++ b/client/src/components/AppSettings.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/client/src/components/AudioPlayer.vue b/client/src/components/AudioPlayer.vue new file mode 100644 index 00000000..453bd35f --- /dev/null +++ b/client/src/components/AudioPlayer.vue @@ -0,0 +1,156 @@ + + diff --git a/client/src/components/ChatBoard.vue b/client/src/components/ChatBoard.vue index a0fc0ddb..7fadfc5b 100644 --- a/client/src/components/ChatBoard.vue +++ b/client/src/components/ChatBoard.vue @@ -1,52 +1,97 @@ - + diff --git a/client/src/components/ChatContact.vue b/client/src/components/ChatContact.vue index 061b15cd..45e2ab87 100644 --- a/client/src/components/ChatContact.vue +++ b/client/src/components/ChatContact.vue @@ -1,31 +1,34 @@ diff --git a/client/src/components/ChatMessage.vue b/client/src/components/ChatMessage.vue index a9938162..9052a3d6 100644 --- a/client/src/components/ChatMessage.vue +++ b/client/src/components/ChatMessage.vue @@ -1,31 +1,56 @@ diff --git a/client/src/components/FileUploader.vue b/client/src/components/FileUploader.vue new file mode 100644 index 00000000..cf4e0a95 --- /dev/null +++ b/client/src/components/FileUploader.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/client/src/components/UserItem.vue b/client/src/components/UserItem.vue index ae0cbf80..0592b7e4 100644 --- a/client/src/components/UserItem.vue +++ b/client/src/components/UserItem.vue @@ -1,22 +1,20 @@ + + diff --git a/client/src/components/message/ChatMessageFile.vue b/client/src/components/message/ChatMessageFile.vue new file mode 100644 index 00000000..c584fe8e --- /dev/null +++ b/client/src/components/message/ChatMessageFile.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/client/src/components/message/ChatMessageImage.vue b/client/src/components/message/ChatMessageImage.vue new file mode 100644 index 00000000..2e577009 --- /dev/null +++ b/client/src/components/message/ChatMessageImage.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/client/src/components/message/ChatMessageText.vue b/client/src/components/message/ChatMessageText.vue new file mode 100644 index 00000000..e881d42d --- /dev/null +++ b/client/src/components/message/ChatMessageText.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/client/src/components/message/ChatMessageVideo.vue b/client/src/components/message/ChatMessageVideo.vue new file mode 100644 index 00000000..0ba8689f --- /dev/null +++ b/client/src/components/message/ChatMessageVideo.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/client/src/main.js b/client/src/main.js index 4f670a37..d7e0dc34 100644 --- a/client/src/main.js +++ b/client/src/main.js @@ -3,8 +3,9 @@ import VueAxios from 'vue-axios'; import App from './App.vue'; import router from './router'; import store from './store'; -import axios from './plugin/axios'; -import apiClient from './plugin/api-client'; +import axios from './plugins/axios'; +import apiClient from './plugins/api-client'; +import vuetify from './plugins/vuetify'; Vue.use(apiClient, axios); Vue.use(VueAxios, axios); @@ -14,5 +15,6 @@ new Vue({ router, store, axios, + vuetify, render: (h) => h(App), }).$mount('#app'); diff --git a/client/src/plugin/api-client.js b/client/src/plugins/api-client.js similarity index 57% rename from client/src/plugin/api-client.js rename to client/src/plugins/api-client.js index 2830b567..d53c26da 100644 --- a/client/src/plugin/api-client.js +++ b/client/src/plugins/api-client.js @@ -1,3 +1,5 @@ +import store from '@/store'; + class ApiClient { constructor(axiosInstance) { this.axiosInstance = axiosInstance; @@ -5,9 +7,9 @@ class ApiClient { /** * Retrieve the list of chats that match the filter. - * @param username - * @param password - * @returns {*} + * @param username {String} the username + * @param password {String} the password + * @returns {Promise>} The promise with the response */ login(username, password) { return this.axiosInstance.post('/auth/login', { username, password }); @@ -28,6 +30,14 @@ class ApiClient { }); } + /** + * Refresh the access token. + * @returns {Promise>} The promise with the response + */ + refreshToken() { + return this.axiosInstance.post('/auth/refresh'); + } + /** * Retrieve the list of users that match the filter. * @param filter {string} The filter to apply @@ -48,15 +58,37 @@ class ApiClient { /** * Create a message in a chat. - * @param chatID {string} The ID of the chat + * @param chatId {string} The ID of the chat * @param message {Message} The message to create * @returns {Promise>} The promise with the response */ - sendMessage(chatID, message) { - return this.axiosInstance.post(`/api/chats/${chatID}/messages`, { + sendMessage(chatId, message) { + return this.axiosInstance.post(`/api/chats/${chatId}/messages`, { message, }); } + + /** + * Send a file in a chat. + * @param chatId {string} The ID of the chat + * @param file {File} The file to send + * @param type {string} The type of the file: image / video / file + * @returns {Promise>} The promise with the response + */ + async sendFile(chatId, file, type) { + // before sending the file, it's best to refresh the token + // to avoid the token to expire while the file is being uploaded + await store.dispatch('refreshToken'); + const formData = new FormData(); + formData.append(type, file); + formData.append('chatId', chatId); + // console.log(chatId, file, type, formData); + return this.axiosInstance.post(`/upload/${type}`, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + } } export default { diff --git a/client/src/plugin/axios.js b/client/src/plugins/axios.js similarity index 100% rename from client/src/plugin/axios.js rename to client/src/plugins/axios.js diff --git a/client/src/plugins/vuetify.js b/client/src/plugins/vuetify.js new file mode 100644 index 00000000..71386cb1 --- /dev/null +++ b/client/src/plugins/vuetify.js @@ -0,0 +1,13 @@ +import Vue from 'vue'; +import Vuetify from 'vuetify/lib/framework'; + +Vue.use(Vuetify); + +export default new Vuetify({ + theme: { + options: { + customProperties: true, + }, + themes: {}, + }, +}); diff --git a/client/src/router/index.js b/client/src/router/index.js index a8860577..f4266f9c 100644 --- a/client/src/router/index.js +++ b/client/src/router/index.js @@ -8,12 +8,18 @@ const routes = [ { path: '/login', name: 'Login', + meta: { + layout: true, + }, component: () => import(/* webpackChunkName: "login" */ '../views/Login.vue'), }, { path: '/signup', name: 'Signup', + meta: { + layout: true, + }, component: () => import(/* webpackChunkName: "signup" */ '../views/Signup.vue'), }, @@ -27,6 +33,12 @@ const routes = [ component: () => import(/* webpackChunkName: "container" */ '../views/AppContainer.vue'), }, + { + path: '/about', + name: 'AboutUs', + component: () => + import(/* webpackChunkName: "about" */ '../views/AboutUs.vue'), + }, ]; const router = new VueRouter({ @@ -38,11 +50,14 @@ const router = new VueRouter({ /** * Middleware to check if user is authenticated before accessing a route */ -router.beforeEach((to, from, next) => { - if ( - to.matched.some((record) => record.meta.requiresAuth) && - !store.getters.isLoggedIn - ) { +router.beforeEach(async (to, from, next) => { + await store.restored; + const requiresAuth = to.matched.some((record) => record.meta.requiresAuth); + + if (requiresAuth && store.getters.isLoggedIn) { + await store.dispatch('refreshToken'); + } + if (requiresAuth && !store.getters.isLoggedIn) { next('/login'); } else { next(); diff --git a/client/src/store/index.js b/client/src/store/index.js index 268a9407..c3d79514 100644 --- a/client/src/store/index.js +++ b/client/src/store/index.js @@ -1,5 +1,19 @@ import Vue from 'vue'; import Vuex from 'vuex'; +import VuexPersistence from 'vuex-persist'; +import localforage from 'localforage'; +import router from '../router'; + +const vuexLocal = new VuexPersistence({ + storage: localforage, + reducer: (state) => ({ + // Only save the state of the module 'auth' + isLoggedIn: state.isLoggedIn, + user: state.user, + theme: state.theme, + }), + asyncStorage: true, +}); Vue.use(Vuex); @@ -8,11 +22,16 @@ export default new Vuex.Store({ isLoggedIn: false, user: null, socket: null, + activeChat: null, + theme: null, }, getters: { isLoggedIn: (state) => state.isLoggedIn, user: (state) => state.user, socket: (state) => state.socket, + activeChat: (state) => state.activeChat, + isMobile: () => window.innerWidth < 600, + theme: (state) => state.theme, }, mutations: { login(state, { user }) { @@ -22,10 +41,19 @@ export default new Vuex.Store({ logout(state) { state.isLoggedIn = false; state.user = null; + router.push('/login').catch(() => {}); + console.log('logout'); + if (state.socket) state.socket.disconnect(); }, setSocket(state, { socket }) { state.socket = socket; }, + setActiveChat(state, { chat }) { + state.activeChat = chat; + }, + setTheme(state, { theme }) { + state.theme = theme; + }, }, actions: { login({ commit }, { username, password }) { @@ -44,6 +72,14 @@ export default new Vuex.Store({ signup({ commit }, { username, email, password }) { return this._vm.$api.signup(email, username, password); }, + async refreshToken({ commit }) { + try { + await this._vm.$api.refreshToken(); + } catch (error) { + commit('logout'); + } + }, }, modules: {}, + plugins: [vuexLocal.plugin], }); diff --git a/client/src/utils.js b/client/src/utils.js new file mode 100644 index 00000000..dc05814b --- /dev/null +++ b/client/src/utils.js @@ -0,0 +1,10 @@ +export function formatTime(time) { + const datetime = new Date(time); + const hours = datetime.getHours().toString().padStart(2, '0'); + const minutes = datetime.getMinutes().toString().padStart(2, '0'); + return `${hours}:${minutes}`; +} + +export function getFilePath(folder, filename) { + return `/media/${folder}/${filename}`; +} diff --git a/client/src/views/AboutUs.vue b/client/src/views/AboutUs.vue new file mode 100644 index 00000000..9b8991e0 --- /dev/null +++ b/client/src/views/AboutUs.vue @@ -0,0 +1,229 @@ + + + + + diff --git a/client/src/views/AppContainer.vue b/client/src/views/AppContainer.vue index 5153c277..b181c2ca 100644 --- a/client/src/views/AppContainer.vue +++ b/client/src/views/AppContainer.vue @@ -1,16 +1,22 @@ - + diff --git a/client/src/views/Signup.vue b/client/src/views/Signup.vue index 8ec4f72d..b175aa71 100644 --- a/client/src/views/Signup.vue +++ b/client/src/views/Signup.vue @@ -1,67 +1,61 @@