├── .gitignore ├── README.md ├── api ├── app.js ├── config │ └── passport.js ├── controllers │ ├── index.js │ └── users.js ├── models │ ├── index.js │ └── users.js ├── package.json ├── serverless.yml └── utils │ └── index.js ├── database └── serverless.yml ├── permissions └── serverless.yml ├── serverless.template.yml └── site ├── README.md ├── package.json ├── public ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── fullstack-app-artwork.png ├── fullstack-app-title.png ├── index.html ├── manifest.json └── robots.txt ├── serverless.yml └── src ├── App.js ├── App.module.css ├── config.js ├── fragments └── Loading │ ├── Loading.js │ ├── Loading.module.css │ └── index.js ├── index.css ├── index.js ├── pages ├── Auth │ ├── Auth.js │ └── Auth.module.css ├── Dashboard │ ├── Dashboard.js │ └── Dashboard.module.css └── Home │ ├── Home.js │ └── Home.module.css └── utils ├── api.js ├── helpers.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.sublime-project 3 | *.sublime-workspace 4 | *.log 5 | .serverless 6 | v8-compile-cache-* 7 | jest/* 8 | coverage 9 | testProjects/*/package-lock.json 10 | testProjects/*/yarn.lock 11 | .serverlessUnzipped 12 | node_modules 13 | .vscode/ 14 | .eslintcache 15 | dist 16 | .idea 17 | build/ 18 | .env* 19 | .cache* 20 | .serverless 21 | .serverless_nextjs 22 | .serverless_plugins 23 | env.js 24 | tmp 25 | package-lock.json 26 | yarn.lock 27 | test 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Serverless Fullstack Application Express React DynamoDB AWS Lambda AWS HTTP API](https://s3.amazonaws.com/assets.github.serverless/components/readme-serverless-framework-fullstack-application.png 2 | )](https://www.serverless-fullstack-app.com) 3 | 4 | A complete, serverless, full-stack application built on AWS Lambda, AWS HTTP API, Express.js, React and DynamoDB. 5 | 6 | #### Live Demo: [https://www.serverless-fullstack-app.com](https://www.serverless-fullstack-app.com) 7 | 8 | ## Quick Start 9 | 10 | Install the latest version of the Serverless Framework: 11 | 12 | 13 | ``` 14 | npm i -g serverless 15 | ``` 16 | 17 | After installation, make sure you connect your AWS account by setting a provider in the org setting page on the [Serverless Dashboard](https://app.serverless.com). 18 | 19 | Then, initialize the `fullstack-app` template: 20 | 21 | ``` 22 | serverless init fullstack-app 23 | cd fullstack-app 24 | ``` 25 | 26 | Then, add the following environment variables in an `.env` file in the root directory, like this: 27 | 28 | ```text 29 | # This signs you JWT tokens used for auth. Enter a random string in here that's ~40 characters in length. 30 | tokenSecret=yourSecretKey 31 | 32 | # Only add this if you want a custom domain. Purchase it on AWS Route53 in your target AWS account first. 33 | domain=serverless-fullstack-app.com 34 | ``` 35 | 36 | In the root folder of the project, run `serverless deploy` 37 | 38 | Lastly, you will need to add your API domain manually to your React application in `./site/src/config.js`, so that you interact with your serverless Express.js back-end. You can find the your API url by going into `./api` and running `serverless info` and copying the `url:` value. It should look something like this `https://9jfalnal19.execute-api.us-east-1.amazonaws.com` or it will look like the custom domain you have set. 39 | 40 | **Note:** Upon the first deployment of your website, it will take a 2-3 minutes for the Cloudfront (CDN) URL to work. Until then, you can access it via the `bucketUrl`. 41 | 42 | After initial deployment, we recommend deploying only the parts you are changing, not the entire thing together (why risk deploying your database with a code change?). To do this, `cd` into a part of the application and run `serverless deploy`. 43 | 44 | When working on the `./api` we highly recommend using `serverless dev`. This command watches your code, auto-deploys it, and streams `console.log()` statements and errors directly to your CLI in real-time! 45 | 46 | If you want to add custom domains to your landing pages and API, either hardcode them in your `serverless.yml` or reference them as environment variables in `serverless.yml`, like this: 47 | 48 | ```yaml 49 | inputs: 50 | domain: ${env:domain} 51 | ``` 52 | 53 | ```text 54 | domain=serverless-fullstack-app.com 55 | ``` 56 | 57 | Support for stages is built in. 58 | 59 | You can deploy everything or individual components to different stages via the `--stage` flag, like this: 60 | 61 | `serverless deploy --stage prod` 62 | 63 | Or, you can hardcode the stage in `serverless.yml` (not recommended): 64 | 65 | ```yaml 66 | app: fullstack 67 | component: express@0.0.20 68 | name: fullstack-api 69 | stage: prod # Put the stage in here 70 | ``` 71 | 72 | Lastly, you can add separate environment variables for each stage using `.env` files with the stage name in them: 73 | 74 | ```bash 75 | .env # Any stage 76 | .env.dev # "dev" stage only 77 | .env.prod # "prod" stage only 78 | ``` 79 | 80 | Then simply reference those environment variables using Serverless Variables in your YAML: 81 | 82 | ```yaml 83 | app: fullstack 84 | component: express@0.0.20 85 | name: fullstack-api 86 | 87 | inputs: 88 | domain: api.${env:domain} 89 | ``` 90 | 91 | And deploy! 92 | 93 | `serverless deploy --stage prod` 94 | 95 | Enjoy! This is a work in progress and we will continue to add funcitonality to this. 96 | 97 | ## Other Resources 98 | 99 | For more details on each part of this fullstack application, check out these resources: 100 | 101 | * [Serverless Components](https://github.com/serverless/components) 102 | * [Serverless Express](https://github.com/serverless-components/express) 103 | * [Serverless Website](https://github.com/serverless-components/website) 104 | * [Serverless AWS DynamoDB](https://github.com/serverless-components/aws-dynamodb) 105 | * [Serverless AWS IAM Role](https://github.com/serverless-components/aws-iam-role) 106 | 107 | ## Guides 108 | 109 | ### How To Debug CORS Errors 110 | 111 | If you are running into CORS errors, see our guide on debugging them [within the Express Component's repo](https://github.com/serverless-components/express/blob/master/README.md#how-to-debug-cors-errors) 112 | -------------------------------------------------------------------------------- /api/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const app = express() 3 | const passport = require('passport') 4 | const { 5 | users 6 | } = require('./controllers') 7 | 8 | /** 9 | * Configure Passport 10 | */ 11 | 12 | try { require('./config/passport')(passport) } 13 | catch (error) { console.log(error) } 14 | 15 | /** 16 | * Configure Express.js Middleware 17 | */ 18 | 19 | // Enable CORS 20 | app.use(function (req, res, next) { 21 | res.header('Access-Control-Allow-Origin', '*') 22 | res.header('Access-Control-Allow-Methods', '*') 23 | res.header('Access-Control-Allow-Headers', '*') 24 | res.header('x-powered-by', 'serverless-express') 25 | next() 26 | }) 27 | 28 | // Initialize Passport and restore authentication state, if any, from the session 29 | app.use(passport.initialize()) 30 | app.use(passport.session()) 31 | 32 | // Enable JSON use 33 | app.use(express.json()) 34 | 35 | // Since Express doesn't support error handling of promises out of the box, 36 | // this handler enables that 37 | const asyncHandler = fn => (req, res, next) => { 38 | return Promise 39 | .resolve(fn(req, res, next)) 40 | .catch(next); 41 | }; 42 | 43 | /** 44 | * Routes - Public 45 | */ 46 | 47 | app.options(`*`, (req, res) => { 48 | res.status(200).send() 49 | }) 50 | 51 | app.post(`/users/register`, asyncHandler(users.register)) 52 | 53 | app.post(`/users/login`, asyncHandler(users.login)) 54 | 55 | app.get(`/test/`, (req, res) => { 56 | res.status(200).send('Request received') 57 | }) 58 | 59 | /** 60 | * Routes - Protected 61 | */ 62 | 63 | app.post(`/user`, passport.authenticate('jwt', { session: false }), asyncHandler(users.get)) 64 | 65 | /** 66 | * Routes - Catch-All 67 | */ 68 | 69 | app.get(`/*`, (req, res) => { 70 | res.status(404).send('Route not found') 71 | }) 72 | 73 | /** 74 | * Error Handler 75 | */ 76 | app.use(function (err, req, res, next) { 77 | console.error(err) 78 | res.status(500).json({ error: `Internal Serverless Error - "${err.message}"` }) 79 | }) 80 | 81 | module.exports = app -------------------------------------------------------------------------------- /api/config/passport.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Config: Passport.js 3 | */ 4 | 5 | const StrategyJWT = require('passport-jwt').Strategy 6 | const ExtractJWT = require('passport-jwt').ExtractJwt 7 | const { users } = require('../models') 8 | const { comparePassword } = require('../utils') 9 | 10 | module.exports = (passport) => { 11 | 12 | const options = {} 13 | options.jwtFromRequest = ExtractJWT.fromAuthHeaderAsBearerToken() 14 | options.secretOrKey = process.env.tokenSecret || 'secret_j91jasf0j1asfkl' // Change this to only use your own secret token 15 | 16 | passport.use(new StrategyJWT(options, async (jwtPayload, done) => { 17 | let user 18 | try { user = await users.getById(jwtPayload.id) } 19 | catch (error) { 20 | console.log(error) 21 | return done(error, null) 22 | } 23 | 24 | if (!user) { return done(null, false) } 25 | return done(null, user) 26 | })) 27 | } -------------------------------------------------------------------------------- /api/controllers/index.js: -------------------------------------------------------------------------------- 1 | const users = require('./users') 2 | 3 | module.exports = { 4 | users, 5 | } -------------------------------------------------------------------------------- /api/controllers/users.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Controllers: Users 3 | */ 4 | 5 | const jwt = require('jsonwebtoken') 6 | const { users } = require('../models') 7 | const { comparePassword } = require('../utils') 8 | 9 | /** 10 | * Save 11 | * @param {*} req 12 | * @param {*} res 13 | * @param {*} next 14 | */ 15 | const register = async (req, res, next) => { 16 | 17 | try { 18 | await users.register(req.body) 19 | } catch (error) { 20 | return res.status(400).json({ error: error.message }) 21 | } 22 | 23 | let user 24 | try { 25 | user = await users.getByEmail(req.body.email) 26 | } catch (error) { 27 | console.log(error) 28 | return next(error, null) 29 | } 30 | 31 | const token = jwt.sign(user, process.env.tokenSecret, { 32 | expiresIn: 604800 // 1 week 33 | }) 34 | 35 | res.json({ message: 'Authentication successful', token }) 36 | } 37 | 38 | /** 39 | * Sign a user in 40 | * @param {*} req 41 | * @param {*} res 42 | * @param {*} next 43 | */ 44 | const login = async (req, res, next) => { 45 | 46 | let user 47 | try { user = await users.getByEmail(req.body.email) } 48 | catch (error) { return done(error, null) } 49 | 50 | if (!user) { 51 | return res.status(404).send({ error: 'Authentication failed. User not found.' }) 52 | } 53 | 54 | const isCorrect = comparePassword(req.body.password, user.password) 55 | if (!isCorrect) { 56 | return res.status(401).send({ error: 'Authentication failed. Wrong password.' }) 57 | } 58 | 59 | const token = jwt.sign(user, process.env.tokenSecret, { 60 | expiresIn: 604800 // 1 week 61 | }) 62 | 63 | res.json({ message: 'Authentication successful', token }) 64 | } 65 | 66 | /** 67 | * Get a user 68 | * @param {*} req 69 | * @param {*} res 70 | * @param {*} next 71 | */ 72 | const get = async (req, res, next) => { 73 | const user = users.convertToPublicFormat(req.user) 74 | res.json({ user }) 75 | } 76 | 77 | module.exports = { 78 | register, 79 | login, 80 | get, 81 | } -------------------------------------------------------------------------------- /api/models/index.js: -------------------------------------------------------------------------------- 1 | const users = require('./users') 2 | 3 | module.exports = { 4 | users, 5 | } -------------------------------------------------------------------------------- /api/models/users.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Model: Users 3 | */ 4 | 5 | const AWS = require('aws-sdk') 6 | const shortid = require('shortid') 7 | const utils = require('../utils') 8 | 9 | const dynamodb = new AWS.DynamoDB.DocumentClient({ 10 | region: process.env.AWS_REGION 11 | }) 12 | 13 | /** 14 | * Register user 15 | * @param {string} user.email User email 16 | * @param {string} user.password User password 17 | */ 18 | const register = async(user = {}) => { 19 | 20 | // Validate 21 | if (!user.email) { 22 | throw new Error(`"email" is required`) 23 | } 24 | if (!user.password) { 25 | throw new Error(`"password" is required`) 26 | } 27 | if (!utils.validateEmailAddress(user.email)) { 28 | throw new Error(`"${user.email}" is not a valid email address`) 29 | } 30 | 31 | // Check if user is already registered 32 | const existingUser = await getByEmail(user.email) 33 | if (existingUser) { 34 | throw new Error(`A user with email "${user.email}" is already registered`) 35 | } 36 | 37 | user.password = utils.hashPassword(user.password) 38 | 39 | // Save 40 | const params = { 41 | TableName: process.env.db, 42 | Item: { 43 | hk: user.email, 44 | sk: 'user', 45 | sk2: shortid.generate(), 46 | createdAt: Date.now(), 47 | updatedAt: Date.now(), 48 | password: user.password, 49 | } 50 | } 51 | 52 | await dynamodb.put(params).promise() 53 | } 54 | 55 | /** 56 | * Get user by email address 57 | * @param {string} email 58 | */ 59 | 60 | const getByEmail = async(email) => { 61 | 62 | // Validate 63 | if (!email) { 64 | throw new Error(`"email" is required`) 65 | } 66 | if (!utils.validateEmailAddress(email)) { 67 | throw new Error(`"${email}" is not a valid email address`) 68 | } 69 | 70 | // Query 71 | const params = { 72 | TableName: process.env.db, 73 | KeyConditionExpression: 'hk = :hk', 74 | ExpressionAttributeValues: { ':hk': email } 75 | } 76 | 77 | let user = await dynamodb.query(params).promise() 78 | 79 | user = user.Items && user.Items[0] ? user.Items[0] : null 80 | if (user) { 81 | user.id = user.sk2 82 | user.email = user.hk 83 | } 84 | return user 85 | } 86 | 87 | /** 88 | * Get user by id 89 | * @param {string} id 90 | */ 91 | 92 | const getById = async(id) => { 93 | 94 | // Validate 95 | if (!id) { 96 | throw new Error(`"id" is required`) 97 | } 98 | 99 | // Query 100 | const params = { 101 | TableName: process.env.db, 102 | IndexName: process.env.dbIndex1, 103 | KeyConditionExpression: 'sk2 = :sk2 and sk = :sk', 104 | ExpressionAttributeValues: { ':sk2': id, ':sk': 'user' } 105 | } 106 | let user = await dynamodb.query(params).promise() 107 | 108 | user = user.Items && user.Items[0] ? user.Items[0] : null 109 | if (user) { 110 | user.id = user.sk2 111 | user.email = user.hk 112 | } 113 | return user 114 | } 115 | 116 | /** 117 | * Convert user record to public format 118 | * This hides the keys used for the dynamodb's single table design and returns human-readable properties. 119 | * @param {*} user 120 | */ 121 | const convertToPublicFormat = (user = {}) => { 122 | user.email = user.hk || null 123 | user.id = user.sk2 || null 124 | if (user.hk) delete user.hk 125 | if (user.sk) delete user.sk 126 | if (user.sk2) delete user.sk2 127 | if (user.password) delete user.password 128 | return user 129 | } 130 | 131 | module.exports = { 132 | register, 133 | getByEmail, 134 | getById, 135 | convertToPublicFormat, 136 | } 137 | -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-fullstack-app-api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "dependencies": { 7 | "bcryptjs": "^2.4.3", 8 | "express": "^4.17.1", 9 | "jsonwebtoken": "^8.5.1", 10 | "passport": "^0.4.1", 11 | "passport-jwt": "^4.0.0", 12 | "shortid": "^2.2.15" 13 | }, 14 | "devDependencies": {}, 15 | "scripts": { 16 | "test": "echo \"Error: no test specified\" && exit 1" 17 | }, 18 | "author": "", 19 | "license": "ISC" 20 | } 21 | -------------------------------------------------------------------------------- /api/serverless.yml: -------------------------------------------------------------------------------- 1 | component: express 2 | name: api 3 | 4 | inputs: 5 | # Express application source code. 6 | src: ./ 7 | # Permissions required for the AWS Lambda function to interact with other resources 8 | roleName: ${output:permissions.name} 9 | # Enable this when you want to set a custom domain. 10 | # domain: api.${env:domain} 11 | # Environment variables 12 | env: 13 | # AWS DynamoDB Table name. Needed for the code to access it. 14 | db: ${output:database.name} 15 | # AWS DynamoDB Table Index name. Needed for the code to access it. 16 | dbIndex1: ${output:database.indexes.gsi1.name} 17 | # A secret token to sign the JWT tokens with. 18 | tokenSecret: secret_1234 # Change to secret via environment variable: ${env:tokenSecret} 19 | -------------------------------------------------------------------------------- /api/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Utils 3 | */ 4 | 5 | const bcrypt = require('bcryptjs') 6 | 7 | /** 8 | * Validate email address 9 | */ 10 | const validateEmailAddress = (email) => { 11 | var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ 12 | return re.test(String(email).toLowerCase()) 13 | } 14 | 15 | /** 16 | * Hash password 17 | * @param {*} user 18 | */ 19 | const hashPassword = (password) => { 20 | const salt = bcrypt.genSaltSync(10) 21 | return bcrypt.hashSync(password, salt) 22 | } 23 | 24 | /** 25 | * Compare password 26 | */ 27 | const comparePassword = (candidatePassword, trustedPassword) => { 28 | return bcrypt.compareSync(candidatePassword, trustedPassword) 29 | } 30 | 31 | module.exports = { 32 | hashPassword, 33 | comparePassword, 34 | validateEmailAddress 35 | } -------------------------------------------------------------------------------- /database/serverless.yml: -------------------------------------------------------------------------------- 1 | component: aws-dynamodb 2 | name: database 3 | 4 | inputs: 5 | name: ${name}-${stage} 6 | region: us-east-1 7 | # Don't delete the Database Table if "serverless remove" is run 8 | deletionPolicy: retain 9 | # Simple, single-table design 10 | attributeDefinitions: 11 | - AttributeName: hk 12 | AttributeType: S 13 | - AttributeName: sk 14 | AttributeType: S 15 | - AttributeName: sk2 16 | AttributeType: S 17 | keySchema: 18 | - AttributeName: hk 19 | KeyType: HASH 20 | - AttributeName: sk 21 | KeyType: RANGE 22 | globalSecondaryIndexes: 23 | - IndexName: gsi1 24 | KeySchema: 25 | - AttributeName: sk2 26 | KeyType: HASH 27 | - AttributeName: sk 28 | KeyType: RANGE 29 | Projection: 30 | ProjectionType: ALL 31 | -------------------------------------------------------------------------------- /permissions/serverless.yml: -------------------------------------------------------------------------------- 1 | component: aws-iam-role 2 | name: permissions 3 | 4 | inputs: 5 | name: ${name}-${stage} 6 | region: us-east-1 7 | service: lambda.amazonaws.com 8 | policy: 9 | # AWS Lambda function containing Express Logs and Assume Role access 10 | - Effect: Allow 11 | Action: 12 | - sts:AssumeRole 13 | - logs:CreateLogGroup 14 | - logs:CreateLogStream 15 | - logs:PutLogEvents 16 | Resource: "*" 17 | # AWS DynamoDB Table access 18 | - Effect: Allow 19 | Action: 20 | - dynamodb:DescribeTable 21 | - dynamodb:Query 22 | - dynamodb:GetItem 23 | - dynamodb:PutItem 24 | - dynamodb:UpdateItem 25 | - dynamodb:DeleteItem 26 | Resource: 27 | - ${output:database.arn} 28 | - ${output:database.arn}/index/* 29 | -------------------------------------------------------------------------------- /serverless.template.yml: -------------------------------------------------------------------------------- 1 | name: fullstack-app 2 | org: serverlessinc 3 | description: Deploy a serverless fullstack application using Express.js and React on AWS Lambda and AWS HTTP API 4 | keywords: serverless, express, react, fullstack, aws, aws lambda 5 | repo: https://github.com/serverless-components/fullstack-app 6 | license: MIT -------------------------------------------------------------------------------- /site/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `yarn start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `yarn test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `yarn build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `yarn eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `yarn build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-fullstack-app-website", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "react": "^16.13.1", 10 | "react-dom": "^16.13.1", 11 | "react-scripts": "3.4.3", 12 | "js-cookie": "^2.2.1", 13 | "moment": "^2.24.0", 14 | "react-router-dom": "^5.1.2" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": "react-app" 24 | }, 25 | "browserslist": { 26 | "production": [ 27 | ">0.2%", 28 | "not dead", 29 | "not op_mini all" 30 | ], 31 | "development": [ 32 | "last 1 chrome version", 33 | "last 1 firefox version", 34 | "last 1 safari version" 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /site/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-components/fullstack-app/188365a1a16eb4d1daeee280d0a15d56dd6d672d/site/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /site/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-components/fullstack-app/188365a1a16eb4d1daeee280d0a15d56dd6d672d/site/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /site/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-components/fullstack-app/188365a1a16eb4d1daeee280d0a15d56dd6d672d/site/public/apple-touch-icon.png -------------------------------------------------------------------------------- /site/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-components/fullstack-app/188365a1a16eb4d1daeee280d0a15d56dd6d672d/site/public/favicon-16x16.png -------------------------------------------------------------------------------- /site/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-components/fullstack-app/188365a1a16eb4d1daeee280d0a15d56dd6d672d/site/public/favicon-32x32.png -------------------------------------------------------------------------------- /site/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-components/fullstack-app/188365a1a16eb4d1daeee280d0a15d56dd6d672d/site/public/favicon.ico -------------------------------------------------------------------------------- /site/public/fullstack-app-artwork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-components/fullstack-app/188365a1a16eb4d1daeee280d0a15d56dd6d672d/site/public/fullstack-app-artwork.png -------------------------------------------------------------------------------- /site/public/fullstack-app-title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless-components/fullstack-app/188365a1a16eb4d1daeee280d0a15d56dd6d672d/site/public/fullstack-app-title.png -------------------------------------------------------------------------------- /site/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 15 | 16 | 20 | 21 | Serverless Fullstack Application 22 | 23 | 24 | 25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /site/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Serverless Fullstack Application", 3 | "name": "Serverless Fullstack Application", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "android-chrome-192x192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "android-chrome-512x512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /site/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /site/serverless.yml: -------------------------------------------------------------------------------- 1 | component: website 2 | name: site 3 | 4 | inputs: 5 | # React application. "hook" runs before deployment to build the source code. "dist" is the built artifact directory which is uploaded. 6 | src: 7 | src: ./ 8 | hook: npm run build 9 | dist: build 10 | # Enable this when you want to set a custom domain. 11 | # domain: ${env:domain} 12 | -------------------------------------------------------------------------------- /site/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { 3 | BrowserRouter as Router, 4 | Switch, 5 | Route, 6 | } from 'react-router-dom' 7 | import Home from './pages/Home/Home' 8 | import Auth from './pages/Auth/Auth' 9 | import Dashboard from './pages/Dashboard/Dashboard' 10 | import { getSession } from './utils' 11 | 12 | export default class App extends Component { 13 | 14 | constructor(props) { 15 | super(props) 16 | this.state = {} 17 | } 18 | 19 | async componentDidMount() { 20 | // console.log(getSession()) 21 | } 22 | 23 | render() { 24 | return ( 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 41 | 42 | 43 | 44 | ) 45 | } 46 | } 47 | 48 | /** 49 | * A component to protect routes. 50 | * Shows Auth page if the user is not authenticated 51 | */ 52 | const PrivateRoute = ({ component, ...options }) => { 53 | 54 | const session = getSession() 55 | 56 | const finalComponent = session ? Dashboard : Home 57 | return 58 | } -------------------------------------------------------------------------------- /site/src/App.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | align-content: center; 5 | justify-content: center; 6 | box-sizing: border-box; 7 | overflow: hidden; 8 | width: 100%; 9 | height: 100vh; 10 | } 11 | 12 | .containerInner { 13 | display: flex; 14 | flex-direction: column; 15 | box-sizing: border-box; 16 | background: #000; 17 | width: 100%; 18 | height: 100%; 19 | align-self: center; 20 | max-width: 700px; 21 | padding: 50px 15px 30px 15px; 22 | } 23 | 24 | .containerLoading { 25 | display: flex; 26 | flex-direction: row; 27 | width: 100%; 28 | height: auto; 29 | justify-content: center; 30 | align-content: center; 31 | padding: 160px 0 0 0; 32 | } 33 | 34 | .column { 35 | display: flex; 36 | flex-direction: row; 37 | box-sizing: border-box; 38 | height: 100%; 39 | width: 50%; 40 | } 41 | 42 | .row { 43 | display: flex; 44 | flex-direction: column; 45 | box-sizing: border-box; 46 | width: 100%; 47 | } 48 | 49 | .heroArtwork { 50 | display: flex; 51 | flex-direction: row; 52 | box-sizing: border-box; 53 | align-content: center; 54 | justify-content: center; 55 | width: 100%; 56 | } 57 | 58 | .heroArtwork img { 59 | width: auto; 60 | height: auto; 61 | align-self: center; 62 | max-height: 100%; 63 | max-width: 500px; 64 | user-select: none; 65 | } 66 | 67 | .heroTitle { 68 | display: flex; 69 | flex-direction: row; 70 | box-sizing: border-box; 71 | align-content: center; 72 | justify-content: center; 73 | width: 100%; 74 | } 75 | 76 | .heroTitle img { 77 | width: auto; 78 | height: auto; 79 | align-self: center; 80 | max-height: 100%; 81 | max-width: 500px; 82 | user-select: none; 83 | } 84 | 85 | .heroDescription { 86 | display: flex; 87 | text-align: center; 88 | font-size: 18px; 89 | line-height: 30px; 90 | margin: 10px 0 32px 0px; 91 | } 92 | 93 | .error { 94 | display: flex; 95 | flex-direction: column; 96 | width: 100%; 97 | margin: 18px 0; 98 | align-content: center; 99 | justify-content: center; 100 | text-align: center; 101 | color: #FD5750; 102 | font-size: 18px; 103 | } 104 | 105 | .success { 106 | display: flex; 107 | flex-direction: column; 108 | width: 100%; 109 | margin: 32px 0; 110 | align-content: center; 111 | justify-content: center; 112 | text-align: center; 113 | text-transform: lowercase; 114 | color: #78f542; 115 | font-size: 22px; 116 | } 117 | 118 | .containerRegister {} 119 | 120 | .containerSignIn {} 121 | 122 | .socialButtons { 123 | display: flex; 124 | flex-direction: row; 125 | box-sizing: border-box; 126 | margin: 15px auto 0px auto; 127 | width: 100%; 128 | } 129 | 130 | .buttonGithub { 131 | display: flex; 132 | flex-direction: row; 133 | justify-content: center; 134 | align-content: center; 135 | box-sizing: border-box; 136 | margin: 0 auto 10px auto; 137 | width: 100%; 138 | padding: 12px; 139 | border-radius: 4px; 140 | font-size: 19px; 141 | font-weight: 500; 142 | opacity: 0.6; 143 | transition: all 0.3s ease; 144 | text-transform: lowercase; 145 | } 146 | 147 | .buttonGithub:hover { 148 | cursor: pointer; 149 | opacity: 1; 150 | } 151 | 152 | .buttonGithub img { 153 | max-height: 28px; 154 | margin-right: 13px; 155 | margin-top: 1.5px; 156 | } 157 | 158 | .buttonGoogle { 159 | display: flex; 160 | flex-direction: row; 161 | justify-content: center; 162 | align-content: center; 163 | box-sizing: border-box; 164 | width: 100%; 165 | padding: 12px; 166 | border-radius: 4px; 167 | font-size: 19px; 168 | font-weight: 500; 169 | opacity: 0.6; 170 | transition: all 0.3s ease; 171 | text-transform: lowercase; 172 | } 173 | 174 | .buttonGoogle:hover { 175 | cursor: pointer; 176 | opacity: 1; 177 | } 178 | 179 | .buttonGoogle img { 180 | max-height: 25px; 181 | margin-right: 11px; 182 | margin-top: 3px; 183 | } 184 | 185 | .formType { 186 | display: flex; 187 | flex-direction: row; 188 | margin: 0px auto 0px auto; 189 | width: 100%; 190 | padding: 10px 0; 191 | font-size: 22px; 192 | color: rgba(255,255,255,0.6); 193 | text-transform: lowercase; 194 | user-select: none; 195 | } 196 | 197 | .formTypeRegister { 198 | display: flex; 199 | width: 50%; 200 | padding: 5px 40px 5px 0; 201 | justify-content: flex-end; 202 | border-right: 2px solid rgba(255,255,255,0.15); 203 | } 204 | 205 | .formTypeRegister:hover { 206 | cursor: pointer; 207 | } 208 | 209 | .formTypeSignIn { 210 | display: flex; 211 | width: 50%; 212 | padding: 5px 0 5px 40px; 213 | text-align: left; 214 | } 215 | 216 | .formTypeSignIn:hover { 217 | cursor: pointer; 218 | } 219 | 220 | .formTypeActive { 221 | color: rgba(255,255,255,1); 222 | } 223 | 224 | .form { 225 | display: flex; 226 | flex-direction: column; 227 | max-width: 380px; 228 | width: 100%; 229 | margin: 10px auto 0 auto; 230 | text-align: center; 231 | } 232 | 233 | .formField { 234 | display: flex; 235 | flex-direction: column; 236 | width: 100%; 237 | margin-bottom: 25px; 238 | } 239 | 240 | 241 | .formLabel { 242 | font-size: 19px; 243 | font-weight: 500; 244 | margin: 0 0 10px 0; 245 | user-select: none; 246 | display: none; 247 | } 248 | 249 | .formInput { 250 | text-align: center; 251 | font-size: 22px; 252 | padding: 10px 0px 22px 0px; 253 | border-bottom: 1.5px solid rgba(255,255,255,0.4); 254 | border-top: 1.5px solid transparent; 255 | border-right: 1.5px solid transparent; 256 | border-left: 1.5px solid transparent; 257 | transition: all 0.6s ease; 258 | background: none; 259 | outline: none; 260 | color: #ffffff; 261 | caret-color: white; 262 | } 263 | 264 | .formInput::placeholder { 265 | text-align: center; 266 | font-size: 22px; 267 | color: rgba(255,255,255,0.5); 268 | margin: 0; 269 | } 270 | 271 | .formInput:focus { 272 | border-bottom: 1.5px solid rgba(255,255,255,1); 273 | } 274 | 275 | .formError { 276 | margin: 10px 0 10px 0; 277 | color: #FD5750; 278 | font-size: 18px; 279 | line-height: 32px; 280 | } 281 | 282 | .formButton { 283 | margin: 16px auto 0 auto!important; 284 | } 285 | 286 | .formButton:hover { 287 | cursor: pointer; 288 | transform: scale(1.05); 289 | } 290 | 291 | .formButton:active { 292 | cursor: pointer; 293 | transform: scale(1); 294 | } 295 | 296 | .forgotPassword { 297 | display: flex; 298 | flex-direction: row; 299 | justify-content: center; 300 | align-content: center; 301 | margin: 20px auto 5px auto; 302 | width: 100%; 303 | padding: 10px 0; 304 | font-size: 19px; 305 | color: rgba(255,255,255,0.6); 306 | transition: all 0.3s ease; 307 | } 308 | 309 | .forgotPassword:hover { 310 | cursor: pointer; 311 | color: rgba(255,255,255,1); 312 | } 313 | 314 | .githubLink { 315 | display: flex; 316 | justify-content: center; 317 | align-content: center; 318 | width: 100%; 319 | margin: 26px 0 40px 0; 320 | } 321 | 322 | .githubLink a { 323 | font-size: 20px; 324 | color: #FD5750; 325 | opacity: 0.7; 326 | transition: all 0.3s ease; 327 | text-decoration: none; 328 | } 329 | 330 | .githubLink:hover a { 331 | cursor: pointer; 332 | opacity: 1; 333 | } -------------------------------------------------------------------------------- /site/src/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Global Config 3 | */ 4 | 5 | const config = {} 6 | 7 | // Domains 8 | config.domains = {} 9 | 10 | /** 11 | * API Domain 12 | * Add the domain from your serverless express.js back-end here. 13 | * This will enable your front-end to communicate with your back-end. 14 | * (e.g. 'https://api.mydomain.com' or 'https://091jafsl10.execute-api.us-east-1.amazonaws.com') 15 | */ 16 | config.domains.api = null 17 | 18 | export default config -------------------------------------------------------------------------------- /site/src/fragments/Loading/Loading.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import styles from './Loading.module.css' 3 | 4 | export default class Loading extends Component { 5 | 6 | constructor(props) { 7 | super(props) 8 | this.state = {} 9 | } 10 | 11 | async componentDidMount() {} 12 | 13 | render() { 14 | return ( 15 |
16 |
17 | 18 | {`Loading`} 25 | 26 |

loading...

27 | 28 |
29 |
30 | ) 31 | } 32 | } -------------------------------------------------------------------------------- /site/src/fragments/Loading/Loading.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | align-content: center; 5 | justify-content: center; 6 | box-sizing: border-box; 7 | width: auto; 8 | height: 100px; 9 | opacity: 0.45; 10 | text-align: center; 11 | } 12 | 13 | .container img { 14 | display: flex; 15 | align-self: center; 16 | height: 100%; 17 | width: auto; 18 | max-width: 100px; 19 | max-height: 100px; 20 | margin: 0 0 20px 0; 21 | animation-duration: 0.4s; 22 | animation-name: pulse; 23 | animation-iteration-count: infinite; 24 | animation-direction: alternate; 25 | animation-timing-function: ease; 26 | filter: none; 27 | -webkit-filter: grayscale(100%); 28 | -moz-filter: grayscale(100%); 29 | -ms-filter: grayscale(100%); 30 | -o-filter: grayscale(100%); 31 | } 32 | 33 | .container p { 34 | font-size: 22px; 35 | color: rgba(255,255,255,0.5); 36 | } 37 | 38 | @keyframes pulse { 39 | from { 40 | transform: scale(1); 41 | } 42 | 43 | to { 44 | transform: scale(1.15); 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /site/src/fragments/Loading/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Loading' -------------------------------------------------------------------------------- /site/src/index.css: -------------------------------------------------------------------------------- 1 | html, body, #root { 2 | display: block; 3 | width: 100%; 4 | height: 100%; 5 | min-height: 100vh; 6 | box-sizing: border-box; 7 | background: #000; 8 | color: #ffffff; 9 | margin: 0; 10 | font-family: 'Titillium Web', sans-serif; 11 | font-weight: 500; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | font-size: 18px; 15 | } 16 | 17 | div, h1, h2, h3, h4, h5, h6, p, span, input, button, li { 18 | font-family: 'Titillium Web', sans-serif; 19 | font-size: 24px; 20 | color: #ffffff; 21 | } 22 | 23 | /** 24 | * Links 25 | */ 26 | 27 | a, .link { 28 | font-family: 'Titillium Web', sans-serif; 29 | font-size: 24px; 30 | font-weight: 400; 31 | color: rgba(255,255,255,0.6); 32 | text-decoration: none; 33 | transition: all 0.14s ease; 34 | } 35 | 36 | a:hover, .link:hover { 37 | cursor: pointer; 38 | color: rgba(255,255,255,1); 39 | } 40 | 41 | a:active, .link:active { 42 | color: rgba(255,255,255,0.6); 43 | } 44 | 45 | /** 46 | * Buttons 47 | */ 48 | 49 | .buttonPrimaryLarge { 50 | text-align: center; 51 | font-size: 18px; 52 | width: 100%; 53 | max-width: 360px; 54 | height: auto; 55 | margin: 20px 0 0 0; 56 | padding: 24px 24px 28px 24px; 57 | background: #fd5750; 58 | color: #fff; 59 | font-family: 'Titillium Web', sans-serif; 60 | font-size: 24px; 61 | font-weight: 600; 62 | border-radius: 4px; 63 | border: 0px; 64 | transition: all 0.14s ease; 65 | user-select: none; 66 | outline: none; 67 | text-transform: lowercase; 68 | } 69 | 70 | .buttonPrimaryLarge:hover { 71 | cursor: pointer; 72 | transform: scale(1.03); 73 | } 74 | 75 | .buttonPrimaryLarge:active { 76 | cursor: pointer; 77 | transform: scale(1); 78 | outline: none; 79 | } 80 | 81 | /** 82 | * Animations 83 | */ 84 | 85 | .animateFadeIn { 86 | animation: fadeIn 0.45s; 87 | } 88 | 89 | .animateScaleIn { 90 | animation: scaleIn 0.35s; 91 | } 92 | 93 | .animateFlicker { 94 | animation: flicker 2.5s infinite; 95 | } 96 | 97 | @keyframes fadeIn { 98 | from { opacity: 0; } 99 | to { opacity: 1; } 100 | } 101 | 102 | @keyframes scaleIn { 103 | from { 104 | opacity: 0; 105 | transform: scale(0.8); 106 | } 107 | to { 108 | opacity: 1; 109 | transform: scale(1); 110 | } 111 | } 112 | 113 | @keyframes flicker { 114 | 0% { 115 | opacity: 0.3; 116 | } 117 | 50% { 118 | opacity: 1; 119 | } 120 | 100% { 121 | opacity: 0.3; 122 | } 123 | } -------------------------------------------------------------------------------- /site/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | import './index.css' 5 | 6 | /** 7 | * Render App 8 | */ 9 | 10 | ReactDOM.render( 11 | 12 | 13 | , 14 | document.getElementById('root') 15 | ) -------------------------------------------------------------------------------- /site/src/pages/Auth/Auth.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { 3 | Link, 4 | withRouter, 5 | } from 'react-router-dom' 6 | import Loading from '../../fragments/Loading' 7 | import styles from './Auth.module.css' 8 | import { 9 | userRegister, 10 | userLogin, 11 | userGet, 12 | saveSession, 13 | } from '../../utils' 14 | 15 | class Auth extends Component { 16 | 17 | constructor(props) { 18 | super(props) 19 | 20 | const pathName = window.location.pathname.replace('/', '') 21 | 22 | this.state = {} 23 | this.state.state = pathName 24 | this.state.loading = true 25 | this.state.error = null 26 | this.state.formEmail = '' 27 | this.state.formPassword = '' 28 | 29 | // Bindings 30 | this.handleFormInput = this.handleFormInput.bind(this) 31 | this.handleFormSubmit = this.handleFormSubmit.bind(this) 32 | this.handleFormTypeChange = this.handleFormTypeChange.bind(this) 33 | } 34 | 35 | /** 36 | * Component did mount 37 | */ 38 | componentDidMount() { 39 | this.setState({ 40 | loading: false 41 | }) 42 | 43 | // Clear query params 44 | const url = document.location.href 45 | window.history.pushState({}, '', url.split('?')[0]) 46 | } 47 | 48 | /** 49 | * Handles a form change 50 | */ 51 | handleFormTypeChange(type) { 52 | this.setState({ state: type }, 53 | () => { 54 | this.props.history.push(`/${type}`) 55 | }) 56 | } 57 | 58 | /** 59 | * Handle text changes within form fields 60 | */ 61 | handleFormInput(field, value) { 62 | value = value.trim() 63 | 64 | const nextState = {} 65 | nextState[field] = value 66 | 67 | this.setState(Object.assign(this.state, nextState)) 68 | } 69 | 70 | /** 71 | * Handles form submission 72 | * @param {object} evt 73 | */ 74 | async handleFormSubmit(evt) { 75 | evt.preventDefault() 76 | 77 | this.setState({ loading: true }) 78 | 79 | // Validate email 80 | if (!this.state.formEmail) { 81 | return this.setState({ 82 | loading: false, 83 | formError: 'email is required' 84 | }) 85 | } 86 | 87 | // Validate password 88 | if (!this.state.formPassword) { 89 | return this.setState({ 90 | loading: false, 91 | formError: 'password is required' 92 | }) 93 | } 94 | 95 | let token 96 | try { 97 | if (this.state.state === 'register') { 98 | token = await userRegister(this.state.formEmail, this.state.formPassword) 99 | } else { 100 | token = await userLogin(this.state.formEmail, this.state.formPassword) 101 | } 102 | } catch (error) { 103 | console.log(error) 104 | if (error.message) { 105 | this.setState({ 106 | formError: error.message, 107 | loading: false 108 | }) 109 | } else { 110 | this.setState({ 111 | formError: 'Sorry, something unknown went wrong. Please try again.', 112 | loading: false 113 | }) 114 | } 115 | return 116 | } 117 | 118 | // Fetch user record and set session in cookie 119 | let user = await userGet(token.token) 120 | user = user.user 121 | saveSession(user.id, user.email, token.token) 122 | 123 | window.location.replace('/') 124 | } 125 | 126 | render() { 127 | 128 | return ( 129 |
130 |
131 | 132 | { /* Logo */} 133 | 134 | 135 | serverless-fullstack-application 140 | 141 | 142 | { /* Loading */} 143 | 144 | {this.state.loading && ( 145 |
146 | {< Loading className={styles.containerLoading} />} 147 |
148 | )} 149 | 150 | { /* Registration Form */} 151 | 152 | {!this.state.loading && ( 153 |
154 |
{ this.handleFormTypeChange('register') }}> 159 | Register 160 |
161 |
{ this.handleFormTypeChange('login') }}> 166 | Sign-In 167 |
168 |
169 | )} 170 | 171 | {this.state.state === 'register' && !this.state.loading && ( 172 |
173 | 174 |
175 |
176 | 177 | { this.handleFormInput('formEmail', e.target.value) }} 183 | /> 184 |
185 |
186 | 187 | { this.handleFormInput('formPassword', e.target.value) }} 193 | /> 194 |
195 | 196 | {this.state.formError && ( 197 |
{this.state.formError}
198 | )} 199 | 200 | 205 | 206 |
207 |
208 | )} 209 | 210 | {this.state.state === 'login' && !this.state.loading && ( 211 |
212 | 213 |
214 |
215 | 216 | { this.handleFormInput('formEmail', e.target.value) }} 222 | /> 223 |
224 |
225 | 226 | { this.handleFormInput('formPassword', e.target.value) }} 232 | /> 233 |
234 | 235 | {this.state.formError && ( 236 |
{this.state.formError}
237 | )} 238 | 239 | 240 |
241 |
242 | )} 243 |
244 |
245 | ) 246 | } 247 | } 248 | 249 | export default withRouter(Auth) -------------------------------------------------------------------------------- /site/src/pages/Auth/Auth.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | align-content: center; 5 | justify-content: center; 6 | box-sizing: border-box; 7 | overflow: hidden; 8 | width: 100%; 9 | height: 100vh; 10 | } 11 | 12 | .containerInner { 13 | display: flex; 14 | flex-direction: column; 15 | box-sizing: border-box; 16 | background: #000; 17 | width: 100%; 18 | height: 100%; 19 | align-self: center; 20 | max-width: 700px; 21 | padding: 50px 15px 30px 15px; 22 | } 23 | 24 | .containerLoading { 25 | display: flex; 26 | flex-direction: row; 27 | width: 100%; 28 | height: auto; 29 | justify-content: center; 30 | align-content: center; 31 | padding: 160px 0 0 0; 32 | } 33 | 34 | .logo { 35 | display: flex; 36 | flex-direction: row; 37 | box-sizing: border-box; 38 | align-content: center; 39 | justify-content: center; 40 | width: 100%; 41 | } 42 | 43 | .logo img { 44 | width: auto; 45 | height: auto; 46 | align-self: center; 47 | max-height: 100%; 48 | max-width: 400px; 49 | user-select: none; 50 | } 51 | 52 | .error { 53 | display: flex; 54 | flex-direction: column; 55 | width: 100%; 56 | margin: 18px 0; 57 | align-content: center; 58 | justify-content: center; 59 | text-align: center; 60 | color: #FD5750; 61 | font-size: 18px; 62 | } 63 | 64 | .success { 65 | display: flex; 66 | flex-direction: column; 67 | width: 100%; 68 | margin: 32px 0; 69 | align-content: center; 70 | justify-content: center; 71 | text-align: center; 72 | text-transform: lowercase; 73 | color: #78f542; 74 | font-size: 22px; 75 | } 76 | 77 | .containerRegister {} 78 | 79 | .containerSignIn {} 80 | 81 | .formType { 82 | display: flex; 83 | flex-direction: row; 84 | margin: 26px auto 0px auto; 85 | width: 100%; 86 | padding: 10px 0; 87 | font-size: 22px; 88 | color: rgba(255,255,255,0.6); 89 | text-transform: lowercase; 90 | user-select: none; 91 | } 92 | 93 | .formTypeRegister { 94 | display: flex; 95 | width: 50%; 96 | padding: 5px 40px 5px 0; 97 | justify-content: flex-end; 98 | border-right: 2px solid rgba(255,255,255,0.15); 99 | color: rgba(255,255,255,0.6); 100 | } 101 | 102 | .formTypeRegister:hover { 103 | cursor: pointer; 104 | } 105 | 106 | .formTypeSignIn { 107 | display: flex; 108 | width: 50%; 109 | padding: 5px 0 5px 40px; 110 | text-align: left; 111 | color: rgba(255,255,255,0.6); 112 | } 113 | 114 | .formTypeSignIn:hover { 115 | cursor: pointer; 116 | } 117 | 118 | .formTypeActive { 119 | color: rgba(255,255,255,1); 120 | } 121 | 122 | .form { 123 | display: flex; 124 | flex-direction: column; 125 | max-width: 380px; 126 | width: 100%; 127 | margin: 10px auto 0 auto; 128 | text-align: center; 129 | } 130 | 131 | .formField { 132 | display: flex; 133 | flex-direction: column; 134 | width: 100%; 135 | margin-bottom: 25px; 136 | } 137 | 138 | 139 | .formLabel { 140 | font-size: 19px; 141 | font-weight: 500; 142 | margin: 0 0 10px 0; 143 | user-select: none; 144 | display: none; 145 | } 146 | 147 | .formInput { 148 | text-align: center; 149 | font-size: 22px; 150 | padding: 10px 0px 22px 0px; 151 | border-bottom: 1.5px solid rgba(255,255,255,0.4); 152 | border-top: 1.5px solid transparent; 153 | border-right: 1.5px solid transparent; 154 | border-left: 1.5px solid transparent; 155 | transition: all 0.6s ease; 156 | background: none; 157 | outline: none; 158 | color: #ffffff; 159 | caret-color: white; 160 | } 161 | 162 | .formInput::placeholder { 163 | text-align: center; 164 | font-size: 22px; 165 | color: rgba(255,255,255,0.5); 166 | margin: 0; 167 | } 168 | 169 | .formInput:focus { 170 | border-bottom: 1.5px solid rgba(255,255,255,1); 171 | } 172 | 173 | .formError { 174 | margin: 10px 0 10px 0; 175 | color: #FD5750; 176 | font-size: 18px; 177 | line-height: 32px; 178 | } 179 | 180 | .formButton { 181 | margin: 16px auto 0 auto!important; 182 | } 183 | 184 | .formButton:hover { 185 | cursor: pointer; 186 | transform: scale(1.05); 187 | } 188 | 189 | .formButton:active { 190 | cursor: pointer; 191 | transform: scale(1); 192 | } 193 | 194 | .forgotPassword { 195 | display: flex; 196 | flex-direction: row; 197 | justify-content: center; 198 | align-content: center; 199 | margin: 20px auto 5px auto; 200 | width: 100%; 201 | padding: 10px 0; 202 | font-size: 19px; 203 | color: rgba(255,255,255,0.6); 204 | transition: all 0.3s ease; 205 | } 206 | 207 | .forgotPassword:hover { 208 | cursor: pointer; 209 | color: rgba(255,255,255,1); 210 | } 211 | 212 | .githubLink { 213 | display: flex; 214 | justify-content: center; 215 | align-content: center; 216 | width: 100%; 217 | margin: 26px 0 40px 0; 218 | } 219 | 220 | .githubLink a { 221 | font-size: 20px; 222 | color: #FD5750; 223 | opacity: 0.7; 224 | transition: all 0.3s ease; 225 | text-decoration: none; 226 | } 227 | 228 | .githubLink:hover a { 229 | cursor: pointer; 230 | opacity: 1; 231 | } -------------------------------------------------------------------------------- /site/src/pages/Dashboard/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { 3 | withRouter 4 | } from 'react-router-dom' 5 | import styles from './Dashboard.module.css' 6 | import { 7 | getSession, 8 | deleteSession 9 | } from '../../utils' 10 | 11 | class Dashboard extends Component { 12 | 13 | constructor(props) { 14 | super(props) 15 | this.state = {} 16 | 17 | // Bindings 18 | this.logout = this.logout.bind(this) 19 | } 20 | 21 | async componentDidMount() { 22 | 23 | const userSession = getSession() 24 | 25 | this.setState({ 26 | session: userSession, 27 | }) 28 | } 29 | 30 | /** 31 | * Log user out by clearing cookie and redirecting 32 | */ 33 | logout() { 34 | deleteSession() 35 | this.props.history.push(`/`) 36 | } 37 | 38 | render() { 39 | 40 | return ( 41 |
42 |
43 | 44 | { /* Navigation */ } 45 | 46 |
47 |
49 | { this.state.session ? this.state.session.userEmail : '' } 50 |
51 |
54 | logout 55 |
56 |
57 | 58 | { /* Content */ } 59 | 60 |
61 | 62 |
63 | serverless-fullstack-application 68 |
69 | 70 |
71 | Welcome to your serverless fullstack dashboard... 72 |
73 | 74 |
75 | 76 |
77 |
78 | ) 79 | } 80 | } 81 | 82 | export default withRouter(Dashboard) -------------------------------------------------------------------------------- /site/src/pages/Dashboard/Dashboard.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | align-content: center; 5 | justify-content: center; 6 | box-sizing: border-box; 7 | overflow: hidden; 8 | width: 100%; 9 | height: 100vh; 10 | } 11 | 12 | .containerInner { 13 | display: flex; 14 | flex-direction: column; 15 | box-sizing: border-box; 16 | background: #000; 17 | width: 100%; 18 | height: 100%; 19 | padding: 0 0 30px 0; 20 | } 21 | 22 | .navigationContainer { 23 | display: flex; 24 | flex-direction: row; 25 | justify-content: flex-end; 26 | align-content: center; 27 | box-sizing: border-box; 28 | background: #000; 29 | width: 100%; 30 | height: 80px; 31 | padding: 25px 45px 0 45px; 32 | } 33 | 34 | .navigationContainer div { 35 | margin-left: 40px; 36 | } 37 | 38 | .contentContainer { 39 | display: flex; 40 | flex-direction: column; 41 | align-content: center; 42 | box-sizing: border-box; 43 | background: #000; 44 | width: 100%; 45 | height: 100%; 46 | padding: 7% 15px 45px 15px; 47 | } 48 | 49 | .welcomeMessage { 50 | display: flex; 51 | flex-direction: column; 52 | align-content: center; 53 | text-align: center; 54 | width: 100%; 55 | height: auto; 56 | font-size: 28px; 57 | } 58 | 59 | .artwork { 60 | display: flex; 61 | flex-direction: row; 62 | box-sizing: border-box; 63 | align-content: center; 64 | justify-content: center; 65 | width: 100%; 66 | height: 250px; 67 | } 68 | 69 | .artwork img { 70 | width: auto; 71 | height: auto; 72 | align-self: center; 73 | max-height: 100%; 74 | max-width: 500px; 75 | user-select: none; 76 | } 77 | 78 | /** 79 | * Special 80 | */ 81 | 82 | ::-moz-selection { 83 | background: #fd5750; 84 | color: #ffffff; 85 | opacity: 1; 86 | } 87 | 88 | ::selection { 89 | background: #fd5750; 90 | color: #ffffff; 91 | opacity: 1; 92 | } -------------------------------------------------------------------------------- /site/src/pages/Home/Home.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { 3 | Link, 4 | withRouter 5 | } from 'react-router-dom' 6 | import styles from './Home.module.css' 7 | 8 | class Home extends Component { 9 | 10 | constructor(props) { 11 | super(props) 12 | this.state = {} 13 | } 14 | 15 | async componentDidMount() { } 16 | 17 | render() { 18 | 19 | return ( 20 |
21 |
22 | 23 | { /* Hero Artwork */} 24 | 25 |
26 | serverless-fullstack-application 31 |
32 |
33 | serverless-fullstack-application 38 |
39 | 40 | { /* Hero Description */} 41 | 42 |
43 | A serverless full-stack application built with AWS Lambda, AWS HTTP API, Express.js, React & AWS DynamoDB. 44 |
45 | 46 | { /* Call To Action */} 47 | 48 |
49 | 50 | 51 | 54 | 55 | 56 | sign-in 57 |
58 |
59 |
60 | ) 61 | } 62 | } 63 | 64 | export default withRouter(Home) -------------------------------------------------------------------------------- /site/src/pages/Home/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | align-content: center; 5 | justify-content: center; 6 | box-sizing: border-box; 7 | overflow: hidden; 8 | width: 100%; 9 | height: 100vh; 10 | } 11 | 12 | .containerInner { 13 | display: flex; 14 | flex-direction: column; 15 | box-sizing: border-box; 16 | background: #000; 17 | width: 100%; 18 | height: 100%; 19 | align-self: center; 20 | max-width: 700px; 21 | padding: 50px 15px 30px 15px; 22 | } 23 | 24 | .heroArtwork { 25 | display: flex; 26 | flex-direction: row; 27 | box-sizing: border-box; 28 | align-content: center; 29 | justify-content: center; 30 | width: 100%; 31 | } 32 | 33 | .heroArtwork img { 34 | width: auto; 35 | height: auto; 36 | align-self: center; 37 | max-height: 100%; 38 | max-width: 500px; 39 | user-select: none; 40 | } 41 | 42 | .heroTitle { 43 | display: flex; 44 | flex-direction: row; 45 | box-sizing: border-box; 46 | align-content: center; 47 | justify-content: center; 48 | width: 100%; 49 | } 50 | 51 | .heroTitle img { 52 | width: auto; 53 | height: auto; 54 | align-self: center; 55 | max-height: 100%; 56 | max-width: 500px; 57 | user-select: none; 58 | } 59 | 60 | .heroDescription { 61 | display: flex; 62 | text-align: center; 63 | font-weight: 400; 64 | font-size: 24px; 65 | line-height: 38px; 66 | margin: 32px 0px 52px 0; 67 | } 68 | 69 | .containerCta { 70 | display: flex; 71 | flex-direction: column; 72 | justify-content: center; 73 | align-content: center; 74 | text-align: center; 75 | } 76 | 77 | .containerCta button { 78 | margin: 0px auto 20px auto; 79 | } 80 | 81 | .containerCta .linkSignIn { 82 | padding: 10px; 83 | color: #fd5750; 84 | opacity: 0.65; 85 | } 86 | 87 | .containerCta .linkSignIn:hover { 88 | opacity: 1; 89 | } 90 | 91 | /** 92 | * Special 93 | */ 94 | 95 | ::-moz-selection { 96 | background: #fd5750; 97 | color: #ffffff; 98 | opacity: 1; 99 | } 100 | 101 | ::selection { 102 | background: #fd5750; 103 | color: #ffffff; 104 | opacity: 1; 105 | } -------------------------------------------------------------------------------- /site/src/utils/api.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Utils: Back-end 3 | */ 4 | 5 | import config from '../config' 6 | 7 | /** 8 | * Register a new user 9 | */ 10 | export const userRegister = async (email, password) => { 11 | return await requestApi('/users/register', 'POST', { email, password }) 12 | } 13 | 14 | /** 15 | * Login a new user 16 | */ 17 | export const userLogin = async (email, password) => { 18 | return await requestApi('/users/login', 'POST', { email, password }) 19 | } 20 | 21 | /** 22 | * userGet 23 | */ 24 | export const userGet = async (token) => { 25 | return await requestApi('/user', 'POST', null, { 26 | Authorization: `Bearer ${token}` 27 | }) 28 | } 29 | 30 | /** 31 | * API request to call the backend 32 | */ 33 | export const requestApi = async ( 34 | path = '', 35 | method = 'GET', 36 | data = null, 37 | headers = {}) => { 38 | 39 | // Check if API URL has been set 40 | if (!config?.domains?.api) { 41 | throw new Error(`Error: Missing API Domain – Please add the API domain from your serverless Express.js back-end to this front-end application. You can do this in the "site" folder, in the "./config.js" file. Instructions are listed there and in the documentation.`) 42 | } 43 | 44 | // Prepare URL 45 | if (!path.startsWith('/')) { 46 | path = `/${path}` 47 | } 48 | const url = `${config.domains.api}${path}` 49 | 50 | // Set headers 51 | headers = Object.assign( 52 | { 'Content-Type': 'application/json' }, 53 | headers 54 | ) 55 | 56 | // Default options are marked with * 57 | const response = await fetch(url, { 58 | method: method.toUpperCase(), 59 | mode: 'cors', 60 | cache: 'no-cache', 61 | headers, 62 | body: data ? JSON.stringify(data) : null 63 | }) 64 | 65 | if (response.status < 200 || response.status >= 300) { 66 | const error = await response.json() 67 | throw new Error(error.error) 68 | } 69 | 70 | return await response.json() 71 | } -------------------------------------------------------------------------------- /site/src/utils/helpers.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | /** 4 | * Format Org and Username correctly for the Serverless Platform backend 5 | */ 6 | export const formatOrgAndUsername = (name = '') => { 7 | name = name.toString().toLowerCase().replace(/[^a-z\d-]+/gi, '-') 8 | // Remove multiple instances of hyphens 9 | name = name.replace(/-{2,}/g, '-') 10 | if (name.length > 40) { 11 | name = name.substring(0, 40) 12 | } 13 | return name 14 | } 15 | 16 | /** 17 | * Parse query parameters in a URL 18 | * @param {*} searchString 19 | */ 20 | export const parseQueryParams = (searchString = null) => { 21 | if (!searchString) { 22 | return null 23 | } 24 | 25 | // Clone string 26 | let clonedParams = (' ' + searchString).slice(1) 27 | 28 | return clonedParams 29 | .substr(1) 30 | .split('&') 31 | .filter((el) => el.length) 32 | .map((el) => el.split('=')) 33 | .reduce( 34 | (accumulator, currentValue) => 35 | Object.assign(accumulator, { 36 | [decodeURIComponent(currentValue.shift())]: decodeURIComponent(currentValue.pop()) 37 | }), 38 | {} 39 | ) 40 | } 41 | 42 | /** 43 | * Parse hash fragment parameters in a URL 44 | */ 45 | export const parseHashFragment = (hashString) => { 46 | const hashData = {} 47 | let hash = decodeURI(hashString) 48 | hash = hash.split('&') 49 | hash.forEach((val) => { 50 | val = val.replace('#', '') 51 | hashData[val.split('=')[0]] = val.split('=')[1] 52 | }) 53 | return hashData 54 | } 55 | 56 | /** 57 | * Save session in browser cookie 58 | */ 59 | export const saveSession = (userId, userEmail, userToken) => { 60 | Cookies.set('serverless', { userId, userEmail, userToken }) 61 | } 62 | 63 | /** 64 | * Get session in browser cookie 65 | */ 66 | export const getSession = () => { 67 | const data = Cookies.get('serverless') 68 | return data ? JSON.parse(data) : null 69 | } 70 | 71 | /** 72 | * Delete session in browser cookie 73 | */ 74 | export const deleteSession = () => { 75 | Cookies.remove('serverless') 76 | } -------------------------------------------------------------------------------- /site/src/utils/index.js: -------------------------------------------------------------------------------- 1 | export * from './helpers' 2 | export * from './api' --------------------------------------------------------------------------------