├── .babelrc
├── .gitignore
├── README.md
├── common
├── actions.js
├── constants.js
├── credentials.js
├── data.js
├── middleware.js
├── strings.js
├── styles
│ └── global.js
└── utilities.js
├── components
├── Form.js
├── GoogleButton.js
├── PageState.js
└── Text.js
├── db.js
├── index.js
├── knexfile.js
├── nodemon.json
├── package.json
├── pages
├── _app.js
├── index.js
├── organization.js
├── sign-in-confirm.js
├── sign-in-error.js
├── sign-in-success.js
└── sign-out.js
├── public
└── static
│ ├── .gitkeep
│ └── SFMono-Medium.woff
├── routes
├── api
│ ├── sign-in.js
│ └── viewer-delete.js
├── index.js
├── sign-in-confirm.js
├── sign-in-success.js
├── sign-in.js
└── target-organization.js
├── scripts
├── drop-database.js
├── index.js
├── seed-database.js
└── setup-database.js
└── server.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "next/babel",
5 | {
6 | "transform-runtime": {
7 | "useESModules": false
8 | }
9 | }
10 | ],
11 | "@emotion/babel-preset-css-prop",
12 | ],
13 | "plugins": [
14 | ["module-resolver", {
15 | "alias": {
16 | "~": "./"
17 | }
18 | }]
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .next
2 | .env
3 | .DS_STORE
4 | package-lock.json
5 | node_modules
6 | DS_STORE
7 |
8 | /**/*/package-lock.json
9 | /**/*/.DS_STORE
10 | /**/*/node_modules
11 | /**/*/.next
12 |
13 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## DEPRECATION NOTICE
2 |
3 | This template is no longer up to date. For an updated template, either as a team or individually, we encourage you to explore our [latest template](https://github.com/internet-development/nextjs-sass-starter) produced by [INTDEV](https://internet.dev). Thank you for your interest in our work!
4 |
5 | # next-postgres
6 |
7 | > **January 4th, 2022** ➝ _I recommend you use [www-react-postgres](https://github.com/jimmylee/www-react-postgres) instead because it does not have an `express` server or a need for `babel`, therefore the template has less dependencies. That means there will be less attention cost required._
8 |
9 | This setup is using:
10 |
11 | - [NextJS 12.0.7](https://nextjs.org/)
12 | - [Postgres 11](https://github.com/brianc/node-postgres)
13 | - [Express 4.17.2](https://github.com/expressjs/express)
14 | - [Emotion CSS-in-JS 11.7.1](https://5bb1495273f2cf57a2cf39cc--emotion.netlify.com/)
15 | - [GoogleAPIs 92.0.0](https://github.com/googleapis/google-api-nodejs-client#readme)
16 | - [Knex 0.95.15](https://knexjs.org/)
17 |
18 | It is for:
19 |
20 | - Running a website with users.
21 | - Using [Google web browser OAuth](https://developers.google.com/identity).
22 | - Replacing my old work with [next-postgres-sequelize](https://github.com/jimmylee/next-postgres-sequelize).
23 | - Deploying with [https://render.com](https://render.com) or something like it.
24 |
25 | ## Setup
26 |
27 | #### Step 1
28 |
29 | Clone this repository!
30 |
31 | #### Step 2
32 |
33 | Create an `.env` file at your project root.
34 |
35 | ```sh
36 | CLIENT_ID=GET_ME_FROM_GOOGLE
37 | CLIENT_SECRET=GET_ME_FROM_GOOGLE
38 | JWT_SECRET=74b8b454-29a6-4282-bdec-7e2895c835eb
39 | PASSWORD_SECRET=\$2b\$10\$oaBusYfHLawNiFDqsqkTM.
40 | ```
41 |
42 | - Generate your own `PASSWORD_SECRET` with `BCrypt.genSaltSync(10)`. You need to escape `$` signs.
43 | - Generate your own `JWT_SECRET`.
44 | - Obtain `CLIENT_ID` and `CLIENT_SECRET` from [https://console.developers.google.com](https://console.developers.google.com) after you setup your application.
45 | - Use `CMD+F` to find `REDIRECT_URIS` in `~/common/credentials`. Google needs this string for the **Authorized redirect URIs** setting. The default is: `http://localhost:1337/sign-in-confirm`.
46 |
47 | #### Step 3
48 |
49 | This is important. Enable [People API](https://console.developers.google.com/apis/api/people.googleapis.com/overview). Otherwise Google Auth will not work.
50 |
51 | ## Setup: Running the website (OSX)
52 |
53 | All steps assume you have [Homebrew](https://brew.sh/) installed on your machine. You might want to install [iTerm](https://iterm2.com/) since you need multiple terminal windows open as well.
54 |
55 | Using another version of Postgres? That may be okay. I use Postgres 11 to share versions with [Render](https://render.com/) but I have tried these steps with Postgres 9 as well.
56 |
57 | #### Installing Postgres 11
58 |
59 | Mileage may vary with a different version.
60 |
61 | ```sh
62 | brew uninstall postgresql
63 | brew install postgresql@11
64 | brew link postgresql@11 --force
65 | ```
66 |
67 | #### Installing Node
68 |
69 | Make sure NodeJS version 10+ is installed on your machine.
70 |
71 | ```sh
72 | brew install node
73 | ```
74 |
75 | #### Installing nodemon
76 |
77 | We use `nodemon` to reload the site whenever changes are made locally.
78 |
79 | ```sh
80 | npm install -g nodemon
81 | ```
82 |
83 | #### Installing Node packages
84 |
85 | Once you have Postgres and Node, run these commands:
86 |
87 | ```sh
88 | npm install
89 | npm run dev
90 | ```
91 |
92 | #### Run Postgres
93 |
94 | In a seperate terminal tab run your postgres version, in this case the command below is referencing Postgres 11.
95 |
96 | ```sh
97 | postgres -D /usr/local/var/postgresql@11 -p 1334
98 | ```
99 |
100 | You may need to run `brew services stop postgresql@11` since we're running postgres on a different port.
101 |
102 | If you get an error that `lock file "postmaster.pid already exists` like I did, you can delete that file with something like `rm /usr/local/var/postgresql@11/postmaster.pid`.
103 |
104 | #### Create a new database
105 |
106 | - Start with creating an admin user.
107 | - Finish with creating a database for testing.
108 |
109 | ```sh
110 | # Enter Postgres console
111 | psql postgres -p 1334
112 |
113 | # Create a new user for yourself
114 | CREATE ROLE admin WITH LOGIN PASSWORD 'oblivion';
115 |
116 | # Allow yourself to create databases
117 | ALTER ROLE admin CREATEDB;
118 |
119 | # You need to do this to install uuid-ossp in a later step
120 | ALTER USER admin WITH SUPERUSER;
121 |
122 | # Exit Postgres console
123 | \q
124 |
125 | # Log in as your new user.
126 | psql postgres -p 1334 -U admin
127 |
128 | # Create a database named: nptdb.
129 | # If you change this, update knexfile.js
130 | CREATE DATABASE nptdb;
131 |
132 | # Give your self privileges
133 | GRANT ALL PRIVILEGES ON DATABASE nptdb TO admin;
134 |
135 | # List all of your databases
136 | \list
137 |
138 | # Connect to your newly created DB as a test
139 | \connect nptdb
140 |
141 | # Exit Postgres console
142 | \q
143 | ```
144 |
145 | ## Setup: Fill database with tables
146 |
147 | Run the following commands:
148 |
149 | ```sh
150 | npm run do-setup-database
151 | npm run do-seed-database
152 | ```
153 |
154 | ## View the website
155 |
156 | View `http://localhost:1337` in your browser.
157 |
158 | ### Scripts
159 |
160 | If you need to run node script without running the server, use this example to get started
161 |
162 | ```sh
163 | npm run script example
164 | ```
165 |
166 | ## Setup: Production deploy
167 |
168 | Coming soon.
169 |
170 | ## Questions?
171 |
172 | Feel free to slang any feels to [@wwwjim](https://twitter.com/wwwjim).
173 |
--------------------------------------------------------------------------------
/common/actions.js:
--------------------------------------------------------------------------------
1 | import 'isomorphic-fetch';
2 |
3 | import Cookies from 'universal-cookie';
4 |
5 | import * as Constants from '~/common/constants';
6 |
7 | const cookies = new Cookies();
8 |
9 | const REQUEST_HEADERS = {
10 | Accept: 'application/json',
11 | 'Content-Type': 'application/json',
12 | };
13 |
14 | const SERVER_PATH = '';
15 |
16 | const getHeaders = () => {
17 | const jwt = cookies.get(Constants.session.key);
18 |
19 | if (jwt) {
20 | return {
21 | ...REQUEST_HEADERS,
22 | authorization: `Bearer ${jwt}`,
23 | };
24 | }
25 |
26 | return REQUEST_HEADERS;
27 | };
28 |
29 | export const onDeleteViewer = async e => {
30 | const options = {
31 | method: 'POST',
32 | headers: getHeaders(),
33 | credentials: 'include',
34 | body: JSON.stringify({}),
35 | };
36 |
37 | const response = await fetch(`${SERVER_PATH}/api/users/delete`, options);
38 | const json = await response.json();
39 |
40 | if (json.error) {
41 | console.log(json.error);
42 | return;
43 | }
44 |
45 | window.location.href = '/';
46 | };
47 |
48 | export const onLocalSignIn = async (e, props, auth) => {
49 | const options = {
50 | method: 'POST',
51 | headers: getHeaders(),
52 | credentials: 'include',
53 | body: JSON.stringify({
54 | ...auth,
55 | }),
56 | };
57 |
58 | const response = await fetch(`${SERVER_PATH}/api/sign-in`, options);
59 | const json = await response.json();
60 |
61 | if (json.error) {
62 | console.log(json.error);
63 | return;
64 | }
65 |
66 | if (json.token) {
67 | cookies.set(Constants.session.key, json.token);
68 | }
69 |
70 | window.location.href = '/sign-in-success';
71 | };
72 |
--------------------------------------------------------------------------------
/common/constants.js:
--------------------------------------------------------------------------------
1 | export const zindex = {
2 | sidebar: 1,
3 | editor: {
4 | menu: 2,
5 | },
6 | };
7 |
8 | export const session = {
9 | key: 'WEB_SERVICE_SESSION_KEY',
10 | };
11 |
12 | export const colors = {
13 | gray: '#F7F8FA',
14 | black: '#000000',
15 | white: '#ffffff',
16 | };
17 |
18 | export const theme = {
19 | buttonBackground: '#C6C6C6',
20 | buttonBackgroundHover: '#E0E0E0',
21 | buttonBackgroundActive: '#A8A8A8',
22 | pageBackground: colors.gray,
23 | pageText: colors.black,
24 | };
25 |
--------------------------------------------------------------------------------
/common/credentials.js:
--------------------------------------------------------------------------------
1 | if (process.env.NODE_ENV !== "production") {
2 | require("dotenv").config();
3 | }
4 |
5 | export const CLIENT_ID = process.env.CLIENT_ID;
6 | export const CLIENT_SECRET = process.env.CLIENT_SECRET;
7 | export const PASSWORD_SECRET = process.env.PASSWORD_SECRET;
8 | export const REDIRECT_URIS = "http://localhost:1337/sign-in-confirm";
9 | export const JWT_SECRET = process.env.JWT_SECRET;
10 |
--------------------------------------------------------------------------------
/common/data.js:
--------------------------------------------------------------------------------
1 | import * as Credentials from '~/common/credentials';
2 | import * as Utilities from '~/common/utilities';
3 |
4 | import DB from '~/db';
5 | import JWT, { decode } from 'jsonwebtoken';
6 |
7 | const google = require('googleapis').google;
8 | const OAuth2 = google.auth.OAuth2;
9 |
10 | const runQuery = async ({ queryFn, errorFn, label }) => {
11 | let response;
12 | try {
13 | response = await queryFn();
14 | } catch (e) {
15 | response = errorFn(e);
16 | }
17 |
18 | console.log('[ database-query ]', { query: label });
19 | return response;
20 | };
21 |
22 | export const deleteUserById = async ({ id }) => {
23 | return await runQuery({
24 | label: 'DELETE_USER_BY_ID',
25 | queryFn: async () => {
26 | const data = await DB.from('users')
27 | .where({ id })
28 | .del();
29 |
30 | return 1 === data;
31 | },
32 | errorFn: async e => {
33 | return {
34 | error: 'DELETE_USER_BY_ID',
35 | source: e,
36 | };
37 | },
38 | });
39 | };
40 |
41 | export const deleteUserFromOrganizationByUserId = async ({ organizationId, userId }) => {
42 | return await runQuery({
43 | label: 'DELETE_USER_FROM_ORGANIZATION_BY_USER_ID',
44 | queryFn: async () => {
45 | const o = await DB.select('*')
46 | .from('organizations')
47 | .where({ id: organizationId })
48 | .first();
49 |
50 | if (!o || !o.id) {
51 | return null;
52 | }
53 |
54 | if (o.data && o.data.ids && o.data.ids.length === 1) {
55 | const data = await DB.from('organizations')
56 | .where({ id: organizationId })
57 | .del();
58 |
59 | return 1 === data;
60 | }
61 |
62 | const data = await DB.from('organizations')
63 | .where('id', o.id)
64 | .update({
65 | data: {
66 | ...o.data,
67 | ids: o.data.ids.filter(each => userId !== each),
68 | },
69 | })
70 | .returning('*');
71 |
72 | const index = data ? data.pop() : null;
73 | return index;
74 | },
75 | errorFn: async e => {
76 | return {
77 | error: 'DELETE_USER_FROM_ORGANIZATION_BY_USER_ID',
78 | source: e,
79 | };
80 | },
81 | });
82 | };
83 |
84 | export const getOrganizationByUserId = async ({ id }) => {
85 | return await runQuery({
86 | label: 'GET_ORGANIZATION_BY_USER_ID',
87 | queryFn: async () => {
88 | const hasUser = userId => DB.raw(`?? @> ?::jsonb`, ['data', JSON.stringify({ ids: [userId] })]);
89 |
90 | const query = await DB.select('*')
91 | .from('organizations')
92 | .where(hasUser(id))
93 | .first();
94 |
95 | if (!query || query.error) {
96 | return null;
97 | }
98 |
99 | if (query.id) {
100 | return query;
101 | }
102 |
103 | return null;
104 | },
105 | errorFn: async e => {
106 | return {
107 | error: 'GET_ORGANIZATION_BY_USER_ID',
108 | source: e,
109 | };
110 | },
111 | });
112 | };
113 |
114 | export const getViewer = async (req, existingToken = undefined) => {
115 | let viewer = null;
116 |
117 | try {
118 | let token = existingToken;
119 | if (!token) {
120 | token = Utilities.getToken(req);
121 | }
122 |
123 | let decode = JWT.verify(token, Credentials.JWT_SECRET);
124 | viewer = await getUserByEmail({ email: decode.email });
125 | } catch (e) {}
126 |
127 | if (!viewer || viewer.error) {
128 | viewer = null;
129 | }
130 |
131 | return { viewer };
132 | };
133 |
134 | export const getOrganizationByDomain = async ({ domain }) => {
135 | return await runQuery({
136 | label: 'GET_ORGANIZATION_BY_DOMAIN',
137 | queryFn: async () => {
138 | const query = await DB.select('*')
139 | .from('organizations')
140 | .where({ domain })
141 | .first();
142 |
143 | if (!query || query.error) {
144 | return null;
145 | }
146 |
147 | if (query.id) {
148 | return query;
149 | }
150 |
151 | return null;
152 | },
153 | errorFn: async e => {
154 | return {
155 | error: 'GET_ORGANIZATION_BY_DOMAIN',
156 | source: e,
157 | };
158 | },
159 | });
160 | };
161 |
162 | export const getUserByEmail = async ({ email }) => {
163 | return await runQuery({
164 | label: 'GET_USER_BY_EMAIL',
165 | queryFn: async () => {
166 | const query = await DB.select('*')
167 | .from('users')
168 | .where({ email })
169 | .first();
170 |
171 | if (!query || query.error) {
172 | return null;
173 | }
174 |
175 | if (query.id) {
176 | return query;
177 | }
178 |
179 | return null;
180 | },
181 | errorFn: async e => {
182 | return {
183 | error: 'GET_USER_BY_EMAIL',
184 | source: e,
185 | };
186 | },
187 | });
188 | };
189 |
190 | export const createOrganization = async ({ domain, data = {} }) => {
191 | return await runQuery({
192 | label: 'CREATE_ORGANIZATION',
193 | queryFn: async () => {
194 | const query = await DB.insert({
195 | domain,
196 | data,
197 | })
198 | .into('organizations')
199 | .returning('*');
200 |
201 | const index = query ? query.pop() : null;
202 | return index;
203 | },
204 | errorFn: async e => {
205 | return {
206 | error: 'CREATE_ORGANIZATION',
207 | source: e,
208 | };
209 | },
210 | });
211 | };
212 |
213 | export const createUser = async ({ email, password, salt, data = {} }) => {
214 | return await runQuery({
215 | label: 'CREATE_USER',
216 | queryFn: async () => {
217 | const query = await DB.insert({
218 | email,
219 | password,
220 | salt,
221 | data,
222 | })
223 | .into('users')
224 | .returning('*');
225 |
226 | const index = query ? query.pop() : null;
227 | return index;
228 | },
229 | errorFn: async e => {
230 | return {
231 | error: 'CREATE_USER',
232 | source: e,
233 | };
234 | },
235 | });
236 | };
237 |
--------------------------------------------------------------------------------
/common/middleware.js:
--------------------------------------------------------------------------------
1 | import * as Strings from '~/common/strings';
2 | import * as Constants from '~/common/constants';
3 | import * as Data from '~/common/data';
4 | import * as Credentials from '~/common/credentials';
5 |
6 | import JWT from 'jsonwebtoken';
7 |
8 | export const CORS = async (req, res, next) => {
9 | res.header('Access-Control-Allow-Origin', '*');
10 | res.header('Access-Control-Allow-Methods', 'GET, POST, PATCH, PUT, DELETE, OPTIONS');
11 | res.header('Access-Control-Allow-Headers', 'Origin, Accept, Content-Type, Authorization');
12 |
13 | if (req.method === 'OPTIONS') {
14 | return res.status(200).end();
15 | }
16 |
17 | next();
18 | };
19 |
20 | export const RequireCookieAuthentication = async (req, res, next) => {
21 | if (Strings.isEmpty(req.headers.cookie)) {
22 | return res.redirect('/sign-in-error');
23 | }
24 |
25 | const token = req.headers.cookie.replace(/(?:(?:^|.*;\s*)WEB_SERVICE_SESSION_KEY\s*\=\s*([^;]*).*$)|^.*$/, '$1');
26 |
27 | try {
28 | var decoded = JWT.verify(token, Credentials.JWT_SECRET);
29 | const user = await Data.getUserByEmail({ email: decoded.email });
30 |
31 | if (!user || user.error) {
32 | return res.redirect('/sign-in-error');
33 | }
34 | } catch (err) {
35 | console.log(err);
36 | return res.redirect('/sign-in-error');
37 | }
38 |
39 | next();
40 | };
41 |
--------------------------------------------------------------------------------
/common/strings.js:
--------------------------------------------------------------------------------
1 | export const isEmpty = string => {
2 | return !string || !string.toString().trim();
3 | };
4 |
5 | export const pluralize = (text, count) => {
6 | return count > 1 || count === 0 ? `${text}s` : text;
7 | };
8 |
9 | export const elide = (string, length = 140, emptyState = '...') => {
10 | if (isEmpty(string)) {
11 | return emptyState;
12 | }
13 |
14 | if (string.length < length) {
15 | return string.trim();
16 | }
17 |
18 | return `${string.substring(0, length)}...`;
19 | };
20 |
21 | export const toDate = data => {
22 | const date = new Date(data);
23 | return `${date.getMonth() + 1}-${date.getDate()}-${date.getFullYear()}`;
24 | };
25 |
26 | export const getDomainFromEmail = email => {
27 | return email.replace(/.*@/, '');
28 | };
29 |
30 | export const capitalizeFirstLetter = word => {
31 | return word.charAt(0).toUpperCase() + word.substring(1);
32 | };
33 |
--------------------------------------------------------------------------------
/common/styles/global.js:
--------------------------------------------------------------------------------
1 | import * as Constants from "~/common/constants";
2 |
3 | import { css } from "@emotion/react";
4 |
5 | /* prettier-ignore */
6 | const GlobalStyles = () => css`
7 | @font-face {
8 | font-family: 'mono';
9 | src: url('/static/SFMono-Medium.woff');
10 | }
11 |
12 | html, body, div, span, applet, object, iframe,
13 | h1, h2, h3, h4, h5, h6, p, blockquote, pre,
14 | a, abbr, acronym, address, big, cite, code,
15 | del, dfn, em, img, ins, kbd, q, s, samp,
16 | small, strike, strong, sub, sup, tt, var,
17 | b, u, i, center,
18 | dl, dt, dd, ol, ul, li,
19 | fieldset, form, label, legend,
20 | table, caption, tbody, tfoot, thead, tr, th, td,
21 | article, aside, canvas, details, embed,
22 | figure, figcaption, footer, header, hgroup,
23 | menu, nav, output, ruby, section, summary,
24 | time, mark, audio, video {
25 | box-sizing: border-box;
26 | margin: 0;
27 | padding: 0;
28 | border: 0;
29 | vertical-align: baseline;
30 | }
31 |
32 | article, aside, details, figcaption, figure,
33 | footer, header, hgroup, menu, nav, section {
34 | display: block;
35 | }
36 |
37 | html, body {
38 | background: ${Constants.theme.pageBackground};
39 | color: ${Constants.theme.pageText};
40 | font-size: 16px;
41 | font-family: 'body', -apple-system, BlinkMacSystemFont, avenir next, avenir, helvetica neue, helvetica,
42 | ubuntu, roboto, noto, segoe ui, arial, sans-serif;
43 |
44 | @media (max-width: 768px) {
45 | font-size: 12px;
46 | }
47 |
48 | ::-webkit-scrollbar {
49 | display: none;
50 | }
51 | }
52 | `;
53 |
54 | export default GlobalStyles;
55 |
--------------------------------------------------------------------------------
/common/utilities.js:
--------------------------------------------------------------------------------
1 | import * as Strings from '~/common/strings';
2 |
3 | // TODO(jim): Refactor this Regex so you can bind the string.
4 | export const getToken = req => {
5 | if (Strings.isEmpty(req.headers.cookie)) {
6 | return null;
7 | }
8 |
9 | return req.headers.cookie.replace(
10 | /(?:(?:^|.*;\s*)WEB_SERVICE_SESSION_KEY\s*\=\s*([^;]*).*$)|^.*$/,
11 | '$1'
12 | );
13 | };
14 |
15 | export const parseAuthHeader = value => {
16 | if (typeof value !== 'string') {
17 | return null;
18 | }
19 |
20 | var matches = value.match(/(\S+)\s+(\S+)/);
21 | return matches && { scheme: matches[1], value: matches[2] };
22 | };
23 |
--------------------------------------------------------------------------------
/components/Form.js:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as Constants from "~/common/constants";
3 |
4 | import { css } from "@emotion/react";
5 |
6 | const STYLES_BUTTON = css`
7 | background: ${Constants.theme.buttonBackground};
8 | transition: 200ms ease background;
9 | font-weight: 700;
10 | border: none;
11 | margin: 0;
12 | padding: 0 24px 0 24px;
13 | height: 48px;
14 | border-radius: 48px;
15 | width: auto;
16 | overflow: visible;
17 | display: flex;
18 | align-items: center;
19 | justify-content: center;
20 | color: inherit;
21 | font: inherit;
22 | line-height: normal;
23 | cursor: pointer;
24 | white-space: nowrap;
25 | position: relative;
26 | text-decoration: none;
27 |
28 | -webkit-font-smoothing: inherit;
29 | -moz-osx-font-smoothing: inherit;
30 | -webkit-appearance: none;
31 |
32 | ::-moz-focus-inner {
33 | border: 0;
34 | outline: 0;
35 | padding: 0;
36 | }
37 |
38 | :hover {
39 | background: ${Constants.theme.buttonBackgroundHover};
40 | }
41 |
42 | :focus {
43 | border: 0;
44 | outline: 0;
45 | }
46 |
47 | :active {
48 | background: ${Constants.theme.buttonBackgroundActive};
49 | border: 0;
50 | outline: 0;
51 | }
52 |
53 | min-width: 280px;
54 | font-size: 18px;
55 | `;
56 |
57 | export const Button = ({ children, style, onClick, href }) => {
58 | if (href) {
59 | return (
60 |
61 | {children}
62 |
63 | );
64 | }
65 |
66 | return (
67 |
70 | );
71 | };
72 |
73 | const STYLES_INPUT = css`
74 | border: none;
75 | outline: 0;
76 | margin: 0;
77 | padding: 0 24px 0 24px;
78 | background: ${Constants.colors.gray3};
79 |
80 | :focus {
81 | border: 0;
82 | outline: 0;
83 | }
84 |
85 | :active {
86 | border: 0;
87 | outline: 0;
88 | }
89 |
90 | -webkit-font-smoothing: inherit;
91 | -moz-osx-font-smoothing: inherit;
92 | -webkit-appearance: none;
93 |
94 | ::-moz-focus-inner {
95 | border: 0;
96 | padding: 0;
97 | }
98 |
99 | height: 48px;
100 | width: 100%;
101 | box-sizing: border-box;
102 | font-weight: 400;
103 | font-size: 18px;
104 | `;
105 |
106 | export const Input = ({
107 | children,
108 | style,
109 | value,
110 | name,
111 | placeholder,
112 | type = "text",
113 | autoComplete = "input-autocomplete-off",
114 | onBlur = (e) => {},
115 | onFocus = (e) => {},
116 | onChange = (e) => {},
117 | }) => {
118 | return (
119 |
130 | {children}
131 |
132 | );
133 | };
134 |
--------------------------------------------------------------------------------
/components/GoogleButton.js:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { css } from "@emotion/react";
4 |
5 | const STYLES_GOOGLE = css`
6 | height: 48px;
7 | padding: 0 24px 0 0;
8 | border-radius: 32px;
9 | background: #000;
10 | color: #fff;
11 | display: inline-flex;
12 | align-items: center;
13 | justify-content: center;
14 | font-weight: 600;
15 | cursor: pointer;
16 | text-decoration: none;
17 | transition: 200ms ease all;
18 | transition-property: color;
19 |
20 | :visited {
21 | color: #fff;
22 | }
23 |
24 | :hover {
25 | color: #fff;
26 | background: #222;
27 | }
28 | `;
29 |
30 | const STYLES_LOGO = css`
31 | height: 32px;
32 | width: 32px;
33 | border-radius: 32px;
34 | display: inline-flex;
35 | background-size: cover;
36 | background-position: 50% 50%;
37 | background-image: url("/static/logos/google.jpg");
38 | margin-right: 16px;
39 | margin-left: 8px;
40 | `;
41 |
42 | export default class GoogleButton extends React.Component {
43 | render() {
44 | return (
45 |
46 |
47 | Sign in with Google
48 |
49 | );
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/components/PageState.js:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as Constants from "~/common/constants";
3 |
4 | import { css } from "@emotion/react";
5 |
6 | const STYLES_PAGE_STATE = css`
7 | font-family: "mono";
8 | width: 100%;
9 | background: ${Constants.colors.black};
10 | color: ${Constants.colors.white};
11 | font-size: 10px;
12 | `;
13 |
14 | const STYLES_SECTION = css`
15 | width: 100%;
16 | white-space: pre-wrap;
17 | padding: 24px;
18 | `;
19 |
20 | const STYLES_TITLE_SECTION = css`
21 | background: #111111;
22 | padding: 24px;
23 | `;
24 |
25 | export default class PageState extends React.Component {
26 | render() {
27 | const testData = {
28 | viewer: this.props.data.viewer,
29 | organization: this.props.data.organization,
30 | };
31 | return (
32 |
33 |
NEXT-POSTGRES 0.1 - DATA VIEWER
34 |
{JSON.stringify(testData, null, 2)}
35 |
36 | );
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/components/Text.js:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as Constants from "~/common/constants";
3 |
4 | import { css } from "@emotion/react";
5 |
6 | const MAX_WIDTH = 768;
7 |
8 | const STYLES_HEADING = css`
9 | overflow-wrap: break-word;
10 | white-space: pre-wrap;
11 | font-weight: 400;
12 | font-size: 3.052rem;
13 | position: relative;
14 | max-width: ${MAX_WIDTH}px;
15 | width: 100%;
16 | padding: 0 24px 0 24px;
17 | margin: 0 auto 0 auto;
18 | `;
19 |
20 | export const H1 = (props) => {
21 | return ;
22 | };
23 |
24 | const STYLES_HEADING_TWO = css`
25 | overflow-wrap: break-word;
26 | white-space: pre-wrap;
27 | font-weight: 400;
28 | font-size: 1.728rem;
29 | position: relative;
30 | max-width: ${MAX_WIDTH}px;
31 | width: 100%;
32 | padding: 0 24px 0 24px;
33 | margin: 0 auto 0 auto;
34 | `;
35 |
36 | export const H2 = (props) => {
37 | return ;
38 | };
39 |
40 | const STYLES_PARAGRAPH = css`
41 | overflow-wrap: break-word;
42 | white-space: pre-wrap;
43 | font-weight: 400;
44 | font-size: 1.44rem;
45 | line-height: 1.5;
46 | position: relative;
47 | max-width: ${MAX_WIDTH}px;
48 | width: 100%;
49 | padding: 0 24px 0 24px;
50 | margin: 0 auto 0 auto;
51 | `;
52 |
53 | export const P = (props) => {
54 | return ;
55 | };
56 |
57 | const STYLES_BODY_TEXT = css`
58 | overflow-wrap: break-word;
59 | white-space: pre-wrap;
60 | font-weight: 400;
61 | font-size: 1rem;
62 | line-height: 1.5;
63 | position: relative;
64 | `;
65 |
66 | export const BODY = (props) => {
67 | return ;
68 | };
69 |
--------------------------------------------------------------------------------
/db.js:
--------------------------------------------------------------------------------
1 | if (process.env.NODE_ENV !== 'production') {
2 | require('dotenv').config();
3 | }
4 |
5 | import configs from '~/knexfile';
6 | import knex from 'knex';
7 |
8 | const environment =
9 | process.env.NODE_ENV !== 'production' ? 'development' : 'production';
10 | const envConfig = configs[environment];
11 | const db = knex(envConfig);
12 |
13 | module.exports = db;
14 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | require('@babel/register')({
2 | presets: ['@babel/preset-env'],
3 | ignore: ['node_modules', '.next'],
4 | });
5 |
6 | module.exports = require('./server.js');
7 |
--------------------------------------------------------------------------------
/knexfile.js:
--------------------------------------------------------------------------------
1 | /* prettier-ignore */
2 | module.exports = {
3 | development: {
4 | client: 'pg',
5 | connection: {
6 | port: 1334,
7 | host: '127.0.0.1',
8 | database: 'nptdb',
9 | user: 'admin',
10 | password: 'oblivion'
11 | }
12 | },
13 | production: {
14 | client: 'pg',
15 | connection: {
16 | port: 1334,
17 | host: '127.0.0.1',
18 | database: 'nptdb',
19 | user: 'admin',
20 | password: 'oblivion'
21 | }
22 | }
23 | };
24 |
--------------------------------------------------------------------------------
/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "verbose": true,
3 | "ignore": ["node_modules", ".next"],
4 | "watch": [
5 | "routes/**/*",
6 | "common/**/*",
7 | "components/**/*",
8 | "pages/**/*",
9 | "public/**/*",
10 | "index.js",
11 | "server.js"
12 | ],
13 | "ext": "js json"
14 | }
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-postgres",
3 | "version": "0.0.2",
4 | "scripts": {
5 | "dev": "nodemon .",
6 | "build": "next build",
7 | "start": "NODE_ENV=production node .",
8 | "script": "NODE_TLS_REJECT_UNAUTHORIZED=0 node --max-old-space-size=8192 node script",
9 | "do-setup-database": "node ./scripts setup-database",
10 | "do-seed-database": "node ./scripts seed-database",
11 | "do-drop-database": "node ./scripts drop-database"
12 | },
13 | "dependencies": {
14 | "@babel/preset-env": "^7.16.5",
15 | "@babel/register": "^7.16.5",
16 | "@emotion/babel-preset-css-prop": "11.2.0",
17 | "@emotion/react": "11.7.1",
18 | "babel-plugin-module-resolver": "^4.1.0",
19 | "bcrypt": "^5.0.1",
20 | "body-parser": "^1.19.0",
21 | "compression": "^1.7.4",
22 | "cookie-parser": "^1.4.6",
23 | "dotenv": "^10.0.0",
24 | "express": "^4.17.2",
25 | "googleapis": "^92.0.0",
26 | "isomorphic-fetch": "^3.0.0",
27 | "jsonwebtoken": "^8.5.1",
28 | "knex": "^0.95.15",
29 | "next": "^12.0.7",
30 | "pg": "^8.7.1",
31 | "react": "^17.0.2",
32 | "react-dom": "^17.0.2",
33 | "universal-cookie": "^4.0.4"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { Global } from "@emotion/react";
4 |
5 | import App from "next/app";
6 | import injectGlobalStyles from "~/common/styles/global";
7 |
8 | function MyApp({ Component, pageProps }) {
9 | return (
10 |
11 |
12 |
13 |
14 | );
15 | }
16 |
17 | MyApp.getInitialProps = async (appContext) => {
18 | const appProps = await App.getInitialProps(appContext);
19 | return { ...appProps };
20 | };
21 |
22 | export default MyApp;
23 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import Cookies from "universal-cookie";
3 |
4 | import * as React from "react";
5 | import * as Actions from "~/common/actions";
6 | import * as Constants from "~/common/constants";
7 | import * as Strings from "~/common/strings";
8 |
9 | import { H1, H2, P } from "~/components/Text";
10 | import { Input, Button } from "~/components/Form";
11 | import { css } from "@emotion/react";
12 |
13 | import PageState from "~/components/PageState";
14 |
15 | const STYLES_FORM = css`
16 | padding: 24px;
17 | width: 100%;
18 | margin: 48px auto 0 auto;
19 | max-width: 768px;
20 | `;
21 |
22 | const STYLES_TOP = css`
23 | margin-top: 48px;
24 | `;
25 |
26 | const STYLES_LAYOUT = css`
27 | padding: 24px 24px 88px 24px;
28 | `;
29 |
30 | function Page(props) {
31 | const [auth, setAuth] = React.useState({ email: "", password: "" });
32 |
33 | return (
34 |
35 |
36 | next-postgres
37 |
38 |
39 |
40 |
Sign in
41 |
44 |
47 | {props.viewer ? (
48 |
53 | ) : null}
54 |
57 |
Actions.onDeleteViewer(e)}
60 | >
61 |
62 | Delete yourself (Must be authenticated).
63 |
64 |
65 |
66 |
E-mail
67 |
72 | setAuth({ ...auth, [e.target.name]: e.target.value })
73 | }
74 | />
75 |
Password
76 |
82 | setAuth({ ...auth, [e.target.name]: e.target.value })
83 | }
84 | />
85 |
86 |
89 |
90 |
91 |
92 |
93 | );
94 | }
95 |
96 | Page.getInitialProps = async (ctx) => {
97 | return {
98 | googleURL: ctx.query.googleURL,
99 | viewer: ctx.query.viewer,
100 | error: ctx.err,
101 | };
102 | };
103 |
104 | export default Page;
105 |
--------------------------------------------------------------------------------
/pages/organization.js:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 |
3 | import * as React from "react";
4 | import * as Constants from "~/common/constants";
5 |
6 | import { H1, H2, P } from "~/components/Text";
7 | import { css } from "@emotion/react";
8 |
9 | import PageState from "~/components/PageState";
10 |
11 | const STYLES_LAYOUT = css`
12 | padding: 24px 24px 88px 24px;
13 | `;
14 |
15 | function Page(props) {
16 | return (
17 |
18 |
19 | next-postgres
20 |
21 |
22 | {props.organization ? (
23 | {props.organization.data.name}
24 | ) : null}
25 |
28 |
31 |
32 | );
33 | }
34 |
35 | Page.getInitialProps = async (ctx) => {
36 | return {
37 | error: ctx.err,
38 | viewer: ctx.query.viewer,
39 | organization: ctx.query.organization,
40 | data: ctx.query.data,
41 | };
42 | };
43 |
44 | export default Page;
45 |
--------------------------------------------------------------------------------
/pages/sign-in-confirm.js:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import Cookies from "universal-cookie";
3 |
4 | import * as React from "react";
5 | import * as Constants from "~/common/constants";
6 |
7 | import { H1, H2, P } from "~/components/Text";
8 | import { css } from "@emotion/react";
9 |
10 | import PageState from "~/components/PageState";
11 |
12 | const cookies = new Cookies();
13 |
14 | const STYLES_LAYOUT = css`
15 | padding: 24px 24px 88px 24px;
16 | `;
17 |
18 | function Page(props) {
19 | React.useEffect(() => {
20 | if (props.jwt) {
21 | cookies.set(Constants.session.key, props.jwt);
22 | return;
23 | }
24 | }, []);
25 |
26 | return (
27 |
28 |
29 | next-postgres
30 |
31 |
32 |
33 |
Sign in confirm
34 |
37 |
40 |
41 |
42 | );
43 | }
44 |
45 | Page.getInitialProps = async (ctx) => {
46 | return {
47 | error: ctx.err,
48 | viewer: ctx.query.viewer,
49 | jwt: ctx.query.jwt,
50 | };
51 | };
52 |
53 | export default Page;
54 |
--------------------------------------------------------------------------------
/pages/sign-in-error.js:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import Cookies from "universal-cookie";
3 |
4 | import * as React from "react";
5 | import * as Constants from "~/common/constants";
6 |
7 | import { H1, H2, P } from "~/components/Text";
8 | import { css } from "@emotion/react";
9 |
10 | import PageState from "~/components/PageState";
11 |
12 | const cookies = new Cookies();
13 |
14 | const STYLES_LAYOUT = css`
15 | padding: 24px 24px 88px 24px;
16 | `;
17 |
18 | function Page(props) {
19 | return (
20 |
21 |
22 | next-postgres
23 |
24 |
25 |
26 |
Error
27 |
30 |
33 |
36 |
37 |
38 | );
39 | }
40 |
41 | Page.getInitialProps = async (ctx) => {
42 | return {
43 | error: ctx.err,
44 | viewer: ctx.query.viewer,
45 | };
46 | };
47 |
48 | export default Page;
49 |
--------------------------------------------------------------------------------
/pages/sign-in-success.js:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 |
3 | import * as React from "react";
4 | import * as Constants from "~/common/constants";
5 |
6 | import { H1, H2, P } from "~/components/Text";
7 | import { css } from "@emotion/react";
8 |
9 | import PageState from "~/components/PageState";
10 |
11 | const STYLES_LAYOUT = css`
12 | padding: 24px 24px 88px 24px;
13 | `;
14 |
15 | function Page(props) {
16 | return (
17 |
18 |
19 | next-postgres
20 |
21 |
22 |
23 |
You can only see this authenticated.
24 |
27 |
30 |
33 |
34 |
35 | );
36 | }
37 |
38 | Page.getInitialProps = async (ctx) => {
39 | return {
40 | error: ctx.err,
41 | viewer: ctx.query.viewer,
42 | data: ctx.query.data,
43 | };
44 | };
45 |
46 | export default Page;
47 |
--------------------------------------------------------------------------------
/pages/sign-out.js:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import Cookies from "universal-cookie";
3 |
4 | import * as React from "react";
5 | import * as Constants from "~/common/constants";
6 |
7 | import { H1, H2, P } from "~/components/Text";
8 | import { css } from "@emotion/react";
9 |
10 | import PageState from "~/components/PageState";
11 |
12 | const cookies = new Cookies();
13 |
14 | const STYLES_LAYOUT = css`
15 | padding: 24px 24px 88px 24px;
16 | `;
17 |
18 | function Page(props) {
19 | React.useEffect(() => {
20 | const jwt = cookies.get(Constants.session.key);
21 | if (jwt) {
22 | cookies.remove(Constants.session.key);
23 | return;
24 | }
25 | }, []);
26 |
27 | return (
28 |
29 |
30 | next-postgres
31 |
32 |
33 |
34 |
Signed out
35 |
38 |
39 |
40 | );
41 | }
42 |
43 | Page.getInitialProps = async (ctx) => {
44 | return {
45 | error: ctx.err,
46 | viewer: ctx.query.viewer,
47 | jwt: ctx.query.jwt,
48 | };
49 | };
50 |
51 | export default Page;
52 |
--------------------------------------------------------------------------------
/public/static/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jimmylee/next-postgres/6f6e93e97b95502d3bb83073b78346adfd9f7721/public/static/.gitkeep
--------------------------------------------------------------------------------
/public/static/SFMono-Medium.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jimmylee/next-postgres/6f6e93e97b95502d3bb83073b78346adfd9f7721/public/static/SFMono-Medium.woff
--------------------------------------------------------------------------------
/routes/api/sign-in.js:
--------------------------------------------------------------------------------
1 | import * as Strings from "~/common/strings";
2 | import * as Data from "~/common/data";
3 | import * as Utilities from "~/common/utilities";
4 | import * as Credentials from "~/common/credentials";
5 |
6 | import JWT from "jsonwebtoken";
7 | import BCrypt from "bcrypt";
8 | import { verifiedaccess } from "googleapis/build/src/apis/verifiedaccess";
9 |
10 | export default async (req, res) => {
11 | if (Strings.isEmpty(req.body.email)) {
12 | return res
13 | .status(500)
14 | .send({ error: "An e-mail address was not provided." });
15 | }
16 |
17 | if (Strings.isEmpty(req.body.password)) {
18 | return res.status(500).send({ error: "A password was not provided." });
19 | }
20 |
21 | let user = await Data.getUserByEmail({ email: req.body.email });
22 | if (!user) {
23 | const salt = BCrypt.genSaltSync(10);
24 | const hash = BCrypt.hashSync(req.body.password, salt);
25 | const double = BCrypt.hashSync(hash, salt);
26 | const triple = BCrypt.hashSync(double, Credentials.PASSWORD_SECRET);
27 |
28 | user = await Data.createUser({
29 | email: req.body.email,
30 | password: triple,
31 | salt,
32 | data: { verified: false },
33 | });
34 | } else {
35 | if (user.error) {
36 | return res
37 | .status(500)
38 | .send({ error: "We could not authenticate you (1)." });
39 | }
40 |
41 | const phaseOne = BCrypt.hashSync(req.body.password, user.salt);
42 | const phaseTwo = BCrypt.hashSync(phaseOne, user.salt);
43 | const phaseThree = BCrypt.hashSync(phaseTwo, Credentials.PASSWORD_SECRET);
44 |
45 | if (phaseThree !== user.password) {
46 | return res
47 | .status(500)
48 | .send({ error: "We could not authenticate you (2)." });
49 | }
50 | }
51 |
52 | const authorization = Utilities.parseAuthHeader(req.headers.authorization);
53 | if (authorization && !Strings.isEmpty(authorization.value)) {
54 | const verfied = JWT.verify(authorization.value, Credentials.JWT_SECRET);
55 |
56 | if (user.email === verfied.email) {
57 | return res.status(200).send({
58 | message: "You are already authenticated. Welcome back!",
59 | viewer: user,
60 | });
61 | }
62 | }
63 |
64 | const token = JWT.sign(
65 | { user: user.id, email: user.email },
66 | Credentials.JWT_SECRET
67 | );
68 |
69 | return res.status(200).send({ token });
70 | };
71 |
--------------------------------------------------------------------------------
/routes/api/viewer-delete.js:
--------------------------------------------------------------------------------
1 | import * as Strings from '~/common/strings';
2 | import * as Data from '~/common/data';
3 | import * as Utilities from '~/common/utilities';
4 | import * as Credentials from '~/common/credentials';
5 |
6 | import JWT from 'jsonwebtoken';
7 |
8 | export default async (req, res) => {
9 | const authorization = Utilities.parseAuthHeader(req.headers.authorization);
10 |
11 | if (!authorization) {
12 | return res.status(500).send({ error: 'viewer-delete (1)' });
13 | }
14 |
15 | const v = JWT.verify(authorization.value, Credentials.JWT_SECRET);
16 |
17 | if (!v || !v.email) {
18 | return res.status(500).send({ error: 'viewer-delete (2)' });
19 | }
20 |
21 | const user = await Data.getUserByEmail({ email: v.email });
22 |
23 | if (!user) {
24 | return res.status(500).send({ error: 'viewer-delete (3)' });
25 | }
26 |
27 | const organization = await Data.getOrganizationByUserId({ id: user.id });
28 |
29 | if (organization && organization.data && organization.data.ids && organization.data.ids.length === 1) {
30 | const co = await Data.deleteUserFromOrganizationByUserId({
31 | organizationId: organization.id,
32 | userId: user.id,
33 | });
34 |
35 | if (!co) {
36 | return res.status(500).send({ error: 'viewer-delete (4)' });
37 | }
38 | }
39 |
40 | const d = await Data.deleteUserById({ id: user.id });
41 | if (!d) {
42 | return res.status(500).send({ error: 'viewer-delete (5)' });
43 | }
44 |
45 | return res.status(200).send({ operation: true });
46 | };
47 |
--------------------------------------------------------------------------------
/routes/index.js:
--------------------------------------------------------------------------------
1 | import signIn from '~/routes/sign-in';
2 | import signInConfirm from '~/routes/sign-in-confirm';
3 | import signInSuccess from '~/routes/sign-in-success';
4 | import targetOrganization from '~/routes/target-organization';
5 |
6 | import apiSignIn from '~/routes/api/sign-in';
7 | import apiViewerDelete from '~/routes/api/viewer-delete';
8 |
9 | module.exports = {
10 | signIn,
11 | signInConfirm,
12 | signInSuccess,
13 | targetOrganization,
14 | api: {
15 | viewerDelete: apiViewerDelete,
16 | signIn: apiSignIn,
17 | },
18 | };
19 |
--------------------------------------------------------------------------------
/routes/sign-in-confirm.js:
--------------------------------------------------------------------------------
1 | import * as Credentials from "~/common/credentials";
2 | import * as Data from "~/common/data";
3 | import * as Strings from "~/common/strings";
4 |
5 | import JWT from "jsonwebtoken";
6 | import BCrypt from "bcrypt";
7 |
8 | const google = require("googleapis").google;
9 | const OAuth2 = google.auth.OAuth2;
10 |
11 | export default async (req, res, app) => {
12 | const client = new OAuth2(
13 | Credentials.CLIENT_ID,
14 | Credentials.CLIENT_SECRET,
15 | Credentials.REDIRECT_URIS
16 | );
17 |
18 | if (req.query.error) {
19 | return res.redirect("/sign-in-error");
20 | }
21 |
22 | client.getToken(req.query.code, async (error, token) => {
23 | if (error) {
24 | return res.redirect("/sign-in-error");
25 | }
26 |
27 | const jwt = JWT.sign(token, Credentials.JWT_SECRET);
28 | const client = new OAuth2(
29 | Credentials.CLIENT_ID,
30 | Credentials.CLIENT_SECRET,
31 | Credentials.REDIRECT_URIS
32 | );
33 | client.credentials = JWT.verify(jwt, Credentials.JWT_SECRET);
34 |
35 | const people = google.people({
36 | version: "v1",
37 | auth: client,
38 | });
39 |
40 | const response = await people.people.get({
41 | resourceName: "people/me",
42 | personFields: "emailAddresses,names,organizations,memberships",
43 | });
44 |
45 | const email = response.data.emailAddresses[0].value;
46 | const name = response.data.names[0].displayName;
47 | const password = BCrypt.genSaltSync(10);
48 |
49 | let user = await Data.getUserByEmail({ email });
50 |
51 | if (!user) {
52 | const salt = BCrypt.genSaltSync(10);
53 | const hash = BCrypt.hashSync(password, salt);
54 | const double = BCrypt.hashSync(hash, salt);
55 | const triple = BCrypt.hashSync(double, Credentials.PASSWORD_SECRET);
56 |
57 | user = await Data.createUser({
58 | email,
59 | password: triple,
60 | salt,
61 | data: { name, verified: true },
62 | });
63 |
64 | // NOTE(jim): Because the domain comes from google.
65 | // If the organization doesn't exist. create it.
66 | const domain = Strings.getDomainFromEmail(email);
67 | const organization = await Data.getOrganizationByDomain({ domain });
68 |
69 | if (!organization) {
70 | const companyName = domain.split(".")[0];
71 | await Data.createOrganization({
72 | domain,
73 | data: {
74 | name: Strings.capitalizeFirstLetter(companyName),
75 | tier: 0,
76 | ids: [user.id],
77 | admins: [],
78 | },
79 | });
80 | }
81 | }
82 |
83 | if (user.error) {
84 | return app.render(req, res, "/sign-in-error", {
85 | jwt: null,
86 | viewer: null,
87 | });
88 | }
89 |
90 | const authToken = JWT.sign(
91 | { user: user.id, email: user.email },
92 | Credentials.JWT_SECRET
93 | );
94 |
95 | return app.render(req, res, "/sign-in-confirm", {
96 | jwt: authToken,
97 | viewer: user,
98 | });
99 | });
100 | };
101 |
--------------------------------------------------------------------------------
/routes/sign-in-success.js:
--------------------------------------------------------------------------------
1 | import * as Data from '~/common/data';
2 |
3 | export default async (req, res, app) => {
4 | const { viewer } = await Data.getViewer(req);
5 |
6 | if (!viewer || viewer.error) {
7 | return app.render(req, res, '/sign-in-error', { viewer: null });
8 | }
9 |
10 | return app.render(req, res, '/sign-in-success', { viewer });
11 | };
12 |
--------------------------------------------------------------------------------
/routes/sign-in.js:
--------------------------------------------------------------------------------
1 | import * as Credentials from "~/common/credentials";
2 | import * as Data from "~/common/data";
3 |
4 | const google = require("googleapis").google;
5 | const OAuth2 = google.auth.OAuth2;
6 |
7 | export default async (req, res, app) => {
8 | const client = new OAuth2(
9 | Credentials.CLIENT_ID,
10 | Credentials.CLIENT_SECRET,
11 | Credentials.REDIRECT_URIS
12 | );
13 |
14 | const googleURL = client.generateAuthUrl({
15 | access_type: "offline",
16 | scope: [
17 | "https://www.googleapis.com/auth/userinfo.email",
18 | "https://www.googleapis.com/auth/userinfo.profile",
19 | "https://www.googleapis.com/auth/user.organization.read",
20 | ],
21 | prompt: "consent",
22 | });
23 |
24 | const { viewer } = await Data.getViewer(req);
25 |
26 | if (!viewer || viewer.error) {
27 | return app.render(req, res, "/", { googleURL, viewer: null });
28 | }
29 |
30 | app.render(req, res, "/", { googleURL, viewer });
31 | };
32 |
--------------------------------------------------------------------------------
/routes/target-organization.js:
--------------------------------------------------------------------------------
1 | import * as Data from '~/common/data';
2 |
3 | // TODO(jim): Do this based on the ruote.
4 | export default async (req, res, app) => {
5 | const { viewer } = await Data.getViewer(req);
6 | const organization = await Data.getOrganizationByDomain({ domain: req.params.name });
7 |
8 | return app.render(req, res, '/organization', { viewer, organization });
9 | };
10 |
--------------------------------------------------------------------------------
/scripts/drop-database.js:
--------------------------------------------------------------------------------
1 | import configs from '~/knexfile';
2 | import knex from 'knex';
3 |
4 | const environment = process.env.NODE_ENV !== 'local-production' ? 'development' : 'production';
5 | const envConfig = configs[environment];
6 |
7 | console.log(`SETUP: database`, envConfig);
8 |
9 | const db = knex(envConfig);
10 |
11 | console.log(`RUNNING: drop-database.js NODE_ENV=${environment}`);
12 |
13 | // --------------------------
14 | // SCRIPTS
15 | // --------------------------
16 |
17 | const dropUserTable = db.schema.dropTable('users');
18 | const dropOrganizationsTable = db.schema.dropTable('organizations');
19 |
20 | // --------------------------
21 | // RUN
22 | // --------------------------
23 |
24 | Promise.all([dropUserTable, dropOrganizationsTable]);
25 |
26 | console.log(`FINISHED: drop-database.js NODE_ENV=${environment}`);
27 |
--------------------------------------------------------------------------------
/scripts/index.js:
--------------------------------------------------------------------------------
1 | require('@babel/register')({
2 | presets: ['@babel/preset-env'],
3 | ignore: ['node_modules', '.next'],
4 | });
5 |
6 | module.exports = require('./' + process.argv[2] + '.js');
7 |
--------------------------------------------------------------------------------
/scripts/seed-database.js:
--------------------------------------------------------------------------------
1 | import configs from '~/knexfile';
2 | import knex from 'knex';
3 |
4 | const environment = process.env.NODE_ENV !== 'local-production' ? 'development' : 'production';
5 | const envConfig = configs[environment];
6 |
7 | console.log(`SETUP: database`, envConfig);
8 |
9 | const db = knex(envConfig);
10 |
11 | console.log(`RUNNING: seed-database.js NODE_ENV=${environment}`);
12 |
13 | // --------------------------
14 | // SCRIPTS
15 | // --------------------------
16 |
17 | const createUserTable = db.schema.createTable('users', function(table) {
18 | table
19 | .uuid('id')
20 | .primary()
21 | .unique()
22 | .notNullable()
23 | .defaultTo(db.raw('uuid_generate_v4()'));
24 |
25 | table
26 | .timestamp('created_at')
27 | .notNullable()
28 | .defaultTo(db.raw('now()'));
29 |
30 | table
31 | .timestamp('updated_at')
32 | .notNullable()
33 | .defaultTo(db.raw('now()'));
34 |
35 | table
36 | .string('email')
37 | .unique()
38 | .notNullable();
39 |
40 | table.string('password').nullable();
41 | table.string('salt').nullable();
42 | table.jsonb('data').nullable();
43 | });
44 |
45 | const createOrganizationsTable = db.schema.createTable('organizations', function(table) {
46 | table
47 | .uuid('id')
48 | .primary()
49 | .unique()
50 | .notNullable()
51 | .defaultTo(db.raw('uuid_generate_v4()'));
52 |
53 | table
54 | .timestamp('created_at')
55 | .notNullable()
56 | .defaultTo(db.raw('now()'));
57 |
58 | table
59 | .timestamp('updated_at')
60 | .notNullable()
61 | .defaultTo(db.raw('now()'));
62 |
63 | table
64 | .string('domain')
65 | .unique()
66 | .notNullable();
67 |
68 | table.jsonb('data').nullable();
69 | });
70 |
71 | // --------------------------
72 | // RUN
73 | // --------------------------
74 |
75 | Promise.all([createUserTable, createOrganizationsTable]);
76 |
77 | console.log(`FINISHED: seed-database.js NODE_ENV=${environment}`);
78 |
--------------------------------------------------------------------------------
/scripts/setup-database.js:
--------------------------------------------------------------------------------
1 | import configs from '~/knexfile';
2 | import knex from 'knex';
3 |
4 | const environment =
5 | process.env.NODE_ENV !== 'local-production' ? 'development' : 'production';
6 | const envConfig = configs[environment];
7 |
8 | console.log(`SETUP: database`, envConfig);
9 |
10 | const db = knex(envConfig);
11 |
12 | console.log(`RUNNING: setup-database.js NODE_ENV=${environment}`);
13 |
14 | Promise.all([db.raw('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"')]);
15 |
16 | console.log(`FINISHED: setup-database.js NODE_ENV=${environment}`);
17 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | import * as Middleware from "~/common/middleware";
2 | import * as Credentials from "~/common/credentials";
3 | import * as Data from "~/common/data";
4 | import * as Routes from "~/routes";
5 |
6 | import express from "express";
7 | import next from "next";
8 | import bodyParser from "body-parser";
9 | import compression from "compression";
10 |
11 | const dev = process.env.NODE_ENV !== "production";
12 | const port = process.env.PORT || 1337;
13 | const app = next({ dev, quiet: false });
14 | const nextRequestHandler = app.getRequestHandler();
15 |
16 | app.prepare().then(async () => {
17 | const server = express();
18 |
19 | if (!dev) {
20 | server.use(compression());
21 | }
22 |
23 | server.use(Middleware.CORS);
24 | server.use("/public", express.static("public"));
25 | server.use(bodyParser.json());
26 | server.use(
27 | bodyParser.urlencoded({
28 | extended: false,
29 | })
30 | );
31 |
32 | server.post("/api/sign-in", async (req, res) => {
33 | return await Routes.api.signIn(req, res);
34 | });
35 |
36 | server.post("/api/users/delete", async (req, res) => {
37 | return await Routes.api.viewerDelete(req, res);
38 | });
39 |
40 | server.get("/", async (req, res) => {
41 | return await Routes.signIn(req, res, app);
42 | });
43 |
44 | server.get("/sign-in-confirm", async (req, res) => {
45 | return await Routes.signInConfirm(req, res, app);
46 | });
47 |
48 | server.get(
49 | "/sign-in-success",
50 | Middleware.RequireCookieAuthentication,
51 | async (req, res) => {
52 | return await Routes.signInSuccess(req, res, app);
53 | }
54 | );
55 |
56 | server.get("/sign-in-error", async (req, res) => {
57 | const { viewer } = await Data.getViewer(req);
58 |
59 | if (!viewer || viewer.error) {
60 | return app.render(req, res, "/sign-in-error", { viewer: null });
61 | }
62 |
63 | return app.render(req, res, "/sign-in-error", { viewer });
64 | });
65 |
66 | server.get("/sign-out", async (req, res) => {
67 | const { viewer } = await Data.getViewer(req);
68 |
69 | if (!viewer || viewer.error) {
70 | return app.render(req, res, "/sign-in-error", { viewer: null });
71 | }
72 |
73 | return app.render(req, res, "/sign-out", { viewer });
74 | });
75 |
76 | /* prettier-ignore */
77 | server.get('/([\$]):name', async (req, res) => {
78 | return await Routes.targetOrganization(req, res, app);
79 | });
80 |
81 | server.get("*", async (req, res) => {
82 | return nextRequestHandler(req, res, req.url);
83 | });
84 |
85 | server.listen(port, (err) => {
86 | if (err) {
87 | throw err;
88 | }
89 |
90 | console.log(`[ next-postgres server ] http://localhost:${port}`);
91 | });
92 | });
93 |
--------------------------------------------------------------------------------