├── client ├── store │ ├── auth │ │ ├── getters.js │ │ ├── mutations.js │ │ ├── index.js │ │ └── actions.js │ ├── flash │ │ ├── index.js │ │ └── mutations.js │ └── index.js ├── components │ ├── Loader.vue │ ├── Flash.vue │ ├── Button.vue │ └── TextInput.vue ├── mixins │ ├── form.js │ ├── flash.js │ └── auth.js ├── pages │ ├── Home.vue │ ├── EmailConfirm.vue │ ├── Main.vue │ ├── ForgotPassword.vue │ ├── ResetPassword.vue │ ├── Login.vue │ └── Register.vue ├── utils │ └── axios.js ├── styles │ └── main.css ├── index.js └── routes.js ├── server ├── mails │ ├── confirm-account │ │ ├── confirm-account.watchHtml.hbs │ │ ├── confirm-account.text.hbs │ │ └── confirm-account.html.hbs │ └── forgot-password │ │ ├── forgot-password.watchHtml.hbs │ │ ├── forgot-password.text.hbs │ │ └── forgot-password.html.hbs ├── public │ ├── app.css │ ├── loading.png │ └── index.html ├── routes │ ├── index.js │ └── v1 │ │ └── auth.js ├── models │ ├── PasswordReset.js │ └── User.js ├── config │ └── index.js ├── validators │ ├── login.js │ ├── email-confirm.js │ ├── forgot-password.js │ ├── register.js │ └── reset-password.js ├── middleware │ └── auth.js ├── index.js └── controllers │ └── v1 │ └── auth.controller.js ├── .gitignore ├── .prettierrc ├── postcss.config.js ├── .babelrc ├── webpack.config.js ├── package.json ├── mail.config.js └── tailwind.js /client/store/auth/getters.js: -------------------------------------------------------------------------------- 1 | export default {} 2 | -------------------------------------------------------------------------------- /server/mails/confirm-account/confirm-account.watchHtml.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/mails/forgot-password/forgot-password.watchHtml.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/public/app.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: red; 3 | } 4 | 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | dist/ 4 | .env 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "semi": false, 4 | "singleQuote": true 5 | } -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require('tailwindcss')('./tailwind.js')] 3 | } 4 | -------------------------------------------------------------------------------- /client/components/Loader.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /server/mails/forgot-password/forgot-password.text.hbs: -------------------------------------------------------------------------------- 1 | Hello {{ name }} 2 | 3 | Click this link to reset your password {{ url }} 4 | -------------------------------------------------------------------------------- /server/public/loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahdcoder/fullstack-nodejs-and-vuejs-from-scratch/HEAD/server/public/loading.png -------------------------------------------------------------------------------- /server/mails/confirm-account/confirm-account.text.hbs: -------------------------------------------------------------------------------- 1 | Hey {{ name }}, 2 | 3 | Please confirm your account by clicking on this url {{ url }} 4 | -------------------------------------------------------------------------------- /client/store/flash/index.js: -------------------------------------------------------------------------------- 1 | import mutations from './mutations' 2 | 3 | export default { 4 | state: { 5 | messages: [] 6 | }, 7 | mutations 8 | } 9 | -------------------------------------------------------------------------------- /server/mails/forgot-password/forgot-password.html.hbs: -------------------------------------------------------------------------------- 1 | Hello {{ name }} 2 | 3 |
4 | 5 | Click this link to reset your password LINK Here 6 | -------------------------------------------------------------------------------- /server/mails/confirm-account/confirm-account.html.hbs: -------------------------------------------------------------------------------- 1 | Hey {{ name }}, 2 | 3 |
4 | 5 | Please confirm your account by clicking on this url LINK 6 | -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | import authRouter from './v1/auth' 3 | 4 | const v1Router = new Router() 5 | 6 | v1Router.use('/api/v1/auth', authRouter) 7 | 8 | export default v1Router 9 | -------------------------------------------------------------------------------- /client/mixins/form.js: -------------------------------------------------------------------------------- 1 | export default { 2 | data: () => ({ 3 | loading: false 4 | }), 5 | 6 | methods: { 7 | toggleLoading() { 8 | this.loading = !this.loading 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /client/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import auth from './auth' 4 | import flash from './flash' 5 | 6 | Vue.use(Vuex) 7 | 8 | export default new Vuex.Store({ 9 | modules: { 10 | auth, 11 | flash 12 | } 13 | }) 14 | -------------------------------------------------------------------------------- /server/models/PasswordReset.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | 3 | const PasswordResetSchema = new mongoose.Schema({ 4 | email: String, 5 | token: String, 6 | createdAt: Date 7 | }) 8 | 9 | export default mongoose.model('PasswordReset', PasswordResetSchema) 10 | -------------------------------------------------------------------------------- /client/pages/Home.vue: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /server/config/index.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv' 2 | 3 | dotenv.config() 4 | 5 | export default { 6 | databaseUrl: 7 | process.env.DATABASE_URL || 'mongodb://localhost:27017/mevnmongo', 8 | url: process.env.APP_URL || 'http://localhost:3000', 9 | jwtSecret: process.env.JWT_SECRET || '1234' 10 | } 11 | -------------------------------------------------------------------------------- /client/store/auth/mutations.js: -------------------------------------------------------------------------------- 1 | import { SET_AUTH, UNSET_AUTH } from './actions' 2 | 3 | export default { 4 | [SET_AUTH](state, { user, token }) { 5 | state.user = user 6 | state.token = token 7 | }, 8 | [UNSET_AUTH](state) { 9 | state.user = null 10 | state.token = null 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /client/components/Flash.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /client/utils/axios.js: -------------------------------------------------------------------------------- 1 | import Axios from 'axios' 2 | import store from '@store' 3 | 4 | const axios = Axios.create({ 5 | baseURL: '/api/v1/' 6 | }) 7 | 8 | axios.interceptors.request.use(function(config) { 9 | if (!!store.state.auth.user && !!store.state.auth.token) { 10 | config.headers = { 11 | access_token: store.state.auth.token 12 | } 13 | } 14 | 15 | return config 16 | }) 17 | 18 | export default axios 19 | -------------------------------------------------------------------------------- /server/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | MEVN 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /client/store/flash/mutations.js: -------------------------------------------------------------------------------- 1 | export const FLASH_MESSAGE = 'FLASH_MESSAGE' 2 | export const CLEAR_FLASH_MESSAGE = 'CLEAR_FLASH_MESSAGE' 3 | 4 | export default { 5 | [FLASH_MESSAGE](state, message) { 6 | state.messages = [ 7 | ...state.messages, 8 | message 9 | ] 10 | }, 11 | [CLEAR_FLASH_MESSAGE](state, id) { 12 | state.messages = state.messages.filter(message => message.id !== id) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /client/store/auth/index.js: -------------------------------------------------------------------------------- 1 | import mutations from './mutations' 2 | import getters from './getters' 3 | import actions from './actions' 4 | 5 | let initialState = null 6 | 7 | try { 8 | initialState = JSON.parse(localStorage.getItem('auth')) 9 | } catch (e) { 10 | initialState = { 11 | user: null, 12 | token: null 13 | } 14 | } 15 | 16 | export default { 17 | state: initialState, 18 | actions, 19 | getters, 20 | mutations 21 | } 22 | -------------------------------------------------------------------------------- /server/validators/login.js: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup' 2 | 3 | const LoginSchema = Yup.object().shape({ 4 | email: Yup.string() 5 | .email() 6 | .required(), 7 | password: Yup.string() 8 | .min(6) 9 | .required() 10 | }) 11 | 12 | export default (req, res, next) => 13 | LoginSchema.validate(req.body) 14 | .then(() => next()) 15 | .catch(error => 16 | res.status(422).json({ 17 | [error.path]: error.message 18 | }) 19 | ) 20 | -------------------------------------------------------------------------------- /client/styles/main.css: -------------------------------------------------------------------------------- 1 | @tailwind preflight; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @import url('https://fonts.googleapis.com/css?family=Merriweather:300,400,700'); 6 | 7 | body { 8 | background-color: #fefbfa !important; 9 | } 10 | 11 | .loader { 12 | animation-name: spin; 13 | animation-duration: 2000ms; 14 | animation-iteration-count: infinite; 15 | animation-timing-function: linear; 16 | } 17 | 18 | @keyframes spin { 19 | from { 20 | transform:rotate(0deg); 21 | } 22 | to { 23 | transform:rotate(360deg); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /server/middleware/auth.js: -------------------------------------------------------------------------------- 1 | import config from '@config' 2 | import jwt from 'jsonwebtoken' 3 | import User from '@models/User' 4 | 5 | export default async (req, res, next) => { 6 | try { 7 | const token = req.headers.access_token 8 | 9 | const data = jwt.verify(token, config.jwtSecret) 10 | 11 | const user = await User.findById(data.id) 12 | 13 | if (!user) { 14 | throw new Error() 15 | } 16 | 17 | req.user = user 18 | 19 | return next() 20 | } catch (error) { 21 | console.log('----------->', error) 22 | return res.status(400).json({ 23 | message: 'Unauthenticated.' 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /client/mixins/flash.js: -------------------------------------------------------------------------------- 1 | import uuid from 'uuid/v4' 2 | import { FLASH_MESSAGE, CLEAR_FLASH_MESSAGE } from '@store/flash/mutations' 3 | 4 | export default { 5 | computed: { 6 | flashMessages() { 7 | return this.$store.state.flash.messages 8 | } 9 | }, 10 | 11 | methods: { 12 | flash(message, type = 'success') { 13 | const id = uuid() 14 | 15 | this.$store.commit(FLASH_MESSAGE, { 16 | id, 17 | type, 18 | message 19 | }) 20 | 21 | setTimeout(() => { 22 | this.$store.commit(CLEAR_FLASH_MESSAGE, id) 23 | }, 3000) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /client/pages/EmailConfirm.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 24 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ], 5 | "plugins": [ 6 | ["module-resolver", { 7 | "root": ["./"], 8 | "alias": { 9 | "@": "./", 10 | "@client": "./client", 11 | "@server": "./server", 12 | "@store": "./client/store", 13 | "@pages": "./client/pages", 14 | "@models": "./server/models", 15 | "@config": "./server/config", 16 | "@routes": "./server/routes", 17 | "@middleware": "./server/middleware", 18 | "@validators": "./server/validators", 19 | "@components": "./client/components", 20 | "@controllers": "./server/controllers" 21 | } 22 | }] 23 | ] 24 | } -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import './styles/main.css' 3 | import store from './store' 4 | import router from './routes' 5 | import Router from 'vue-router' 6 | import Main from './pages/Main.vue' 7 | import Validator from 'vee-validate' 8 | import Button from '@components/Button.vue' 9 | import Loader from '@components/Loader.vue' 10 | import authMixin from '@client/mixins/auth' 11 | import flashMixin from '@client/mixins/flash' 12 | import TextInput from '@components/TextInput.vue' 13 | 14 | Vue.use(Router) 15 | Vue.use(Validator) 16 | Vue.mixin(authMixin) 17 | Vue.mixin(flashMixin) 18 | Vue.component('btn', Button) 19 | Vue.component('loader', Loader) 20 | Vue.component('text-input', TextInput) 21 | 22 | const app = new Vue({ 23 | el: '#app', 24 | router, 25 | store, 26 | render: h => h(Main) 27 | }) 28 | -------------------------------------------------------------------------------- /server/validators/email-confirm.js: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup' 2 | import User from '@models/User' 3 | 4 | const EmailConfirmSchema = Yup.object().shape({ 5 | token: Yup.string() 6 | .required() 7 | }) 8 | 9 | export default async (req, res, next) => { 10 | const { token } = req.body 11 | 12 | try { 13 | await EmailConfirmSchema.validate(req.body) 14 | 15 | const user = await User.findOne({ emailConfirmCode: token }) 16 | 17 | if (! user) { 18 | throw new Yup.ValidationError( 19 | 'Invalid confirmation code', 20 | req.body, 21 | 'token' 22 | ) 23 | } 24 | 25 | req.user = user 26 | 27 | return next() 28 | } catch (error) { 29 | return res.status(422).json({ 30 | [error.type]: error.message 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /client/components/Button.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 28 | -------------------------------------------------------------------------------- /server/validators/forgot-password.js: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup' 2 | import User from '@models/User' 3 | import PasswordReset from '@models/PasswordReset' 4 | 5 | const ForgotPasswordSchema = Yup.object().shape({ 6 | email: Yup.string() 7 | .email() 8 | .required() 9 | }) 10 | 11 | export default async (req, res, next) => { 12 | const { email } = req.body 13 | 14 | try { 15 | await ForgotPasswordSchema.validate(req.body) 16 | 17 | const user = await User.findOne({ email }) 18 | 19 | if (!user) { 20 | throw new Yup.ValidationError( 21 | 'This user does not exist.', 22 | req.body, 23 | 'email' 24 | ) 25 | } 26 | 27 | req.user = user 28 | 29 | return next() 30 | } catch (error) { 31 | res.status(422).json({ 32 | [error.path]: error.message 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /server/validators/register.js: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup' 2 | import User from '@models/User' 3 | 4 | const RegisterSchema = Yup.object().shape({ 5 | name: Yup.string().required(), 6 | email: Yup.string() 7 | .email() 8 | .required(), 9 | password: Yup.string() 10 | .min(6) 11 | .required() 12 | }) 13 | 14 | export default async (req, res, next) => { 15 | const { name, email, password } = req.body 16 | 17 | try { 18 | await RegisterSchema.validate({ name, email, password }) 19 | 20 | const existingUser = await User.findOne({ email }) 21 | 22 | if (existingUser) { 23 | throw new Yup.ValidationError( 24 | 'This user already exists.', 25 | req.body, 26 | 'email' 27 | ) 28 | } 29 | 30 | return next() 31 | } catch (error) { 32 | return res.status(422).json({ 33 | [error.path]: error.message 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /client/store/auth/actions.js: -------------------------------------------------------------------------------- 1 | import client from '@client/utils/axios' 2 | 3 | export const SET_AUTH = 'SET_AUTH' 4 | export const UNSET_AUTH = 'UNSET_AUTH' 5 | export const POST_LOGIN = 'POST_LOGIN' 6 | export const POST_REGISTER = 'POST_REGISTER' 7 | export const POST_CONFIRM_EMAIL = 'POST_CONFIRM_EMAIL' 8 | export const POST_RESET_PASSWORD = 'POST_RESET_PASSWORD' 9 | export const POST_FORGOT_PASSWORD = 'POST_FORGOT_PASSWORD' 10 | export const POST_RESENT_EMAIL_CONFIRM = 'POST_RESENT_EMAIL_CONFIRM' 11 | 12 | export default { 13 | [POST_REGISTER]: (context, data) => client.post('auth/register', data), 14 | [POST_LOGIN]: (context, data) => client.post('auth/login', data), 15 | [POST_FORGOT_PASSWORD]: (context, data) => 16 | client.post('auth/passwords/email', data), 17 | [POST_RESET_PASSWORD]: (context, data) => client.post('auth/passwords/reset', data), 18 | [POST_CONFIRM_EMAIL]: (context, data) => client.post('auth/emails/confirm', data), 19 | [POST_RESENT_EMAIL_CONFIRM]: (context, data) => client.post('auth/emails/resend', data) 20 | } 21 | -------------------------------------------------------------------------------- /client/routes.js: -------------------------------------------------------------------------------- 1 | import Router from 'vue-router' 2 | 3 | import Home from '@pages/Home.vue' 4 | import Login from '@pages/Login.vue' 5 | import Register from '@pages/Register.vue' 6 | import EmailConfirm from '@pages/EmailConfirm.vue' 7 | import ResetPassword from '@pages/ResetPassword.vue' 8 | import ForgotPassword from '@pages/ForgotPassword.vue' 9 | 10 | export default new Router({ 11 | mode: 'history', 12 | routes: [ 13 | { 14 | path: '/auth/login', 15 | component: Login 16 | }, 17 | { 18 | path: '/auth/register', 19 | component: Register 20 | }, 21 | { 22 | path: '/', 23 | component: Home 24 | }, 25 | { 26 | path: '/auth/passwords/email', 27 | component: ForgotPassword 28 | }, 29 | { 30 | path: '/auth/passwords/reset/:token', 31 | component: ResetPassword 32 | }, 33 | { 34 | path: '/auth/emails/confirm/:token', 35 | component: EmailConfirm 36 | } 37 | ] 38 | }) 39 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import config from '@config' 3 | import Express from 'express' 4 | import Webpack from 'webpack' 5 | import v1Router from '@routes' 6 | import Mongoose from 'mongoose' 7 | import BodyParser from 'body-parser' 8 | import WebpackConfig from '@/webpack.config' 9 | import WebpackHotMiddleware from 'webpack-hot-middleware' 10 | import WebpackDevMiddleware from 'webpack-dev-middleware' 11 | 12 | Mongoose.connect(config.databaseUrl, { useNewUrlParser: true }) 13 | 14 | const app = Express() 15 | 16 | app.use(BodyParser.json()) 17 | 18 | const compiler = Webpack(WebpackConfig) 19 | 20 | app.use( 21 | WebpackDevMiddleware(compiler, { 22 | hot: true, 23 | publicPath: WebpackConfig.output.publicPath 24 | }) 25 | ) 26 | 27 | app.use(WebpackHotMiddleware(compiler)) 28 | 29 | app.use(v1Router) 30 | 31 | app.use(Express.static(path.resolve(__dirname, 'public'))) 32 | 33 | app.get('*', (req, res) => { 34 | res.sendFile(path.resolve(__dirname, 'public/index.html')) 35 | }) 36 | 37 | app.listen(3000, () => { 38 | console.log('server started succesfully.') 39 | }) 40 | -------------------------------------------------------------------------------- /server/routes/v1/auth.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | import authMiddleware from '@middleware/auth' 3 | import loginValidator from '@validators/login' 4 | import registerValidator from '@validators/register' 5 | import authController from '@controllers/v1/auth.controller' 6 | import emailConfirmValidator from '@validators/email-confirm' 7 | import resetPasswordValidator from '@validators/reset-password' 8 | import forgotPasswordValidator from '@validators/forgot-password' 9 | 10 | const authRouter = new Router() 11 | 12 | authRouter.post('/login', loginValidator, authController.login) 13 | 14 | authRouter.post('/register', registerValidator, authController.register) 15 | 16 | authRouter.post( 17 | '/passwords/email', 18 | forgotPasswordValidator, 19 | authController.forgotPassword 20 | ) 21 | 22 | authRouter.post('/passwords/reset', resetPasswordValidator, authController.resetPassword) 23 | 24 | authRouter.post('/emails/confirm', emailConfirmValidator, authController.confirmEmail) 25 | 26 | authRouter.post('/emails/resend', authMiddleware, authController.resendConfirmEmail) 27 | 28 | export default authRouter 29 | -------------------------------------------------------------------------------- /client/components/TextInput.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 47 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const path = require('path') 3 | const webpack = require('webpack') 4 | const VueLoaderPlugin = require('vue-loader/lib/plugin') 5 | const MiniExtracCssPlugin = require('mini-css-extract-plugin') 6 | 7 | module.exports = { 8 | mode: process.env.NODE_ENV || 'development', 9 | entry: ['webpack-hot-middleware/client?reload=true', './client/index.js'], 10 | output: { 11 | filename: 'app.js', 12 | publicPath: '/', 13 | path: path.resolve(__dirname, 'server/public') 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.js$/, 19 | use: { 20 | loader: 'babel-loader' 21 | } 22 | }, 23 | { 24 | test: /\.vue$/, 25 | use: { 26 | loader: 'vue-loader' 27 | } 28 | }, 29 | { 30 | test: /\.css$/, 31 | use: [ 32 | MiniExtracCssPlugin.loader, 33 | 'css-loader', 34 | 'postcss-loader' 35 | ] 36 | } 37 | ] 38 | }, 39 | plugins: [ 40 | new webpack.HotModuleReplacementPlugin(), 41 | new VueLoaderPlugin(), 42 | new MiniExtracCssPlugin({ 43 | filename: 'app.css' 44 | }) 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /client/mixins/auth.js: -------------------------------------------------------------------------------- 1 | import { SET_AUTH, UNSET_AUTH, POST_RESENT_EMAIL_CONFIRM } from '@store/auth/actions' 2 | 3 | export default { 4 | computed: { 5 | auth() { 6 | return !!this.$store.state.auth.user 7 | }, 8 | user() { 9 | return this.$store.state.auth.user 10 | }, 11 | confirmed() { 12 | return !!this.$store.state.auth.user.emailConfirmedAt 13 | } 14 | }, 15 | 16 | methods: { 17 | setAuth(payload) { 18 | localStorage.setItem('auth', JSON.stringify(payload)) 19 | this.$store.commit(SET_AUTH, payload) 20 | 21 | this.$router.push('/') 22 | }, 23 | 24 | unsetAuth() { 25 | localStorage.removeItem('auth') 26 | this.$store.commit(UNSET_AUTH) 27 | 28 | this.flash('Successfully logged out.') 29 | 30 | this.$router.push('/') 31 | }, 32 | 33 | resendEmailConfirm() { 34 | this.$store.dispatch(POST_RESENT_EMAIL_CONFIRM) 35 | .then(() => { 36 | this.flash('Successfully resent confirm email.') 37 | 38 | this.$router.push('/') 39 | }) 40 | .catch(() => { 41 | this.flash('Error resending confirm email.') 42 | 43 | this.$router.push('/') 44 | }) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /client/pages/Main.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 31 | -------------------------------------------------------------------------------- /server/validators/reset-password.js: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup' 2 | import User from '@models/User' 3 | import PasswordReset from '@models/PasswordReset' 4 | 5 | const ResetPasswordSchema = Yup.object().shape({ 6 | password: Yup.string() 7 | .min(6) 8 | .required() 9 | }) 10 | 11 | export default async (req, res, next) => { 12 | const { password, token } = req.body 13 | 14 | try { 15 | await ResetPasswordSchema.validate(req.body) 16 | 17 | const existingReset = await PasswordReset.findOne({ token }) 18 | 19 | if (!existingReset) { 20 | throw new Yup.ValidationError( 21 | 'Invalid reset token.', 22 | req.body, 23 | 'password' 24 | ) 25 | } 26 | 27 | const timeInMinutes = Math.ceil((new Date().getTime() - new Date(existingReset.createdAt).getTime()) / 60000) 28 | 29 | if (timeInMinutes > 5) { 30 | await PasswordReset.findOneAndDelete({ token }) 31 | 32 | throw new Yup.ValidationError( 33 | 'Reset token expired.', 34 | req.body, 35 | 'password' 36 | ) 37 | } 38 | 39 | const user = await User.findOne({ email: existingReset.email }) 40 | 41 | req.user = user 42 | 43 | return next() 44 | } catch (error) { 45 | res.status(422).json({ 46 | [error.path]: error.message 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mevn", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "nodemon --exec babel-node server/index", 8 | "prettier": "prettier --write './**/*.js'", 9 | "build:client": "webpack --mode=production", 10 | "build": "npm run build:client && npm run build:server", 11 | "build:server": "babel server/ --out-dir dist/ --copy-files" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "@fullstackjs/mail": "^1.0.7", 18 | "axios": "^0.18.0", 19 | "bcryptjs": "^2.4.3", 20 | "body-parser": "^1.18.3", 21 | "dotenv": "^7.0.0", 22 | "express": "^4.16.4", 23 | "jsonwebtoken": "^8.5.1", 24 | "mongoose": "^5.4.20", 25 | "randomstring": "^1.1.5", 26 | "uuid": "^3.3.2", 27 | "vee-validate": "^2.2.0", 28 | "vue": "^2.6.10", 29 | "vue-router": "^3.0.2", 30 | "vuex": "^3.1.0", 31 | "yup": "^0.27.0" 32 | }, 33 | "devDependencies": { 34 | "@babel/cli": "^7.2.3", 35 | "@babel/core": "^7.4.0", 36 | "@babel/node": "^7.2.2", 37 | "@babel/preset-env": "^7.4.2", 38 | "babel-loader": "^8.0.5", 39 | "babel-plugin-module-resolver": "^3.2.0", 40 | "css-loader": "^2.1.1", 41 | "mini-css-extract-plugin": "^0.5.0", 42 | "nodemon": "^1.18.10", 43 | "postcss": "^7.0.14", 44 | "postcss-loader": "^3.0.0", 45 | "prettier": "^1.16.4", 46 | "tailwindcss": "^0.7.4", 47 | "vue-loader": "^15.7.0", 48 | "vue-template-compiler": "^2.6.10", 49 | "webpack": "^4.29.6", 50 | "webpack-cli": "^3.3.0", 51 | "webpack-dev-middleware": "^3.6.1", 52 | "webpack-hot-middleware": "^2.24.3" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /server/models/User.js: -------------------------------------------------------------------------------- 1 | import config from '@config' 2 | import Bcrypt from 'bcryptjs' 3 | import jwt from 'jsonwebtoken' 4 | import mongoose from 'mongoose' 5 | import Mail from '@fullstackjs/mail' 6 | import randomstring from 'randomstring' 7 | import PasswordReset from '@models/PasswordReset' 8 | 9 | const UserSchema = new mongoose.Schema({ 10 | name: String, 11 | email: String, 12 | createdAt: Date, 13 | updatedAt: Date, 14 | password: String, 15 | emailConfirmedAt: Date, 16 | emailConfirmCode: String 17 | }) 18 | 19 | UserSchema.pre('save', function() { 20 | this.password = Bcrypt.hashSync(this.password) 21 | this.emailConfirmCode = randomstring.generate(72) 22 | 23 | this.createdAt = new Date() 24 | }) 25 | 26 | UserSchema.post('save', async function() { 27 | await this.sendEmailConfirmation() 28 | }) 29 | 30 | UserSchema.methods.generateToken = function() { 31 | return jwt.sign({ id: this._id }, config.jwtSecret) 32 | } 33 | 34 | UserSchema.methods.sendEmailConfirmation = async function () { 35 | await new Mail('confirm-account') 36 | .to(this.email, this.name) 37 | .subject('Please confirm your account') 38 | .data({ 39 | name: this.name, 40 | url: `${config.url}/auth/emails/confirm/${this.emailConfirmCode}` 41 | }) 42 | .send() 43 | } 44 | 45 | UserSchema.methods.comparePasswords = function(plainPassword) { 46 | return Bcrypt.compareSync(plainPassword, this.password) 47 | } 48 | 49 | UserSchema.methods.forgotPassword = async function() { 50 | const token = randomstring.generate(72) 51 | 52 | await PasswordReset.create({ 53 | token, 54 | email: this.email, 55 | createdAt: new Date() 56 | }) 57 | 58 | await new Mail('forgot-password') 59 | .to(this.email, this.name) 60 | .subject('Password reset') 61 | .data({ 62 | url: `${config.url}/auth/passwords/reset/${token}`, 63 | name: this.name 64 | }) 65 | .send() 66 | } 67 | 68 | export default mongoose.model('User', UserSchema) 69 | -------------------------------------------------------------------------------- /server/controllers/v1/auth.controller.js: -------------------------------------------------------------------------------- 1 | import Bcrypt from 'bcryptjs' 2 | import User from '@models/User' 3 | import PasswordReset from '@models/PasswordReset' 4 | 5 | const login = async (req, res) => { 6 | const { email, password } = req.body 7 | 8 | const user = await User.findOne({ email }) 9 | 10 | if (user) { 11 | if (user.comparePasswords(password)) { 12 | const token = user.generateToken() 13 | 14 | return res.json({ 15 | user, 16 | token 17 | }) 18 | } 19 | } 20 | 21 | return res.status(400).json({ 22 | email: 'These credentials do not match our records.' 23 | }) 24 | } 25 | 26 | const register = async (req, res) => { 27 | const { name, email, password } = req.body 28 | 29 | const user = await User.create({ 30 | name, 31 | email, 32 | password 33 | }) 34 | 35 | const token = user.generateToken() 36 | 37 | return res.status(201).json({ user, token }) 38 | } 39 | 40 | const forgotPassword = async (req, res) => { 41 | await req.user.forgotPassword() 42 | 43 | return res.json({ 44 | message: 'Password reset link sent.' 45 | }) 46 | } 47 | 48 | const resetPassword = async (req, res) => { 49 | const { user } = req 50 | 51 | await User.findOneAndUpdate({ 52 | email: user.email 53 | }, { 54 | password: Bcrypt.hashSync(req.body.password) 55 | }) 56 | 57 | await PasswordReset.findOneAndDelete({ 58 | email: user.email 59 | }) 60 | 61 | return res.json({ 62 | message: 'Password reset successfully.' 63 | }) 64 | } 65 | 66 | const confirmEmail = async (req, res) => { 67 | const user = await User.findOneAndUpdate({ 68 | email: req.user.email 69 | }, { 70 | emailConfirmCode: null, 71 | emailConfirmedAt: new Date() 72 | }, { new: true }) 73 | 74 | const token = user.generateToken() 75 | 76 | return res.json({ 77 | user, 78 | token 79 | }) 80 | } 81 | 82 | const resendConfirmEmail = async (req, res) => { 83 | if (!req.user.emailConfirmedAt) { 84 | await req.user.sendEmailConfirmation() 85 | } 86 | 87 | return res.json({ 88 | message: 'Email confirm sent.' 89 | }) 90 | } 91 | 92 | export default { 93 | login, 94 | register, 95 | forgotPassword, 96 | resetPassword, 97 | confirmEmail, 98 | resendConfirmEmail 99 | } 100 | -------------------------------------------------------------------------------- /client/pages/ForgotPassword.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 73 | -------------------------------------------------------------------------------- /client/pages/ResetPassword.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 77 | -------------------------------------------------------------------------------- /client/pages/Login.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 86 | -------------------------------------------------------------------------------- /client/pages/Register.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 92 | -------------------------------------------------------------------------------- /mail.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /* 3 | |-------------------------------------------------------------------------- 4 | | Connection 5 | |-------------------------------------------------------------------------- 6 | | 7 | | Connection to be used for sending emails. Each connection needs to 8 | | define a driver too. 9 | | 10 | */ 11 | connection: process.env.MAIL_CONNECTION || 'smtp', 12 | 13 | /* 14 | |-------------------------------------------------------------------------- 15 | | Views 16 | |-------------------------------------------------------------------------- 17 | | 18 | | This configuration defines the folder in which all emails are stored. 19 | | If it's not defined, /mails is used as default. 20 | | 21 | */ 22 | views: 'server/mails', 23 | 24 | /* 25 | |-------------------------------------------------------------------------- 26 | | View engine 27 | |-------------------------------------------------------------------------- 28 | | 29 | | This is the view engine that should be used. The currently supported are: 30 | | handlebars, edge 31 | | 32 | */ 33 | viewEngine: 'handlebars', 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | SMTP 38 | |-------------------------------------------------------------------------- 39 | | 40 | | Here we define configuration for sending emails via SMTP. 41 | | 42 | */ 43 | smtp: { 44 | driver: 'smtp', 45 | pool: true, 46 | port: process.env.SMTP_PORT || 2525, 47 | host: process.env.SMTP_HOST || 'smtp.mailtrap.io', 48 | secure: false, 49 | auth: { 50 | user: process.env.MAIL_USERNAME, 51 | pass: process.env.MAIL_PASSWORD 52 | }, 53 | maxConnections: 5, 54 | maxMessages: 100, 55 | rateLimit: 10 56 | }, 57 | 58 | /* 59 | |-------------------------------------------------------------------------- 60 | | SparkPost 61 | |-------------------------------------------------------------------------- 62 | | 63 | | Here we define configuration for spark post. Extra options can be defined 64 | | inside the "extra" object. 65 | | 66 | | https://developer.sparkpost.com/api/transmissions.html#header-options-attributes 67 | | 68 | | extras: { 69 | | campaign_id: 'sparkpost campaign id', 70 | | options: { // sparkpost options } 71 | | } 72 | | 73 | */ 74 | sparkpost: { 75 | driver: 'sparkpost', 76 | // endpoint: 'https://api.eu.sparkpost.com/api/v1', 77 | apiKey: process.env.SPARKPOST_API_KEY, 78 | extras: {} 79 | }, 80 | 81 | /* 82 | |-------------------------------------------------------------------------- 83 | | Mailgun 84 | |-------------------------------------------------------------------------- 85 | | 86 | | Here we define configuration for mailgun. Extra options can be defined 87 | | inside the "extra" object. 88 | | 89 | | https://mailgun-documentation.readthedocs.io/en/latest/api-sending.html#sending 90 | | 91 | | extras: { 92 | | 'o:tag': '', 93 | | 'o:campaign': '',, 94 | | . . . 95 | | } 96 | | 97 | */ 98 | mailgun: { 99 | driver: 'mailgun', 100 | domain: process.env.MAILGUN_DOMAIN, 101 | apiKey: process.env.MAILGUN_API_KEY, 102 | extras: {} 103 | }, 104 | 105 | /* 106 | |-------------------------------------------------------------------------- 107 | | Ethereal 108 | |-------------------------------------------------------------------------- 109 | | 110 | | Ethereal driver to quickly test emails in your browser. A disposable 111 | | account is created automatically for you. 112 | | 113 | | https://ethereal.email 114 | | 115 | */ 116 | ethereal: { 117 | driver: 'ethereal' 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /tailwind.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Tailwind - The Utility-First CSS Framework 4 | 5 | A project by Adam Wathan (@adamwathan), Jonathan Reinink (@reinink), 6 | David Hemphill (@davidhemphill) and Steve Schoger (@steveschoger). 7 | 8 | Welcome to the Tailwind config file. This is where you can customize 9 | Tailwind specifically for your project. Don't be intimidated by the 10 | length of this file. It's really just a big JavaScript object and 11 | we've done our very best to explain each section. 12 | 13 | View the full documentation at https://tailwindcss.com. 14 | 15 | 16 | |------------------------------------------------------------------------------- 17 | | The default config 18 | |------------------------------------------------------------------------------- 19 | | 20 | | This variable contains the default Tailwind config. You don't have 21 | | to use it, but it can sometimes be helpful to have available. For 22 | | example, you may choose to merge your custom configuration 23 | | values with some of the Tailwind defaults. 24 | | 25 | */ 26 | 27 | let defaultConfig = require('tailwindcss/defaultConfig')() 28 | 29 | /* 30 | |------------------------------------------------------------------------------- 31 | | Colors https://tailwindcss.com/docs/colors 32 | |------------------------------------------------------------------------------- 33 | | 34 | | Here you can specify the colors used in your project. To get you started, 35 | | we've provided a generous palette of great looking colors that are perfect 36 | | for prototyping, but don't hesitate to change them for your project. You 37 | | own these colors, nothing will break if you change everything about them. 38 | | 39 | | We've used literal color names ("red", "blue", etc.) for the default 40 | | palette, but if you'd rather use functional names like "primary" and 41 | | "secondary", or even a numeric scale like "100" and "200", go for it. 42 | | 43 | */ 44 | 45 | let colors = { 46 | transparent: 'transparent', 47 | 48 | black: '#22292f', 49 | 'grey-darkest': '#3d4852', 50 | 'grey-darker': '#606f7b', 51 | 'grey-dark': '#8795a1', 52 | grey: '#b8c2cc', 53 | 'grey-light': '#dae1e7', 54 | 'grey-lighter': '#f1f5f8', 55 | 'grey-lightest': '#f8fafc', 56 | white: '#ffffff', 57 | 58 | 'red-darkest': '#3b0d0c', 59 | 'red-darker': '#621b18', 60 | 'red-dark': '#cc1f1a', 61 | red: '#e3342f', 62 | 'red-light': '#ef5753', 63 | 'red-lighter': '#f9acaa', 64 | 'red-lightest': '#fcebea', 65 | 66 | 'orange-darkest': '#462a16', 67 | 'orange-darker': '#613b1f', 68 | 'orange-dark': '#de751f', 69 | orange: '#f6993f', 70 | 'orange-light': '#faad63', 71 | 'orange-lighter': '#fcd9b6', 72 | 'orange-lightest': '#fff5eb', 73 | 74 | 'yellow-darkest': '#453411', 75 | 'yellow-darker': '#684f1d', 76 | 'yellow-dark': '#f2d024', 77 | yellow: '#ffed4a', 78 | 'yellow-light': '#fff382', 79 | 'yellow-lighter': '#fff9c2', 80 | 'yellow-lightest': '#fcfbeb', 81 | 82 | 'green-darkest': '#0f2f21', 83 | 'green-darker': '#1a4731', 84 | 'green-dark': '#1f9d55', 85 | green: '#38c172', 86 | 'green-light': '#51d88a', 87 | 'green-lighter': '#a2f5bf', 88 | 'green-lightest': '#e3fcec', 89 | 90 | 'teal-darkest': '#0d3331', 91 | 'teal-darker': '#20504f', 92 | 'teal-dark': '#38a89d', 93 | teal: '#4dc0b5', 94 | 'teal-light': '#64d5ca', 95 | 'teal-lighter': '#a0f0ed', 96 | 'teal-lightest': '#e8fffe', 97 | 98 | 'emerald-darkest': '#20393B', 99 | 'emerald-darker': '#407375', 100 | 'emerald-dark': '#5FACB0', 101 | emerald: '#6ABFC3', 102 | 'emerald-light': '#97D2D5', 103 | 'emerald-lighter': '#C3E5E7', 104 | 'emerald-lightest': '#F0F9F9', 105 | 106 | 'gold-darkest': '#3E2D13', 107 | 'gold-darker': '#7C5B25', 108 | 'gold-dark': '#B98838', 109 | gold: '#CE973E', 110 | 'gold-light': '#DDB678', 111 | 'gold-lighter': '#EBD5B2', 112 | 'gold-lightest': '#FAF5EC', 113 | 114 | 'brown-darkest': '#262626', 115 | 'brown-darker': '#4D4D4D', 116 | 'brown-dark': '#737373', 117 | brown: '#808080', 118 | 'brown-light': '#A6A6A6', 119 | 'brown-lighter': '#CCCCCC', 120 | 'brown-lightest': '#F2F2F2', 121 | 122 | 'blue-darkest': '#12283a', 123 | 'blue-darker': '#1c3d5a', 124 | 'blue-dark': '#2779bd', 125 | blue: '#3490dc', 126 | 'blue-light': '#6cb2eb', 127 | 'blue-lighter': '#bcdefa', 128 | 'blue-lightest': '#eff8ff', 129 | 130 | 'indigo-darkest': '#191e38', 131 | 'indigo-darker': '#2f365f', 132 | 'indigo-dark': '#5661b3', 133 | indigo: '#6574cd', 134 | 'indigo-light': '#7886d7', 135 | 'indigo-lighter': '#b2b7ff', 136 | 'indigo-lightest': '#e6e8ff', 137 | 138 | 'purple-darkest': '#21183c', 139 | 'purple-darker': '#382b5f', 140 | 'purple-dark': '#794acf', 141 | purple: '#9561e2', 142 | 'purple-light': '#a779e9', 143 | 'purple-lighter': '#d6bbfc', 144 | 'purple-lightest': '#f3ebff', 145 | 146 | 'pink-darkest': '#451225', 147 | 'pink-darker': '#6f213f', 148 | 'pink-dark': '#eb5286', 149 | pink: '#f66d9b', 150 | 'pink-light': '#fa7ea8', 151 | 'pink-lighter': '#ffbbca', 152 | 'pink-lightest': '#ffebef' 153 | } 154 | 155 | module.exports = { 156 | /* 157 | |----------------------------------------------------------------------------- 158 | | Colors https://tailwindcss.com/docs/colors 159 | |----------------------------------------------------------------------------- 160 | | 161 | | The color palette defined above is also assigned to the "colors" key of 162 | | your Tailwind config. This makes it easy to access them in your CSS 163 | | using Tailwind's config helper. For example: 164 | | 165 | | .error { color: config('colors.red') } 166 | | 167 | */ 168 | 169 | colors: colors, 170 | 171 | /* 172 | |----------------------------------------------------------------------------- 173 | | Screens https://tailwindcss.com/docs/responsive-design 174 | |----------------------------------------------------------------------------- 175 | | 176 | | Screens in Tailwind are translated to CSS media queries. They define the 177 | | responsive breakpoints for your project. By default Tailwind takes a 178 | | "mobile first" approach, where each screen size represents a minimum 179 | | viewport width. Feel free to have as few or as many screens as you 180 | | want, naming them in whatever way you'd prefer for your project. 181 | | 182 | | Tailwind also allows for more complex screen definitions, which can be 183 | | useful in certain situations. Be sure to see the full responsive 184 | | documentation for a complete list of options. 185 | | 186 | | Class name: .{screen}:{utility} 187 | | 188 | */ 189 | 190 | screens: { 191 | sm: '576px', 192 | md: '768px', 193 | lg: '992px', 194 | xl: '1200px' 195 | }, 196 | 197 | /* 198 | |----------------------------------------------------------------------------- 199 | | Fonts https://tailwindcss.com/docs/fonts 200 | |----------------------------------------------------------------------------- 201 | | 202 | | Here is where you define your project's font stack, or font families. 203 | | Keep in mind that Tailwind doesn't actually load any fonts for you. 204 | | If you're using custom fonts you'll need to import them prior to 205 | | defining them here. 206 | | 207 | | By default we provide a native font stack that works remarkably well on 208 | | any device or OS you're using, since it just uses the default fonts 209 | | provided by the platform. 210 | | 211 | | Class name: .font-{name} 212 | | CSS property: font-family 213 | | 214 | */ 215 | 216 | fonts: { 217 | sans: [ 218 | 'system-ui', 219 | 'BlinkMacSystemFont', 220 | '-apple-system', 221 | 'Segoe UI', 222 | 'Roboto', 223 | 'Oxygen', 224 | 'Ubuntu', 225 | 'Cantarell', 226 | 'Fira Sans', 227 | 'Droid Sans', 228 | 'Helvetica Neue', 229 | 'sans-serif' 230 | ], 231 | serif: [ 232 | 'Constantia', 233 | 'Lucida Bright', 234 | 'Lucidabright', 235 | 'Lucida Serif', 236 | 'Lucida', 237 | 'DejaVu Serif', 238 | 'Bitstream Vera Serif', 239 | 'Liberation Serif', 240 | 'Georgia', 241 | 'serif' 242 | ], 243 | mono: [ 244 | 'Menlo', 245 | 'Monaco', 246 | 'Consolas', 247 | 'Liberation Mono', 248 | 'Courier New', 249 | 'monospace' 250 | ], 251 | primary: ['Merriweather'] 252 | }, 253 | 254 | /* 255 | |----------------------------------------------------------------------------- 256 | | Text sizes https://tailwindcss.com/docs/text-sizing 257 | |----------------------------------------------------------------------------- 258 | | 259 | | Here is where you define your text sizes. Name these in whatever way 260 | | makes the most sense to you. We use size names by default, but 261 | | you're welcome to use a numeric scale or even something else 262 | | entirely. 263 | | 264 | | By default Tailwind uses the "rem" unit type for most measurements. 265 | | This allows you to set a root font size which all other sizes are 266 | | then based on. That said, you are free to use whatever units you 267 | | prefer, be it rems, ems, pixels or other. 268 | | 269 | | Class name: .text-{size} 270 | | CSS property: font-size 271 | | 272 | */ 273 | 274 | textSizes: { 275 | xs: '.75rem', // 12px 276 | sm: '.875rem', // 14px 277 | base: '1rem', // 16px 278 | lg: '1.125rem', // 18px 279 | xl: '1.25rem', // 20px 280 | '2xl': '1.5rem', // 24px 281 | '3xl': '1.875rem', // 30px 282 | '4xl': '2.25rem', // 36px 283 | '5xl': '3rem' // 48px 284 | }, 285 | 286 | /* 287 | |----------------------------------------------------------------------------- 288 | | Font weights https://tailwindcss.com/docs/font-weight 289 | |----------------------------------------------------------------------------- 290 | | 291 | | Here is where you define your font weights. We've provided a list of 292 | | common font weight names with their respective numeric scale values 293 | | to get you started. It's unlikely that your project will require 294 | | all of these, so we recommend removing those you don't need. 295 | | 296 | | Class name: .font-{weight} 297 | | CSS property: font-weight 298 | | 299 | */ 300 | 301 | fontWeights: { 302 | hairline: 100, 303 | thin: 200, 304 | light: 300, 305 | normal: 400, 306 | medium: 500, 307 | semibold: 600, 308 | bold: 700, 309 | extrabold: 800, 310 | black: 900 311 | }, 312 | 313 | /* 314 | |----------------------------------------------------------------------------- 315 | | Leading (line height) https://tailwindcss.com/docs/line-height 316 | |----------------------------------------------------------------------------- 317 | | 318 | | Here is where you define your line height values, or as we call 319 | | them in Tailwind, leadings. 320 | | 321 | | Class name: .leading-{size} 322 | | CSS property: line-height 323 | | 324 | */ 325 | 326 | leading: { 327 | none: 1, 328 | tight: 1.25, 329 | normal: 1.5, 330 | loose: 2 331 | }, 332 | 333 | /* 334 | |----------------------------------------------------------------------------- 335 | | Tracking (letter spacing) https://tailwindcss.com/docs/letter-spacing 336 | |----------------------------------------------------------------------------- 337 | | 338 | | Here is where you define your letter spacing values, or as we call 339 | | them in Tailwind, tracking. 340 | | 341 | | Class name: .tracking-{size} 342 | | CSS property: letter-spacing 343 | | 344 | */ 345 | 346 | tracking: { 347 | tight: '-0.05em', 348 | normal: '0', 349 | wide: '0.05em' 350 | }, 351 | 352 | /* 353 | |----------------------------------------------------------------------------- 354 | | Text colors https://tailwindcss.com/docs/text-color 355 | |----------------------------------------------------------------------------- 356 | | 357 | | Here is where you define your text colors. By default these use the 358 | | color palette we defined above, however you're welcome to set these 359 | | independently if that makes sense for your project. 360 | | 361 | | Class name: .text-{color} 362 | | CSS property: color 363 | | 364 | */ 365 | 366 | textColors: colors, 367 | 368 | /* 369 | |----------------------------------------------------------------------------- 370 | | Background colors https://tailwindcss.com/docs/background-color 371 | |----------------------------------------------------------------------------- 372 | | 373 | | Here is where you define your background colors. By default these use 374 | | the color palette we defined above, however you're welcome to set 375 | | these independently if that makes sense for your project. 376 | | 377 | | Class name: .bg-{color} 378 | | CSS property: background-color 379 | | 380 | */ 381 | 382 | backgroundColors: colors, 383 | 384 | /* 385 | |----------------------------------------------------------------------------- 386 | | Background sizes https://tailwindcss.com/docs/background-size 387 | |----------------------------------------------------------------------------- 388 | | 389 | | Here is where you define your background sizes. We provide some common 390 | | values that are useful in most projects, but feel free to add other sizes 391 | | that are specific to your project here as well. 392 | | 393 | | Class name: .bg-{size} 394 | | CSS property: background-size 395 | | 396 | */ 397 | 398 | backgroundSize: { 399 | auto: 'auto', 400 | cover: 'cover', 401 | contain: 'contain' 402 | }, 403 | 404 | /* 405 | |----------------------------------------------------------------------------- 406 | | Border widths https://tailwindcss.com/docs/border-width 407 | |----------------------------------------------------------------------------- 408 | | 409 | | Here is where you define your border widths. Take note that border 410 | | widths require a special "default" value set as well. This is the 411 | | width that will be used when you do not specify a border width. 412 | | 413 | | Class name: .border{-side?}{-width?} 414 | | CSS property: border-width 415 | | 416 | */ 417 | 418 | borderWidths: { 419 | default: '1px', 420 | '0': '0', 421 | '2': '2px', 422 | '4': '4px', 423 | '8': '8px' 424 | }, 425 | 426 | /* 427 | |----------------------------------------------------------------------------- 428 | | Border colors https://tailwindcss.com/docs/border-color 429 | |----------------------------------------------------------------------------- 430 | | 431 | | Here is where you define your border colors. By default these use the 432 | | color palette we defined above, however you're welcome to set these 433 | | independently if that makes sense for your project. 434 | | 435 | | Take note that border colors require a special "default" value set 436 | | as well. This is the color that will be used when you do not 437 | | specify a border color. 438 | | 439 | | Class name: .border-{color} 440 | | CSS property: border-color 441 | | 442 | */ 443 | 444 | borderColors: global.Object.assign( 445 | { default: colors['grey-light'] }, 446 | colors 447 | ), 448 | 449 | /* 450 | |----------------------------------------------------------------------------- 451 | | Border radius https://tailwindcss.com/docs/border-radius 452 | |----------------------------------------------------------------------------- 453 | | 454 | | Here is where you define your border radius values. If a `default` radius 455 | | is provided, it will be made available as the non-suffixed `.rounded` 456 | | utility. 457 | | 458 | | If your scale includes a `0` value to reset already rounded corners, it's 459 | | a good idea to put it first so other values are able to override it. 460 | | 461 | | Class name: .rounded{-side?}{-size?} 462 | | CSS property: border-radius 463 | | 464 | */ 465 | 466 | borderRadius: { 467 | none: '0', 468 | sm: '.125rem', 469 | default: '.25rem', 470 | lg: '.5rem', 471 | full: '9999px' 472 | }, 473 | 474 | /* 475 | |----------------------------------------------------------------------------- 476 | | Width https://tailwindcss.com/docs/width 477 | |----------------------------------------------------------------------------- 478 | | 479 | | Here is where you define your width utility sizes. These can be 480 | | percentage based, pixels, rems, or any other units. By default 481 | | we provide a sensible rem based numeric scale, a percentage 482 | | based fraction scale, plus some other common use-cases. You 483 | | can, of course, modify these values as needed. 484 | | 485 | | 486 | | It's also worth mentioning that Tailwind automatically escapes 487 | | invalid CSS class name characters, which allows you to have 488 | | awesome classes like .w-2/3. 489 | | 490 | | Class name: .w-{size} 491 | | CSS property: width 492 | | 493 | */ 494 | 495 | width: { 496 | auto: 'auto', 497 | px: '1px', 498 | '1': '0.25rem', 499 | '2': '0.5rem', 500 | '3': '0.75rem', 501 | '4': '1rem', 502 | '5': '1.25rem', 503 | '6': '1.5rem', 504 | '8': '2rem', 505 | '10': '2.5rem', 506 | '12': '3rem', 507 | '16': '4rem', 508 | '24': '6rem', 509 | '32': '8rem', 510 | '48': '12rem', 511 | '64': '16rem', 512 | '1/2': '50%', 513 | '1/3': '33.33333%', 514 | '2/3': '66.66667%', 515 | '1/4': '25%', 516 | '3/4': '75%', 517 | '1/5': '20%', 518 | '2/5': '40%', 519 | '3/5': '60%', 520 | '4/5': '80%', 521 | '1/6': '16.66667%', 522 | '5/6': '83.33333%', 523 | full: '100%', 524 | screen: '100vw' 525 | }, 526 | 527 | /* 528 | |----------------------------------------------------------------------------- 529 | | Height https://tailwindcss.com/docs/height 530 | |----------------------------------------------------------------------------- 531 | | 532 | | Here is where you define your height utility sizes. These can be 533 | | percentage based, pixels, rems, or any other units. By default 534 | | we provide a sensible rem based numeric scale plus some other 535 | | common use-cases. You can, of course, modify these values as 536 | | needed. 537 | | 538 | | Class name: .h-{size} 539 | | CSS property: height 540 | | 541 | */ 542 | 543 | height: { 544 | auto: 'auto', 545 | px: '1px', 546 | '1': '0.25rem', 547 | '2': '0.5rem', 548 | '3': '0.75rem', 549 | '4': '1rem', 550 | '5': '1.25rem', 551 | '6': '1.5rem', 552 | '8': '2rem', 553 | '10': '2.5rem', 554 | '12': '3rem', 555 | '16': '4rem', 556 | '24': '6rem', 557 | '32': '8rem', 558 | '48': '12rem', 559 | '64': '16rem', 560 | full: '100%', 561 | screen: '100vh' 562 | }, 563 | 564 | /* 565 | |----------------------------------------------------------------------------- 566 | | Minimum width https://tailwindcss.com/docs/min-width 567 | |----------------------------------------------------------------------------- 568 | | 569 | | Here is where you define your minimum width utility sizes. These can 570 | | be percentage based, pixels, rems, or any other units. We provide a 571 | | couple common use-cases by default. You can, of course, modify 572 | | these values as needed. 573 | | 574 | | Class name: .min-w-{size} 575 | | CSS property: min-width 576 | | 577 | */ 578 | 579 | minWidth: { 580 | '0': '0', 581 | full: '100%' 582 | }, 583 | 584 | /* 585 | |----------------------------------------------------------------------------- 586 | | Minimum height https://tailwindcss.com/docs/min-height 587 | |----------------------------------------------------------------------------- 588 | | 589 | | Here is where you define your minimum height utility sizes. These can 590 | | be percentage based, pixels, rems, or any other units. We provide a 591 | | few common use-cases by default. You can, of course, modify these 592 | | values as needed. 593 | | 594 | | Class name: .min-h-{size} 595 | | CSS property: min-height 596 | | 597 | */ 598 | 599 | minHeight: { 600 | '0': '0', 601 | full: '100%', 602 | screen: '100vh' 603 | }, 604 | 605 | /* 606 | |----------------------------------------------------------------------------- 607 | | Maximum width https://tailwindcss.com/docs/max-width 608 | |----------------------------------------------------------------------------- 609 | | 610 | | Here is where you define your maximum width utility sizes. These can 611 | | be percentage based, pixels, rems, or any other units. By default 612 | | we provide a sensible rem based scale and a "full width" size, 613 | | which is basically a reset utility. You can, of course, 614 | | modify these values as needed. 615 | | 616 | | Class name: .max-w-{size} 617 | | CSS property: max-width 618 | | 619 | */ 620 | 621 | maxWidth: { 622 | xs: '20rem', 623 | sm: '30rem', 624 | md: '40rem', 625 | lg: '50rem', 626 | xl: '60rem', 627 | '2xl': '70rem', 628 | '3xl': '80rem', 629 | '4xl': '90rem', 630 | '5xl': '100rem', 631 | full: '100%' 632 | }, 633 | 634 | /* 635 | |----------------------------------------------------------------------------- 636 | | Maximum height https://tailwindcss.com/docs/max-height 637 | |----------------------------------------------------------------------------- 638 | | 639 | | Here is where you define your maximum height utility sizes. These can 640 | | be percentage based, pixels, rems, or any other units. We provide a 641 | | couple common use-cases by default. You can, of course, modify 642 | | these values as needed. 643 | | 644 | | Class name: .max-h-{size} 645 | | CSS property: max-height 646 | | 647 | */ 648 | 649 | maxHeight: { 650 | full: '100%', 651 | screen: '100vh' 652 | }, 653 | 654 | /* 655 | |----------------------------------------------------------------------------- 656 | | Padding https://tailwindcss.com/docs/padding 657 | |----------------------------------------------------------------------------- 658 | | 659 | | Here is where you define your padding utility sizes. These can be 660 | | percentage based, pixels, rems, or any other units. By default we 661 | | provide a sensible rem based numeric scale plus a couple other 662 | | common use-cases like "1px". You can, of course, modify these 663 | | values as needed. 664 | | 665 | | Class name: .p{side?}-{size} 666 | | CSS property: padding 667 | | 668 | */ 669 | 670 | padding: { 671 | px: '1px', 672 | '0': '0', 673 | '1': '0.25rem', 674 | '2': '0.5rem', 675 | '3': '0.75rem', 676 | '4': '1rem', 677 | '5': '1.25rem', 678 | '6': '1.5rem', 679 | '8': '2rem', 680 | '10': '2.5rem', 681 | '12': '3rem', 682 | '16': '4rem', 683 | '20': '5rem', 684 | '24': '6rem', 685 | '32': '8rem' 686 | }, 687 | 688 | /* 689 | |----------------------------------------------------------------------------- 690 | | Margin https://tailwindcss.com/docs/margin 691 | |----------------------------------------------------------------------------- 692 | | 693 | | Here is where you define your margin utility sizes. These can be 694 | | percentage based, pixels, rems, or any other units. By default we 695 | | provide a sensible rem based numeric scale plus a couple other 696 | | common use-cases like "1px". You can, of course, modify these 697 | | values as needed. 698 | | 699 | | Class name: .m{side?}-{size} 700 | | CSS property: margin 701 | | 702 | */ 703 | 704 | margin: { 705 | auto: 'auto', 706 | px: '1px', 707 | '0': '0', 708 | '1': '0.25rem', 709 | '2': '0.5rem', 710 | '3': '0.75rem', 711 | '4': '1rem', 712 | '5': '1.25rem', 713 | '6': '1.5rem', 714 | '8': '2rem', 715 | '10': '2.5rem', 716 | '12': '3rem', 717 | '16': '4rem', 718 | '20': '5rem', 719 | '24': '6rem', 720 | '32': '8rem' 721 | }, 722 | 723 | /* 724 | |----------------------------------------------------------------------------- 725 | | Negative margin https://tailwindcss.com/docs/negative-margin 726 | |----------------------------------------------------------------------------- 727 | | 728 | | Here is where you define your negative margin utility sizes. These can 729 | | be percentage based, pixels, rems, or any other units. By default we 730 | | provide matching values to the padding scale since these utilities 731 | | generally get used together. You can, of course, modify these 732 | | values as needed. 733 | | 734 | | Class name: .-m{side?}-{size} 735 | | CSS property: margin 736 | | 737 | */ 738 | 739 | negativeMargin: { 740 | px: '1px', 741 | '0': '0', 742 | '1': '0.25rem', 743 | '2': '0.5rem', 744 | '3': '0.75rem', 745 | '4': '1rem', 746 | '5': '1.25rem', 747 | '6': '1.5rem', 748 | '8': '2rem', 749 | '10': '2.5rem', 750 | '12': '3rem', 751 | '16': '4rem', 752 | '20': '5rem', 753 | '24': '6rem', 754 | '32': '8rem' 755 | }, 756 | 757 | /* 758 | |----------------------------------------------------------------------------- 759 | | Shadows https://tailwindcss.com/docs/shadows 760 | |----------------------------------------------------------------------------- 761 | | 762 | | Here is where you define your shadow utilities. As you can see from 763 | | the defaults we provide, it's possible to apply multiple shadows 764 | | per utility using comma separation. 765 | | 766 | | If a `default` shadow is provided, it will be made available as the non- 767 | | suffixed `.shadow` utility. 768 | | 769 | | Class name: .shadow-{size?} 770 | | CSS property: box-shadow 771 | | 772 | */ 773 | 774 | shadows: { 775 | default: '0 2px 4px 0 rgba(0,0,0,0.10)', 776 | md: '0 4px 8px 0 rgba(0,0,0,0.12), 0 2px 4px 0 rgba(0,0,0,0.08)', 777 | lg: '0 15px 30px 0 rgba(0,0,0,0.11), 0 5px 15px 0 rgba(0,0,0,0.08)', 778 | inner: 'inset 0 2px 4px 0 rgba(0,0,0,0.06)', 779 | outline: '0 0 0 3px rgba(52,144,220,0.5)', 780 | none: 'none' 781 | }, 782 | 783 | /* 784 | |----------------------------------------------------------------------------- 785 | | Z-index https://tailwindcss.com/docs/z-index 786 | |----------------------------------------------------------------------------- 787 | | 788 | | Here is where you define your z-index utility values. By default we 789 | | provide a sensible numeric scale. You can, of course, modify these 790 | | values as needed. 791 | | 792 | | Class name: .z-{index} 793 | | CSS property: z-index 794 | | 795 | */ 796 | 797 | zIndex: { 798 | auto: 'auto', 799 | '0': 0, 800 | '10': 10, 801 | '20': 20, 802 | '30': 30, 803 | '40': 40, 804 | '50': 50 805 | }, 806 | 807 | /* 808 | |----------------------------------------------------------------------------- 809 | | Opacity https://tailwindcss.com/docs/opacity 810 | |----------------------------------------------------------------------------- 811 | | 812 | | Here is where you define your opacity utility values. By default we 813 | | provide a sensible numeric scale. You can, of course, modify these 814 | | values as needed. 815 | | 816 | | Class name: .opacity-{name} 817 | | CSS property: opacity 818 | | 819 | */ 820 | 821 | opacity: { 822 | '0': '0', 823 | '25': '.25', 824 | '50': '.5', 825 | '75': '.75', 826 | '100': '1' 827 | }, 828 | 829 | /* 830 | |----------------------------------------------------------------------------- 831 | | SVG fill https://tailwindcss.com/docs/svg 832 | |----------------------------------------------------------------------------- 833 | | 834 | | Here is where you define your SVG fill colors. By default we just provide 835 | | `fill-current` which sets the fill to the current text color. This lets you 836 | | specify a fill color using existing text color utilities and helps keep the 837 | | generated CSS file size down. 838 | | 839 | | Class name: .fill-{name} 840 | | CSS property: fill 841 | | 842 | */ 843 | 844 | svgFill: { 845 | current: 'currentColor' 846 | }, 847 | 848 | /* 849 | |----------------------------------------------------------------------------- 850 | | SVG stroke https://tailwindcss.com/docs/svg 851 | |----------------------------------------------------------------------------- 852 | | 853 | | Here is where you define your SVG stroke colors. By default we just provide 854 | | `stroke-current` which sets the stroke to the current text color. This lets 855 | | you specify a stroke color using existing text color utilities and helps 856 | | keep the generated CSS file size down. 857 | | 858 | | Class name: .stroke-{name} 859 | | CSS property: stroke 860 | | 861 | */ 862 | 863 | svgStroke: { 864 | current: 'currentColor' 865 | }, 866 | 867 | /* 868 | |----------------------------------------------------------------------------- 869 | | Modules https://tailwindcss.com/docs/configuration#modules 870 | |----------------------------------------------------------------------------- 871 | | 872 | | Here is where you control which modules are generated and what variants are 873 | | generated for each of those modules. 874 | | 875 | | Currently supported variants: 876 | | - responsive 877 | | - hover 878 | | - focus 879 | | - focus-within 880 | | - active 881 | | - group-hover 882 | | 883 | | To disable a module completely, use `false` instead of an array. 884 | | 885 | */ 886 | 887 | modules: { 888 | appearance: ['responsive'], 889 | backgroundAttachment: ['responsive'], 890 | backgroundColors: ['responsive', 'hover', 'focus'], 891 | backgroundPosition: ['responsive'], 892 | backgroundRepeat: ['responsive'], 893 | backgroundSize: ['responsive'], 894 | borderCollapse: [], 895 | borderColors: ['responsive', 'hover', 'focus'], 896 | borderRadius: ['responsive'], 897 | borderStyle: ['responsive'], 898 | borderWidths: ['responsive'], 899 | cursor: ['responsive'], 900 | display: ['responsive'], 901 | flexbox: ['responsive'], 902 | float: ['responsive'], 903 | fonts: ['responsive'], 904 | fontWeights: ['responsive', 'hover', 'focus'], 905 | height: ['responsive'], 906 | leading: ['responsive'], 907 | lists: ['responsive'], 908 | margin: ['responsive'], 909 | maxHeight: ['responsive'], 910 | maxWidth: ['responsive'], 911 | minHeight: ['responsive'], 912 | minWidth: ['responsive'], 913 | negativeMargin: ['responsive'], 914 | objectFit: false, 915 | objectPosition: false, 916 | opacity: ['responsive'], 917 | outline: ['focus'], 918 | overflow: ['responsive'], 919 | padding: ['responsive'], 920 | pointerEvents: ['responsive'], 921 | position: ['responsive'], 922 | resize: ['responsive'], 923 | shadows: ['responsive', 'hover', 'focus'], 924 | svgFill: [], 925 | svgStroke: [], 926 | tableLayout: ['responsive'], 927 | textAlign: ['responsive'], 928 | textColors: ['responsive', 'hover', 'focus'], 929 | textSizes: ['responsive'], 930 | textStyle: ['responsive', 'hover', 'focus'], 931 | tracking: ['responsive'], 932 | userSelect: ['responsive'], 933 | verticalAlign: ['responsive'], 934 | visibility: ['responsive'], 935 | whitespace: ['responsive'], 936 | width: ['responsive'], 937 | zIndex: ['responsive'] 938 | }, 939 | 940 | /* 941 | |----------------------------------------------------------------------------- 942 | | Plugins https://tailwindcss.com/docs/plugins 943 | |----------------------------------------------------------------------------- 944 | | 945 | | Here is where you can register any plugins you'd like to use in your 946 | | project. Tailwind's built-in `container` plugin is enabled by default to 947 | | give you a Bootstrap-style responsive container component out of the box. 948 | | 949 | | Be sure to view the complete plugin documentation to learn more about how 950 | | the plugin system works. 951 | | 952 | */ 953 | 954 | plugins: [ 955 | require('tailwindcss/plugins/container')({ 956 | // center: true, 957 | // padding: '1rem', 958 | }) 959 | ], 960 | 961 | /* 962 | |----------------------------------------------------------------------------- 963 | | Advanced Options https://tailwindcss.com/docs/configuration#options 964 | |----------------------------------------------------------------------------- 965 | | 966 | | Here is where you can tweak advanced configuration options. We recommend 967 | | leaving these options alone unless you absolutely need to change them. 968 | | 969 | */ 970 | 971 | options: { 972 | prefix: '', 973 | important: false, 974 | separator: ':' 975 | } 976 | } 977 | --------------------------------------------------------------------------------