authentication: adds user auth concept, sign out, and adds 3 images to a bucket upon user creation

This commit is contained in:
@wwwjim 2020-07-21 04:36:50 -07:00
parent 8a63467e6f
commit f66129f16d
18 changed files with 354 additions and 107 deletions

View File

@ -68,6 +68,21 @@ export const sendWalletAddressFilecoin = async (data) => {
// NOTE(jim):
// New WWW Requests.
export const signIn = async (data) => {
const options = {
method: "POST",
headers: REQUEST_HEADERS,
credentials: "include",
body: JSON.stringify({ data }),
};
const response = await fetch(`/api/sign-in`, options);
const json = await response.json();
return json;
};
export const hydrateAuthenticatedUser = async (data) => {
const options = {
method: "POST",

View File

@ -1,3 +1,3 @@
if (process.env.NODE_ENV !== "production") {
require("dotenv").config();
}
export const session = {
key: "WEB_SERVICE_SESSION_KEY",
};

View File

@ -39,6 +39,10 @@ const transformPeers = (peersList) => {
};
export const getSelectedState = (props) => {
if (!props) {
return null;
}
const {
status,
messageList,
@ -61,6 +65,10 @@ export const getSelectedState = (props) => {
};
export const getInitialState = (props) => {
if (!props) {
return null;
}
const {
status,
messageList,

View File

@ -51,7 +51,7 @@ const STYLES_ICON_ELEMENT = css`
const STYLES_ICON_ELEMENT_CUSTOM = css`
border-radius: 3px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.07);
background-image: url('/static/social.png');
background-image: url("/static/social.png");
background-size: cover;
height: 48px;
width: 48px;
@ -169,10 +169,12 @@ export default class ApplicationHeader extends React.Component {
style={{ right: 0, top: "48px", cursor: "pointer" }}
onNavigateTo={this.props.onNavigateTo}
onAction={this.props.onAction}
onSignOut={this.props.onSignOut}
navigation={[
{ text: "Edit account", value: 13 },
{ text: "Filecoin settings", value: 14 },
{ text: "API Key & Tokens", value: 16 },
{ text: "Sign out", value: 0, action: "SIGN_OUT" },
]}
/>
}

View File

@ -44,6 +44,18 @@ export class PopoverNavigation extends React.Component {
return (
<div css={STYLES_POPOVER} style={this.props.style}>
{this.props.navigation.map((each) => {
if (each.action === "SIGN_OUT") {
return (
<div
key={each.value}
css={STYLES_POPOVER_ITEM}
onClick={this.props.onSignOut}
>
{each.text}
</div>
);
}
return (
<div
key={each.value}

View File

@ -1,13 +1,15 @@
import * as Environment from "~/node_common/environment";
module.exports = {
development: {
client: "pg",
connection: {
ssl: true,
port: 5432,
host: process.env.POSTGRES_HOSTNAME,
database: process.env.POSTGRES_DATABASE,
user: process.env.POSTGRES_ADMIN_USERNAME,
password: process.env.POSTGRES_ADMIN_PASSWORD,
host: Environment.POSTGRES_HOSTNAME,
database: Environment.POSTGRES_DATABASE,
user: Environment.POSTGRES_ADMIN_USERNAME,
password: Environment.POSTGRES_ADMIN_PASSWORD,
},
},
};

View File

@ -1,7 +1,3 @@
if (process.env.NODE_ENV !== "production") {
require("dotenv").config();
}
import configs from "~/knexfile";
import knex from "knex";

View File

@ -0,0 +1,14 @@
if (process.env.NODE_ENV !== "production") {
require("dotenv").config();
}
export const POSTGRES_ADMIN_PASSWORD = process.env.POSTGRES_ADMIN_PASSWORD;
export const POSTGRES_ADMIN_USERNAME = process.env.POSTGRES_ADMIN_USERNAME;
export const POSTGRES_HOSTNAME = process.env.POSTGRES_HOSTNAME;
export const POSTGRES_DATABASE = process.env.POSTGRES_DATABASE;
export const JWT_SECRET = process.env.JWT_SECRET;
export const LOCAL_PASSWORD_SECRET = `$2b$13$${
process.env.LOCAL_PASSWORD_SECRET
}`;
export const TEXTILE_HUB_KEY = process.env.TEXTILE_HUB_KEY;
export const TEXTILE_HUB_SECRET = process.env.TEXTILE_HUB_SECRET;

View File

@ -1,3 +1,10 @@
import JWT from "jsonwebtoken";
import * as Environment from "~/node_common/environment";
import * as Credentials from "~/common/credentials";
import * as Strings from "~/common/strings";
import * as Data from "~/node_common/data";
export const init = (middleware) => {
return (req, res) =>
new Promise((resolve, reject) => {
@ -29,3 +36,36 @@ export const CORS = async (req, res, next) => {
next();
};
export const RequireCookieAuthentication = async (req, res, next) => {
if (Strings.isEmpty(req.headers.cookie)) {
return res
.status(403)
.json({ decorator: "SERVER_AUTH_USER_NO_TOKEN", error: true });
}
const token = req.headers.cookie.replace(
/(?:(?:^|.*;\s*)WEB_SERVICE_SESSION_KEY\s*\=\s*([^;]*).*$)|^.*$/,
"$1"
);
try {
const decoded = JWT.verify(token, Environment.JWT_SECRET);
const user = await Data.getUserByUsername({
username: decoded.username,
});
if (!user || user.error) {
return res
.status(403)
.json({ decorator: "SERVER_AUTH_USER_NOT_FOUND", error: true });
}
} catch (err) {
console.log(err);
return res
.status(403)
.json({ decorator: "SERVER_AUTH_USER_ERROR", error: true });
}
next();
};

55
node_common/models.js Normal file
View File

@ -0,0 +1,55 @@
import * as Utilities from "~/node_common/utilities";
import * as Data from "~/node_common/data";
import PG from "~/node_common/powergate";
export const getViewer = async ({ username }) => {
const user = await Data.getUserByUsername({
username,
});
if (!user) {
return null;
}
if (user.error) {
return null;
}
const {
buckets,
bucketKey,
bucketName,
} = await Utilities.getBucketAPIFromUserToken(user.data.tokens.api);
let data = {
id: user.id,
data: user.data,
peersList: null,
messageList: null,
status: null,
addrsList: null,
info: null,
state: null,
local: {
photo: null,
name: `node`,
settings_deals_auto_approve: false,
},
library: user.data.library,
};
PG.setToken(user.data.tokens.pg);
const updates = await Utilities.refresh({ PG });
const updatesWithToken = await Utilities.refreshWithToken({
PG,
});
data = await Utilities.updateStateData(data, {
...updates,
...updatesWithToken,
});
return data;
};

View File

@ -1,3 +1,4 @@
import * as Environment from "~/node_common/environment";
import * as Constants from "./constants";
import * as Converter from "~/vendor/bytes-base64-converter.js";
@ -9,8 +10,17 @@ import { Libp2pCryptoIdentity } from "@textile/threads-core";
const BUCKET_NAME = "data";
const TEXTILE_KEY_INFO = {
key: process.env.TEXTILE_HUB_KEY,
secret: process.env.TEXTILE_HUB_SECRET,
key: Environment.TEXTILE_HUB_KEY,
secret: Environment.TEXTILE_HUB_SECRET,
};
export const parseAuthHeader = (value) => {
if (typeof value !== "string") {
return null;
}
var matches = value.match(/(\S+)\s+(\S+)/);
return matches && { scheme: matches[1], value: matches[2] };
};
// NOTE(jim): Requires @textile/hub

View File

@ -68,6 +68,7 @@
"react-tippy": "^1.3.4",
"regenerator-runtime": "^0.13.5",
"three": "^0.108.0",
"universal-cookie": "^4.0.3",
"uuid": "^8.0.0"
},
"devDependencies": {

View File

@ -1,89 +1,14 @@
import * as MW from "~/node_common/middleware";
import * as Utilities from "~/node_common/utilities";
import * as Constants from "~/node_common/constants";
import * as Data from "~/node_common/data";
import PG from "~/node_common/powergate";
import FS from "fs-extra";
import path from "path";
import * as Models from "~/node_common/models";
const initCORS = MW.init(MW.CORS);
const initAuth = MW.init(MW.RequireCookieAuthentication);
export default async (req, res) => {
initCORS(req, res);
initAuth(req, res);
const user = await Data.getUserByUsername({
username: req.body.data.username,
});
if (!user) {
return res.status(200).json({ decorator: "SERVER_HYDRATE", error: true });
}
if (user.error) {
return res.status(200).json({ decorator: "SERVER_HYDRATE", error: true });
}
const {
buckets,
bucketKey,
bucketName,
} = await Utilities.getBucketAPIFromUserToken(user.data.tokens.api);
// TODO(jim): This is obviously a test!
// Slates will hold an index
// Library will hold an index
let data = {
peersList: null,
messageList: null,
status: null,
addrsList: null,
info: null,
state: null,
local: {
photo: null,
name: `node`,
settings_deals_auto_approve: false,
},
library: [
{
...Utilities.createFolder({ id: bucketName, name: "Data" }),
children: [
await Utilities.addFileFromFilePath({
buckets,
bucketKey,
filePath: "./public/static/social.jpg",
}),
await Utilities.addFileFromFilePath({
buckets,
bucketKey,
filePath: "./public/static/cube_000.jpg",
}),
await Utilities.addFileFromFilePath({
buckets,
bucketKey,
filePath: "./public/static/cube_f7f7f7.jpg",
}),
],
},
],
};
// NOTE(jim): Should render a list of buckets.
const roots = await buckets.list();
console.log({ roots });
PG.setToken(user.data.tokens.pg);
const updates = await Utilities.refresh({ PG });
const updatesWithToken = await Utilities.refreshWithToken({
PG,
});
data = await Utilities.updateStateData(data, {
...updates,
...updatesWithToken,
});
const data = await Models.getViewer({ username: req.body.data.username });
return res
.status(200)

80
pages/api/sign-in.js Normal file
View File

@ -0,0 +1,80 @@
import * as Environment from "~/node_common/environment";
import * as MW from "~/node_common/middleware";
import * as Utilities from "~/node_common/utilities";
import * as Constants from "~/node_common/constants";
import * as Credentials from "~/common/credentials";
import * as Data from "~/node_common/data";
import * as Strings from "~/common/strings";
import PG from "~/node_common/powergate";
import FS from "fs-extra";
import path from "path";
import JWT from "jsonwebtoken";
import BCrypt from "bcrypt";
const initCORS = MW.init(MW.CORS);
export default async (req, res) => {
initCORS(req, res);
if (Strings.isEmpty(req.body.data.username)) {
return res.status(500).send({ decorator: "SERVER_SIGN_IN", error: true });
}
if (Strings.isEmpty(req.body.data.password)) {
return res.status(500).send({ decorator: "SERVER_SIGN_IN", error: true });
}
let user;
try {
user = await Data.getUserByUsername({ username: req.body.data.username });
} catch (e) {
console.log(e);
}
if (!user) {
return res
.status(403)
.send({ decorator: "SERVER_SIGN_IN_FIND_USER", error: true });
}
if (user.error) {
return res
.status(500)
.send({ decorator: "SERVER_SIGN_IN_FIND_USER", error: true });
}
const phaseOne = await BCrypt.hash(req.body.data.password, user.salt);
const phaseTwo = await BCrypt.hash(phaseOne, user.salt);
const phaseThree = await BCrypt.hash(
phaseTwo,
Environment.LOCAL_PASSWORD_SECRET
);
if (phaseThree !== user.password) {
return res
.status(403)
.send({ decorator: "SERVER_SIGN_IN_AUTH", error: true });
}
const authorization = Utilities.parseAuthHeader(req.headers.authorization);
if (authorization && !Strings.isEmpty(authorization.value)) {
const verfied = JWT.verify(authorization.value, Environment.JWT_SECRET);
if (user.username === verfied.username) {
return res.status(200).send({
message: "You are already authenticated. Welcome back!",
viewer: user,
});
}
}
const token = JWT.sign(
{ user: user.id, username: user.username },
Environment.JWT_SECRET
);
return res
.status(200)
.send({ decorator: "SERVER_SIGN_IN", success: true, token });
};

View File

@ -1,6 +1,8 @@
import * as MW from "~/node_common/middleware";
import * as Data from "~/node_common/data";
import * as Strings from "~/common/strings";
import * as Environment from "~/node_common/environment";
import * as Utilities from "~/node_common/utilities";
import PG from "~/node_common/powergate";
import JWT from "jsonwebtoken";
@ -9,7 +11,6 @@ import BCrypt from "bcrypt";
import { Libp2pCryptoIdentity } from "@textile/threads-core";
const initCORS = MW.init(MW.CORS);
const SECRET = `$2b$13$${process.env.LOCAL_PASSWORD_SECRET}`;
export default async (req, res) => {
initCORS(req, res);
@ -29,7 +30,7 @@ export default async (req, res) => {
const salt = await BCrypt.genSalt(13);
const hash = await BCrypt.hash(req.body.data.password, salt);
const double = await BCrypt.hash(hash, salt);
const triple = await BCrypt.hash(double, SECRET);
const triple = await BCrypt.hash(double, Environment.LOCAL_PASSWORD_SECRET);
const FFS = await PG.ffs.create();
const pg = FFS.token ? FFS.token : null;
@ -38,12 +39,46 @@ export default async (req, res) => {
const identity = await Libp2pCryptoIdentity.fromRandom();
const api = identity.toString();
// TODO(jim):
// Don't do this once you refactor.
const {
buckets,
bucketKey,
bucketName,
} = await Utilities.getBucketAPIFromUserToken(api);
const user = await Data.createUser({
email: req.body.data.email,
password: triple,
salt,
username: req.body.data.username,
data: { tokens: { pg, api } },
data: {
tokens: { pg, api },
// TODO(jim):
// Get rid of this after the refactor.
library: [
{
...Utilities.createFolder({ id: bucketName, name: "Data" }),
children: [
await Utilities.addFileFromFilePath({
buckets,
bucketKey,
filePath: "./public/static/social.jpg",
}),
await Utilities.addFileFromFilePath({
buckets,
bucketKey,
filePath: "./public/static/cube_000.jpg",
}),
await Utilities.addFileFromFilePath({
buckets,
bucketKey,
filePath: "./public/static/cube_f7f7f7.jpg",
}),
],
},
],
},
});
if (!user) {

View File

@ -1,3 +1,4 @@
import * as Environment from "~/node_common/environment";
import * as MW from "~/node_common/middleware";
import * as Data from "~/node_common/data";
@ -7,8 +8,8 @@ import { Libp2pCryptoIdentity } from "@textile/threads-core";
const initCORS = MW.init(MW.CORS);
const TEXTILE_KEY_INFO = {
key: process.env.TEXTILE_HUB_KEY,
secret: process.env.TEXTILE_HUB_SECRET,
key: Environment.TEXTILE_HUB_KEY,
secret: Environment.TEXTILE_HUB_SECRET,
};
export default async (req, res) => {

View File

@ -2,6 +2,7 @@ import * as React from "react";
import * as NavigationData from "~/common/navigation-data";
import * as Actions from "~/common/actions";
import * as State from "~/common/state";
import * as Credentials from "~/common/credentials";
import SceneDataTransfer from "~/scenes/SceneDataTransfer";
import SceneDeals from "~/scenes/SceneDeals";
@ -38,6 +39,9 @@ import ApplicationNavigation from "~/components/core/ApplicationNavigation";
import ApplicationHeader from "~/components/core/ApplicationHeader";
import ApplicationLayout from "~/components/core/ApplicationLayout";
import WebsitePrototypeWrapper from "~/components/core/WebsitePrototypeWrapper";
import Cookies from "universal-cookie";
const cookies = new Cookies();
const getCurrentNavigationStateById = (navigation, targetId) => {
let target = null;
@ -79,8 +83,8 @@ export default class ApplicationPage extends React.Component {
history: [{ id: 1, scrollTop: 0 }],
currentIndex: 0,
data: null,
selected: null,
viewer: null,
selected: State.getSelectedState(this.props.viewer),
viewer: State.getInitialState(this.props.viewer),
sidebar: null,
file: null,
};
@ -204,18 +208,48 @@ export default class ApplicationPage extends React.Component {
console.log(response);
response = await Actions.signIn({
username: "test",
password: "test",
});
console.log(response);
if (response.error) {
console.log("authentication error");
return null;
}
if (response.token) {
cookies.set(Credentials.session.key, response.token);
}
response = await Actions.hydrateAuthenticatedUser({
username: "test",
});
console.log(response);
if (!response || response.error) {
console.log("You probably needed to be authenticated.");
return null;
}
this.setState({
viewer: State.getInitialState(response.data),
selected: State.getSelectedState(response.data),
});
};
_handleSignOut = () => {
const jwt = cookies.get(Credentials.session.key);
if (jwt) {
cookies.remove(Credentials.session.key);
window.location.reload();
}
};
_handleViewerChange = (e) => {
this.setState({
viewer: { ...this.state.viewer, [e.target.name]: e.target.value },
@ -348,7 +382,6 @@ export default class ApplicationPage extends React.Component {
};
render() {
// TODO(jim): Render Sign In Screen.
if (!this.state.viewer) {
return (
<WebsitePrototypeWrapper
@ -390,6 +423,7 @@ export default class ApplicationPage extends React.Component {
history={this.state.history}
onNavigateTo={this._handleNavigateTo}
onAction={this._handleAction}
onSignOut={this._handleSignOut}
/>
);

View File

@ -1,15 +1,12 @@
if (process.env.NODE_ENV !== "www") {
require("dotenv").config();
}
import * as Environment from "~/node_common/environment";
import * as Strings from "./common/strings";
import * as Middleware from "./node_common/middleware";
import * as Utilities from "./node_common/utilities";
import * as Constants from "./node_common/constants";
import * as Models from "./node_common/models";
import express from "express";
import next from "next";
import compression from "compression";
import JWT from "jsonwebtoken";
const production =
process.env.NODE_ENV === "production" || process.env.NODE_ENV === "www";
@ -27,9 +24,29 @@ app.prepare().then(async () => {
server.use("/public", express.static("public"));
server.get("/application", async (req, res) => {
let viewer = null;
if (!Strings.isEmpty(req.headers.cookie)) {
const token = req.headers.cookie.replace(
/(?:(?:^|.*;\s*)WEB_SERVICE_SESSION_KEY\s*\=\s*([^;]*).*$)|^.*$/,
"$1"
);
if (!Strings.isEmpty(token)) {
try {
const decoded = JWT.verify(token, Environment.JWT_SECRET);
if (decoded.username) {
viewer = await Models.getViewer({ username: decoded.username });
}
} catch (e) {}
}
}
return app.render(req, res, "/application", {
wsPort: null,
production: productionWeb,
viewer,
});
});