From 8e7512b8aacd37b9b4b27e214326e13fe9c77589 Mon Sep 17 00:00:00 2001 From: protob Date: Tue, 8 Oct 2024 19:27:54 +0200 Subject: [PATCH] feat:[TEMPLATE] - Vue + Express - FullStack #96 --- .../Vue(Frontend)+Express(Backend)/README.md | 133 ++++++++++++++++++ .../client/.env.example | 1 + .../client/index.html | 13 ++ .../client/package.json | 29 ++++ .../client/postcss.config.js | 6 + .../client/public/logo.webp | Bin 0 -> 26460 bytes .../client/src/App.vue | 21 +++ .../client/src/api.js | 23 +++ .../client/src/components/Cards.vue | 42 ++++++ .../src/components/CardsListingHover.vue | 83 +++++++++++ .../client/src/components/Footer.vue | 13 ++ .../client/src/components/Header.vue | 54 +++++++ .../client/src/composables/useForm.js | 75 ++++++++++ .../client/src/index.css | 3 + .../client/src/main.js | 21 +++ .../client/src/router/index.js | 59 ++++++++ .../client/src/stores/auth.js | 46 ++++++ .../client/src/views/Account.vue | 40 ++++++ .../client/src/views/Home.vue | 10 ++ .../client/src/views/Signin.vue | 87 ++++++++++++ .../client/src/views/Signup.vue | 101 +++++++++++++ .../client/tailwind.config.js | 11 ++ .../client/vite.config.js | 17 +++ .../server/.env.example | 3 + .../server/controllers/auth.controller.js | 60 ++++++++ .../server/controllers/user.controller.js | 26 ++++ .../server/index.js | 53 +++++++ .../server/middleware/auth.middleware.js | 18 +++ .../server/models/user.js | 22 +++ .../server/package.json | 24 ++++ .../server/routes/auth.route.js | 9 ++ .../server/routes/user.route.js | 11 ++ .../server/utils/error.js | 6 + .../2.Vue(Frontend)+Express(Backend).md | 35 +++++ 34 files changed, 1155 insertions(+) create mode 100755 template/FullStack/Vue(Frontend)+Express(Backend)/README.md create mode 100644 template/FullStack/Vue(Frontend)+Express(Backend)/client/.env.example create mode 100644 template/FullStack/Vue(Frontend)+Express(Backend)/client/index.html create mode 100755 template/FullStack/Vue(Frontend)+Express(Backend)/client/package.json create mode 100755 template/FullStack/Vue(Frontend)+Express(Backend)/client/postcss.config.js create mode 100755 template/FullStack/Vue(Frontend)+Express(Backend)/client/public/logo.webp create mode 100755 template/FullStack/Vue(Frontend)+Express(Backend)/client/src/App.vue create mode 100644 template/FullStack/Vue(Frontend)+Express(Backend)/client/src/api.js create mode 100755 template/FullStack/Vue(Frontend)+Express(Backend)/client/src/components/Cards.vue create mode 100644 template/FullStack/Vue(Frontend)+Express(Backend)/client/src/components/CardsListingHover.vue create mode 100755 template/FullStack/Vue(Frontend)+Express(Backend)/client/src/components/Footer.vue create mode 100755 template/FullStack/Vue(Frontend)+Express(Backend)/client/src/components/Header.vue create mode 100644 template/FullStack/Vue(Frontend)+Express(Backend)/client/src/composables/useForm.js create mode 100755 template/FullStack/Vue(Frontend)+Express(Backend)/client/src/index.css create mode 100755 template/FullStack/Vue(Frontend)+Express(Backend)/client/src/main.js create mode 100755 template/FullStack/Vue(Frontend)+Express(Backend)/client/src/router/index.js create mode 100755 template/FullStack/Vue(Frontend)+Express(Backend)/client/src/stores/auth.js create mode 100644 template/FullStack/Vue(Frontend)+Express(Backend)/client/src/views/Account.vue create mode 100755 template/FullStack/Vue(Frontend)+Express(Backend)/client/src/views/Home.vue create mode 100755 template/FullStack/Vue(Frontend)+Express(Backend)/client/src/views/Signin.vue create mode 100755 template/FullStack/Vue(Frontend)+Express(Backend)/client/src/views/Signup.vue create mode 100755 template/FullStack/Vue(Frontend)+Express(Backend)/client/tailwind.config.js create mode 100755 template/FullStack/Vue(Frontend)+Express(Backend)/client/vite.config.js create mode 100755 template/FullStack/Vue(Frontend)+Express(Backend)/server/.env.example create mode 100755 template/FullStack/Vue(Frontend)+Express(Backend)/server/controllers/auth.controller.js create mode 100755 template/FullStack/Vue(Frontend)+Express(Backend)/server/controllers/user.controller.js create mode 100755 template/FullStack/Vue(Frontend)+Express(Backend)/server/index.js create mode 100644 template/FullStack/Vue(Frontend)+Express(Backend)/server/middleware/auth.middleware.js create mode 100755 template/FullStack/Vue(Frontend)+Express(Backend)/server/models/user.js create mode 100755 template/FullStack/Vue(Frontend)+Express(Backend)/server/package.json create mode 100755 template/FullStack/Vue(Frontend)+Express(Backend)/server/routes/auth.route.js create mode 100755 template/FullStack/Vue(Frontend)+Express(Backend)/server/routes/user.route.js create mode 100755 template/FullStack/Vue(Frontend)+Express(Backend)/server/utils/error.js create mode 100644 website/content/Templates/FullStack/2.Vue(Frontend)+Express(Backend).md diff --git a/template/FullStack/Vue(Frontend)+Express(Backend)/README.md b/template/FullStack/Vue(Frontend)+Express(Backend)/README.md new file mode 100755 index 0000000..b2d2a07 --- /dev/null +++ b/template/FullStack/Vue(Frontend)+Express(Backend)/README.md @@ -0,0 +1,133 @@ +# FullStack Template: Vue.js + Express.js + +This repository provides a simple FullStack template for building modern web applications using **Vue.js** for the frontend and **Express.js** for the backend. It includes essential features such as user authentication, session management, and an user interface. + +## Features + +- **User Authentication**: Supports user registration and login with session management using JWT. +- **Frontend**: Built with Vue.js, offering a dynamic and responsive user interface. +- **Backend**: Powered by Express.js with Node.js, providing a robust RESTful API. +- **Session Management**: Utilizes JSON Web Tokens (JWT) to manage user authentication. + +## Technologies Used + +### Frontend + +- **Vue.js 3**: A progressive JavaScript framework for building user interfaces, using the Composition API. +- **Vue Router**: Handles navigation and routing in the application. +- **Pinia**: State management library for Vue.js applications. +- **Tailwind CSS**: Utility-first CSS framework for rapid UI development. +- **Vue Toastification**: For displaying toast notifications. +- **@vueuse/motion**: For improved animation experience. + +### Backend + +- **Node.js**: A JavaScript runtime built on Chrome's V8 JavaScript engine. +- **Express.js**: A minimal and flexible Node.js web application framework. +- **MongoDB**: A NoSQL database known for its flexibility and scalability. +- **Mongoose**: An elegant MongoDB object modeling for Node.js. + +## Installation + +### Prerequisites + +- **Node.js**: Ensure you have Node.js installed. You can download it from [here](https://nodejs.org/). +- **MongoDB**: Make sure MongoDB is installed and running locally or accessible remotely. + +### Steps + +1. **Populate the `.env.example` File**: + + ```bash + # server/.env + MONGO_URL= + JWT_SECRET= + PORT=3000 + ``` + +2. **Install the Required Dependencies**: + + ```bash + cd client && npm install + cd server && npm install + ``` + +3. **Ensure MongoDB is Running**: + - Make sure your MongoDB server is running locally or accessible remotely. + +4. **Run the Application**: + + ```bash + # both client and server + npm run dev + ``` + +5. **Access the Application**: + + Visit `http://localhost:5173/` in your web browser. + +## Routes and Functionalities + +- **`/api/auth/signup` [POST]**: + - Handles user registration. + - **Request Body**: + - `username`: String + - `emailid`: String + - `password`: String + + +- **`/api/auth/signin` [POST]**: + - **Description**: Handles user login. + - **Request Body**: + - `emailid`: String + - `password`: String + +- **`/api/user/signout` [POST]**: + - Logs the user out by clearing the authentication cookie. +- **`/api/user/test` [GET]**: + - **Description**: Test route to verify API is working. + +- **`/api/user/profile` [GET]**: + - **Description**: Retrieves the user profile information of the authenticated user. + - **Response**: + - **Success**: Returns user profile details such as `id`, `username`, and `emailid`. + - **Error**: Returns an error if the user is not authenticated. + + + +### Frontend Routes + +- **`/` [GET]**: + - **Description**: Renders the homepage of the application. + +- **`/signin` [GET]**: + - Renders the login page where users can log in with their credentials. + +- **`/signup` [GET]**: + - Renders the registration page where new users can sign up. + +## Flash Messages + +The application uses flash messages to communicate the following events to the user: + +- **Signup**: + - **Success**: "Signup successful! Please sign in." + - **Error**: "Username or Email already exists. Please choose a different one." + +- **Signin**: + - **Error**: "Invalid email or password. Please try again." + +These messages are displayed on the frontend in the registration and login pages using toast notifications. + +## Database + +The application uses **MongoDB** for storing user information. The `users` collection in the MongoDB database contains the following fields for each user: + +- **_id**: `ObjectId`, the unique identifier for each document (user). +- **username**: `String`, unique, cannot be null. +- **emailid**: `String`, unique, cannot be null. +- **password**: `String`, hashed, cannot be null. + +--- + +Made using [Universal-Box](https://github.com/Abhishek-Mallick/universal-box) \ No newline at end of file diff --git a/template/FullStack/Vue(Frontend)+Express(Backend)/client/.env.example b/template/FullStack/Vue(Frontend)+Express(Backend)/client/.env.example new file mode 100644 index 0000000..41e024d --- /dev/null +++ b/template/FullStack/Vue(Frontend)+Express(Backend)/client/.env.example @@ -0,0 +1 @@ +VITE_BASE_URL=http://localhost:PORT \ No newline at end of file diff --git a/template/FullStack/Vue(Frontend)+Express(Backend)/client/index.html b/template/FullStack/Vue(Frontend)+Express(Backend)/client/index.html new file mode 100644 index 0000000..8388c4b --- /dev/null +++ b/template/FullStack/Vue(Frontend)+Express(Backend)/client/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Vue + + +
+ + + diff --git a/template/FullStack/Vue(Frontend)+Express(Backend)/client/package.json b/template/FullStack/Vue(Frontend)+Express(Backend)/client/package.json new file mode 100755 index 0000000..3605699 --- /dev/null +++ b/template/FullStack/Vue(Frontend)+Express(Backend)/client/package.json @@ -0,0 +1,29 @@ +{ + "name": "vue-frontend", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "lint": "eslint ." + }, + "dependencies": { + "@vueuse/motion": "^2.2.5", + "axios": "^1.4.0", + "pinia": "^2.1.0", + "pinia-plugin-persistedstate": "^4.1.1", + "vue": "^3.3.4", + "vue-router": "^4.2.3", + "vue-toastification": "next" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.1.4", + "vite": "^5.4.8", + "autoprefixer": "^10.4.7", + "eslint": "^8.28.0", + "postcss": "^8.4.14", + "tailwindcss": "^3.4.10" + } +} \ No newline at end of file diff --git a/template/FullStack/Vue(Frontend)+Express(Backend)/client/postcss.config.js b/template/FullStack/Vue(Frontend)+Express(Backend)/client/postcss.config.js new file mode 100755 index 0000000..88a3558 --- /dev/null +++ b/template/FullStack/Vue(Frontend)+Express(Backend)/client/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, + } \ No newline at end of file diff --git a/template/FullStack/Vue(Frontend)+Express(Backend)/client/public/logo.webp b/template/FullStack/Vue(Frontend)+Express(Backend)/client/public/logo.webp new file mode 100755 index 0000000000000000000000000000000000000000..ffc4377c81b320a8a0e343e87acb3e51143dc758 GIT binary patch literal 26460 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 literal 0 HcmV?d00001 diff --git a/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/App.vue b/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/App.vue new file mode 100755 index 0000000..d14b852 --- /dev/null +++ b/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/App.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/api.js b/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/api.js new file mode 100644 index 0000000..9865a41 --- /dev/null +++ b/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/api.js @@ -0,0 +1,23 @@ +import axios from 'axios' +const baseURL = import.meta.env.VITE_BASE_URL || 'http://localhost:3000' + +const api = axios.create({ + baseURL, + withCredentials: true, // Ensure credentials/cookies are sent with cross-origin requests +}) + +// Set up a response interceptor to handle errors globally +api.interceptors.response.use( + (response) => response, // return the response if no errors + + (error) => { + // Check and reject 401 (Unauthorized) error + if (error.response && error.response.status === 401) { + return Promise.reject(error) + } + console.error('API Error:', error) + return Promise.reject(error) + } +) + +export default api \ No newline at end of file diff --git a/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/components/Cards.vue b/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/components/Cards.vue new file mode 100755 index 0000000..e2ca6c2 --- /dev/null +++ b/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/components/Cards.vue @@ -0,0 +1,42 @@ + + + \ No newline at end of file diff --git a/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/components/CardsListingHover.vue b/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/components/CardsListingHover.vue new file mode 100644 index 0000000..2c76701 --- /dev/null +++ b/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/components/CardsListingHover.vue @@ -0,0 +1,83 @@ + + + \ No newline at end of file diff --git a/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/components/Footer.vue b/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/components/Footer.vue new file mode 100755 index 0000000..92e68c2 --- /dev/null +++ b/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/components/Footer.vue @@ -0,0 +1,13 @@ + + + \ No newline at end of file diff --git a/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/components/Header.vue b/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/components/Header.vue new file mode 100755 index 0000000..36e9a93 --- /dev/null +++ b/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/components/Header.vue @@ -0,0 +1,54 @@ + + + diff --git a/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/composables/useForm.js b/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/composables/useForm.js new file mode 100644 index 0000000..8b9d372 --- /dev/null +++ b/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/composables/useForm.js @@ -0,0 +1,75 @@ +import { reactive, ref } from 'vue' +import { useToast } from 'vue-toastification' + +// regex to check if an email is in valid format +const validEmail = (email) => { + const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + return re.test(email) +} + +// Composable function to manage form state, validation, and submission +export const useForm = (initialState, validations, submitAction) => { + + const formData = reactive({ ...initialState }) + const errors = reactive({}) + const serverError = ref('') + const successMessage = ref('') + const loading = ref(false) + const toast = useToast() + + // Validates the form fields using the provided validation rules + const validateForm = () => { + let isValid = true + Object.keys(validations).forEach(field => { + const error = validations[field](formData[field]) + if (error) { + errors[field] = error + isValid = false + } else { + errors[field] = '' + } + }) + return isValid + } + + // Handles form submission logic: validation, API requests, and error/success + const handleSubmit = async () => { + serverError.value = '' + successMessage.value = '' + + if (!validateForm()) return + + try { + loading.value = true + const message = await submitAction(formData) + loading.value = false + successMessage.value = message + toast.success(message) + if (handleSubmit.onSuccess) handleSubmit.onSuccess() + } catch (error) { + loading.value = false + serverError.value = error.response?.data?.message || error.message || 'An error occurred' + toast.error(serverError.value) + } + } + + return { + formData, + errors, + serverError, + successMessage, + loading, + handleSubmit, + validateForm, + } +} + +// Common validation helper +export const commonValidations = { + required: (fieldName) => (value) => + value ? '' : `${fieldName} is required`, + minLength: (fieldName, minLength) => (value) => + value.length >= minLength ? '' : `${fieldName} must be at least ${minLength} characters`, + email: (value) => + validEmail(value) ? '' : 'Invalid email format', +} \ No newline at end of file diff --git a/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/index.css b/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/index.css new file mode 100755 index 0000000..bd6213e --- /dev/null +++ b/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/index.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/main.js b/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/main.js new file mode 100755 index 0000000..44e1ef5 --- /dev/null +++ b/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/main.js @@ -0,0 +1,21 @@ +import { createApp } from 'vue' +import App from './App.vue' +import router from './router' +import { createPinia } from 'pinia' +import { MotionPlugin } from '@vueuse/motion' +import Toast from 'vue-toastification' +import 'vue-toastification/dist/index.css' +import './index.css' +import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' + +const app = createApp(App) + +const pinia = createPinia() +pinia.use(piniaPluginPersistedstate) + +app.use(MotionPlugin) +app.use(pinia) +app.use(router) +app.use(Toast) + +app.mount('#app') diff --git a/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/router/index.js b/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/router/index.js new file mode 100755 index 0000000..9edad45 --- /dev/null +++ b/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/router/index.js @@ -0,0 +1,59 @@ +import { createRouter, createWebHistory } from 'vue-router' +import Home from '../views/Home.vue' +import Signin from '../views/Signin.vue' +import Signup from '../views/Signup.vue' +import Account from '../views/Account.vue' +import { useAuthStore } from '../stores/auth' + +// routes with route guards +const routes = [ + { + path: '/', + name: 'Home', + component: Home + }, + { + path: '/signin', + name: 'Signin', + component: Signin, + meta: { requiresGuest: true } // Only for non-authenticated users + }, + { + path: '/signup', + name: 'Signup', + component: Signup, + meta: { requiresGuest: true } // Only for non-authenticated users + }, + { + path: '/account', + name: 'Account', + component: Account, + meta: { requiresAuth: true } // Only for authenticated users + } +] + +const router = createRouter({ + history: createWebHistory(), + routes +}) + +// Route guard to check authentication before navigating +router.beforeEach(async (to, from, next) => { + const authStore = useAuthStore() + +// Fetch user data if not already available + if (authStore.user === null) { + await authStore.fetchUser() + } + + // Redirect based on authentication and route meta properties + if (to.meta.requiresAuth && !authStore.isAuthenticated) { + next('/signin') + } else if (to.meta.requiresGuest && authStore.isAuthenticated) { + next('/account') + } else { + next() + } +}) + +export default router \ No newline at end of file diff --git a/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/stores/auth.js b/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/stores/auth.js new file mode 100755 index 0000000..7ae96a5 --- /dev/null +++ b/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/stores/auth.js @@ -0,0 +1,46 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import api from '../api.js' + +// Pinia setup store +export const useAuthStore = defineStore('auth', () => { + const user = ref(null) + const isAuthenticated = computed(() => !!user.value) + const getUser = computed(() => user.value) + + // user sign-in with server API request + const signin = async (credentials) => { + try { + const response = await api.post('/api/auth/signin', credentials) + user.value = response.data.user + } catch (error) { + throw error + } + } + + // user sign-out + const signout = async () => { + await api.post('/api/user/signout') + user.value = null + } + + // fetch the current user profile from the server + const fetchUser = async () => { + try { + const response = await api.get('/api/user/profile') + user.value = response.data.user + } catch (error) { + user.value = null + } + } + return { + user, + isAuthenticated, + getUser, + signin, + signout, + fetchUser, + } +}, { + persist: true +}) \ No newline at end of file diff --git a/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/views/Account.vue b/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/views/Account.vue new file mode 100644 index 0000000..80bb3e5 --- /dev/null +++ b/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/views/Account.vue @@ -0,0 +1,40 @@ + + + \ No newline at end of file diff --git a/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/views/Home.vue b/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/views/Home.vue new file mode 100755 index 0000000..3fdf3ec --- /dev/null +++ b/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/views/Home.vue @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/views/Signin.vue b/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/views/Signin.vue new file mode 100755 index 0000000..a7ff27b --- /dev/null +++ b/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/views/Signin.vue @@ -0,0 +1,87 @@ + + + \ No newline at end of file diff --git a/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/views/Signup.vue b/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/views/Signup.vue new file mode 100755 index 0000000..01ccf5f --- /dev/null +++ b/template/FullStack/Vue(Frontend)+Express(Backend)/client/src/views/Signup.vue @@ -0,0 +1,101 @@ + + + \ No newline at end of file diff --git a/template/FullStack/Vue(Frontend)+Express(Backend)/client/tailwind.config.js b/template/FullStack/Vue(Frontend)+Express(Backend)/client/tailwind.config.js new file mode 100755 index 0000000..12f8da6 --- /dev/null +++ b/template/FullStack/Vue(Frontend)+Express(Backend)/client/tailwind.config.js @@ -0,0 +1,11 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "./index.html", + "./src/**/*.{vue,js,ts,jsx,tsx}", + ], + theme: { + extend: {}, + }, + plugins: [], +} \ No newline at end of file diff --git a/template/FullStack/Vue(Frontend)+Express(Backend)/client/vite.config.js b/template/FullStack/Vue(Frontend)+Express(Backend)/client/vite.config.js new file mode 100755 index 0000000..39e531d --- /dev/null +++ b/template/FullStack/Vue(Frontend)+Express(Backend)/client/vite.config.js @@ -0,0 +1,17 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import tailwindcss from 'tailwindcss' +import autoprefixer from 'autoprefixer' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue()], + css: { + postcss: { + plugins: [ + tailwindcss(), + autoprefixer(), + ], + }, + }, +}) \ No newline at end of file diff --git a/template/FullStack/Vue(Frontend)+Express(Backend)/server/.env.example b/template/FullStack/Vue(Frontend)+Express(Backend)/server/.env.example new file mode 100755 index 0000000..1b19f12 --- /dev/null +++ b/template/FullStack/Vue(Frontend)+Express(Backend)/server/.env.example @@ -0,0 +1,3 @@ +MONGO_URL=mongodb://admin:password@address:port/ +JWT_SECRET=password +PORT=3000 \ No newline at end of file diff --git a/template/FullStack/Vue(Frontend)+Express(Backend)/server/controllers/auth.controller.js b/template/FullStack/Vue(Frontend)+Express(Backend)/server/controllers/auth.controller.js new file mode 100755 index 0000000..3bed482 --- /dev/null +++ b/template/FullStack/Vue(Frontend)+Express(Backend)/server/controllers/auth.controller.js @@ -0,0 +1,60 @@ +import User from "../models/user.js" +import bcryptjs from "bcryptjs" +import { errorHandler } from "../utils/error.js" +import jwt from 'jsonwebtoken' + +export const signup = async (req, res, next) => { + const { username, emailid, password } = req.body + + if (!username || !emailid || !password || username === "" || emailid === "" || password === "") { + return next(errorHandler(400, "All fields are required!!")) + } + + const hashedPassword = bcryptjs.hashSync(password, 10) + + const newUser = new User({ + username, + emailid, + password: hashedPassword, + }) + + try { + await newUser.save() + res.json({ success: true, message: "SignUp successful!!" }) + } catch (error) { + if (error.code === 11000) { + return next(errorHandler(400, "Username or Email already exists.")) + } + next(error) + } +} + +export const signin = async (req, res, next) => { + const { emailid, password } = req.body + + if (!emailid || !password || emailid === '' || password === '') { + return next(errorHandler(400, 'All fields are required')) + } + try { + const user = await User.findOne({ emailid }) + if (!user) { + return next(errorHandler(400, 'Invalid email or password. Please try again.')) + } + const validPassword = bcryptjs.compareSync(password, user.password) + if (!validPassword) { + return next(errorHandler(400, 'Invalid email or password. Please try again.')) + } + const token = jwt.sign( + { id: user._id }, process.env.JWT_SECRET, { expiresIn: '1d' } + ) + + res.cookie('access_token', token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + maxAge: 24 * 60 * 60 * 1000 // 1 day + }).json({ success: true, user: { id: user._id, username: user.username, emailid: user.emailid } }) + } catch (error) { + next(error) + } +} diff --git a/template/FullStack/Vue(Frontend)+Express(Backend)/server/controllers/user.controller.js b/template/FullStack/Vue(Frontend)+Express(Backend)/server/controllers/user.controller.js new file mode 100755 index 0000000..ebba7f2 --- /dev/null +++ b/template/FullStack/Vue(Frontend)+Express(Backend)/server/controllers/user.controller.js @@ -0,0 +1,26 @@ +import User from '../models/user.js' +import { errorHandler } from '../utils/error.js' + +export const test = (req, res) => { + res.json({ message: 'API is working!' }) +} + +export const signout = (req, res, next) => { + try { + res.clearCookie('access_token').status(200).json({ success: true, message: 'User has been signed out!!' }) + } catch (error) { + next(error) + } +} + +export const getProfile = async (req, res, next) => { + try { + const user = await User.findById(req.userId).select('-password') + if (!user) { + return next(errorHandler(404, 'User not found')) + } + res.json({ success: true, user }) + } catch (error) { + next(error) + } +} \ No newline at end of file diff --git a/template/FullStack/Vue(Frontend)+Express(Backend)/server/index.js b/template/FullStack/Vue(Frontend)+Express(Backend)/server/index.js new file mode 100755 index 0000000..e127ccd --- /dev/null +++ b/template/FullStack/Vue(Frontend)+Express(Backend)/server/index.js @@ -0,0 +1,53 @@ +import express from 'express' +import mongoose from 'mongoose' +import dotenv from 'dotenv' +import cors from 'cors' +import cookieParser from 'cookie-parser' +import userRoutes from './routes/user.route.js' +import authRoutes from './routes/auth.route.js' + +dotenv.config() + +if (!process.env.MONGO_URL) { + console.error('Error: MONGO_URL is not defined in .env file.') + process.exit(1) +} + +const app = express() +const PORT = process.env.PORT || 3000 + +const corsOptions = { + origin: 'http://localhost:5173', + methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', + credentials: true, + optionsSuccessStatus: 204 +} + +app.use(cors(corsOptions)) +app.use(cookieParser()) +app.use(express.json()) + +mongoose.connect(process.env.MONGO_URL) + .then(() => { + console.log("MongoDB is connected") + }) + .catch((err) => { + console.log(err) + }) + +app.use('/api/user', userRoutes) +app.use('/api/auth', authRoutes) + +app.use((err, req, res, next) => { + const statusCode = err.statusCode || 500 + const message = err.message || 'Internal Server Error' + res.status(statusCode).json({ + success: false, + statusCode, + message + }) +}) + +app.listen(PORT, () => { + console.log(`Server is running at port ${PORT}!`) +}) \ No newline at end of file diff --git a/template/FullStack/Vue(Frontend)+Express(Backend)/server/middleware/auth.middleware.js b/template/FullStack/Vue(Frontend)+Express(Backend)/server/middleware/auth.middleware.js new file mode 100644 index 0000000..4df4fbc --- /dev/null +++ b/template/FullStack/Vue(Frontend)+Express(Backend)/server/middleware/auth.middleware.js @@ -0,0 +1,18 @@ +import jwt from 'jsonwebtoken' +import { errorHandler } from '../utils/error.js' + +export const verifyToken = (req, res, next) => { + const token = req.cookies.access_token + + if (!token) { + return next(errorHandler(401, 'Unauthorized')) + } + + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET) + req.userId = decoded.id + next() + } catch (error) { + next(errorHandler(401, 'Invalid Token')) + } +} \ No newline at end of file diff --git a/template/FullStack/Vue(Frontend)+Express(Backend)/server/models/user.js b/template/FullStack/Vue(Frontend)+Express(Backend)/server/models/user.js new file mode 100755 index 0000000..abcd8bb --- /dev/null +++ b/template/FullStack/Vue(Frontend)+Express(Backend)/server/models/user.js @@ -0,0 +1,22 @@ +import mongoose from 'mongoose' + +const userSchema = new mongoose.Schema({ + username: { + type: String, + required: true, + unique: true + }, + emailid: { + type: String, + required: true, + unique: true + }, + password: { + type: String, + required: true, + }, +}, { timestamps: true }) + +const User = mongoose.model('User', userSchema) + +export default User \ No newline at end of file diff --git a/template/FullStack/Vue(Frontend)+Express(Backend)/server/package.json b/template/FullStack/Vue(Frontend)+Express(Backend)/server/package.json new file mode 100755 index 0000000..1ea5281 --- /dev/null +++ b/template/FullStack/Vue(Frontend)+Express(Backend)/server/package.json @@ -0,0 +1,24 @@ +{ + "name": "express-backend", + "version": "1.0.0", + "type": "module", + "main": "index.js", + "scripts": { + "start": "node index.js", + "dev": "nodemon index.js" + }, + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "bcryptjs": "^2.4.3", + "cookie-parser": "^1.4.6", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.20.0", + "jsonwebtoken": "^9.0.2", + "mongoose": "^8.5.3", + "nodemon": "^3.1.4", + "path": "^0.12.7" + } +} \ No newline at end of file diff --git a/template/FullStack/Vue(Frontend)+Express(Backend)/server/routes/auth.route.js b/template/FullStack/Vue(Frontend)+Express(Backend)/server/routes/auth.route.js new file mode 100755 index 0000000..4ab7d7e --- /dev/null +++ b/template/FullStack/Vue(Frontend)+Express(Backend)/server/routes/auth.route.js @@ -0,0 +1,9 @@ +import express from "express" +import { signup, signin } from "../controllers/auth.controller.js" + +const router = express.Router() + +router.post('/signup', signup) +router.post('/signin', signin) + +export default router \ No newline at end of file diff --git a/template/FullStack/Vue(Frontend)+Express(Backend)/server/routes/user.route.js b/template/FullStack/Vue(Frontend)+Express(Backend)/server/routes/user.route.js new file mode 100755 index 0000000..243bcc2 --- /dev/null +++ b/template/FullStack/Vue(Frontend)+Express(Backend)/server/routes/user.route.js @@ -0,0 +1,11 @@ +import express from "express" +import { signout, test, getProfile } from "../controllers/user.controller.js" +import { verifyToken } from '../middleware/auth.middleware.js' + +const router = express.Router() + +router.get("/test", test) +router.post("/signout", signout) +router.get("/profile", verifyToken, getProfile) + +export default router \ No newline at end of file diff --git a/template/FullStack/Vue(Frontend)+Express(Backend)/server/utils/error.js b/template/FullStack/Vue(Frontend)+Express(Backend)/server/utils/error.js new file mode 100755 index 0000000..85a97b7 --- /dev/null +++ b/template/FullStack/Vue(Frontend)+Express(Backend)/server/utils/error.js @@ -0,0 +1,6 @@ +export const errorHandler = (statusCode, message) => { + const error = new Error() + error.statusCode = statusCode + error.message = message + return error + } \ No newline at end of file diff --git a/website/content/Templates/FullStack/2.Vue(Frontend)+Express(Backend).md b/website/content/Templates/FullStack/2.Vue(Frontend)+Express(Backend).md new file mode 100644 index 0000000..9ee0cc7 --- /dev/null +++ b/website/content/Templates/FullStack/2.Vue(Frontend)+Express(Backend).md @@ -0,0 +1,35 @@ +# FullStack Template: Vue.js + Express.js + +## Introduction + +The **FullStack Template: Vue.js + Express.js** is a starter template for building web applications using **Vue.js** for the frontend and **Express.js** for the backend. It supports user authentication using JSON Web Tokens (JWT), session management. Vue.js and Tailwind.css is used to build responsive user interfaces. + +## Features + +- **User Authentication**: Users can sign up, log in, and log out. Passwords are hashed using **bcryptjs**, and **JWT** is used for session management. +- **JWT-Based Session Management**: JWT tokens are generated during login and stored in **HTTP-only cookies**. +- **Frontend**: Built with **Vue.js**, **Pinia** for state management, and **Tailwind CSS** for styling. +- **Backend**: The backend is built with **Express.js** and connects to a **MongoDB** database through **Mongoose**. It defines several API routes for user actions like signing up, logging in, and logging out. + + +## Technologies Used + +### Frontend + +- **Vue.js**: Framework for building the frontend UI. +- **Tailwind CSS**: Utility-based CSS framework for styling and responsive design. +- **Vue Router**: Handles page navigation and routing. +- **Pinia**: Manages application state. +- **Vue Toastification**: Displays toast notifications. + +### Backend + +- **Node.js**: The runtime environment used to run the backend JavaScript code. +- **Express.js**: Handles the API requests and routing for the backend. +- **MongoDB**: NoSQL database for storing user data. +- **Mongoose**: used to model data and interact with MongoDB from the backend. +- **JWT (jsonwebtoken)**: Used to create and verify JWTs for user authentication. +- **bcryptjs**: Used for hashing user passwords securely. + + +Visit codebase [here](https://github.com/Abhishek-Mallick/universal-box/tree/main/template/FullStack/Vue(Frontend)+Express(Backend)) \ No newline at end of file