├── 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 |
2 |
3 |
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 |
2 |
3 |
4 | Mevn Auth
5 |
6 |
7 | Welcome, {{ user.name }}
8 |
9 |
10 |
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 |
2 |
3 |
4 | {{ message.message }}
5 |
6 |
7 |
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 |
2 |
3 | Confirming email ...
4 |
5 |
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 |
2 |
6 |
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 |
2 |
3 |
14 |
15 | {{ error }}
16 |
17 |
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 |
2 |
3 |
4 |
5 | Please confirm your email address. Didn't receive an email? Click here to resend email.
6 |
7 |
8 |
9 |
Mevn
10 |
11 | Sign In
12 | Join Now
13 |
14 |
15 | Logout
16 |
17 |
18 |
19 |
20 |
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 |
2 |
3 |
4 |
Forgot Password
5 |
6 |
7 |
15 |
21 |
22 |
23 |
24 |
25 |
26 |
73 |
--------------------------------------------------------------------------------
/client/pages/ResetPassword.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Reset Your Password
5 |
6 |
7 |
16 |
22 |
23 |
24 |
25 |
26 |
27 |
77 |
--------------------------------------------------------------------------------
/client/pages/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Login
5 |
6 |
7 |
15 |
24 |
25 | Forgot Password ?
26 |
27 |
33 |
34 |
35 |
36 |
37 |
38 |
86 |
--------------------------------------------------------------------------------
/client/pages/Register.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Register
5 |
6 |
7 |
15 |
23 |
32 |
38 |
39 |
40 |
41 |
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 |
--------------------------------------------------------------------------------