Compare commits

..

10 Commits

Author SHA1 Message Date
benjaminramey
6bedf7ec24 Use bootstrap 2021-12-04 21:17:18 -06:00
3742b86cf7 Latest stuff 2021-11-30 18:40:18 -06:00
58b62ee47c Start on header 2021-10-13 09:31:09 -05:00
00e0aeb0d3 Font and some styling 2021-10-06 09:20:35 -05:00
53ca2e063d Start on styling and component cleanup 2021-10-05 09:38:48 -05:00
d4e76486fc Fix api env variable usage 2021-10-04 17:18:55 -05:00
cc44c409d7 Get config variables in auth0 in env file 2021-10-04 09:59:29 -05:00
737be09b5f Use auth0 jwt in api 2021-09-28 22:01:16 -05:00
8855709a34 Working Auth0 on site 2021-09-28 21:29:15 -05:00
d4d904d165 Install and use prettier 2021-09-24 20:24:39 -05:00
40 changed files with 588 additions and 593 deletions

2
.prettierignore Normal file
View File

@@ -0,0 +1,2 @@
node_modules
dist

1
.prettierrc.json Normal file
View File

@@ -0,0 +1 @@
{}

View File

@@ -1,5 +1,4 @@
[![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 [![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)](https://www.serverless-fullstack-app.com)
)](https://www.serverless-fullstack-app.com)
A complete, serverless, full-stack application built on AWS Lambda, AWS HTTP API, Express.js, React and DynamoDB. A complete, serverless, full-stack application built on AWS Lambda, AWS HTTP API, Express.js, React and DynamoDB.
@@ -9,7 +8,6 @@ A complete, serverless, full-stack application built on AWS Lambda, AWS HTTP API
Install the latest version of the Serverless Framework: Install the latest version of the Serverless Framework:
``` ```
npm i -g serverless npm i -g serverless
``` ```
@@ -99,11 +97,11 @@ Enjoy! This is a work in progress and we will continue to add funcitonality to
For more details on each part of this fullstack application, check out these resources: For more details on each part of this fullstack application, check out these resources:
* [Serverless Components](https://github.com/serverless/components) - [Serverless Components](https://github.com/serverless/components)
* [Serverless Express](https://github.com/serverless-components/express) - [Serverless Express](https://github.com/serverless-components/express)
* [Serverless Website](https://github.com/serverless-components/website) - [Serverless Website](https://github.com/serverless-components/website)
* [Serverless AWS DynamoDB](https://github.com/serverless-components/aws-dynamodb) - [Serverless AWS DynamoDB](https://github.com/serverless-components/aws-dynamodb)
* [Serverless AWS IAM Role](https://github.com/serverless-components/aws-iam-role) - [Serverless AWS IAM Role](https://github.com/serverless-components/aws-iam-role)
## Guides ## Guides

View File

@@ -1,16 +1,10 @@
const express = require('express') const express = require("express");
const app = express() const jwtAuthz = require("express-jwt-authz");
const passport = require('passport') const asyncHandler = require("express-async-handler");
const { const { users } = require("./controllers");
users const secure = require("./middleware/secure");
} = require('./controllers')
/** const app = express();
* Configure Passport
*/
try { require('./config/passport')(passport) }
catch (error) { console.log(error) }
/** /**
* Configure Express.js Middleware * Configure Express.js Middleware
@@ -18,64 +12,54 @@ catch (error) { console.log(error) }
// Enable CORS // Enable CORS
app.use(function (req, res, next) { app.use(function (req, res, next) {
res.header('Access-Control-Allow-Origin', '*') res.header("Access-Control-Allow-Origin", "*");
res.header('Access-Control-Allow-Methods', '*') res.header("Access-Control-Allow-Methods", "*");
res.header('Access-Control-Allow-Headers', '*') res.header("Access-Control-Allow-Headers", "*");
res.header('x-powered-by', 'serverless-express') next();
next() });
})
// Initialize Passport and restore authentication state, if any, from the session
app.use(passport.initialize())
app.use(passport.session())
// Enable JSON use // Enable JSON use
app.use(express.json()) 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 * Routes - Public
*/ */
app.options(`*`, (req, res) => { app.options(`*`, (req, res) => {
res.status(200).send() res.status(200).send();
}) });
app.post(`/users/register`, asyncHandler(users.register))
app.post(`/users/login`, asyncHandler(users.login))
app.get(`/test/`, (req, res) => { app.get(`/test/`, (req, res) => {
res.status(200).send('Request received') res.status(200).send("Request received");
}) });
/** /**
* Routes - Protected * 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 * Routes - Catch-All
*/ */
app.get(`/*`, (req, res) => { app.get(`/*`, (req, res) => {
res.status(404).send('Route not found') res.status(404).json({ error: "Not Found" });
}) });
/** /**
* Error Handler * Error Handler
*/ */
app.use(function (err, req, res, next) { app.use(function (err, req, res, next) {
console.error(err) console.error(err);
res.status(500).json({ error: `Internal Serverless Error - "${err.message}"` }) res.status(500).json({ error: `Internal Server Error - "${err.message}"` });
}) });
module.exports = app module.exports = app;

View File

@@ -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)
}))
}

View File

@@ -1,5 +1,5 @@
const users = require('./users') const users = require("./users");
module.exports = { module.exports = {
users, users,
} };

View File

@@ -2,9 +2,9 @@
* Controllers: Users * Controllers: Users
*/ */
const jwt = require('jsonwebtoken') const jwt = require("jsonwebtoken");
const { users } = require('../models') const { users } = require("../models");
const { comparePassword } = require('../utils') const { comparePassword } = require("../utils");
/** /**
* Save * Save
@@ -13,27 +13,26 @@ const { comparePassword } = require('../utils')
* @param {*} next * @param {*} next
*/ */
const register = async (req, res, next) => { const register = async (req, res, next) => {
try { try {
await users.register(req.body) await users.register(req.body);
} catch (error) { } catch (error) {
return res.status(400).json({ error: error.message }) return res.status(400).json({ error: error.message });
} }
let user let user;
try { try {
user = await users.getByEmail(req.body.email) user = await users.getByEmail(req.body.email);
} catch (error) { } catch (error) {
console.log(error) console.log(error);
return next(error, null) return next(error, null);
} }
const token = jwt.sign(user, process.env.tokenSecret, { 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 * Sign a user in
@@ -42,26 +41,32 @@ const register = async (req, res, next) => {
* @param {*} next * @param {*} next
*/ */
const login = async (req, res, next) => { const login = async (req, res, next) => {
let user;
let user try {
try { user = await users.getByEmail(req.body.email) } user = await users.getByEmail(req.body.email);
catch (error) { return done(error, null) } } catch (error) {
return done(error, null);
if (!user) {
return res.status(404).send({ error: 'Authentication failed. User not found.' })
} }
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) { 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, { 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 * Get a user
@@ -70,12 +75,12 @@ const login = async (req, res, next) => {
* @param {*} next * @param {*} next
*/ */
const get = async (req, res, next) => { const get = async (req, res, next) => {
const user = users.convertToPublicFormat(req.user) const user = users.convertToPublicFormat(req.user);
res.json({ user }) res.json({ user });
} };
module.exports = { module.exports = {
register, register,
login, login,
get, get,
} };

19
api/middleware/secure.js Normal file
View 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;

View File

@@ -1,5 +1,5 @@
const users = require('./users') const users = require("./users");
module.exports = { module.exports = {
users, users,
} };

View File

@@ -2,13 +2,13 @@
* Model: Users * Model: Users
*/ */
const AWS = require('aws-sdk') const AWS = require("aws-sdk");
const shortid = require('shortid') const shortid = require("shortid");
const utils = require('../utils') const utils = require("../utils");
const dynamodb = new AWS.DynamoDB.DocumentClient({ const dynamodb = new AWS.DynamoDB.DocumentClient({
region: process.env.AWS_REGION region: process.env.AWS_REGION,
}) });
/** /**
* Register user * Register user
@@ -16,102 +16,97 @@ const dynamodb = new AWS.DynamoDB.DocumentClient({
* @param {string} user.password User password * @param {string} user.password User password
*/ */
const register = async (user = {}) => { const register = async (user = {}) => {
// Validate // Validate
if (!user.email) { if (!user.email) {
throw new Error(`"email" is required`) throw new Error(`"email" is required`);
} }
if (!user.password) { if (!user.password) {
throw new Error(`"password" is required`) throw new Error(`"password" is required`);
} }
if (!utils.validateEmailAddress(user.email)) { 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 // Check if user is already registered
const existingUser = await getByEmail(user.email) const existingUser = await getByEmail(user.email);
if (existingUser) { 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 // Save
const params = { const params = {
TableName: process.env.db, TableName: process.env.db,
Item: { Item: {
hk: user.email, hk: user.email,
sk: 'user', sk: "user",
sk2: shortid.generate(), sk2: shortid.generate(),
createdAt: Date.now(), createdAt: Date.now(),
updatedAt: Date.now(), updatedAt: Date.now(),
password: user.password, password: user.password,
} },
} };
await dynamodb.put(params).promise() await dynamodb.put(params).promise();
} };
/** /**
* Get user by email address * Get user by email address
* @param {string} email * @param {string} email
*/ */
const getByEmail = async (email) => { const getByEmail = async (email) => {
// Validate // Validate
if (!email) { if (!email) {
throw new Error(`"email" is required`) throw new Error(`"email" is required`);
} }
if (!utils.validateEmailAddress(email)) { 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 // Query
const params = { const params = {
TableName: process.env.db, TableName: process.env.db,
KeyConditionExpression: 'hk = :hk', KeyConditionExpression: "hk = :hk",
ExpressionAttributeValues: { ':hk': email } 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) { if (user) {
user.id = user.sk2 user.id = user.sk2;
user.email = user.hk user.email = user.hk;
}
return user
} }
return user;
};
/** /**
* Get user by id * Get user by id
* @param {string} id * @param {string} id
*/ */
const getById = async (id) => { const getById = async (id) => {
// Validate // Validate
if (!id) { if (!id) {
throw new Error(`"id" is required`) throw new Error(`"id" is required`);
} }
// Query // Query
const params = { const params = {
TableName: process.env.db, TableName: process.env.db,
IndexName: process.env.dbIndex1, IndexName: process.env.dbIndex1,
KeyConditionExpression: 'sk2 = :sk2 and sk = :sk', KeyConditionExpression: "sk2 = :sk2 and sk = :sk",
ExpressionAttributeValues: { ':sk2': id, ':sk': 'user' } ExpressionAttributeValues: { ":sk2": id, ":sk": "user" },
} };
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) { if (user) {
user.id = user.sk2 user.id = user.sk2;
user.email = user.hk user.email = user.hk;
}
return user
} }
return user;
};
/** /**
* Convert user record to public format * Convert user record to public format
@@ -119,18 +114,18 @@ const getById = async(id) => {
* @param {*} user * @param {*} user
*/ */
const convertToPublicFormat = (user = {}) => { const convertToPublicFormat = (user = {}) => {
user.email = user.hk || null user.email = user.hk || null;
user.id = user.sk2 || null user.id = user.sk2 || null;
if (user.hk) delete user.hk if (user.hk) delete user.hk;
if (user.sk) delete user.sk if (user.sk) delete user.sk;
if (user.sk2) delete user.sk2 if (user.sk2) delete user.sk2;
if (user.password) delete user.password if (user.password) delete user.password;
return user return user;
} };
module.exports = { module.exports = {
register, register,
getByEmail, getByEmail,
getById, getById,
convertToPublicFormat, convertToPublicFormat,
} };

View File

@@ -1,20 +1,22 @@
{ {
"name": "serverless-fullstack-app-api", "name": "forgetmenot",
"version": "1.0.0", "version": "1.0.0",
"description": "",
"main": "app.js", "main": "app.js",
"dependencies": { "dependencies": {
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"express": "^4.17.1", "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", "jsonwebtoken": "^8.5.1",
"passport": "^0.4.1", "jwks-rsa": "^2.0.4",
"passport-jwt": "^4.0.0",
"shortid": "^2.2.15" "shortid": "^2.2.15"
}, },
"devDependencies": {}, "devDependencies": {},
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"author": "", "author": "Ben Ramey <benramey@fastmail.com>",
"license": "ISC" "license": "ISC",
"description": ""
} }

View File

@@ -16,3 +16,5 @@ inputs:
dbIndex1: ${output:database.indexes.gsi1.name} dbIndex1: ${output:database.indexes.gsi1.name}
# A secret token to sign the JWT tokens with. # A secret token to sign the JWT tokens with.
tokenSecret: ${env:tokenSecret} # Change to secret via environment variable: ${env:tokenSecret} tokenSecret: ${env:tokenSecret} # Change to secret via environment variable: ${env:tokenSecret}
auth0Audience: "https://${env:SNOWPACK_PUBLIC_API_DOMAIN}/"
auth0Domain: ${env:SNOWPACK_PUBLIC_AUTH0_DOMAIN}

View File

@@ -2,34 +2,35 @@
* Utils * Utils
*/ */
const bcrypt = require('bcryptjs') const bcrypt = require("bcryptjs");
/** /**
* Validate email address * Validate email address
*/ */
const validateEmailAddress = (email) => { 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,}))$/ var re =
return re.test(String(email).toLowerCase()) /^(([^<>()\[\]\\.,;:\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 * Hash password
* @param {*} user * @param {*} user
*/ */
const hashPassword = (password) => { const hashPassword = (password) => {
const salt = bcrypt.genSaltSync(10) const salt = bcrypt.genSaltSync(10);
return bcrypt.hashSync(password, salt) return bcrypt.hashSync(password, salt);
} };
/** /**
* Compare password * Compare password
*/ */
const comparePassword = (candidatePassword, trustedPassword) => { const comparePassword = (candidatePassword, trustedPassword) => {
return bcrypt.compareSync(candidatePassword, trustedPassword) return bcrypt.compareSync(candidatePassword, trustedPassword);
} };
module.exports = { module.exports = {
hashPassword, hashPassword,
comparePassword, comparePassword,
validateEmailAddress validateEmailAddress,
} };

20
package.json Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "forgetmenot-serverless",
"version": "1.0.0",
"description": "[![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\r )](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": {}
}

View File

@@ -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: All commands are run from the root of the project, from a terminal:
| Command | Action | | Command | Action |
|:----------------|:--------------------------------------------| | :-------------- | :------------------------------------------ |
| `npm install` | Installs dependencies | | `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:3000` | | `npm run dev` | Starts local dev server at `localhost:3000` |
| `npm run build` | Build your production site to `./dist/` | | `npm run build` | Build your production site to `./dist/` |

View File

@@ -12,7 +12,5 @@ export default {
// port: 3000, // The port 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' // tailwindConfig: '', // Path to tailwind.config.js if used, e.g. './tailwind.config.js'
}, },
renderers: [ renderers: ["@astrojs/renderer-react"],
"@astrojs/renderer-react"
],
}; };

View File

@@ -9,7 +9,13 @@
"preview": "astro preview" "preview": "astro preview"
}, },
"devDependencies": { "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"
} }
} }

Binary file not shown.

View File

@@ -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

View File

@@ -1 +0,0 @@
https://rawcdn.githack.com/snowpackjs/astro/main/examples/starter/public/favicon.ico

View File

@@ -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;
}
}

View File

@@ -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
View File

@@ -0,0 +1,3 @@
export default {
plugins: [["@snowpack/plugin-dotenv", { dir: "../" }]],
};

View 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;

View File

@@ -0,0 +1,6 @@
---
import HeaderNav from "./HeaderNav.astro";
---
<header>
<HeaderNav />
</header>

View 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;

View 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>

View 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;

View 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;

View 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;

View 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;

View File

@@ -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>
);
}

View File

@@ -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>

View File

@@ -2,10 +2,10 @@
* Global Config * Global Config
*/ */
const config = {} const config = {};
// Domains // Domains
config.domains = {} config.domains = {};
/** /**
* API Domain * API Domain
@@ -13,6 +13,14 @@ config.domains = {}
* This will enable your front-end to communicate with your back-end. * 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') * (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;

View 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>

View File

@@ -1,58 +1,7 @@
--- ---
// Component Imports import BaseLayout from '../layouts/BaseLayout.astro';
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/
--- ---
<html lang="en"> <BaseLayout title="Forget Me Not">
<head> <p>Index page</p>
<meta charset="UTF-8"> </BaseLayout>
<meta name="viewport" content="width=device-width">
<title>{title}</title>
<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>

View File

@@ -0,0 +1,4 @@
@font-face {
font-family: 'NotoSerif';
src: url('/assets/fonts/NotoSerif-Regular.ttf');
}

View File

@@ -2,70 +2,69 @@
* Utils: Back-end * Utils: Back-end
*/ */
import config from '../config' import config from "../config";
/** /**
* Register a new user * Register a new user
*/ */
export const userRegister = async (email, password) => { 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 * Login a new user
*/ */
export const userLogin = async (email, password) => { export const userLogin = async (email, password) => {
return await requestApi('/users/login', 'POST', { email, password }) return await requestApi("/users/login", "POST", { email, password });
} };
/** /**
* userGet * userGet
*/ */
export const userGet = async (token) => { export const userGet = async (token) => {
return await requestApi('/user', 'POST', null, { return await requestApi("/user", "POST", null, {
Authorization: `Bearer ${token}` Authorization: `Bearer ${token}`,
}) });
} };
/** /**
* API request to call the backend * API request to call the backend
*/ */
export const requestApi = async ( export const requestApi = async (
path = '', path = "",
method = 'GET', method = "GET",
data = null, data = null,
headers = {}) => { headers = {}
) => {
// Check if API URL has been set // Check if API URL has been set
if (!config?.domains?.api) { 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 // Prepare URL
if (!path.startsWith('/')) { if (!path.startsWith("/")) {
path = `/${path}` path = `/${path}`;
} }
const url = `${config.domains.api}${path}` const url = `${config.domains.api}${path}`;
// Set headers // Set headers
headers = Object.assign( headers = Object.assign({ "Content-Type": "application/json" }, headers);
{ 'Content-Type': 'application/json' },
headers
)
// Default options are marked with * // Default options are marked with *
const response = await fetch(url, { const response = await fetch(url, {
method: method.toUpperCase(), method: method.toUpperCase(),
mode: 'cors', mode: "cors",
cache: 'no-cache', cache: "no-cache",
headers, headers,
body: data ? JSON.stringify(data) : null body: data ? JSON.stringify(data) : null,
}) });
if (response.status < 200 || response.status >= 300) { if (response.status < 200 || response.status >= 300) {
const error = await response.json() const error = await response.json();
throw new Error(error.error) throw new Error(error.error);
} }
return await response.json() return await response.json();
} };

View File

@@ -1 +1 @@
export * from './api' export * from "./api";