Compare commits
10 Commits
996e3610ac
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6bedf7ec24 | ||
| 3742b86cf7 | |||
| 58b62ee47c | |||
| 00e0aeb0d3 | |||
| 53ca2e063d | |||
| d4e76486fc | |||
| cc44c409d7 | |||
| 737be09b5f | |||
| 8855709a34 | |||
| d4d904d165 |
2
.prettierignore
Normal file
2
.prettierignore
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
dist
|
||||
1
.prettierrc.json
Normal file
1
.prettierrc.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
222
README.md
222
README.md
@@ -1,112 +1,110 @@
|
||||
[](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)
|
||||
[](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)
|
||||
|
||||
76
api/app.js
76
api/app.js
@@ -1,16 +1,10 @@
|
||||
const express = require('express')
|
||||
const app = express()
|
||||
const passport = require('passport')
|
||||
const {
|
||||
users
|
||||
} = require('./controllers')
|
||||
const express = require("express");
|
||||
const jwtAuthz = require("express-jwt-authz");
|
||||
const asyncHandler = require("express-async-handler");
|
||||
const { users } = require("./controllers");
|
||||
const secure = require("./middleware/secure");
|
||||
|
||||
/**
|
||||
* Configure Passport
|
||||
*/
|
||||
|
||||
try { require('./config/passport')(passport) }
|
||||
catch (error) { console.log(error) }
|
||||
const app = express();
|
||||
|
||||
/**
|
||||
* Configure Express.js Middleware
|
||||
@@ -18,64 +12,54 @@ catch (error) { console.log(error) }
|
||||
|
||||
// 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())
|
||||
res.header("Access-Control-Allow-Origin", "*");
|
||||
res.header("Access-Control-Allow-Methods", "*");
|
||||
res.header("Access-Control-Allow-Headers", "*");
|
||||
next();
|
||||
});
|
||||
|
||||
// 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);
|
||||
};
|
||||
app.use(express.json());
|
||||
|
||||
/**
|
||||
* Routes - Public
|
||||
*/
|
||||
|
||||
app.options(`*`, (req, res) => {
|
||||
res.status(200).send()
|
||||
})
|
||||
|
||||
app.post(`/users/register`, asyncHandler(users.register))
|
||||
|
||||
app.post(`/users/login`, asyncHandler(users.login))
|
||||
res.status(200).send();
|
||||
});
|
||||
|
||||
app.get(`/test/`, (req, res) => {
|
||||
res.status(200).send('Request received')
|
||||
})
|
||||
res.status(200).send("Request received");
|
||||
});
|
||||
|
||||
/**
|
||||
* Routes - Protected
|
||||
*/
|
||||
|
||||
app.post(`/user`, passport.authenticate('jwt', { session: false }), asyncHandler(users.get))
|
||||
app.get(
|
||||
"/test-authorized/",
|
||||
secure,
|
||||
jwtAuthz(["read:stuff"]),
|
||||
function (req, res) {
|
||||
res.send("Secured Resource");
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Routes - Catch-All
|
||||
*/
|
||||
|
||||
app.get(`/*`, (req, res) => {
|
||||
res.status(404).send('Route not found')
|
||||
})
|
||||
res.status(404).json({ error: "Not Found" });
|
||||
});
|
||||
|
||||
/**
|
||||
* Error Handler
|
||||
*/
|
||||
app.use(function (err, req, res, next) {
|
||||
console.error(err)
|
||||
res.status(500).json({ error: `Internal Serverless Error - "${err.message}"` })
|
||||
})
|
||||
console.error(err);
|
||||
res.status(500).json({ error: `Internal Server Error - "${err.message}"` });
|
||||
});
|
||||
|
||||
module.exports = app
|
||||
module.exports = app;
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
/**
|
||||
* Config: Passport.js
|
||||
*/
|
||||
|
||||
const StrategyJWT = require('passport-jwt').Strategy
|
||||
const ExtractJWT = require('passport-jwt').ExtractJwt
|
||||
const { users } = require('../models')
|
||||
const { comparePassword } = require('../utils')
|
||||
|
||||
module.exports = (passport) => {
|
||||
|
||||
const options = {}
|
||||
options.jwtFromRequest = ExtractJWT.fromAuthHeaderAsBearerToken()
|
||||
options.secretOrKey = process.env.tokenSecret || 'secret_j91jasf0j1asfkl' // Change this to only use your own secret token
|
||||
|
||||
passport.use(new StrategyJWT(options, async (jwtPayload, done) => {
|
||||
let user
|
||||
try { user = await users.getById(jwtPayload.id) }
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
return done(error, null)
|
||||
}
|
||||
|
||||
if (!user) { return done(null, false) }
|
||||
return done(null, user)
|
||||
}))
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
const users = require('./users')
|
||||
const users = require("./users");
|
||||
|
||||
module.exports = {
|
||||
users,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,80 +2,85 @@
|
||||
* Controllers: Users
|
||||
*/
|
||||
|
||||
const jwt = require('jsonwebtoken')
|
||||
const { users } = require('../models')
|
||||
const { comparePassword } = require('../utils')
|
||||
const jwt = require("jsonwebtoken");
|
||||
const { users } = require("../models");
|
||||
const { comparePassword } = require("../utils");
|
||||
|
||||
/**
|
||||
* Save
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
* @param {*} next
|
||||
*/
|
||||
const register = async (req, res, next) => {
|
||||
|
||||
try {
|
||||
await users.register(req.body)
|
||||
await users.register(req.body);
|
||||
} catch (error) {
|
||||
return res.status(400).json({ error: error.message })
|
||||
return res.status(400).json({ error: error.message });
|
||||
}
|
||||
|
||||
let user
|
||||
let user;
|
||||
try {
|
||||
user = await users.getByEmail(req.body.email)
|
||||
user = await users.getByEmail(req.body.email);
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
return next(error, null)
|
||||
console.log(error);
|
||||
return next(error, null);
|
||||
}
|
||||
|
||||
const token = jwt.sign(user, process.env.tokenSecret, {
|
||||
expiresIn: 604800 // 1 week
|
||||
})
|
||||
expiresIn: 604800, // 1 week
|
||||
});
|
||||
|
||||
res.json({ message: 'Authentication successful', token })
|
||||
}
|
||||
res.json({ message: "Authentication successful", token });
|
||||
};
|
||||
|
||||
/**
|
||||
* Sign a user in
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
* @param {*} next
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
* @param {*} next
|
||||
*/
|
||||
const login = async (req, res, next) => {
|
||||
|
||||
let user
|
||||
try { user = await users.getByEmail(req.body.email) }
|
||||
catch (error) { return done(error, null) }
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).send({ error: 'Authentication failed. User not found.' })
|
||||
let user;
|
||||
try {
|
||||
user = await users.getByEmail(req.body.email);
|
||||
} catch (error) {
|
||||
return done(error, null);
|
||||
}
|
||||
|
||||
const isCorrect = comparePassword(req.body.password, user.password)
|
||||
if (!user) {
|
||||
return res
|
||||
.status(404)
|
||||
.send({ error: "Authentication failed. User not found." });
|
||||
}
|
||||
|
||||
const isCorrect = comparePassword(req.body.password, user.password);
|
||||
if (!isCorrect) {
|
||||
return res.status(401).send({ error: 'Authentication failed. Wrong password.' })
|
||||
return res
|
||||
.status(401)
|
||||
.send({ error: "Authentication failed. Wrong password." });
|
||||
}
|
||||
|
||||
const token = jwt.sign(user, process.env.tokenSecret, {
|
||||
expiresIn: 604800 // 1 week
|
||||
})
|
||||
expiresIn: 604800, // 1 week
|
||||
});
|
||||
|
||||
res.json({ message: 'Authentication successful', token })
|
||||
}
|
||||
res.json({ message: "Authentication successful", token });
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a user
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
* @param {*} next
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
* @param {*} next
|
||||
*/
|
||||
const get = async (req, res, next) => {
|
||||
const user = users.convertToPublicFormat(req.user)
|
||||
res.json({ user })
|
||||
}
|
||||
const user = users.convertToPublicFormat(req.user);
|
||||
res.json({ user });
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
register,
|
||||
login,
|
||||
get,
|
||||
}
|
||||
};
|
||||
|
||||
19
api/middleware/secure.js
Normal file
19
api/middleware/secure.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const jwt = require("express-jwt");
|
||||
const jwksRsa = require("jwks-rsa");
|
||||
|
||||
const secure = jwt({
|
||||
// Dynamically provide a signing key based on the kid in the header and the signing keys provided by the JWKS endpoint
|
||||
secret: jwksRsa.expressJwtSecret({
|
||||
cache: true,
|
||||
rateLimit: true,
|
||||
jwksRequestsPerMinute: 5,
|
||||
jwksUri: `https://${process.env.auth0Domain}/.well-known/jwks.json`,
|
||||
}),
|
||||
|
||||
// Validate the audience and the issuer
|
||||
audience: process.env.auth0Audience, //replace with your API's audience, available at Dashboard > APIs
|
||||
issuer: `https://${process.env.auth0Domain}/`,
|
||||
algorithms: ["RS256"],
|
||||
});
|
||||
|
||||
module.exports = secure;
|
||||
@@ -1,5 +1,5 @@
|
||||
const users = require('./users')
|
||||
const users = require("./users");
|
||||
|
||||
module.exports = {
|
||||
users,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,135 +2,130 @@
|
||||
* Model: Users
|
||||
*/
|
||||
|
||||
const AWS = require('aws-sdk')
|
||||
const shortid = require('shortid')
|
||||
const utils = require('../utils')
|
||||
const AWS = require("aws-sdk");
|
||||
const shortid = require("shortid");
|
||||
const utils = require("../utils");
|
||||
|
||||
const dynamodb = new AWS.DynamoDB.DocumentClient({
|
||||
region: process.env.AWS_REGION
|
||||
})
|
||||
region: process.env.AWS_REGION,
|
||||
});
|
||||
|
||||
/**
|
||||
* Register user
|
||||
* @param {string} user.email User email
|
||||
* @param {string} user.password User password
|
||||
*/
|
||||
const register = async(user = {}) => {
|
||||
|
||||
const register = async (user = {}) => {
|
||||
// Validate
|
||||
if (!user.email) {
|
||||
throw new Error(`"email" is required`)
|
||||
throw new Error(`"email" is required`);
|
||||
}
|
||||
if (!user.password) {
|
||||
throw new Error(`"password" is required`)
|
||||
throw new Error(`"password" is required`);
|
||||
}
|
||||
if (!utils.validateEmailAddress(user.email)) {
|
||||
throw new Error(`"${user.email}" is not a valid email address`)
|
||||
throw new Error(`"${user.email}" is not a valid email address`);
|
||||
}
|
||||
|
||||
// Check if user is already registered
|
||||
const existingUser = await getByEmail(user.email)
|
||||
const existingUser = await getByEmail(user.email);
|
||||
if (existingUser) {
|
||||
throw new Error(`A user with email "${user.email}" is already registered`)
|
||||
throw new Error(`A user with email "${user.email}" is already registered`);
|
||||
}
|
||||
|
||||
user.password = utils.hashPassword(user.password)
|
||||
user.password = utils.hashPassword(user.password);
|
||||
|
||||
// Save
|
||||
const params = {
|
||||
TableName: process.env.db,
|
||||
Item: {
|
||||
hk: user.email,
|
||||
sk: 'user',
|
||||
sk: "user",
|
||||
sk2: shortid.generate(),
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
password: user.password,
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
await dynamodb.put(params).promise()
|
||||
}
|
||||
await dynamodb.put(params).promise();
|
||||
};
|
||||
|
||||
/**
|
||||
* Get user by email address
|
||||
* @param {string} email
|
||||
*/
|
||||
|
||||
const getByEmail = async(email) => {
|
||||
|
||||
const getByEmail = async (email) => {
|
||||
// Validate
|
||||
if (!email) {
|
||||
throw new Error(`"email" is required`)
|
||||
throw new Error(`"email" is required`);
|
||||
}
|
||||
if (!utils.validateEmailAddress(email)) {
|
||||
throw new Error(`"${email}" is not a valid email address`)
|
||||
throw new Error(`"${email}" is not a valid email address`);
|
||||
}
|
||||
|
||||
// Query
|
||||
const params = {
|
||||
TableName: process.env.db,
|
||||
KeyConditionExpression: 'hk = :hk',
|
||||
ExpressionAttributeValues: { ':hk': email }
|
||||
}
|
||||
KeyConditionExpression: "hk = :hk",
|
||||
ExpressionAttributeValues: { ":hk": email },
|
||||
};
|
||||
|
||||
let user = await dynamodb.query(params).promise()
|
||||
let user = await dynamodb.query(params).promise();
|
||||
|
||||
user = user.Items && user.Items[0] ? user.Items[0] : null
|
||||
user = user.Items && user.Items[0] ? user.Items[0] : null;
|
||||
if (user) {
|
||||
user.id = user.sk2
|
||||
user.email = user.hk
|
||||
user.id = user.sk2;
|
||||
user.email = user.hk;
|
||||
}
|
||||
return user
|
||||
}
|
||||
return user;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get user by id
|
||||
* @param {string} id
|
||||
*/
|
||||
|
||||
const getById = async(id) => {
|
||||
|
||||
const getById = async (id) => {
|
||||
// Validate
|
||||
if (!id) {
|
||||
throw new Error(`"id" is required`)
|
||||
throw new Error(`"id" is required`);
|
||||
}
|
||||
|
||||
// Query
|
||||
const params = {
|
||||
TableName: process.env.db,
|
||||
IndexName: process.env.dbIndex1,
|
||||
KeyConditionExpression: 'sk2 = :sk2 and sk = :sk',
|
||||
ExpressionAttributeValues: { ':sk2': id, ':sk': 'user' }
|
||||
}
|
||||
let user = await dynamodb.query(params).promise()
|
||||
KeyConditionExpression: "sk2 = :sk2 and sk = :sk",
|
||||
ExpressionAttributeValues: { ":sk2": id, ":sk": "user" },
|
||||
};
|
||||
let user = await dynamodb.query(params).promise();
|
||||
|
||||
user = user.Items && user.Items[0] ? user.Items[0] : null
|
||||
user = user.Items && user.Items[0] ? user.Items[0] : null;
|
||||
if (user) {
|
||||
user.id = user.sk2
|
||||
user.email = user.hk
|
||||
user.id = user.sk2;
|
||||
user.email = user.hk;
|
||||
}
|
||||
return user
|
||||
}
|
||||
return user;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert user record to public format
|
||||
* This hides the keys used for the dynamodb's single table design and returns human-readable properties.
|
||||
* @param {*} user
|
||||
* @param {*} user
|
||||
*/
|
||||
const convertToPublicFormat = (user = {}) => {
|
||||
user.email = user.hk || null
|
||||
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
|
||||
}
|
||||
user.email = user.hk || null;
|
||||
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;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
register,
|
||||
getByEmail,
|
||||
getById,
|
||||
convertToPublicFormat,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
{
|
||||
"name": "serverless-fullstack-app-api",
|
||||
"name": "forgetmenot",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "app.js",
|
||||
"dependencies": {
|
||||
"bcryptjs": "^2.4.3",
|
||||
"express": "^4.17.1",
|
||||
"express-async-handler": "^1.1.4",
|
||||
"express-jwt": "^6.1.0",
|
||||
"express-jwt-authz": "^2.4.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"passport": "^0.4.1",
|
||||
"passport-jwt": "^4.0.0",
|
||||
"jwks-rsa": "^2.0.4",
|
||||
"shortid": "^2.2.15"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
"author": "Ben Ramey <benramey@fastmail.com>",
|
||||
"license": "ISC",
|
||||
"description": ""
|
||||
}
|
||||
|
||||
@@ -16,3 +16,5 @@ inputs:
|
||||
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}
|
||||
auth0Audience: "https://${env:SNOWPACK_PUBLIC_API_DOMAIN}/"
|
||||
auth0Domain: ${env:SNOWPACK_PUBLIC_AUTH0_DOMAIN}
|
||||
|
||||
@@ -1,35 +1,36 @@
|
||||
/**
|
||||
* Utils
|
||||
* Utils
|
||||
*/
|
||||
|
||||
const bcrypt = require('bcryptjs')
|
||||
const bcrypt = require("bcryptjs");
|
||||
|
||||
/**
|
||||
* Validate email address
|
||||
*/
|
||||
const validateEmailAddress = (email) => {
|
||||
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,}))$/
|
||||
return re.test(String(email).toLowerCase())
|
||||
}
|
||||
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,}))$/;
|
||||
return re.test(String(email).toLowerCase());
|
||||
};
|
||||
|
||||
/**
|
||||
* Hash password
|
||||
* @param {*} user
|
||||
* @param {*} user
|
||||
*/
|
||||
const hashPassword = (password) => {
|
||||
const salt = bcrypt.genSaltSync(10)
|
||||
return bcrypt.hashSync(password, salt)
|
||||
}
|
||||
const salt = bcrypt.genSaltSync(10);
|
||||
return bcrypt.hashSync(password, salt);
|
||||
};
|
||||
|
||||
/**
|
||||
* Compare password
|
||||
*/
|
||||
const comparePassword = (candidatePassword, trustedPassword) => {
|
||||
return bcrypt.compareSync(candidatePassword, trustedPassword)
|
||||
}
|
||||
return bcrypt.compareSync(candidatePassword, trustedPassword);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
hashPassword,
|
||||
comparePassword,
|
||||
validateEmailAddress
|
||||
}
|
||||
validateEmailAddress,
|
||||
};
|
||||
|
||||
20
package.json
Normal file
20
package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "forgetmenot-serverless",
|
||||
"version": "1.0.0",
|
||||
"description": "[](https://www.serverless-fullstack-app.com)",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://grimfere@dev.azure.com/grimfere/ForgetMeNot/_git/forgetmenot-serverless"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"prettier": "2.4.1"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
app: forgetmenot
|
||||
org: yemarnn
|
||||
org: yemarnn
|
||||
|
||||
@@ -32,7 +32,7 @@ Any static assets, like images, can be placed in the `public/` directory.
|
||||
All commands are run from the root of the project, from a terminal:
|
||||
|
||||
| Command | Action |
|
||||
|:----------------|:--------------------------------------------|
|
||||
| :-------------- | :------------------------------------------ |
|
||||
| `npm install` | Installs dependencies |
|
||||
| `npm run dev` | Starts local dev server at `localhost:3000` |
|
||||
| `npm run build` | Build your production site to `./dist/` |
|
||||
|
||||
@@ -5,14 +5,12 @@ export default {
|
||||
// public: './public', // A folder of static files Astro will copy to the root. Useful for favicons, images, and other files that don’t need processing.
|
||||
buildOptions: {
|
||||
// site: 'http://example.com', // Your public domain, e.g.: https://my-site.dev/. Used to generate sitemaps and canonical URLs.
|
||||
sitemap: true, // Generate sitemap (set to "false" to disable)
|
||||
sitemap: true, // Generate sitemap (set to "false" to disable)
|
||||
},
|
||||
devOptions: {
|
||||
// hostname: 'localhost', // The hostname to run the dev server on.
|
||||
// port: 3000, // The port to run the dev server on.
|
||||
// tailwindConfig: '', // Path to tailwind.config.js if used, e.g. './tailwind.config.js'
|
||||
},
|
||||
renderers: [
|
||||
"@astrojs/renderer-react"
|
||||
],
|
||||
renderers: ["@astrojs/renderer-react"],
|
||||
};
|
||||
|
||||
@@ -9,7 +9,13 @@
|
||||
"preview": "astro preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"astro": "^0.20.7",
|
||||
"@astrojs/renderer-react": "^0.2.1"
|
||||
"@astrojs/renderer-react": "^0.2.1",
|
||||
"@snowpack/plugin-dotenv": "^2.2.0",
|
||||
"astro": "^0.20.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@auth0/auth0-react": "^1.8.0",
|
||||
"bootstrap": "^5.1.3",
|
||||
"normalize.css": "^8.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
site/public/assets/fonts/NotoSerif-Regular.ttf
Normal file
BIN
site/public/assets/fonts/NotoSerif-Regular.ttf
Normal file
Binary file not shown.
@@ -1,12 +0,0 @@
|
||||
<svg width="193" height="256" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
#flame { fill: #FF5D01; }
|
||||
#a { fill: #000014; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
#a { fill: #fff; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<path id="a" fill-rule="evenodd" clip-rule="evenodd" d="M131.496 18.929c1.943 2.413 2.935 5.67 4.917 12.181l43.309 142.27a180.277 180.277 0 00-51.778-17.53L99.746 60.56a3.67 3.67 0 00-7.042.01l-27.857 95.232a180.224 180.224 0 00-52.01 17.557l43.52-142.281c1.989-6.502 2.983-9.752 4.927-12.16a15.999 15.999 0 016.484-4.798c2.872-1.154 6.271-1.154 13.07-1.154h31.085c6.807 0 10.211 0 13.085 1.157a16 16 0 016.488 4.806z" fill="url(#paint0_linear)"/>
|
||||
<path id="flame" fill-rule="evenodd" clip-rule="evenodd" d="M136.678 180.151c-7.14 6.105-21.39 10.268-37.804 10.268-20.147 0-37.033-6.272-41.513-14.707-1.602 4.835-1.962 10.367-1.962 13.902 0 0-1.055 17.355 11.016 29.426 0-6.268 5.081-11.349 11.349-11.349 10.743 0 10.731 9.373 10.721 16.977v.679c0 11.542 7.054 21.436 17.086 25.606a23.27 23.27 0 01-2.339-10.2c0-11.008 6.463-15.107 13.973-19.87 5.977-3.79 12.616-8.001 17.192-16.449a31.013 31.013 0 003.744-14.82c0-3.299-.513-6.479-1.463-9.463z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1 +0,0 @@
|
||||
https://rawcdn.githack.com/snowpackjs/astro/main/examples/starter/public/favicon.ico
|
||||
@@ -1,28 +0,0 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji;
|
||||
font-size: 1rem;
|
||||
--user-font-scale: 1rem - 16px;
|
||||
font-size: clamp(0.875rem, 0.4626rem + 1.0309vw + var(--user-font-scale), 1.125rem);
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 4rem 2rem;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
justify-content: center;
|
||||
background: #f9fafb;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background: #111827;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
:root {
|
||||
--font-mono: Consolas, 'Andale Mono WT', 'Andale Mono', 'Lucida Console', 'Lucida Sans Typewriter', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Liberation Mono',
|
||||
'Nimbus Mono L', Monaco, 'Courier New', Courier, monospace;
|
||||
--color-light: #f3f4f6;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-light: #1f2937;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
header > div {
|
||||
font-size: clamp(2rem, -0.4742rem + 6.1856vw, 2.75rem);
|
||||
}
|
||||
|
||||
header > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
}
|
||||
header img {
|
||||
width: 2em;
|
||||
height: 2.667em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-weight: 500;
|
||||
font-size: clamp(1.5rem, 1rem + 1.25vw, 2rem);
|
||||
}
|
||||
|
||||
.counter {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
gap: 1em;
|
||||
font-size: 2rem;
|
||||
justify-content: center;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
.counter > pre {
|
||||
text-align: center;
|
||||
min-width: 3ch;
|
||||
}
|
||||
3
site/snowpack.config.mjs
Normal file
3
site/snowpack.config.mjs
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
plugins: [["@snowpack/plugin-dotenv", { dir: "../" }]],
|
||||
};
|
||||
26
site/src/components/AuthTest.jsx
Normal file
26
site/src/components/AuthTest.jsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from "react";
|
||||
import { Auth0Provider } from "@auth0/auth0-react";
|
||||
import LoginButton from "./LoginButton";
|
||||
import LogoutButton from "./LogoutButton";
|
||||
import Profile from "./Profile";
|
||||
import ProfileFromApi from "./ProfileFromApi";
|
||||
import config from "../config";
|
||||
|
||||
const AuthTest = () => {
|
||||
return (
|
||||
<Auth0Provider
|
||||
domain={config.auth0.domain}
|
||||
clientId={config.auth0.clientId}
|
||||
redirectUri={window.location.origin}
|
||||
audience={config.auth0.audience}
|
||||
scope="read:stuff"
|
||||
>
|
||||
<LoginButton />
|
||||
<LogoutButton />
|
||||
<Profile />
|
||||
<ProfileFromApi />
|
||||
</Auth0Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthTest;
|
||||
6
site/src/components/Header.astro
Normal file
6
site/src/components/Header.astro
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
import HeaderNav from "./HeaderNav.astro";
|
||||
---
|
||||
<header>
|
||||
<HeaderNav />
|
||||
</header>
|
||||
23
site/src/components/HeaderAuth.jsx
Normal file
23
site/src/components/HeaderAuth.jsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
import { Auth0Provider } from "@auth0/auth0-react";
|
||||
import config from "../config";
|
||||
import LoginButton from "./LoginButton";
|
||||
import LogoutButton from "./LogoutButton";
|
||||
|
||||
const HeaderAuth = () => {
|
||||
return (
|
||||
<div class="d-flex">
|
||||
<Auth0Provider
|
||||
domain={config.auth0.domain}
|
||||
clientId={config.auth0.clientId}
|
||||
redirectUri={window.location.origin}
|
||||
audience={config.auth0.audience}
|
||||
scope="read:stuff">
|
||||
<LoginButton />
|
||||
<LogoutButton />
|
||||
</Auth0Provider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderAuth;
|
||||
10
site/src/components/HeaderNav.astro
Normal file
10
site/src/components/HeaderNav.astro
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
import HeaderAuth from "./HeaderAuth.jsx";
|
||||
---
|
||||
<nav class="navbar navbar-expand-lg navbar-light" tabIndex="-1">
|
||||
<div class="container-fluid">
|
||||
<a href="#" class="navbar-brand">forget me not</a>
|
||||
|
||||
<HeaderAuth client:only />
|
||||
</div>
|
||||
</nav>
|
||||
17
site/src/components/LoginButton.jsx
Normal file
17
site/src/components/LoginButton.jsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from "react";
|
||||
import { useAuth0 } from "@auth0/auth0-react";
|
||||
|
||||
const LoginButton = () => {
|
||||
const { loginWithRedirect, isAuthenticated } = useAuth0();
|
||||
|
||||
return (
|
||||
!isAuthenticated &&
|
||||
<button
|
||||
class="btn btn-link"
|
||||
onClick={() => loginWithRedirect()}>
|
||||
Log In
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginButton;
|
||||
17
site/src/components/LogoutButton.jsx
Normal file
17
site/src/components/LogoutButton.jsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from "react";
|
||||
import { useAuth0 } from "@auth0/auth0-react";
|
||||
|
||||
const LogoutButton = () => {
|
||||
const { logout, isAuthenticated } = useAuth0();
|
||||
|
||||
return (
|
||||
isAuthenticated &&
|
||||
<button
|
||||
class="btn btn-link"
|
||||
onClick={() => logout({ returnTo: window.location.origin })}>
|
||||
Log Out
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogoutButton;
|
||||
22
site/src/components/Profile.jsx
Normal file
22
site/src/components/Profile.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from "react";
|
||||
import { useAuth0 } from "@auth0/auth0-react";
|
||||
|
||||
const Profile = () => {
|
||||
const { user, isAuthenticated, isLoading } = useAuth0();
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading ...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
isAuthenticated && (
|
||||
<div>
|
||||
<img src={user.picture} alt={user.name} />
|
||||
<h2>{user.name}</h2>
|
||||
<p>{user.email}</p>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default Profile;
|
||||
49
site/src/components/ProfileFromApi.jsx
Normal file
49
site/src/components/ProfileFromApi.jsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useAuth0 } from "@auth0/auth0-react";
|
||||
import config from "../config";
|
||||
|
||||
const ProfileFromApi = () => {
|
||||
const { user, isAuthenticated, getAccessTokenSilently } = useAuth0();
|
||||
const [message, setMessage] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const getMessage = async () => {
|
||||
try {
|
||||
const accessToken = await getAccessTokenSilently({
|
||||
audience: config.auth0.audience,
|
||||
scope: "read:stuff",
|
||||
});
|
||||
|
||||
const authorizedUrl = `${config.domains.api}/test-authorized/`;
|
||||
|
||||
const authorizedResponse = await fetch(authorizedUrl, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
const message = await authorizedResponse.text();
|
||||
|
||||
setMessage(message);
|
||||
} catch (e) {
|
||||
console.log(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
getMessage();
|
||||
}, [getAccessTokenSilently, user?.sub]);
|
||||
|
||||
return (
|
||||
isAuthenticated && (
|
||||
<div>
|
||||
<img src={user.picture} alt={user.name} />
|
||||
<h2>{user.name}</h2>
|
||||
<p>{user.email}</p>
|
||||
<h3>API Message</h3>
|
||||
{message ? <pre>{message}</pre> : "No message found"}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileFromApi;
|
||||
@@ -1,15 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function ReactCounter() {
|
||||
const [count, setCount] = useState(0);
|
||||
const add = () => setCount((i) => i + 1);
|
||||
const subtract = () => setCount((i) => i - 1);
|
||||
|
||||
return (
|
||||
<div id="react" className="counter">
|
||||
<button onClick={subtract}>-</button>
|
||||
<pre>{count}</pre>
|
||||
<button onClick={add}>+</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
---
|
||||
import { Markdown } from 'astro/components';
|
||||
---
|
||||
<article>
|
||||
<div class="banner">
|
||||
<p><strong>🧑🚀 Seasoned astronaut?</strong> Delete this file. Have fun!</p>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<Markdown>
|
||||
## 🚀 Project Structure
|
||||
|
||||
Inside of your Astro project, you'll see the following folders and files:
|
||||
|
||||
```
|
||||
/
|
||||
├── public/
|
||||
│ ├── robots.txt
|
||||
│ └── favicon.ico
|
||||
├── src/
|
||||
│ ├── components/
|
||||
│ │ └── Tour.astro
|
||||
│ └── pages/
|
||||
│ └── index.astro
|
||||
└── package.json
|
||||
```
|
||||
|
||||
Astro looks for `.astro` or `.md` files in the `src/pages/` directory.
|
||||
Each page is exposed as a route based on its file name.
|
||||
|
||||
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
|
||||
|
||||
Any static assets, like images, can be placed in the `public/` directory.
|
||||
</Markdown>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>👀 Want to learn more?</h2>
|
||||
<p>Feel free to check <a href="https://github.com/snowpackjs/astro">our documentation</a> or jump into our <a href="https://astro.build/chat">Discord server</a>.</p>
|
||||
</section>
|
||||
|
||||
</article>
|
||||
|
||||
<style>
|
||||
article {
|
||||
padding-top: 2em;
|
||||
line-height: 1.5;
|
||||
}
|
||||
section {
|
||||
margin-top: 2em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
max-width: 70ch;
|
||||
}
|
||||
|
||||
.banner {
|
||||
text-align: center;
|
||||
font-size: 1.2rem;
|
||||
background: var(--color-light);
|
||||
padding: 1em 1.5em;
|
||||
padding-left: 0.75em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
pre,
|
||||
code {
|
||||
font-family: var(--font-mono);
|
||||
background: var(--color-light);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 1em 1.5em;
|
||||
}
|
||||
|
||||
.tree {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
code:not(.tree) {
|
||||
padding: 0.125em;
|
||||
margin: 0 -0.125em;
|
||||
}
|
||||
</style>
|
||||
@@ -2,10 +2,10 @@
|
||||
* Global Config
|
||||
*/
|
||||
|
||||
const config = {}
|
||||
const config = {};
|
||||
|
||||
// Domains
|
||||
config.domains = {}
|
||||
config.domains = {};
|
||||
|
||||
/**
|
||||
* API Domain
|
||||
@@ -13,6 +13,14 @@ config.domains = {}
|
||||
* 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://1t8da6s66e.execute-api.us-east-1.amazonaws.com'
|
||||
config.domains.api = `https://${import.meta.env.SNOWPACK_PUBLIC_API_DOMAIN}`;
|
||||
|
||||
export default config
|
||||
/**
|
||||
* Auth0 config values
|
||||
*/
|
||||
config.auth0 = {};
|
||||
config.auth0.domain = import.meta.env.SNOWPACK_PUBLIC_AUTH0_DOMAIN;
|
||||
config.auth0.clientId = import.meta.env.SNOWPACK_PUBLIC_AUTH0_CLIENT_ID;
|
||||
config.auth0.audience = `https://${import.meta.env.SNOWPACK_PUBLIC_API_DOMAIN}/`;
|
||||
|
||||
export default config;
|
||||
|
||||
50
site/src/layouts/BaseLayout.astro
Normal file
50
site/src/layouts/BaseLayout.astro
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
import Header from "../components/Header.astro";
|
||||
let { title } = Astro.props;
|
||||
---
|
||||
<style global lang="scss">
|
||||
@import "../../node_modules/bootstrap/scss/functions";
|
||||
|
||||
// bootstrap variable overrides
|
||||
$body-bg: #fff;
|
||||
|
||||
// required bootstrap modules
|
||||
@import "../../node_modules/bootstrap/scss/variables";
|
||||
@import "../../node_modules/bootstrap/scss/mixins";
|
||||
@import "../../node_modules/bootstrap/scss/root";
|
||||
|
||||
// optional bootstrap modules
|
||||
@import "../../node_modules/bootstrap/scss/utilities";
|
||||
@import "../../node_modules/bootstrap/scss/reboot";
|
||||
@import "../../node_modules/bootstrap/scss/type";
|
||||
@import "../../node_modules/bootstrap/scss/containers";
|
||||
@import "../../node_modules/bootstrap/scss/nav";
|
||||
@import "../../node_modules/bootstrap/scss/navbar";
|
||||
@import "../../node_modules/bootstrap/scss/forms";
|
||||
@import "../../node_modules/bootstrap/scss/buttons";
|
||||
@import "../../node_modules/bootstrap/scss/helpers";
|
||||
|
||||
@import "../node_modules/bootstrap/scss/utilities/api";
|
||||
|
||||
// local styles
|
||||
@import "../styles/_typography";
|
||||
|
||||
body {
|
||||
font-family: 'NotoSerif';
|
||||
}
|
||||
</style>
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{title}</title>
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
</head>
|
||||
<body>
|
||||
<Header />
|
||||
<main class="container">
|
||||
<slot/>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,58 +1,7 @@
|
||||
---
|
||||
// Component Imports
|
||||
import Tour from '../components/Tour.astro';
|
||||
// You can import components from any supported Framework here!
|
||||
import ReactCounter from '../components/ReactCounter.jsx';
|
||||
|
||||
// Component Script:
|
||||
// You can write any JavaScript/TypeScript that you'd like here.
|
||||
// It will run during the build, but never in the browser.
|
||||
// All variables are available to use in the HTML template below.
|
||||
let title = 'My Astro Site';
|
||||
|
||||
// Full Astro Component Syntax:
|
||||
// https://docs.astro.build/core-concepts/astro-components/
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
---
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>{title}</title>
|
||||
<BaseLayout title="Forget Me Not">
|
||||
<p>Index page</p>
|
||||
</BaseLayout>
|
||||
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
|
||||
<link rel="stylesheet" href="/style/global.css">
|
||||
<link rel="stylesheet" href="/style/home.css">
|
||||
|
||||
<style>
|
||||
header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
max-width: min(100%, 68ch);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<header>
|
||||
<div>
|
||||
<img width="60" height="80" src="/assets/logo.svg" alt="Astro logo">
|
||||
<h1>Welcome to <a href="https://astro.build/">Astro</a></h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<Tour />
|
||||
|
||||
<!--
|
||||
- You can also use imported framework components directly in your markup!
|
||||
-
|
||||
- Note: by default, these components are NOT interactive on the client.
|
||||
- The `:visible` directive tells Astro to make it interactive.
|
||||
-
|
||||
- See https://docs.astro.build/core-concepts/component-hydration/
|
||||
-->
|
||||
<ReactCounter client:visible />
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
4
site/src/styles/_typography.scss
Normal file
4
site/src/styles/_typography.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
@font-face {
|
||||
font-family: 'NotoSerif';
|
||||
src: url('/assets/fonts/NotoSerif-Regular.ttf');
|
||||
}
|
||||
@@ -2,70 +2,69 @@
|
||||
* Utils: Back-end
|
||||
*/
|
||||
|
||||
import config from '../config'
|
||||
import config from "../config";
|
||||
|
||||
/**
|
||||
* Register a new user
|
||||
*/
|
||||
export const userRegister = async (email, password) => {
|
||||
return await requestApi('/users/register', 'POST', { 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 })
|
||||
}
|
||||
return await requestApi("/users/login", "POST", { email, password });
|
||||
};
|
||||
|
||||
/**
|
||||
* userGet
|
||||
*/
|
||||
export const userGet = async (token) => {
|
||||
return await requestApi('/user', 'POST', null, {
|
||||
Authorization: `Bearer ${token}`
|
||||
})
|
||||
}
|
||||
return await requestApi("/user", "POST", null, {
|
||||
Authorization: `Bearer ${token}`,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* API request to call the backend
|
||||
*/
|
||||
export const requestApi = async (
|
||||
path = '',
|
||||
method = 'GET',
|
||||
path = "",
|
||||
method = "GET",
|
||||
data = null,
|
||||
headers = {}) => {
|
||||
|
||||
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.`)
|
||||
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}`
|
||||
if (!path.startsWith("/")) {
|
||||
path = `/${path}`;
|
||||
}
|
||||
const url = `${config.domains.api}${path}`
|
||||
const url = `${config.domains.api}${path}`;
|
||||
|
||||
// Set headers
|
||||
headers = Object.assign(
|
||||
{ 'Content-Type': 'application/json' },
|
||||
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',
|
||||
mode: "cors",
|
||||
cache: "no-cache",
|
||||
headers,
|
||||
body: data ? JSON.stringify(data) : null
|
||||
})
|
||||
body: data ? JSON.stringify(data) : null,
|
||||
});
|
||||
|
||||
if (response.status < 200 || response.status >= 300) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error)
|
||||
const error = await response.json();
|
||||
throw new Error(error.error);
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
}
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from './api'
|
||||
export * from "./api";
|
||||
|
||||
Reference in New Issue
Block a user