Remove node.js app
@@ -1,22 +0,0 @@
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
# Matches multiple files with brace expansion notation
|
||||
# Set default charset
|
||||
[*.{js}]
|
||||
charset = utf-8
|
||||
|
||||
# 4 space indentation
|
||||
[*.js]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
|
||||
# Matches the exact files either package.json or .travis.yml
|
||||
[{package.json,serverless.yml}]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
28
.gitignore
vendored
@@ -1,28 +0,0 @@
|
||||
.DS_Store
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
*.log
|
||||
.serverless
|
||||
v8-compile-cache-*
|
||||
jest/*
|
||||
coverage
|
||||
testProjects/*/package-lock.json
|
||||
testProjects/*/yarn.lock
|
||||
.serverlessUnzipped
|
||||
node_modules
|
||||
.vscode/
|
||||
.eslintcache
|
||||
dist
|
||||
.idea
|
||||
build/
|
||||
.env*
|
||||
.cache*
|
||||
.serverless
|
||||
.serverless_nextjs
|
||||
.serverless_plugins
|
||||
env.js
|
||||
tmp
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
test
|
||||
logs/
|
||||
112
README.md
@@ -1,112 +0,0 @@
|
||||
[](https://www.serverless-fullstack-app.com)
|
||||
|
||||
A complete, serverless, full-stack application built on AWS Lambda, AWS HTTP API, Express.js, React and DynamoDB.
|
||||
|
||||
#### Live Demo: [https://www.serverless-fullstack-app.com](https://www.serverless-fullstack-app.com)
|
||||
|
||||
## Quick Start
|
||||
|
||||
Install the latest version of the Serverless Framework:
|
||||
|
||||
|
||||
```
|
||||
npm i -g serverless
|
||||
```
|
||||
|
||||
Then, initialize the `fullstack-app` template:
|
||||
|
||||
```
|
||||
serverless init fullstack-app
|
||||
cd fullstack-app
|
||||
```
|
||||
|
||||
Then, add your AWS credentials in the `.env` file in the root directory, like this:
|
||||
|
||||
```text
|
||||
AWS_ACCESS_KEY_ID=JAFJ89109JASFKLJASF
|
||||
AWS_SECRET_ACCESS_KEY=AJ91J9A0SFA0S9FSKAFLASJFLJ
|
||||
|
||||
# This signs you JWT tokens used for auth. Enter a random string in here that's ~40 characters in length.
|
||||
tokenSecret=yourSecretKey
|
||||
|
||||
# Only add this if you want a custom domain. Purchase it on AWS Route53 in your target AWS account first.
|
||||
domain=serverless-fullstack-app.com
|
||||
```
|
||||
|
||||
In the root folder of the project, run `serverless deploy`
|
||||
|
||||
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.
|
||||
|
||||
**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`.
|
||||
|
||||
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`.
|
||||
|
||||
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!
|
||||
|
||||
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:
|
||||
|
||||
```yaml
|
||||
inputs:
|
||||
domain: ${env:domain}
|
||||
```
|
||||
|
||||
```text
|
||||
domain=serverless-fullstack-app.com
|
||||
```
|
||||
|
||||
Support for stages is built in.
|
||||
|
||||
You can deploy everything or individual components to different stages via the `--stage` flag, like this:
|
||||
|
||||
`serverless deploy --stage prod`
|
||||
|
||||
Or, you can hardcode the stage in `serverless.yml` (not recommended):
|
||||
|
||||
```yaml
|
||||
app: fullstack
|
||||
component: express@0.0.20
|
||||
name: fullstack-api
|
||||
stage: prod # Put the stage in here
|
||||
```
|
||||
|
||||
Lastly, you can add separate environment variables for each stage using `.env` files with the stage name in them:
|
||||
|
||||
```bash
|
||||
.env # Any stage
|
||||
.env.dev # "dev" stage only
|
||||
.env.prod # "prod" stage only
|
||||
```
|
||||
|
||||
Then simply reference those environment variables using Serverless Variables in your YAML:
|
||||
|
||||
```yaml
|
||||
app: fullstack
|
||||
component: express@0.0.20
|
||||
name: fullstack-api
|
||||
|
||||
inputs:
|
||||
domain: api.${env:domain}
|
||||
```
|
||||
|
||||
And deploy!
|
||||
|
||||
`serverless deploy --stage prod`
|
||||
|
||||
Enjoy! This is a work in progress and we will continue to add funcitonality to this.
|
||||
|
||||
## Other Resources
|
||||
|
||||
For more details on each part of this fullstack application, check out these resources:
|
||||
|
||||
* [Serverless Components](https://github.com/serverless/components)
|
||||
* [Serverless Express](https://github.com/serverless-components/express)
|
||||
* [Serverless Website](https://github.com/serverless-components/website)
|
||||
* [Serverless AWS DynamoDB](https://github.com/serverless-components/aws-dynamodb)
|
||||
* [Serverless AWS IAM Role](https://github.com/serverless-components/aws-iam-role)
|
||||
|
||||
## Guides
|
||||
|
||||
### How To Debug CORS Errors
|
||||
|
||||
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)
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"extends": ["eslint:recommended", "prettier"],
|
||||
"env": {
|
||||
"node": true,
|
||||
"browser": false,
|
||||
"es6": true,
|
||||
"mocha": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
package.json
|
||||
package-lock.json
|
||||
105
api/app.js
@@ -1,105 +0,0 @@
|
||||
const express = require(`express`);
|
||||
const pino = require(`pino`);
|
||||
const pinoHttp = require("pino-http");
|
||||
const app = express();
|
||||
const passport = require(`passport`);
|
||||
const { users } = require(`./controllers`);
|
||||
|
||||
/**
|
||||
* logging
|
||||
*/
|
||||
let logger = pino();
|
||||
if (process.env.name === `test`) {
|
||||
logger = pino(pino.destination({ dest: `../logs/api.json`, sync: false }));
|
||||
}
|
||||
app.use(
|
||||
pinoHttp({
|
||||
logger: logger,
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* Configure Passport
|
||||
*/
|
||||
|
||||
try {
|
||||
require(`./config/passport`)(passport);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure Express.js Middleware
|
||||
*/
|
||||
|
||||
// Enable CORS
|
||||
app.use(function (req, res, next) {
|
||||
res.header(`Access-Control-Allow-Origin`, `*`);
|
||||
res.header(`Access-Control-Allow-Methods`, `*`);
|
||||
res.header(`Access-Control-Allow-Headers`, `*`);
|
||||
res.header(`x-powered-by`, `serverless-express`);
|
||||
next();
|
||||
});
|
||||
|
||||
// Initialize Passport and restore authentication state, if any, from the session
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
// Enable JSON use
|
||||
app.use(express.json());
|
||||
|
||||
// Since Express doesn't support error handling of promises out of the box,
|
||||
// this handler enables that
|
||||
const asyncHandler = (fn) => (req, res, next) => {
|
||||
return Promise.resolve(fn(req, res, next)).catch(next);
|
||||
};
|
||||
|
||||
/**
|
||||
* Routes - Public
|
||||
*/
|
||||
|
||||
app.options(`*`, (req, res) => {
|
||||
res.status(200).send();
|
||||
});
|
||||
|
||||
app.post(`/users/register`, asyncHandler(users.register));
|
||||
|
||||
app.post(`/users/login`, asyncHandler(users.login));
|
||||
|
||||
app.get(`/test/`, (req, res) => {
|
||||
res.status(200).send(`Request received`);
|
||||
});
|
||||
|
||||
/**
|
||||
* Routes - Protected
|
||||
*/
|
||||
|
||||
app.post(
|
||||
`/user`,
|
||||
passport.authenticate(`jwt`, { session: false }),
|
||||
asyncHandler(users.get)
|
||||
);
|
||||
|
||||
/**
|
||||
* Routes - Catch-All
|
||||
*/
|
||||
|
||||
app.get(`/*`, (req, res) => {
|
||||
res.status(404).send(`Route not found`);
|
||||
});
|
||||
|
||||
/**
|
||||
* Error Handler
|
||||
*/
|
||||
app.use(function (err, req, res, next) {
|
||||
if (res.headersSent) {
|
||||
return next(err);
|
||||
}
|
||||
req.log.error(err);
|
||||
res.status(err.code || 500);
|
||||
res.json({
|
||||
error: err.message || `Internal Server Error - "${err.message}"`,
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* Config: Passport.js
|
||||
*/
|
||||
|
||||
const StrategyJWT = require(`passport-jwt`).Strategy;
|
||||
const ExtractJWT = require(`passport-jwt`).ExtractJwt;
|
||||
const userUtils = require(`../controllers/users/utils`);
|
||||
const db = require(`../db`);
|
||||
|
||||
module.exports = (passport) => {
|
||||
const options = {};
|
||||
options.jwtFromRequest = ExtractJWT.fromAuthHeaderAsBearerToken();
|
||||
options.secretOrKey = process.env.tokenSecret;
|
||||
|
||||
passport.use(
|
||||
new StrategyJWT(options, async (jwtPayload, done) => {
|
||||
let user;
|
||||
try {
|
||||
user = await db.getById(`user`, jwtPayload.id);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return done(error, null);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return done(null, false);
|
||||
}
|
||||
return done(null, userUtils.convertToPublicFormat(user));
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
* Controller base actions
|
||||
*
|
||||
* This module contains base actions that follow
|
||||
* a template for CRUD operations on a DB and (optionally)
|
||||
* do custom things afterward to return a response.
|
||||
*/
|
||||
|
||||
const validation = require(`../../validation`);
|
||||
const db = require(`../../db`);
|
||||
|
||||
const createAction = async (req, res, config) => {
|
||||
validation.validateOrThrow(config.validationSchema, req.body);
|
||||
|
||||
const key = config.getObjectKey();
|
||||
let existingObject = await db.getByKey(key);
|
||||
if (existingObject) {
|
||||
throw customError(
|
||||
`An object with key "${key}" already exists in the database.`,
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
const dbObject = config.buildDbObject(req.body);
|
||||
await db.put(config.dbKind, dbObject);
|
||||
const createdObject = await db.getByKey(key);
|
||||
|
||||
const responseBody = config.buildResponseBody(createdObject);
|
||||
res.json(responseBody);
|
||||
};
|
||||
|
||||
const getByKeyAction = async (req, res, config) => {
|
||||
validation.validateOrThrow(config.validationSchema, req.body);
|
||||
const key = config.getObjectKey();
|
||||
let existingObject = await db.getByKey(key);
|
||||
|
||||
if (!existingObject) {
|
||||
return res
|
||||
.status(404)
|
||||
.send({ error: config.notFoundMessage });
|
||||
}
|
||||
|
||||
const responseBody = config.buildResponseBody(existingObject);
|
||||
res.json(responseBody);
|
||||
};
|
||||
|
||||
/**
|
||||
* Build a custom error
|
||||
* @param {string} message Error message
|
||||
* @param {number} code Error code, should be HTTP status code
|
||||
*/
|
||||
const customError = (message = "An error occurred.", code = 500) => {
|
||||
const error = new Error(message);
|
||||
error.code = code;
|
||||
return error;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createAction,
|
||||
getByKeyAction,
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
const users = require(`./users`);
|
||||
|
||||
module.exports = {
|
||||
users,
|
||||
};
|
||||
@@ -1,77 +0,0 @@
|
||||
/**
|
||||
* Controllers: Users
|
||||
*/
|
||||
|
||||
const schemas = require(`./schemas`);
|
||||
const base = require(`../base`);
|
||||
const userUtils = require(`./utils`);
|
||||
|
||||
/**
|
||||
* Save
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
const register = async (req, res) => {
|
||||
const config = {
|
||||
validationSchema: schemas.create,
|
||||
getObjectKey: () => req.body.email,
|
||||
buildDbObject: () => {
|
||||
return {
|
||||
hk: req.body.email,
|
||||
password: userUtils.hashPassword(req.body.password)
|
||||
};
|
||||
},
|
||||
dbKind: `user`,
|
||||
buildResponseBody: createdUser => {
|
||||
return {
|
||||
message: `Authentication successful`,
|
||||
token: userUtils.getToken(createdUser),
|
||||
};
|
||||
}
|
||||
};
|
||||
await base.createAction(req, res, config);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sign a user in
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
const login = async (req, res) => {
|
||||
const config = {
|
||||
validationSchema: schemas.login,
|
||||
getObjectKey: () => req.body.email,
|
||||
notFoundMessage: `Authentication failed. User not found.`,
|
||||
buildResponseBody: user => {
|
||||
const isCorrect = userUtils.comparePassword(req.body.password, user.password);
|
||||
if (!isCorrect) {
|
||||
return res
|
||||
.status(401)
|
||||
.send({ error: `Authentication failed. Wrong password.` });
|
||||
}
|
||||
|
||||
return {
|
||||
message: `Authentication successful`,
|
||||
token: userUtils.getToken(user),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
await base.getByKeyAction(req, res, config);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a user
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
const get = async (req, res) => {
|
||||
const user = userUtils.convertToPublicFormat(req.user);
|
||||
res.json({ user });
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
register,
|
||||
login,
|
||||
get,
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
const commonSchemas = require(`../../validation/commonSchemas`);
|
||||
|
||||
const login = {
|
||||
$schema: "http://json-schema.org/draft-07/schema#",
|
||||
title: "Login User",
|
||||
description: "An object to log-in a new user",
|
||||
type: "object",
|
||||
properties: {
|
||||
email: commonSchemas.email,
|
||||
password: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
required: ["email", "password"],
|
||||
};
|
||||
|
||||
const create = {
|
||||
$schema: "http://json-schema.org/draft-07/schema#",
|
||||
title: "Create User",
|
||||
description: "An object to create a new user",
|
||||
type: "object",
|
||||
properties: {
|
||||
email: commonSchemas.email,
|
||||
password: {
|
||||
type: "string",
|
||||
minLength: 10,
|
||||
maxLength: 50,
|
||||
},
|
||||
},
|
||||
required: ["email", "password"],
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
login,
|
||||
create
|
||||
};
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* User utilities
|
||||
*/
|
||||
|
||||
const bcrypt = require(`bcryptjs`);
|
||||
const jwt = require(`jsonwebtoken`);
|
||||
|
||||
const getToken = (user) => {
|
||||
return jwt.sign(
|
||||
convertToPublicFormat(user),
|
||||
process.env.tokenSecret,
|
||||
{
|
||||
expiresIn: 604800, // 1 week
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const convertToPublicFormat = (user = {}) => {
|
||||
user.email = user.email || user.hk || null;
|
||||
user.id = user.id || user.sk2 || null;
|
||||
if (user.hk) {
|
||||
delete user.hk;
|
||||
}
|
||||
if (user.sk) {
|
||||
delete user.sk;
|
||||
}
|
||||
if (user.sk2) {
|
||||
delete user.sk2;
|
||||
}
|
||||
if (user.password) {
|
||||
delete user.password;
|
||||
}
|
||||
return user;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hash password
|
||||
* @param {string} password Password to hash
|
||||
*/
|
||||
const hashPassword = (password) => {
|
||||
const salt = bcrypt.genSaltSync(10);
|
||||
return bcrypt.hashSync(password, salt);
|
||||
};
|
||||
|
||||
/**
|
||||
* Compare password
|
||||
* @param {string} candidatePassword Hashed password supplied by user
|
||||
* @param {string} trustedPassword Hashed password on record for user
|
||||
*/
|
||||
const comparePassword = (candidatePassword, trustedPassword) => {
|
||||
return bcrypt.compareSync(candidatePassword, trustedPassword);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getToken,
|
||||
convertToPublicFormat,
|
||||
hashPassword,
|
||||
comparePassword,
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
const AWS = require(`aws-sdk`);
|
||||
const shortid = require(`shortid`);
|
||||
const commonSchemas = require(`../validation/commonSchemas`);
|
||||
const validation = require(`../validation`);
|
||||
|
||||
const dynamodb = new AWS.DynamoDB.DocumentClient({
|
||||
region: process.env.AWS_REGION,
|
||||
});
|
||||
|
||||
const defaultParams = {
|
||||
TableName: process.env.db,
|
||||
};
|
||||
|
||||
const put = async (kind, item) => {
|
||||
const params = {
|
||||
...defaultParams,
|
||||
Item: {
|
||||
...item,
|
||||
sk: kind,
|
||||
sk2: shortid.generate(),
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
};
|
||||
|
||||
await dynamodb.put(params).promise();
|
||||
};
|
||||
|
||||
const getByKey = async (key) => {
|
||||
const params = {
|
||||
...defaultParams,
|
||||
KeyConditionExpression: `hk = :hk`,
|
||||
ExpressionAttributeValues: { ":hk": key },
|
||||
};
|
||||
|
||||
const obj = await dynamodb.query(params).promise();
|
||||
return obj.Items && obj.Items[0] ? obj.Items[0] : null;
|
||||
};
|
||||
|
||||
const getById = async (kind, id) => {
|
||||
validation.validateOrThrow(commonSchemas.id, id);
|
||||
|
||||
const params = {
|
||||
...defaultParams,
|
||||
IndexName: process.env.dbIndex1,
|
||||
KeyConditionExpression: `sk2 = :sk2 and sk = :sk`,
|
||||
ExpressionAttributeValues: {
|
||||
":sk2": id,
|
||||
":sk": kind,
|
||||
},
|
||||
};
|
||||
|
||||
const obj = await dynamodb.query(params).promise();
|
||||
return obj.Items && obj.Items[0] ? obj.Items[0] : null;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
put,
|
||||
getByKey,
|
||||
getById,
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"name": "forgetmenot-api",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "app.js",
|
||||
"dependencies": {
|
||||
"ajv": "^6.12.6",
|
||||
"aws-sdk": "^2.792.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"express": "^4.17.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"passport": "^0.4.1",
|
||||
"passport-jwt": "^4.0.0",
|
||||
"pino-http": "^5.3.0",
|
||||
"shortid": "^2.2.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^4.2.0",
|
||||
"chai-http": "^4.3.0",
|
||||
"eslint": "^7.12.1",
|
||||
"eslint-config-prettier": "^6.15.0",
|
||||
"faker": "^5.1.0",
|
||||
"mocha": "^8.2.1",
|
||||
"prettier": "^2.1.2"
|
||||
},
|
||||
"scripts": {
|
||||
"pretest": "eslint --ignore-path ../.gitignore .",
|
||||
"test": "mocha",
|
||||
"format": "prettier --write \"**/*.{js,json,md,yml}\""
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
component: express
|
||||
name: api
|
||||
|
||||
inputs:
|
||||
# Express application source code.
|
||||
src: ./
|
||||
# Permissions required for the AWS Lambda function to interact with other resources
|
||||
roleName: ${output:permissions.name}
|
||||
# Enable this when you want to set a custom domain.
|
||||
# domain: api.${env:domain}
|
||||
# Environment variables
|
||||
env:
|
||||
# AWS DynamoDB Table name. Needed for the code to access it.
|
||||
db: ${output:database.name}
|
||||
# AWS DynamoDB Table Index name. Needed for the code to access it.
|
||||
dbIndex1: ${output:database.indexes.gsi1.name}
|
||||
# A secret token to sign the JWT tokens with.
|
||||
tokenSecret: ${env:tokenSecret} # Change to secret via environment variable: ${env:tokenSecret}
|
||||
@@ -1,10 +0,0 @@
|
||||
module.exports = {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/*
|
||||
* Validation
|
||||
*/
|
||||
|
||||
const Ajv = require(`ajv`);
|
||||
|
||||
const validateOrThrow = (schema, data) => {
|
||||
const ajv = new Ajv();
|
||||
const valid = ajv.validate(schema, data);
|
||||
|
||||
if (!valid) {
|
||||
throw validationError(ajv.errors);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Build a custom validation error
|
||||
* @param {array[string]} validationErrors Validation errors
|
||||
*/
|
||||
const validationError = (validationErrors = []) => {
|
||||
let error = new Error("Validation error(s)");
|
||||
error.validationErrors = validationErrors;
|
||||
error.code = 400;
|
||||
return error;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
validateOrThrow,
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
component: aws-dynamodb
|
||||
name: database
|
||||
|
||||
inputs:
|
||||
name: ${name}-${stage}
|
||||
region: us-east-1
|
||||
# Don't delete the Database Table if "serverless remove" is run
|
||||
deletionPolicy: retain
|
||||
# Simple, single-table design
|
||||
attributeDefinitions:
|
||||
- AttributeName: hk
|
||||
AttributeType: S
|
||||
- AttributeName: sk
|
||||
AttributeType: S
|
||||
- AttributeName: sk2
|
||||
AttributeType: S
|
||||
keySchema:
|
||||
- AttributeName: hk
|
||||
KeyType: HASH
|
||||
- AttributeName: sk
|
||||
KeyType: RANGE
|
||||
globalSecondaryIndexes:
|
||||
- IndexName: gsi1
|
||||
KeySchema:
|
||||
- AttributeName: sk2
|
||||
KeyType: HASH
|
||||
- AttributeName: sk
|
||||
KeyType: RANGE
|
||||
Projection:
|
||||
ProjectionType: ALL
|
||||
@@ -1,28 +0,0 @@
|
||||
component: aws-iam-role
|
||||
name: permissions
|
||||
|
||||
inputs:
|
||||
name: ${name}-${stage}
|
||||
region: us-east-1
|
||||
service: lambda.amazonaws.com
|
||||
policy:
|
||||
# AWS Lambda function containing Express Logs and Assume Role access
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- sts:AssumeRole
|
||||
- logs:CreateLogGroup
|
||||
- logs:CreateLogStream
|
||||
- logs:PutLogEvents
|
||||
Resource: "*"
|
||||
# AWS DynamoDB Table access
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- dynamodb:DescribeTable
|
||||
- dynamodb:Query
|
||||
- dynamodb:GetItem
|
||||
- dynamodb:PutItem
|
||||
- dynamodb:UpdateItem
|
||||
- dynamodb:DeleteItem
|
||||
Resource:
|
||||
- ${output:database.arn}
|
||||
- ${output:database.arn}/index/*
|
||||
@@ -1,2 +0,0 @@
|
||||
app: forget-me-not
|
||||
org: benjaminramey
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"extends": ["eslint:recommended", "prettier"],
|
||||
"env": {
|
||||
"node": false,
|
||||
"browser": true,
|
||||
"es6": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018
|
||||
}
|
||||
}
|
||||
69
site/.gitignore
vendored
@@ -1,69 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Typescript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# dotenv environment variable files
|
||||
.env*
|
||||
|
||||
# gatsby files
|
||||
.cache/
|
||||
public
|
||||
|
||||
# Mac files
|
||||
.DS_Store
|
||||
|
||||
# Yarn
|
||||
yarn-error.log
|
||||
.pnp/
|
||||
.pnp.js
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
@@ -1,4 +0,0 @@
|
||||
.cache
|
||||
package.json
|
||||
package-lock.json
|
||||
public
|
||||
14
site/LICENSE
@@ -1,14 +0,0 @@
|
||||
The BSD Zero Clause License (0BSD)
|
||||
|
||||
Copyright (c) 2020 Gatsby Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
@@ -1,99 +0,0 @@
|
||||
<!-- AUTO-GENERATED-CONTENT:START (STARTER) -->
|
||||
<p align="center">
|
||||
<a href="https://www.gatsbyjs.com">
|
||||
<img alt="Gatsby" src="https://www.gatsbyjs.com/Gatsby-Monogram.svg" width="60" />
|
||||
</a>
|
||||
</p>
|
||||
<h1 align="center">
|
||||
Gatsby's hello-world starter
|
||||
</h1>
|
||||
|
||||
Kick off your project with this hello-world boilerplate. This starter ships with the main Gatsby configuration files you might need to get up and running blazing fast with the blazing fast app generator for React.
|
||||
|
||||
_Have another more specific idea? You may want to check out our vibrant collection of [official and community-created starters](https://www.gatsbyjs.com/docs/gatsby-starters/)._
|
||||
|
||||
## 🚀 Quick start
|
||||
|
||||
1. **Create a Gatsby site.**
|
||||
|
||||
Use the Gatsby CLI to create a new site, specifying the hello-world starter.
|
||||
|
||||
```shell
|
||||
# create a new Gatsby site using the hello-world starter
|
||||
gatsby new my-hello-world-starter https://github.com/gatsbyjs/gatsby-starter-hello-world
|
||||
```
|
||||
|
||||
1. **Start developing.**
|
||||
|
||||
Navigate into your new site’s directory and start it up.
|
||||
|
||||
```shell
|
||||
cd my-hello-world-starter/
|
||||
gatsby develop
|
||||
```
|
||||
|
||||
1. **Open the source code and start editing!**
|
||||
|
||||
Your site is now running at `http://localhost:8000`!
|
||||
|
||||
_Note: You'll also see a second link: _`http://localhost:8000/___graphql`_. This is a tool you can use to experiment with querying your data. Learn more about using this tool in the [Gatsby tutorial](https://www.gatsbyjs.com/tutorial/part-five/#introducing-graphiql)._
|
||||
|
||||
Open the `my-hello-world-starter` directory in your code editor of choice and edit `src/pages/index.js`. Save your changes and the browser will update in real time!
|
||||
|
||||
## 🧐 What's inside?
|
||||
|
||||
A quick look at the top-level files and directories you'll see in a Gatsby project.
|
||||
|
||||
.
|
||||
├── node_modules
|
||||
├── src
|
||||
├── .gitignore
|
||||
├── .prettierrc
|
||||
├── gatsby-browser.js
|
||||
├── gatsby-config.js
|
||||
├── gatsby-node.js
|
||||
├── gatsby-ssr.js
|
||||
├── LICENSE
|
||||
├── package-lock.json
|
||||
├── package.json
|
||||
└── README.md
|
||||
|
||||
1. **`/node_modules`**: This directory contains all of the modules of code that your project depends on (npm packages) are automatically installed.
|
||||
|
||||
2. **`/src`**: This directory will contain all of the code related to what you will see on the front-end of your site (what you see in the browser) such as your site header or a page template. `src` is a convention for “source code”.
|
||||
|
||||
3. **`.gitignore`**: This file tells git which files it should not track / not maintain a version history for.
|
||||
|
||||
4. **`.prettierrc`**: This is a configuration file for [Prettier](https://prettier.io/). Prettier is a tool to help keep the formatting of your code consistent.
|
||||
|
||||
5. **`gatsby-browser.js`**: This file is where Gatsby expects to find any usage of the [Gatsby browser APIs](https://www.gatsbyjs.com/docs/browser-apis/) (if any). These allow customization/extension of default Gatsby settings affecting the browser.
|
||||
|
||||
6. **`gatsby-config.js`**: This is the main configuration file for a Gatsby site. This is where you can specify information about your site (metadata) like the site title and description, which Gatsby plugins you’d like to include, etc. (Check out the [config docs](https://www.gatsbyjs.com/docs/gatsby-config/) for more detail).
|
||||
|
||||
7. **`gatsby-node.js`**: This file is where Gatsby expects to find any usage of the [Gatsby Node APIs](https://www.gatsbyjs.com/docs/node-apis/) (if any). These allow customization/extension of default Gatsby settings affecting pieces of the site build process.
|
||||
|
||||
8. **`gatsby-ssr.js`**: This file is where Gatsby expects to find any usage of the [Gatsby server-side rendering APIs](https://www.gatsbyjs.com/docs/ssr-apis/) (if any). These allow customization of default Gatsby settings affecting server-side rendering.
|
||||
|
||||
9. **`LICENSE`**: This Gatsby starter is licensed under the 0BSD license. This means that you can see this file as a placeholder and replace it with your own license.
|
||||
|
||||
10. **`package-lock.json`** (See `package.json` below, first). This is an automatically generated file based on the exact versions of your npm dependencies that were installed for your project. **(You won’t change this file directly).**
|
||||
|
||||
11. **`package.json`**: A manifest file for Node.js projects, which includes things like metadata (the project’s name, author, etc). This manifest is how npm knows which packages to install for your project.
|
||||
|
||||
12. **`README.md`**: A text file containing useful reference information about your project.
|
||||
|
||||
## 🎓 Learning Gatsby
|
||||
|
||||
Looking for more guidance? Full documentation for Gatsby lives [on the website](https://www.gatsbyjs.com/). Here are some places to start:
|
||||
|
||||
- **For most developers, we recommend starting with our [in-depth tutorial for creating a site with Gatsby](https://www.gatsbyjs.com/tutorial/).** It starts with zero assumptions about your level of ability and walks through every step of the process.
|
||||
|
||||
- **To dive straight into code samples, head [to our documentation](https://www.gatsbyjs.com/docs/).** In particular, check out the _Guides_, _API Reference_, and _Advanced Tutorials_ sections in the sidebar.
|
||||
|
||||
## 💫 Deploy
|
||||
|
||||
[](https://app.netlify.com/start/deploy?repository=https://github.com/gatsbyjs/gatsby-starter-hello-world)
|
||||
|
||||
[](https://vercel.com/import/project?template=https://github.com/gatsbyjs/gatsby-starter-hello-world)
|
||||
|
||||
<!-- AUTO-GENERATED-CONTENT:END -->
|
||||
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* Global Config
|
||||
*/
|
||||
|
||||
const config = {};
|
||||
|
||||
// Domains
|
||||
config.domains = {};
|
||||
|
||||
/**
|
||||
* API Domain
|
||||
* Add the domain from your serverless express.js back-end here.
|
||||
* This will enable your front-end to communicate with your back-end.
|
||||
* (e.g. 'https://api.mydomain.com' or 'https://091jafsl10.execute-api.us-east-1.amazonaws.com')
|
||||
*/
|
||||
config.domains.api = "https://13gtp0hrak.execute-api.us-east-1.amazonaws.com";
|
||||
|
||||
export default config;
|
||||
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* Configure your Gatsby site with this file.
|
||||
*
|
||||
* See: https://www.gatsbyjs.com/docs/gatsby-config/
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
/* Your site config here */
|
||||
plugins: [],
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"name": "forget-me-not-site",
|
||||
"private": true,
|
||||
"description": "Forget Me Not site for saving bookmarks",
|
||||
"version": "0.1.0",
|
||||
"license": "0BSD",
|
||||
"scripts": {
|
||||
"build": "gatsby build",
|
||||
"develop": "gatsby develop",
|
||||
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md,yml}\"",
|
||||
"start": "npm run develop",
|
||||
"serve": "gatsby serve",
|
||||
"clean": "gatsby clean",
|
||||
"test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1"
|
||||
},
|
||||
"dependencies": {
|
||||
"gatsby": "^2.25.3",
|
||||
"js-cookie": "^2.2.1",
|
||||
"moment": "^2.24.0",
|
||||
"react": "^16.12.0",
|
||||
"react-bootstrap": "^1.4.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-router-dom": "^5.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^7.12.1",
|
||||
"eslint-config-prettier": "^6.15.0",
|
||||
"prettier": "^2.1.2"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/gatsbyjs/gatsby-starter-hello-world"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/gatsbyjs/gatsby/issues"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 568 B |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
@@ -1 +0,0 @@
|
||||
<!DOCTYPE html><html><head><meta charSet="utf-8"/><meta http-equiv="x-ua-compatible" content="ie=edge"/><meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/><meta name="note" content="environment=development"/><script src="/socket.io/socket.io.js"></script></head><body><div id="___gatsby"></div><script src="/polyfill.js" nomodule=""></script><script src="/commons.js"></script></body></html>
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"short_name": "Forget Me Not",
|
||||
"name": "Forget Me Not",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "android-chrome-192x192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "android-chrome-512x512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@@ -1,11 +0,0 @@
|
||||
component: website
|
||||
name: site
|
||||
|
||||
inputs:
|
||||
# React application. "hook" runs before deployment to build the source code. "dist" is the built artifact directory which is uploaded.
|
||||
src:
|
||||
src: ./
|
||||
hook: npm run build
|
||||
dist: public
|
||||
# Enable this when you want to set a custom domain.
|
||||
# domain: ${env:domain}
|
||||
@@ -1,14 +0,0 @@
|
||||
import React from "react";
|
||||
import { Container, Row, Col } from "react-bootstrap";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<Container>
|
||||
<Row>
|
||||
<Col>
|
||||
<p>Hello world!</p>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
/**
|
||||
* Utils: Back-end
|
||||
*/
|
||||
|
||||
import config from "../../config";
|
||||
|
||||
/**
|
||||
* Register a new user
|
||||
*/
|
||||
export const userRegister = async (email, password) => {
|
||||
return await requestApi("/users/register", "POST", { email, password });
|
||||
};
|
||||
|
||||
/**
|
||||
* Login a new user
|
||||
*/
|
||||
export const userLogin = async (email, password) => {
|
||||
return await requestApi("/users/login", "POST", { email, password });
|
||||
};
|
||||
|
||||
/**
|
||||
* userGet
|
||||
*/
|
||||
export const userGet = async (token) => {
|
||||
return await requestApi("/user", "POST", null, {
|
||||
Authorization: `Bearer ${token}`,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* API request to call the backend
|
||||
*/
|
||||
export const requestApi = async (
|
||||
path = "",
|
||||
method = "GET",
|
||||
data = null,
|
||||
headers = {}
|
||||
) => {
|
||||
// Check if API URL has been set
|
||||
if (!config?.domains?.api) {
|
||||
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.`
|
||||
);
|
||||
}
|
||||
|
||||
// Prepare URL
|
||||
if (!path.startsWith("/")) {
|
||||
path = `/${path}`;
|
||||
}
|
||||
const url = `${config.domains.api}${path}`;
|
||||
|
||||
// Set headers
|
||||
headers = Object.assign({ "Content-Type": "application/json" }, headers);
|
||||
|
||||
// Default options are marked with *
|
||||
const response = await fetch(url, {
|
||||
method: method.toUpperCase(),
|
||||
mode: "cors",
|
||||
cache: "no-cache",
|
||||
headers,
|
||||
body: data ? JSON.stringify(data) : null,
|
||||
});
|
||||
|
||||
if (response.status < 200 || response.status >= 300) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
};
|
||||
@@ -1,81 +0,0 @@
|
||||
import Cookies from "js-cookie";
|
||||
|
||||
/**
|
||||
* Format Org and Username correctly for the Serverless Platform backend
|
||||
*/
|
||||
export const formatOrgAndUsername = (name = "") => {
|
||||
name = name
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z\d-]+/gi, "-");
|
||||
// Remove multiple instances of hyphens
|
||||
name = name.replace(/-{2,}/g, "-");
|
||||
if (name.length > 40) {
|
||||
name = name.substring(0, 40);
|
||||
}
|
||||
return name;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse query parameters in a URL
|
||||
* @param {*} searchString
|
||||
*/
|
||||
export const parseQueryParams = (searchString = null) => {
|
||||
if (!searchString) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Clone string
|
||||
let clonedParams = (" " + searchString).slice(1);
|
||||
|
||||
return clonedParams
|
||||
.substr(1)
|
||||
.split("&")
|
||||
.filter((el) => el.length)
|
||||
.map((el) => el.split("="))
|
||||
.reduce(
|
||||
(accumulator, currentValue) =>
|
||||
Object.assign(accumulator, {
|
||||
[decodeURIComponent(
|
||||
currentValue.shift()
|
||||
)]: decodeURIComponent(currentValue.pop()),
|
||||
}),
|
||||
{}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse hash fragment parameters in a URL
|
||||
*/
|
||||
export const parseHashFragment = (hashString) => {
|
||||
const hashData = {};
|
||||
let hash = decodeURI(hashString);
|
||||
hash = hash.split("&");
|
||||
hash.forEach((val) => {
|
||||
val = val.replace("#", "");
|
||||
hashData[val.split("=")[0]] = val.split("=")[1];
|
||||
});
|
||||
return hashData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Save session in browser cookie
|
||||
*/
|
||||
export const saveSession = (userId, userEmail, userToken) => {
|
||||
Cookies.set("serverless", { userId, userEmail, userToken });
|
||||
};
|
||||
|
||||
/**
|
||||
* Get session in browser cookie
|
||||
*/
|
||||
export const getSession = () => {
|
||||
const data = Cookies.get("serverless");
|
||||
return data ? JSON.parse(data) : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete session in browser cookie
|
||||
*/
|
||||
export const deleteSession = () => {
|
||||
Cookies.remove("serverless");
|
||||
};
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./helpers";
|
||||
export * from "./api";
|
||||
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 568 B |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"short_name": "Forget Me Not",
|
||||
"name": "Forget Me Not",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "android-chrome-192x192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "android-chrome-512x512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||