├── .eslintrc
├── .gitignore
├── .nvmrc
├── .prettierignore
├── .prettierrc
├── LICENSE
├── README.org
├── app
├── .env
├── .eslintignore
├── .eslintrc.cjs
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc
├── jsconfig.json
├── package.json
├── src
│ ├── app.d.ts
│ ├── app.html
│ ├── components
│ │ ├── Auth.svelte
│ │ ├── Nav.svelte
│ │ ├── PassReset.svelte
│ │ ├── Presentation.svelte
│ │ ├── SideBarLayout.svelte
│ │ ├── layout
│ │ │ ├── Box.svelte
│ │ │ ├── Bracket.svelte
│ │ │ ├── Cluster.svelte
│ │ │ ├── Cover.svelte
│ │ │ ├── Frame.svelte
│ │ │ ├── Grid.svelte
│ │ │ ├── Imposter.svelte
│ │ │ ├── Reel.svelte
│ │ │ ├── Sidebar.svelte
│ │ │ ├── Stack.svelte
│ │ │ ├── Switcher.svelte
│ │ │ └── index.js
│ │ └── lib
│ │ │ └── helpers.js
│ ├── lib
│ │ ├── notifications.js
│ │ └── stores.js
│ └── routes
│ │ ├── +page.svelte
│ │ ├── __layout.svelte
│ │ ├── _error.svelte
│ │ ├── auth
│ │ ├── +page.svelte
│ │ ├── login
│ │ │ └── +page.svelte
│ │ ├── pass-reset-new
│ │ │ └── +page.svelte
│ │ ├── pass-reset
│ │ │ └── +page.svelte
│ │ ├── register
│ │ │ └── +page.svelte
│ │ ├── social
│ │ │ └── +page.svelte
│ │ ├── unapproved-user
│ │ │ └── +page.svelte
│ │ ├── unvalidated-user
│ │ │ └── +page.svelte
│ │ └── validate
│ │ │ └── +page.svelte
│ │ ├── box
│ │ └── +page.svelte
│ │ ├── bracket
│ │ └── +page.svelte
│ │ ├── cluster
│ │ └── +page.svelte
│ │ ├── cover
│ │ └── +page.svelte
│ │ ├── frame
│ │ └── +page.svelte
│ │ ├── imposter
│ │ └── +page.svelte
│ │ ├── index.svelte
│ │ ├── reel
│ │ └── +page.svelte
│ │ ├── sidebar
│ │ └── +page.svelte
│ │ ├── stack.svelte
│ │ ├── stack
│ │ └── +page.svelte
│ │ └── switcher
│ │ └── +page.svelte
├── static
│ ├── favicon.png
│ ├── fonts
│ │ ├── Bevan-Regular.ttf
│ │ ├── JosefinSlab-Bold.ttf
│ │ ├── JosefinSlab-BoldItalic.ttf
│ │ ├── JosefinSlab-Light.ttf
│ │ ├── JosefinSlab-LightItalic.ttf
│ │ ├── JosefinSlab-Regular.ttf
│ │ ├── JosefinSlab-RegularItalic.ttf
│ │ ├── JosefinSlab-SemiBold.ttf
│ │ ├── JosefinSlab-SemiBoldItalic.ttf
│ │ ├── JosefinSlab-Thin.ttf
│ │ ├── JosefinSlab-ThinItalic.ttf
│ │ └── OFL.txt
│ ├── global.css
│ ├── great-success.png
│ ├── layouts-global.css
│ ├── logo-192.png
│ ├── logo-512.png
│ └── manifest.json
├── svelte.config.js
└── vite.config.js
├── cli
├── .env
├── .gitignore
├── email_worker.js
├── package.json
└── sendmail.js
├── common
├── .gitignore
├── funcs.js
├── package.json
└── postgrest.js
├── db_init.sh
├── envs
└── local.tpl
├── lambda
├── .env
├── .gitignore
├── .nvmrc
├── README.org
├── index.js
├── pack-update-test.sh
├── pack.sh
├── package.json
├── postgrest-download.sh
├── postgrest.js
├── rds-create.sh
├── trust-policy.tpl.json
└── update.sh
├── lint.sh
├── npm-init.sh
├── pg_schema_dump.sh
├── postgrest.conf
├── psql-admin.sh
├── psql.sh
├── server
├── .env
├── .gitignore
├── index.js
└── package.json
├── setenv.sh
├── sql
├── fixtures.sql
├── pgjwt-nords.sql
├── pgjwt.sql
└── schema
│ ├── 00
│ ├── EXTENSION.pgcrypto.COMMENT.sql
│ ├── EXTENSION.pgjwt.COMMENT.sql
│ ├── FUNCTION.role.email.cv.newrole.cv.ACL.sql
│ ├── FUNCTION.user_delete.email.cv.ACL.sql
│ ├── FUNCTION.user_insert.email.cv.ACL.sql
│ ├── FUNCTION.validate.email.cv.ACL.sql
│ ├── FUNCTION.verify_token.token.text,.algorithm.text.ACL.sql
│ ├── basic_auth.SCHEMA.sql
│ ├── check_role_exists.FUNCTION.sql
│ ├── encrypt_pass.FUNCTION.sql
│ ├── invite.cv.FUNCTION.sql
│ ├── jwt_test.FUNCTION.sql
│ ├── jwt_token.TYPE.sql
│ ├── login.text,.text.FUNCTION.sql
│ ├── order.txt
│ ├── pass_reset.text.FUNCTION.sql
│ ├── pass_reset_new.text,.text.FUNCTION.sql
│ ├── pgcrypto.EXTENSION.sql
│ ├── pgjwt.EXTENSION.sql
│ ├── refresh.FUNCTION.sql
│ ├── register.text,.text,.text.FUNCTION.sql
│ ├── register.text,.text.FUNCTION.sql
│ ├── role.cv.cv.FUNCTION.sql
│ ├── set_user_role.text,.text.FUNCTION.sql
│ ├── settings.TABLE.sql
│ ├── settings.table_name_pk.CONSTRAINT.sql
│ ├── unvalidate.cv.FUNCTION.sql
│ ├── user_approved.text,.boolean.FUNCTION.sql
│ ├── user_delete.cv.FUNCTION.sql
│ ├── user_insert.cv.FUNCTION.sql
│ ├── user_role.text,.text.FUNCTION.sql
│ ├── user_validated.text,.boolean.FUNCTION.sql
│ ├── user_validated.text,.text.FUNCTION.sql
│ ├── users.TABLE.sql
│ ├── users.VIEW.sql
│ ├── users.encrypt_pass.TRIGGER.sql
│ ├── users.ensure_user_role_exists.TRIGGER.sql
│ ├── users.users_notify.TRIGGER.sql
│ ├── users.users_pass_reset_notify_trig.TRIGGER.sql
│ ├── users.users_pkey.CONSTRAINT.sql
│ ├── users_notify_trigger.FUNCTION.sql
│ ├── users_pass_reset_notify.FUNCTION.sql
│ ├── validate.cv.FUNCTION.sql
│ ├── validate_token.cv.FUNCTION.sql
│ ├── validation_reset.FUNCTION.sql
│ └── verify_token.text,.text.FUNCTION.sql
├── systemd.sample
└── tmux.sh
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true,
5 | "node": true
6 | },
7 | "parser": "babel-eslint",
8 | "parserOptions": {
9 | "ecmaVersion": 2019,
10 | "sourceType": "module"
11 | },
12 | "plugins": ["svelte3"],
13 | "extends": ["eslint:recommended"],
14 | "overrides": [
15 | {
16 | "files": ["**/*.svelte"],
17 | "processor": "svelte3/svelte3"
18 | }
19 | ],
20 | "rules": {}
21 | }
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/*
3 | yarn-error.log
4 | /cypress/screenshots/
5 | \#*
6 | *~
7 | .env
8 | .bash_logout
9 | .bashrc
10 | .cloud-locale-test.skip
11 | .config/configstore/update-notifier-npm.json
12 | .npm/anonymous-cli-metrics.json
13 | .nvm
14 | .profile
15 | .psql_history
16 | .degit/
17 | .emacs
18 | .emacs.d/
19 | .gitconfig
20 | .npm/
21 | .ssh/
22 | .bash_history
23 | .idea/
24 | engine/
25 | .env.sh
26 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v14.16.0
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "semi": false,
4 | "singleQuote": true,
5 | "trailingComma": "es5",
6 | "plugins": ["prettier-plugin-svelte"],
7 | "svelteStrictMode": false,
8 | "svelteBracketNewLine": true,
9 | "svelteAllowShorthand": true,
10 | "htmlWhitespaceSensitivity": "ignore",
11 | "jsxBracketSameLine": true,
12 | "endOfLine": "lf"
13 | }
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Guy Romm
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.org:
--------------------------------------------------------------------------------
1 | * what & why?
2 | - [[https://svelte.dev/][svelte]] is the hot new FE dev stuff. do [[https://svelte.dev/tutorial/basics][the tutorial]] to get hooked. [[https://kit.svelte.dev/][SvelteKit]] - its equivalent of SSR
3 | framework, not unlike next.js
4 | - [[http://postgrest.org/][postgrest]] is a really nice backend to quickly prototype apps by
5 | definining data & permissions, without having to write backend.
6 | - [[https://every-layout.dev/][every layout]] is a fresh approach to plain css, with reusable components that make sense. borrowed from [[https://github.com/SilvanCodes/svelte-layout-components][SilvanCodes/svelte-layout-components]].
7 | - some social login love thrown in for good measure, uses [[https://github.com/beyonk-adventures/svelte-social-auth][beyonk-adventures/svelte-social-auth]]
8 | - tmux is used to run multiple processes and an editor, because nothing beats tmux and emacs (use [[http://web-mode.org/][web-mode]] to edit svelte!)
9 | * prequisites for dev env
10 | 1. [[https://github.com/PostgREST/postgrest/releases/latest][postgrest]] - install according to [[http://postgrest.org/en/v6.0/tutorials/tut0.html][instructions]].
11 | 1. [[https://github.com/michelp/pgjwt][pgjwt]] - postgresql jwt extension for postgrest auth
12 | 2. [[https://github.com/nvm-sh/nvm][nvm]] - to easily swap node/npm versions. tested with node v13.11.0.
13 | * cloning
14 | #+BEGIN_SRC bash
15 | npx degit https://github.com/guyromm/svelte-postgrest-template svelte-postgrest-app
16 | #+END_SRC
17 | * package.json dependencies
18 | #+BEGIN_SRC bash
19 | nvm use ; ./npm-init.sh
20 | #+END_SRC
21 |
22 | * .env
23 | use envs/local.tpl to create an envs/local .env shell file, and then
24 | expand/eval it using ./setenv.sh
25 | #+BEGIN_SRC bash
26 | function freeport() {
27 | FROM=$1
28 | TO=$2
29 | HOWMANY=$3
30 | comm -23 \
31 | <(seq "$FROM" "$TO" | sort) \
32 | <(ss -Htan | awk '{print $4}' | cut -d':' -f2 | sort -u) \
33 | | shuf | head -n "$HOWMANY"
34 | }
35 | export APPNAME=$(basename $(pwd))
36 | export DBNAME=$APPNAME
37 | export APPPORT=$(freeport 3000 4000 1)
38 | export POSTGRESTPORT=$[APPPORT+1]
39 | export SERVERPORT=$[APPPORT+2]
40 | export JWTSECRET=$(head /dev/urandom | tr -dc A-F0-9 | head -c 64 ; echo '')
41 |
42 | cp envs/local.tpl envs/local
43 | sed -i -E "s/APPPORTREPLACE/$APPPORT/g" envs/local
44 | sed -i -E "s/SERVERPORTREPLACE/$SERVERPORT/g" envs/local
45 | sed -i -E "s/POSTGRESTPORTREPLACE/$POSTGRESTPORT/g" envs/local
46 | sed -i -E "s/DBNAMEREPLACE/$DBNAME/g" envs/local
47 | sed -i -E "s/JWTSECRETREPLACE/$JWTSECRET/g" envs/local
48 | ./setenv.sh local
49 | #+END_SRC
50 |
51 | * deploying on aws
52 | ** create a separate env
53 | #+BEGIN_SRC bash
54 | test -f envs/aws || cp envs/local envs/aws
55 | sed -i -E "s/^RDS=''$/RDS=1/g" envs/aws
56 | echo POSTGREST_PATH_AS_ARG=1 >> envs/aws
57 | echo 'VITE_POSTGREST_PATH_AS_ARG=$POSTGREST_PATH_AS_ARG' >> envs/aws
58 | RDS_PASSWORD=$(tr -dc A-Za-z0-9 > envs/aws
61 | egrep '^RDS_PASSWORD=(.+)$' envs/aws || echo "RDS_PASSWORD=$RDS_PASSWORD" >> envs/aws
62 | ./setenv.sh aws
63 | lambda/postgrest-download.sh
64 | #+END_SRC
65 |
66 | ** create the rds
67 | #+BEGIN_SRC bash
68 | egrep '^RDS_HOSTNAME=(.+)$' envs/aws || (./lambda/rds-create.sh | egrep '^RDS_HOSTNAME=' | tee -a envs/aws
69 | echo 'DBURIADMIN="postgres://postgres:$RDS_PASSWORD@$RDS_HOSTNAME/template1"' | tee -a envs/aws
70 | echo 'DBURI="postgres://postgres:$RDS_PASSWORD@$RDS_HOSTNAME/"' | tee -a envs/aws
71 | )
72 | ./setenv.sh aws
73 | #+END_SRC
74 | ** create & deploy the lambda func
75 | #+BEGIN_SRC bash
76 | source .env
77 | egrep '^LAMBDA_ROLE=(.+)$' envs/aws || (
78 | aws iam create-role --role-name $APPNAME --assume-role-policy-document file://lambda/trust-policy.tpl.json | tee envs/$ENV.role.json
79 | aws iam attach-role-policy --role-name $APPNAME --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
80 | )
81 | egrep '^LAMBDA_ROLE=(.+)$' envs/aws && echo 'LAMBDA_ROLE_ALREADY_SET' || (test -f envs/$ENV.role.json && (echo LAMBDA_ROLE=$(jq .Role.Arn envs/$ENV.role.json -r) | tee -a envs/aws) || echo 'NO_ROLE_FILE')
82 | ./setenv.sh aws
83 | source .env
84 | lambda/pack.sh
85 | aws lambda create-function --function-name $APPNAME-postgrest --runtime nodejs14.x --role "$LAMBDA_ROLE" --zip-file fileb://lambda/function.zip --handler index.handler --timeout 15 | tee envs/$ENV.lambda.json
86 | egrep '^AWS_POSTGREST_LAMBDA_FUNC=(.+)$' envs/aws || echo 'AWS_POSTGREST_LAMBDA_FUNC='$(jq .FunctionName envs/$ENV.lambda.json -r) | tee -a envs/aws
87 | aws lambda create-function-url-config --function-name $APPNAME-postgrest --auth-type NONE --cors 'AllowOrigins=*' | tee envs/$ENV.lambda.url.json
88 | test -f envs/$ENV.lambda.url.json && sed -i -E 's/^POSTGREST_BASE_URI=(.*)$/POSTGREST_BASE_URI="'$(jq .FunctionUrl envs/$ENV.lambda.url.json -r | sed -E 's/\//\\\//g')'"/g' envs/aws
89 | #+END_SRC
90 | * database initialization
91 | #+BEGIN_SRC bash
92 | source .env
93 | echo 'DBNAME:'$DBNAME
94 | ./db_init.sh
95 | #+END_SRC
96 | * launch
97 | #+BEGIN_SRC bash
98 | ./tmux.sh
99 | #+END_SRC
100 |
101 |
--------------------------------------------------------------------------------
/app/.env:
--------------------------------------------------------------------------------
1 | ../.env
--------------------------------------------------------------------------------
/app/.eslintignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /.svelte-kit
5 | /package
6 | .env
7 | .env.*
8 | !.env.example
9 |
10 | # Ignore files for PNPM, NPM and YARN
11 | pnpm-lock.yaml
12 | package-lock.json
13 | yarn.lock
14 |
--------------------------------------------------------------------------------
/app/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: ['eslint:recommended', 'prettier'],
4 | plugins: ['svelte3'],
5 | overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
6 | parserOptions: {
7 | sourceType: 'module',
8 | ecmaVersion: 2020
9 | },
10 | env: {
11 | browser: true,
12 | es2017: true,
13 | node: true
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /functions
5 | /.svelte-kit
6 | /package
7 | .env
8 | .env.*
9 | !.env.example
10 | vite.config.js.timestamp-*
11 | vite.config.ts.timestamp-*
12 |
--------------------------------------------------------------------------------
/app/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
2 |
--------------------------------------------------------------------------------
/app/.prettierignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /.svelte-kit
5 | /package
6 | .env
7 | .env.*
8 | !.env.example
9 |
10 | # Ignore files for PNPM, NPM and YARN
11 | pnpm-lock.yaml
12 | package-lock.json
13 | yarn.lock
14 |
--------------------------------------------------------------------------------
/app/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": false,
3 | "singleQuote": true,
4 | "trailingComma": "none",
5 | "printWidth": 100,
6 | "tabWidth": 2,
7 | "plugins": ["prettier-plugin-svelte"],
8 | "pluginSearchDirs": ["."],
9 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
10 | }
11 |
--------------------------------------------------------------------------------
/app/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.svelte-kit/tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "checkJs": true,
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "resolveJsonModule": true,
9 | "skipLibCheck": true,
10 | "sourceMap": true,
11 | "strict": true
12 | }
13 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files
14 | //
15 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
16 | // from the referenced tsconfig.json - TypeScript does not merge them in
17 | }
18 |
--------------------------------------------------------------------------------
/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "TODO",
3 | "version": "0.0.1",
4 | "scripts": {
5 | "dev": "vite dev",
6 | "build": "vite build",
7 | "preview": "vite preview",
8 | "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
9 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
10 | "upload_s3": "bash -c 'source .env && aws s3 sync --acl public-read build s3://$APP_S3_BUCKET && echo UPLOADED'",
11 | "lint": "prettier --plugin-search-dir . --check . && eslint .",
12 | "format": "prettier --plugin-search-dir . --write ."
13 | },
14 | "devDependencies": {
15 | "@sveltejs/adapter-auto": "^1.0.0",
16 | "@sveltejs/kit": "^1.0.0",
17 | "eslint": "^8.28.0",
18 | "eslint-config-prettier": "^8.5.0",
19 | "eslint-plugin-svelte3": "^4.0.0",
20 | "prettier": "^2.8.0",
21 | "prettier-plugin-svelte": "^2.8.1",
22 | "svelte": "^3.54.0",
23 | "svelte-check": "^2.9.2",
24 | "typescript": "^4.9.3",
25 | "vite": "^4.0.0"
26 | },
27 | "type": "module",
28 | "dependencies": {
29 | "@beyonk/svelte-social-auth": "^2.1.1",
30 | "dotenv": "^10.0.0",
31 | "jwt-decode": "^3.1.2",
32 | "lorem-ipsum": "^2.0.3",
33 | "qs": "^6.10.1",
34 | "rollup-plugin-babel": "^4.4.0",
35 | "rollup-plugin-svelte": "^7.1.0"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/src/app.d.ts:
--------------------------------------------------------------------------------
1 | // See https://kit.svelte.dev/docs/types#app
2 | // for information about these interfaces
3 | // and what to do when importing types
4 | declare namespace App {
5 | // interface Error {}
6 | // interface Locals {}
7 | // interface PageData {}
8 | // interface Platform {}
9 | }
10 |
--------------------------------------------------------------------------------
/app/src/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | element in src/app.html
8 | /*target: '#svelte',*/
9 | paths: { base: "/" },
10 | //trailingSlash:'ignore',
11 | //prerender:{enabled:true},
12 | adapter: adapter(),
13 | //ssr:true,
14 | paths: {base:''},
15 | adapter: adapter({
16 | pages: 'build',
17 | assets: 'build',
18 | fallback: null
19 | })
20 | }
21 | };
22 |
23 | export default config;
24 |
--------------------------------------------------------------------------------
/app/vite.config.js:
--------------------------------------------------------------------------------
1 | import { sveltekit } from '@sveltejs/kit/vite';
2 |
3 | const l = console.log;
4 | l('process.env?')
5 | l(process.env.HMR_PROTO)
6 | /** @type {import('vite').UserConfig} */
7 | const config = {
8 | plugins: [sveltekit()],
9 | server: {
10 | hmr: {
11 | protocol: process.env.HMR_PROTO || 'ws',
12 | port: process.env.HMR_PORT || process.env.APP_PORT
13 | }
14 | }
15 | };
16 |
17 | export default config;
18 |
--------------------------------------------------------------------------------
/cli/.env:
--------------------------------------------------------------------------------
1 | ../.env
--------------------------------------------------------------------------------
/cli/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/cli/email_worker.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import dotenv from 'dotenv'
3 | dotenv.config();
4 | import pg from 'pg'
5 | import commandLineArgs from 'command-line-args'
6 | import createSubscriber from 'pg-listen'
7 | import nodemailer from 'nodemailer'
8 |
9 | const CHANNELS = {pass_reset:'users_pass_reset',
10 | validated:'validated',
11 | inserted:'users_insert',
12 | };
13 |
14 | const l = console.log
15 | const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
16 | if (!process.env.APP_BASE_URI) throw new Error('no APP_BASE_URI!')
17 |
18 | const subscriber = createSubscriber({ connectionString: process.env.DBURI })
19 |
20 | const nmargs = {
21 | service: process.env.EMAIL_SERVICE,
22 | host: process.env.EMAIL_HOST ? process.env.EMAIL_HOST : undefined,
23 | port: process.env.EMAIL_PORT ? process.env.EMAIL_PORT : undefined,
24 | auth: {
25 | user: process.env.EMAIL_USER,
26 | pass: process.env.EMAIL_PASS,
27 | },
28 | }
29 | const smtpTransport = nodemailer.createTransport(nmargs)
30 | //l('transport',nmargs); // process.exit();
31 |
32 | // this defines additional tables (resources) and their columns
33 | // which can be shared between users
34 | const colTrans={
35 | //SRCTBL:'fkey_id',
36 | };
37 |
38 | async function processWelcome(user,{client}) {
39 | l('about to welcome',user.email); // return;
40 | const info = await smtpTransport.sendMail({from:process.env.EMAIL_USER,
41 | to:user.email,
42 | subject:`welcome to ${process.env.APPNAME}!`,
43 | html: `
You've succesfully validated!
`});
44 |
45 | await client.query(`update basic_auth.users set
46 | validation_info=jsonb_set(
47 | coalesce(validation_info,'{}'::jsonb),
48 | '{welcome_sent_at}'::text[],
49 | concat('"',to_char (now()::timestamp at time zone 'UTC', 'YYYY-MM-DDTHH24:MI:SSZ'),'"')::jsonb
50 |
51 | ) where email=$1`,[user.email]);
52 |
53 | l('welcomed',user.email);
54 | }
55 |
56 | function genRandToken(length) {
57 | var result = '';
58 | var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
59 | var charactersLength = characters.length;
60 | for ( var i = 0; i < length; i++ ) {
61 | result += characters.charAt(Math.floor(Math.random() *
62 | charactersLength));
63 | }
64 | return result;
65 | }
66 |
67 | async function processInserted(user,{client}) {
68 | const token = genRandToken(32);
69 | l('about to send validation to',user.email);
70 | let validate_link = `${process.env.APP_BASE_URI}/auth/validate?token=${encodeURIComponent(token)}`;
71 | let subject;
72 | subject=`${process.env.APPNAME} validation`;
73 | const info = await smtpTransport.sendMail({from:process.env.EMAIL_USER,
74 | to:user.email,
75 | subject,
76 | html: `
please follow the following link in order to validate your email
77 |
validate
`});
78 | await client.query(`update basic_auth.users set
79 | validation_info=
80 | jsonb_set(jsonb_set(
81 | coalesce(validation_info,'{}'::jsonb),
82 | '{token}'::text[],
83 | concat('"${token}"')::jsonb
84 | ),'{email_sent}','true'::jsonb) where email=$1`,[user.email]);
85 |
86 |
87 | l('welcomed',user.email);
88 | }
89 |
90 | async function processPasswordReset (user, { client }) {
91 | l('processPasswordReset:', user.email)
92 | const reset_pass_link = `${process.env.SITE_BASE_URI}/auth/pass-reset-new?token=${user.pass_reset_info.token}`
93 | const info = await smtpTransport.sendMail({
94 | from: process.env.EMAIL_USER,
95 | to: user.email,
96 | subject: 'password reset',
97 | html: `Reset password:
${reset_pass_link}`
98 | })
99 | l('email sent', info)
100 | await client.query(`
101 | UPDATE basic_auth.users
102 | SET pass_reset_info=jsonb_set(pass_reset_info, '{email_sent}', 'true')
103 | WHERE email = $1`, [user.email]
104 | )
105 |
106 | l('processing has been finished', user.email)
107 | }
108 |
109 | let isSubscriberConnected=false;
110 | async function scon(opts) {
111 | if (!opts.noConnect && !isSubscriberConnected) {
112 | l('SUBSCRIBING!');
113 | await subscriber.connect();
114 | isSubscriberConnected=true;
115 |
116 | }
117 | }
118 | async function sclose(opts) {
119 | if (!opts.noConnect) {
120 | l('CLOSING',subscriber);
121 | await subscriber.close()
122 | }
123 | }
124 | async function validation_run(opts) {
125 | l('validation_run');
126 | //scon(opts);
127 | const client = opts.client;
128 | let res;
129 | if (opts.user)
130 | res = await client.query("select * from basic_auth.users where validated is not null and validation_info->'welcome_sent_at' is null and email = $1",[opts.user]);
131 | else
132 | res = await client.query("select * from basic_auth.users where validated is not null and validation_info->'welcome_sent_at' is null");
133 | l('validated not welcome:',res.rows.length);
134 | for (const r of res.rows) {
135 | if (opts.notify) {
136 | l('notifying: validation',r.email);
137 | await subscriber.notify(CHANNELS.validated,{email:r.email});
138 | } else {
139 | await processWelcome(r,{client});
140 | }
141 | }
142 | }
143 |
144 | async function inserted_run(opts) {
145 | l('inserted_run');
146 | const client = opts.client;
147 | let res;
148 | if (opts.user)
149 | res = await client.query("select * from basic_auth.users where validated is null and (validation_info is null or validation_info->'token' is null) and email = $1",[opts.user]);
150 | else
151 | res = await client.query("select * from basic_auth.users where validated is null and (validation_info is null or validation_info->'token' is null)");
152 | l('validated not welcome:',res.rows.length);
153 | for (const r of res.rows) {
154 | if (opts.notify) {
155 | l('notifying: welcome',r.email);
156 | await subscriber.notify(CHANNELS.inserted,{email:r.email});
157 | } else {
158 | await processInserted(r,{client});
159 | }
160 | }
161 | }
162 |
163 |
164 | async function pass_reset_run (opts) {
165 | l('pass_reset_run()');
166 | //scon(opts)
167 | const client = opts.client;
168 | let res;
169 | if (opts.user)
170 | res = await client.query('SELECT * FROM basic_auth.users WHERE (pass_reset_info->>\'email_sent\')::bool IS FALSE and email = $1',[opts.user]);
171 | else
172 | res = await client.query('SELECT * FROM basic_auth.users WHERE (pass_reset_info->>\'email_sent\')::bool IS FALSE');
173 |
174 | l('email reset token present but no email sent:',res.rows.length);
175 | for (const r of res.rows) {
176 | if (opts.notify) {
177 | l('notifying: reset pass', r.email)
178 | await subscriber.notify(CHANNELS.pass_reset, { email: r.email })
179 | } else {
180 | await processPasswordReset(r, { client });
181 | }
182 | }
183 |
184 | //l('disconnecting.')
185 | //await client.end()
186 | //sclose(opts);
187 |
188 | l('pass_reset_run() out.')
189 | }
190 |
191 |
192 | function validateWelcomeRow(row) {
193 | const rt = (row.validated && (!row.validation_info || !row.validation_info.welcome_sent_at));
194 | l('validateWelcomeRow',row.validated,row.validation_info,'=>',rt);
195 | return rt;
196 | }
197 |
198 | function validateInsertedRow(row) {
199 | const rt = (!row.validated && !row.validation_info);
200 | l('validateInsertedRow',row.validated,row.validation_info,'=>',rt);
201 | return rt;
202 | }
203 |
204 | function validatePassResetRow(row) {
205 | const rt = (row.pass_reset_info && row.pass_reset_info.email_sent === false)
206 | l('validatePassResetRow',row.pass_reset_info,'=>',rt);
207 | return rt;
208 | }
209 |
210 |
211 | async function insert_listen(opts) {
212 | let args = {chan:CHANNELS.inserted,
213 | validateRow:validateInsertedRow,
214 | processItem:processInserted,
215 | run_func:inserted_run};
216 | return await listen(args,opts);
217 | }
218 |
219 | async function validation_listen(opts) {
220 | let args = {chan:CHANNELS.validated,
221 | validateRow:validateWelcomeRow,
222 | processItem:processWelcome,
223 | run_func:validation_run};
224 |
225 | return await listen(args,opts);
226 | }
227 |
228 | async function pass_reset_listen (opts) {
229 | let args = {chan:CHANNELS.pass_reset,
230 | validateRow:validatePassResetRow,
231 | processItem:processPasswordReset,
232 | run_func:pass_reset_run};
233 | return await listen(args,opts);
234 | }
235 |
236 | async function listen({chan,validateRow,processItem,run_func},opts) {
237 | l('listen', chan);
238 | const client = opts.client;
239 |
240 | let toRun = []
241 | l('subscriber.notifications.on',chan);
242 | subscriber.notifications.on(chan, async (payload) => {
243 | l(chan,'payload:',payload);
244 | const result = await client.query('SELECT * FROM basic_auth.users WHERE email=$1', [payload.email])
245 | for (const row of result.rows) {
246 | //l('about to run validateRow',validateRow,'on',row);
247 | if (validateRow(row,payload)) toRun.push([row,payload])
248 | else l(row.email,'already processed for',chan,'. skipping.')
249 | }
250 | })
251 | //scon(opts)
252 | l('connected. listening.')
253 | await subscriber.listenTo(chan)
254 | process.on('exit', () => {
255 | sclose()
256 | client.end()
257 | })
258 | l('listen setup complete. running notifies for older stuff.')
259 |
260 | run_func({...opts, notify: true, noConnect: true })
261 |
262 | while (true) {
263 | if (toRun.length) {
264 | l('trying to pop an item from a', toRun.length, 'long queue.')
265 | let [qi,payload] = toRun.pop();
266 | l('popped qi=',qi,'payload=',payload);
267 | if (qi) await processItem(qi, { client,payload });
268 | }
269 | if (!toRun.length) await sleep(50);
270 | }
271 | }
272 |
273 | const opts = commandLineArgs([
274 | { name: 'user', alias: 'u',type:String},
275 | { name: 'notify', alias: 'n', type: Boolean },
276 | { name: 'listen', alias: 'l', type: Boolean },
277 | { name: 'runs', alias:'r',type:String, multiple:true},
278 | ])
279 |
280 | const runs = {pass_reset:pass_reset_run,
281 | validation:validation_run,
282 | users_insert:inserted_run,
283 | };
284 |
285 | const listens = {pass_reset:pass_reset_listen,
286 | validation:validation_listen,
287 | users_insert:insert_listen,
288 | };
289 |
290 | async function run() {
291 | l('DBURI',process.env.DBURI);
292 | opts.client = new pg.Client(process.env.DBURI);
293 | await opts.client.connect();
294 | await scon(opts);
295 |
296 | if (opts.listen)
297 | {
298 | for (let [runk,runf] of Object.entries(runs))
299 | {
300 | if (!opts.runs || opts.runs.includes(runk))
301 | {
302 | l('listen',runk);
303 | listens[runk](opts);
304 | l('listen',runk,'end');
305 | }
306 | }
307 | }
308 | else
309 | {
310 | l('opts',Object.keys(opts));
311 | for (let [runk,runf] of Object.entries(runs))
312 | {
313 | if (!opts.runs || !opts.runs.length || opts.runs.includes(runk))
314 | {
315 | l('run',runk,'start');
316 | runs[runk](opts);
317 | l('run',runk,'end');
318 | }
319 | }
320 | }
321 | }
322 | run();
323 |
--------------------------------------------------------------------------------
/cli/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cli",
3 | "type": "module",
4 | "version": "1.0.0",
5 | "description": "",
6 | "main": "token_verify-worker.js",
7 | "scripts": {
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "command-line-args": "^5.1.1",
14 | "dotenv": "^8.6.0",
15 | "forever": "^3.0.4",
16 | "google-auth-library": "^6.0.0",
17 | "nodemailer": "^6.5.0",
18 | "pg": "^8.5.1",
19 | "pg-listen": "^1.5.0"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/cli/sendmail.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import 'dotenv/config.js'
3 | import nodemailer from 'nodemailer'
4 | import commandLineArgs from 'command-line-args'
5 | import fs from "fs";
6 | const l = console.log;
7 | const nmargs= {
8 | service: process.env.EMAIL_SERVICE,
9 | host: (process.env.EMAIL_HOST?process.env.EMAIL_HOST:undefined),
10 | port: (process.env.EMAIL_PORT?process.env.EMAIL_PORT:undefined),
11 | auth: {
12 | user: process.env.EMAIL_USER,
13 | pass: process.env.EMAIL_PASS
14 | }
15 | };
16 |
17 | const opts = commandLineArgs([
18 | { name: 'recipients', alias: 'r',type:String,multiple:true},
19 | { name: 'subject', alias: 's',type:String},
20 | ]);
21 | l(opts);
22 | let stdinBuffer = fs.readFileSync(0); // STDIN_FILENO = 0
23 | let stdinText = stdinBuffer.toString();
24 |
25 | const smtpTransport = nodemailer.createTransport(nmargs)
26 |
27 | smtpTransport.sendMail({from:process.env.EMAIL_USER,
28 | to:opts.recipients,
29 | subject:opts.subject,
30 | text:stdinText});
31 |
--------------------------------------------------------------------------------
/common/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/common/funcs.js:
--------------------------------------------------------------------------------
1 |
2 | export const postfix = (import.meta.env.MODE==='production')?'.html':'';
3 |
4 |
--------------------------------------------------------------------------------
/common/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "common",
3 | "type": "module",
4 | "version": "1.0.0",
5 | "description": "",
6 | "main": "config.js",
7 | "scripts": {
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "js-cookies": "^1.0.4",
14 | "jwt-decode": "^2.2.0",
15 | "node-fetch": "^2.6.0",
16 | "qs": "^6.9.3"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/common/postgrest.js:
--------------------------------------------------------------------------------
1 | //import fetch from 'node-fetch'
2 | import jwt_decode from 'jwt-decode'
3 | import qs from 'qs'
4 | import {postfix} from './funcs.js';
5 | const l = console.log
6 |
7 | export const isnode = () =>
8 | typeof process !== 'undefined' &&
9 | process.release &&
10 | process.release.name === 'node'
11 |
12 | export const DB = import.meta.env.VITE_POSTGREST_BASE_URI
13 |
14 | export const hdrs = { 'Content-Type': 'application/json' }
15 |
16 | export async function F() {
17 | return fetch;
18 | //return !isnode() ? fetch : (await import('node-fetch')).default
19 | }
20 |
21 | export async function login(
22 | login,
23 | pass,
24 | mode = 'login',
25 | validation_info = null
26 | ) {
27 | let payload = {}
28 | let h = { ...hdrs }
29 | if (mode === 'refresh') h = await getHeaders()
30 | else payload = { em: login, ps: pass }
31 | if (validation_info) payload.vinfo = validation_info
32 | let f = await F()
33 | //l('attempting to',mode,'with payload',payload);
34 | const pth = '/rpc/' + mode
35 | const furl =
36 | DB +
37 | (import.meta.env.VITE_POSTGREST_PATH_AS_ARG
38 | ? '?_path=' + encodeURIComponent(pth)
39 | : pth)
40 | let res = await f(furl, {
41 | method: 'POST',
42 | headers: h,
43 | body: JSON.stringify(payload),
44 | })
45 | let txt, json
46 | try {
47 | txt = await res.text()
48 | json = JSON.parse(txt)
49 | //l(furl,payload,'response:',json);
50 | if (json.code) throw new Error('could not login')
51 | if (!json.length === 1)
52 | throw new Error('wrong length returned by login response.')
53 | } catch (err) {
54 | l('error obtaining json from res', err)
55 | l('login error is', txt)
56 | //throw err;
57 | let cause =
58 | txt && txt.includes('already exists') ? 'user exists' : 'unknown'
59 | return { status: 'error', error: err, cause, json, txt }
60 | }
61 | return { status: 'ok', result: json }
62 | //return json[0];
63 | }
64 |
65 | function redirToAuth() {
66 | if (isnode()) throw new Error('authentication error')
67 | let rdirt = `/auth/login${postfix}?redir=` + encodeURIComponent(window.location.href)
68 | //l('about to redir to',rdirt,'with backredir being',window.location.href);
69 | //throw new Error(`about to redir to ${rdirt}`);
70 | window.location.href = rdirt
71 | }
72 |
73 | export async function authLogic(rta) {
74 | //l('ORIGINAL AUTH FUNC');
75 | let cookie, gad, headers
76 | //l('JWT TOKEN=',CFG.JWT_TOKEN);
77 | //l('CFG',CFG);
78 | //if (!CFG.JWT_TOKEN) throw new Error('no jwt token biatch');
79 | if (CFG.JWT_TOKEN) {
80 | //l('GOT a token set as global var',CFG.JWT_TOKEN);
81 | cookie = CFG.JWT_TOKEN
82 | gad = getAuthData(cookie)
83 | } else if (CFG.login && CFG.pass) {
84 | //l('got a login/password in config.');
85 | const pth = '/rpc/login';
86 | const lurl = DB + (import.meta.env.VITE_POSTGREST_PATH_AS_ARG?'?_path='+encodeURIComponent(pth):pth)
87 | let res = await fetch(lurl, {
88 | method: 'POST',
89 | headers: hdrs,
90 | body: JSON.stringify({ em: CFG.login, ps: CFG.pass }),
91 | })
92 | let txt, json
93 | try {
94 | txt = await res.text()
95 | json = JSON.parse(txt)
96 | if (json.length) {
97 | l('assigning a new token from json', json)
98 | CFG.JWT_TOKEN = cookie = json[0].token
99 | } else {
100 | l(json)
101 | throw new Error('could not find token in auth response')
102 | }
103 | } catch (err) {
104 | l('error obtaining json from res', err)
105 | l('login error is', txt)
106 | throw err
107 | }
108 | } else {
109 | //l('invoking redirToAuth');
110 | await CFG.redirToAuth()
111 | }
112 |
113 | //cookie = CFG.JWT_TOKEN;
114 | if (cookie) {
115 | //l('examining for expiry.');
116 | let gad = getAuthData(cookie)
117 | if (gad.is_expired) {
118 | l('expired token.', gad)
119 | CFG.JWT_TOKEN = undefined
120 | return await authLogic(rta)
121 | }
122 | }
123 | if (cookie) {
124 | //l('returning header with bearer',cookie);
125 | return { ...hdrs, Authorization: 'Bearer ' + cookie }
126 | } else if (rta) rta()
127 | return { cookie, gad, hdrs }
128 | }
129 |
130 | export async function webAuthLogic() {
131 | const sc = { JWT_TOKEN: getCookie('auth') }
132 | setConfig(sc)
133 | return await authLogic()
134 | }
135 |
136 | export async function cliAuthLogic() {
137 | setConfig({
138 | login: import.meta.env.VITE_POSTGREST_CLI_LOGIN,
139 | pass: import.meta.env.VITE_POSTGREST_CLI_PASS,
140 | })
141 | return await authLogic()
142 | }
143 |
144 | export async function isoAuthLogic() {
145 | if (isnode()) return await cliAuthLogic()
146 | else return await webAuthLogic()
147 | }
148 |
149 | let CFG = { authLogic: isoAuthLogic, redirToAuth }
150 |
151 | export function setConfig(args) {
152 | //l('setConfig',args);
153 | for (const [k, v] of Object.entries(args)) CFG[k] = v
154 | }
155 |
156 | export function getConfig() {
157 | return CFG
158 | }
159 |
160 | export async function getHeaders() {
161 | const al = await CFG.authLogic(CFG.redirToAuth)
162 | //l('authLogic returned',al,'into',al);
163 | return al
164 | }
165 |
166 | export function getCookie(name = 'auth') {
167 | const rt = readCookie(name)
168 | //l('readCookie',name,'=>',rt);
169 | return rt
170 | }
171 |
172 | export function createCookie(name, value, days) {
173 | if (days) {
174 | var date = new Date()
175 | date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000)
176 | var expires = '; expires=' + date.toGMTString()
177 | } else {
178 | var expires = ''
179 | }
180 | document.cookie = name + '=' + value + expires + ';path=/'
181 | }
182 |
183 | function readCookie(name) {
184 | var nameEQ = name + '='
185 | var ca = document.cookie.split(';')
186 | for (var i = 0; i < ca.length; i++) {
187 | var c = ca[i]
188 | while (c.charAt(0) == ' ') {
189 | c = c.substring(1, c.length)
190 | }
191 | if (c.indexOf(nameEQ) == 0) {
192 | return c.substring(nameEQ.length, c.length)
193 | }
194 | }
195 | return null
196 | }
197 |
198 | export function eraseCookie(name) {
199 | //l('current',readCookie(name));
200 | createCookie(name, '', -1)
201 | }
202 |
203 |
204 | export function getAuthData(tok) {
205 | let rt = {}
206 | if (!tok) {
207 | tok = getCookie('auth')
208 | //l('gotten token',tok);
209 | }
210 | if (!tok) {
211 | rt.loginForm = true
212 | } else if (tok) {
213 | if (typeof tok === undefined) throw 'bad token!'
214 | rt.auth = tok
215 | rt.auth_decoded = jwt_decode(tok)
216 | let now = new Date()
217 | rt.exp = new Date(rt.auth_decoded.exp * 1000)
218 | if (now > rt.exp) rt.is_expired = true
219 | else rt.is_expired = false
220 | //l('AUTH',rt.auth,rt.auth_decoded,now,exp,now>exp);
221 | }
222 | return rt
223 | }
224 | export async function del(path, conds = {}) {
225 | if (!Object.entries(conds).length) throw new Error('conds is empty.')
226 | let furl;
227 | if (import.meta.env.VITE_POSTGREST_PATH_AS_ARG)
228 | furl = DB + '/' + '?' + qs.stringify({...conds,_path:path});
229 | else
230 | furl = DB + '/' + path + '?' + qs.stringify(conds);
231 | let res = await fetch(furl, {
232 | method: 'DELETE',
233 | headers: await getHeaders(),
234 | })
235 | return res
236 | }
237 | export async function update(path, doc, errok, key = ['id']) {
238 | //l('IN UPDATE',doc.parsed.scores); throw 'bye';
239 | if (typeof key !== 'object') throw 'wrong key type in update'
240 | //l('updating',path,doc[xokey]);throw 'bye';
241 | let str = JSON.stringify(doc)
242 | let keys
243 | if (Array.isArray(key))
244 | keys = Object.fromEntries(
245 | Object.entries(doc)
246 | .filter((df) => key.indexOf(df[0]) !== -1)
247 | .map((df) => [df[0], 'eq.' + df[1]])
248 | )
249 | else
250 | keys = Object.fromEntries(
251 | Object.entries(key).map(([k, v]) => [k, 'eq.' + v])
252 | )
253 |
254 | //throw 'cond='+cond;
255 | let updurl;
256 | let cond;
257 |
258 | if (import.meta.env.VITE_POSTGREST_PATH_AS_ARG)
259 | {
260 | cond = qs.stringify({...keys,_path:path});
261 | updurl = DB + '/' + '?' + cond
262 | }
263 | else
264 | {
265 | cond = qs.stringify(keys);
266 | updurl = DB + '/' + path + '?' + cond;
267 | }
268 | //l('PATCHing',updurl,'with',str.length);
269 | //l('updurl=',updurl); l('body:',doc)
270 | let res = await fetch(updurl, {
271 | method: 'PATCH',
272 | headers: await getHeaders(),
273 | body: str,
274 | })
275 | //l('received response',res); throw 'bye';
276 | if (!errok && res.status !== 204) {
277 | l('code', res.status)
278 | l('message:', res.statusText)
279 | for (let [k, v] of Object.entries(doc)) {
280 | let js = JSON.stringify(v)
281 | if (js) l(k + '.length=', js.length)
282 | else l(k, 'is', typeof js)
283 | }
284 | l('attempted to update', str.length, 'long doc')
285 | l('res', await res.text())
286 | throw Error(
287 | 'could not update ' +
288 | path +
289 | ' with ' +
290 | cond +
291 | ' (' +
292 | res.status +
293 | '): ' +
294 | res.statusText +
295 | ' with key ' +
296 | JSON.stringify(key) +
297 | ' valued ' +
298 | JSON.stringify(
299 | Object.entries(doc).filter(
300 | (x) => Array.isArray(key) && key.includes(x[0])
301 | )
302 | )
303 | )
304 | }
305 | return res
306 | }
307 | export async function upsert(path, obj, key = ['id']) {
308 | if (typeof key !== 'object') throw 'wrong key type in upsert'
309 | let res = await insert(path, obj, true,{'Prefer':'resolution=merge-duplicates'})
310 | if (res.status === 409) {
311 | let res2 = await update(path, obj, false, key)
312 | //l('upsert pt2',res2);
313 | return res2
314 | } else {
315 | return res
316 | }
317 | }
318 | export async function insert(path, obj, errok, headersOverride = {}) {
319 | let h = { ...(await getHeaders()), ...headersOverride }
320 | //l('using headers',h);
321 | let furl;
322 | if (import.meta.env.VITE_POSTGREST_PATH_AS_ARG)
323 | furl = DB + '/?_path=' + encodeURIComponent(path);
324 | else
325 | furl = DB + '/' + path;
326 | let res = await fetch(furl, {
327 | method: 'POST',
328 | headers: {'Prefer':'return=representation',...h},
329 | body: JSON.stringify(obj),
330 | })
331 | //l('insert returned',res);
332 | if (!errok && res.status !== 204 && res.status !== 201) {
333 | throw Error('bad insert response ' + res.status + ': ' + (await res.text()))
334 | }
335 | return res
336 | }
337 |
338 | export async function select(path, args) {
339 | if (!DB) throw new Error('DB not defined.')
340 | let h = await getHeaders()
341 | let furl;
342 | if (import.meta.env.VITE_POSTGREST_PATH_AS_ARG)
343 | furl = DB + '/' + '?' + qs.stringify({...args,_path:path});
344 | else
345 | furl = DB + '/' + path + '?' + qs.stringify(args);
346 | let res = await fetch(furl, {
347 | method: 'GET',
348 | headers: h,
349 | })
350 | let txt, json
351 | try {
352 | txt = await res.text()
353 | try {
354 | json = JSON.parse(txt)
355 | if (json.message === 'JWT expired' || res.code === 401) {
356 | l('headers', h)
357 | let cookie = getCookie('auth')
358 | l('cookie is', cookie)
359 | let dec = jwt_decode(cookie)
360 | l('dec=', dec)
361 | throw new Error('jwt has expired wtf')
362 | }
363 | if (json.code && json.message)
364 | throw new Error(
365 | 'select from ' +
366 | path +
367 | ' errored with code ' +
368 | json.code +
369 | '; ' +
370 | json.message
371 | )
372 | } catch (err) {
373 | throw new Error('could not json parse the response ' + txt)
374 | }
375 | if (json.message && json.message.startsWith('JWSError')) {
376 | l('JWSError:', json) //l(path,args,'=>',json);
377 | await CFG.redirToAuth()
378 | }
379 | return json
380 | } catch (err) {
381 | l('could not parse/return response', txt)
382 | throw err
383 | }
384 | }
385 | export async function selectOne(path, args, errOk = false) {
386 | let res = await select(path, args)
387 | if (!errOk && res.length !== 1) {
388 | l(res)
389 | throw new Error(
390 | 'selection from ' + path + ' is not 1 in length - ' + res.length
391 | )
392 | }
393 | if (res.length) return res[0]
394 | else return null
395 | }
396 |
397 | export async function pass_reset(email) {
398 | let furl;
399 | if (import.meta.env.VITE_POSTGREST_PATH_AS_ARG)
400 | furl=`${DB}?_path=/rpc/pass_reset`;
401 | else
402 | furl = `${DB}/rpc/pass_reset`;
403 | return await fetch(furl, {
404 | method: 'POST',
405 | headers: { ...hdrs },
406 | body: JSON.stringify({ email }),
407 | })
408 | }
409 |
410 | export async function pass_reset_new(obj) {
411 | let furl;
412 | if (import.meta.env.VITE_POSTGREST_PATH_AS_ARG)
413 | furl = `${DB}?_path=/rpc/pass_reset_new`
414 | else
415 | furl = `${DB}/rpc/pass_reset_new`;
416 | return await fetch(furl, {
417 | method: 'POST',
418 | headers: { ...hdrs },
419 | body: JSON.stringify(obj),
420 | })
421 | }
422 |
423 | export async function validateByToken(token, authData) {
424 | l('validateByToken', token, authData)
425 | if (token) {
426 | let furl;
427 | if (import.meta.env.VITE_POSTGREST_PATH_AS_ARG)
428 | furl = DB + '?_path=/rpc/validate_token';
429 | else
430 | furl = DB + '/rpc/validate_token';
431 |
432 | let f = await fetch(furl, {
433 | method: 'POST',
434 | headers: hdrs,
435 | body: JSON.stringify({ token }),
436 | })
437 | let j = await f.json()
438 | l('validateByToken j=', j)
439 | if (j && j.token) return { token: j.token, email: j.email }
440 | else return null
441 | }
442 | return null
443 | }
444 |
445 | export async function validationReset() {
446 | let h = await getHeaders()
447 | let furl;
448 | if (import.meta.env.VITE_POSTGREST_PATH_AS_ARG)
449 | furl = `${DB}?_path=/rpc/validation_reset`;
450 | else
451 | furl = `${DB}/rpc/validation_reset`;
452 | return await fetch(furl, {
453 | method: 'POST',
454 | headers: h,
455 | })
456 | }
457 |
--------------------------------------------------------------------------------
/db_init.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
4 | source $DIR/.env
5 |
6 | PGJWTFN=pgjwt/pgjwt--0.1.0.sql
7 | [[ ! -z "$RDS" ]] && [[ -f "$PGJWTFN" ]] && (
8 | echo '* rds' ;
9 | (
10 | echo 'create extension pgcrypto;' ;
11 | echo 'drop role if exists admin;';
12 | echo 'drop role if exists client;';
13 | echo 'drop role if exists anon;';
14 | echo 'create role admin;';
15 | echo 'create role client;';
16 | echo 'create role anon;';
17 | tail -n+2 $PGJWTFN | sed -E 's/@extschema@/public/g' ) > ./sql/pgjwt.sql
18 | ) ;
19 | echo '* db creation' && \
20 | $DIR/psql-admin.sh -c 'drop database if exists "'$DBNAME'"' && \
21 | $DIR/psql-admin.sh -c 'create database "'$DBNAME'"' && \
22 | echo '* schema' && \
23 | (
24 | (cd sql/schema ;
25 | #cat order.txt | xargs -t -n1 sh -c '../../psql.sh -v ON_ERROR_STOP=ON -f $0 || exit 255' ;
26 | ( ( [[ ! -z "$RDS" ]] && echo ../pgjwt.sql || echo '../pgjwt-nords.sql' ) ; echo 00 ; cat order.txt | grep -v 'pgjwt' ) | xargs cat | \
27 | ( [[ ! -z "$RDS" ]] && sed -E "s/current_setting\('app.jwt_secret'\)/'"$JWTSECRET"'/g" || cat )
28 |
29 | ) | $DIR/psql.sh --quiet -v ON_ERROR_STOP=ON ) && \
30 | echo '* fixtures' && \
31 | (cat $DIR/sql/fixtures.sql | \
32 | $DIR/psql.sh -v ON_ERROR_STOP=ON ) && \
33 | echo '* jwtsecret' && \
34 | ( [[ -z "$RDS" ]] && $DIR/psql-admin.sh -c 'ALTER DATABASE "'$DBNAME'"'" SET app.jwt_secret = '"$JWTSECRET"'")
35 | #cat ../schema.sql | ../../psql.sh -v ON_ERROR_STOP=ON ;
36 |
37 |
--------------------------------------------------------------------------------
/envs/local.tpl:
--------------------------------------------------------------------------------
1 | APPNAME=DBNAMEREPLACE
2 | APP_PORT=APPPORTREPLACE
3 | APP_BASE_URI=http://localhost:$APP_PORT
4 | DBHOST=localhost
5 | DBNAME=DBNAMEREPLACE
6 | DBADMINSUDO=postgres
7 | DBUSER=''
8 | DBURIADMIN='postgres:///template1'
9 | DBURI="postgres:///$DBNAME"
10 | JWTSECRET=JWTSECRETREPLACE
11 | POSTGRESTPORT=POSTGRESTPORTREPLACE
12 | POSTGREST_BASE_URI=http://localhost:$POSTGRESTPORT
13 | RDS=''
14 | PYTHON_EXEC="../../../../.pyenv/shims/python"
15 | SERVER_PORT=SERVERPORTREPLACE
16 | SERVER_BASE_URI=http://localhost:$SERVER_PORT/server
17 | SITE_SOCKETIO_URI=ws://localhost:$SERVER_PORT
18 | VITE_APP_BASE_URI=$APP_BASE_URI
19 | VITE_DB=$DBNAME
20 | VITE_POSTGREST_BASE_URI=$POSTGREST_BASE_URI
21 | VITE_SERVER_BASE_URI=$SERVER_BASE_URI
22 | VITE_SITE_SOCKETIO_URI=$SITE_SOCKETIO_URI
23 |
--------------------------------------------------------------------------------
/lambda/.env:
--------------------------------------------------------------------------------
1 | ../.env
--------------------------------------------------------------------------------
/lambda/.gitignore:
--------------------------------------------------------------------------------
1 | function.zip
2 | node_modules
3 | postgrest
4 |
--------------------------------------------------------------------------------
/lambda/.nvmrc:
--------------------------------------------------------------------------------
1 | v14.16.0
2 |
--------------------------------------------------------------------------------
/lambda/README.org:
--------------------------------------------------------------------------------
1 | * set up rds
2 | ** create the database
3 | ** RDS=1 in .env
4 | * install postgrest
5 | #+BEGIN_SRC bash
6 | cd lambda/ && curl -L -s 'https://github.com/PostgREST/postgrest/releases/download/v9.0.0/postgrest-v9.0.0-linux-static-x64.tar.xz' | tar -Jxvf - ; cd -
7 | #+END_SRC
8 | * create lambda func
9 | function definition:
10 | - Timoeut :: >= 5s
11 | - Enable function URL - new :: true
12 | - Auth type :: NONE
13 | - Configure cross-origin resource sharing (CORS) :: true
14 | AWS_POSTGREST_LAMBDA_FUNC=the:function:arn
15 | POSTGREST_BASE_URI=https://ABCDEF.lambda-url.us-east-1.on.aws/
16 | DBURI='postgres://postgres:RDSPASS@DBHOST/'
17 | VITE_POSTGREST_PATH_AS_ARG=1
18 | POSTGREST_PATH_AS_ARG=1
19 |
--------------------------------------------------------------------------------
/lambda/index.js:
--------------------------------------------------------------------------------
1 | const dotenv = require('dotenv/config.js')
2 | const postgrest = require('./postgrest.js')
3 | const getPort = require('get-port')
4 | const micro = require('micro')
5 | const { URL } = require('url')
6 | const fetch = require('node-fetch')
7 | const delay = require('delay')
8 | //const qs = require('qs');
9 |
10 | const l = (...a) => console.log(process.env.REV, 'IDX', ...a)
11 | let postgrestPort, postgrestUrl, server
12 | l('dsn:', process.env.DBURI)
13 | async function startServer() {
14 | l('starting server...')
15 | postgrestPort = await getPort()
16 | l('port', postgrestPort)
17 | server = await postgrest.startServer({
18 | dbUri:process.env.DBURI,
19 | dbAnonRole: 'anon',
20 | dbSchema: 'public',
21 | serverPort: postgrestPort,
22 | dbPool: 2,
23 | jwtSecret: process.env.JWTSECRET,
24 | })
25 | postgrestUrl = `http://localhost:${postgrestPort}`
26 | l('started at url', postgrestUrl)
27 | }
28 |
29 | async function startOrResetServer() {
30 | l('startOrResetServer', Boolean(server))
31 | if (!server) {
32 | l('server not yet initialized, initializing...')
33 | await startServer()
34 | l({ postgrestUrl })
35 | } else {
36 | try {
37 | l('attempting to fetch(', postgrestUrl, ')')
38 | const res = await fetch(postgrestUrl)
39 | l('res', Object.keys(res))
40 | if (!(res.status >= 200 && res.status < 300)) {
41 | l("Couldn't connect to previous postgrest instance")
42 | throw new Error("Couldn't connect to previous postgrest instance")
43 | }
44 | } catch (e) {
45 | l('exception:', e.toString())
46 | l('Restarting postgrest...')
47 | await server.stop()
48 | await startServer()
49 | }
50 | }
51 | }
52 |
53 | module.exports = {
54 | handler: async (req, res) => {
55 | //l('in handler',Object.keys(req),'req.url=',req.url);
56 | //res.statusCode=200; res.body = JSON.stringify(req); return res;
57 |
58 | await startOrResetServer()
59 | //let path = req.url?req.url.replace(/^\/api/, "/"):'/';
60 | const qsp =
61 | req && req.queryStringParameters ? req.queryStringParameters : {}
62 | let path = qsp._path ? qsp._path : '/' // req.url?req.url.replace('testicle/','/'):'/';
63 | if (!path.startsWith('/')) path = '/' + path
64 | //let get = qs.stringify({...req.queryStringParameters,_path:undefined});
65 | let get = ''
66 | for (let [k, v] of Object.entries(qsp)) {
67 | if (['_path'].includes(k)) continue
68 | get +=
69 | (get.length ? '&' : '?') +
70 | encodeURIComponent(k) +
71 | '=' +
72 | encodeURIComponent(v)
73 | }
74 | if (get.length) path += get
75 | const proxyTo = `${postgrestUrl}${path}`
76 | l('proxying to', proxyTo)
77 | const proxyRes = await fetch(proxyTo, {
78 | method: req.requestContext.http.method,
79 | headers: Object.assign(
80 | { 'x-forwarded-host': req.headers ? req.headers.host : undefined },
81 | req.headers,
82 | { host: req.host }
83 | ),
84 | body: req.body,
85 | redirect: 'manual',
86 | })
87 | //l('proxy res=',proxyRes);
88 | res.statusCode = proxyRes.status
89 | res.headers = {
90 | "Access-Control-Allow-Headers" : "*",
91 | "Access-Control-Allow-Origin": "*",
92 | 'Content-Type': 'application/json',
93 | }
94 | l('res.headers=',res.headers)
95 | res.isBase64Encoded = false
96 | l('proxy statusCode', res.statusCode)
97 | //l('proxy size',res.size);
98 | //proxyRes.body.pipe(res,{end:true});
99 | //await res.send(await proxyRes.text());
100 | //res.end();
101 | res.body = await proxyRes.text()
102 | return res
103 | l('done piping?')
104 | },
105 | }
106 |
--------------------------------------------------------------------------------
/lambda/pack-update-test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | source .env && \
3 | lambda/pack.sh && \
4 | lambda/update.sh && \
5 | curl -vvv $POSTGREST_BASE_URI
6 |
--------------------------------------------------------------------------------
/lambda/pack.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
3 | F=function.zip
4 | R=$(tr -dc A-Za-z0-9 > $DIR/../.env) && \
9 | zip -r $F index.js postgrest.js postgrest package.json node_modules .env --exclude node_modules/postgrest/postgrest && \
10 | echo 'REV='$R && \
11 | du -h $F && \
12 | cd -
13 |
--------------------------------------------------------------------------------
/lambda/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "postgrest-on-vercel",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "dependencies": {
6 | "bent": "^7.3.12",
7 | "delay": "^4.4.0",
8 | "dotenv": "^10.0.0",
9 | "get-port": "^5.1.1",
10 | "micro": "^9.3.4",
11 | "node-fetch": "^2.6.1",
12 | "tmp": "^0.2.1"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/lambda/postgrest-download.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | cd lambda/ && \
3 | echo '** downloading & extracting' && \
4 | curl -L -s 'https://github.com/PostgREST/postgrest/releases/download/v9.0.0/postgrest-v9.0.0-linux-static-x64.tar.xz' | tar -Jxvf - && \
5 | echo '** success' && \
6 | ls -la postgrest
7 | cd -
8 |
--------------------------------------------------------------------------------
/lambda/postgrest.js:
--------------------------------------------------------------------------------
1 | const child_process = require('child_process')
2 | const fs = require('fs')
3 | const tmp = require('tmp')
4 | //const decamelize = require("decamelize")
5 | const path = require('path')
6 | const bent = require('bent')
7 |
8 | const getJSON = bent('json')
9 | const l = (...a) => console.log(process.env.REV, 'PG', ...a)
10 |
11 | const handlePreserveConsecutiveUppercase = (decamelized, separator) => {
12 | // Lowercase all single uppercase characters. As we
13 | // want to preserve uppercase sequences, we cannot
14 | // simply lowercase the separated string at the end.
15 | // `data_For_USACounties` → `data_for_USACounties`
16 | decamelized = decamelized.replace(
17 | /((? $0.toLowerCase()
19 | )
20 |
21 | // Remaining uppercase sequences will be separated from lowercase sequences.
22 | // `data_For_USACounties` → `data_for_USA_counties`
23 | return decamelized.replace(
24 | /(\p{Uppercase_Letter}+)(\p{Uppercase_Letter}\p{Lowercase_Letter}+)/gu,
25 | (_, $1, $2) => $1 + separator + $2.toLowerCase()
26 | )
27 | }
28 |
29 | function decamelize(
30 | text,
31 | { separator = '_', preserveConsecutiveUppercase = false } = {}
32 | ) {
33 | if (!(typeof text === 'string' && typeof separator === 'string')) {
34 | throw new TypeError(
35 | 'The `text` and `separator` arguments should be of type `string`'
36 | )
37 | }
38 |
39 | // Checking the second character is done later on. Therefore process shorter strings here.
40 | if (text.length < 2) {
41 | return preserveConsecutiveUppercase ? text : text.toLowerCase()
42 | }
43 |
44 | const replacement = `$1${separator}$2`
45 |
46 | // Split lowercase sequences followed by uppercase character.
47 | // `dataForUSACounties` → `data_For_USACounties`
48 | // `myURLstring → `my_URLstring`
49 | const decamelized = text.replace(
50 | /([\p{Lowercase_Letter}\d])(\p{Uppercase_Letter})/gu,
51 | replacement
52 | )
53 |
54 | if (preserveConsecutiveUppercase) {
55 | return handlePreserveConsecutiveUppercase(decamelized, separator)
56 | }
57 |
58 | // Split multiple uppercase characters followed by one or more lowercase characters.
59 | // `my_URLstring` → `my_ur_lstring`
60 | return decamelized
61 | .replace(
62 | /(\p{Uppercase_Letter})(\p{Uppercase_Letter}\p{Lowercase_Letter}+)/gu,
63 | replacement
64 | )
65 | .toLowerCase()
66 | }
67 |
68 | module.exports.startServer = async (config) => {
69 | l('in startServer; config=', config)
70 | let configPath
71 | let serverPort
72 | let connectedToDB = false
73 |
74 | if (typeof config === 'string') {
75 | l('config is a string, attempting to extract my port')
76 | configPath = config
77 | const configContent = fs.readFileSync(configPath).toString()
78 | serverPort = parseInt(/^server-port=(.*)$/.match(configContent))
79 | } else {
80 | l('config is an object or is empty? generating from the object')
81 | serverPort = config.serverPort
82 | const postgrestConfigContent = `${Object.entries(config)
83 | .map(([k, v]) => `${decamelize(k, { separator: '-' })}="${v}"`)
84 | .join('\n')}`
85 | configPath = tmp.tmpNameSync() + '.conf'
86 | l('generating postgrest config:', postgrestConfigContent)
87 | fs.writeFileSync(configPath, postgrestConfigContent)
88 | }
89 | const pth = path.resolve(__dirname, 'postgrest')
90 | const args = [configPath]
91 | const opts = {
92 | shell: true,
93 | }
94 | l('spawning process', { pth, args, opts })
95 |
96 | connectedToDB = false
97 | const proc = child_process.spawn(pth, args, opts)
98 | l('proc spawned')
99 | function checkConnGood(data) {
100 | if (data.includes('Connection successful')) {
101 | connectedToDB = true
102 | l('CONNECTION SUCCESFUL! connectedToDB=true')
103 | }
104 | }
105 |
106 | proc.stdout.on('data', (data) => {
107 | l(`stdout: ${data}`)
108 | checkConnGood(data)
109 | })
110 |
111 | proc.stderr.on('data', (data) => {
112 | l(`stderr: ${data}`)
113 | checkConnGood(data)
114 | })
115 |
116 | let isClosed = false
117 | proc.on('close', (code) => {
118 | l('closed', { code })
119 | isClosed = true
120 | })
121 |
122 | await new Promise((resolve, reject) => {
123 | const processCloseTimeout = setTimeout(() => {
124 | if (isClosed) {
125 | l('did not start projerly.')
126 | reject("Postgrest didn't start properly")
127 | } else {
128 | l('did not respond.')
129 | reject(`Postgrest didn't respond`)
130 | proc.kill('SIGINT')
131 | }
132 | }, 20000)
133 |
134 | async function checkIfPostgrestRunning() {
135 | //l(`is running at http://localhost:${serverPort}?`);
136 | if (!connectedToDB) {
137 | //l('no point checking connectivity while not connected to db.');
138 | return setTimeout(checkIfPostgrestRunning, 25)
139 | }
140 | const jurl = `http://localhost:${serverPort}`
141 | l('checking if running', { jurl })
142 | const result = await getJSON(jurl).catch((err) =>
143 | l('caught an error', err)
144 | )
145 | //l('is running res=',result);
146 | if (result) {
147 | l('IS RUNNING!')
148 | clearTimeout(processCloseTimeout)
149 | resolve()
150 | } else {
151 | l('setting to re-check')
152 | setTimeout(checkIfPostgrestRunning, 200)
153 | }
154 | }
155 | checkIfPostgrestRunning()
156 | })
157 |
158 | return {
159 | proc,
160 | stop: async () => {
161 | proc.kill('SIGINT')
162 | },
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/lambda/rds-create.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | source .env
3 | (aws rds create-db-instance \
4 | --db-instance-identifier $APPNAME \
5 | --db-instance-class db.t3.micro \
6 | --engine postgres \
7 | --engine-version 14.2 \
8 | --master-user-password $RDS_PASSWORD \
9 | --db-subnet-group-name $RDS_VPC_GROUP \
10 | --master-username postgres \
11 | --publicly-accessible\
12 | --allocated-storage 20 || echo 'ERROR_CREATING:'$?) | tee envs/$ENV.rds-ins.json
13 |
14 | while (true) ; do
15 | echo '** awaiting instance creation'
16 | aws rds describe-db-instances --db-instance-identifier $APPNAME > envs/$ENV.rds.json
17 | EP=$(jq '.DBInstances[0].Endpoint.Address' envs/$ENV.rds.json -r | sed -E 's/^null$//g')
18 | echo '*** sleeping'
19 | [[ ! -z "$EP" ]] && break
20 | sleep 5
21 | done
22 | echo "RDS_HOSTNAME="$EP
23 |
--------------------------------------------------------------------------------
/lambda/trust-policy.tpl.json:
--------------------------------------------------------------------------------
1 | {
2 | "Version": "2012-10-17",
3 | "Statement": [
4 | {
5 | "Effect": "Allow",
6 | "Principal": {
7 | "Service": "lambda.amazonaws.com"
8 | },
9 | "Action": "sts:AssumeRole"
10 | }
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/lambda/update.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
3 | source $DIR/.env
4 | time aws lambda update-function-code --function-name $AWS_POSTGREST_LAMBDA_FUNC --zip-file fileb://$DIR/function.zip
5 |
--------------------------------------------------------------------------------
/lint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # use -l to get a return code of whether everything is fine (0)
3 | # use --write to enforce the prettier style
4 | npx prettier "$@" '**/*.{css,html,js,svelte}'
5 |
--------------------------------------------------------------------------------
/npm-init.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | echo '* forking npm i' && \
3 | (cd app && npm install ; cd - ) & \
4 | (cd common && npm install ; cd - ) & \
5 | (cd cli && npm install ; cd - ) & \
6 | (cd server && npm install ; cd - ) & \
7 | echo '* waiting' && \
8 | wait && \
9 | echo '* all done!'
10 |
11 |
--------------------------------------------------------------------------------
/pg_schema_dump.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
3 | source $DIR/.env
4 | SCHEMAFN=$DIR/sql/schema.sql
5 | RSCHEMAFN=$DIR/sql/schema-reconstructed.sql
6 | mkdir -p sql/schema && \
7 | pg_dump --schema-only --no-owner $DBURI > $SCHEMAFN && \
8 | cd sql/schema/ && \
9 | (rm * ||:) && \
10 | csplit $SCHEMAFN -f '' -b '%02d' '/^\-\- Name: .*/' '{*}' && \
11 | for FN in `ls -1v *` ; do
12 | echo $FN
13 | NF="$(egrep -o "Name: ([^;]+); Type: ([^;]+)" $FN | sed -E 's/(Name: |Type: |; |\(|\)| )/./g' | sed -E 's/(\.+)/./g' | sed -E 's/$/.sql/' | sed -E "s/^\.//g" | sed -E 's/character\.varying([^\.]*)\./cv\./g' | sed -E 's/timestamp.with.time.zone/tstz/g')";
14 | #mv -n $FN $FN$NF ;
15 | (
16 | #echo '-- SPLIT '$FN ; # disabled to not trigger train diffs
17 | cat $FN
18 |
19 | echo "$NF" >> order.txt
20 | #echo $NF ;
21 | ) | sed -E "s/ '"$JWTSECRET"'/ current_setting('app.jwt_secret')/" > $NF && rm $FN
22 | done
23 | [ "$(wc -l < order.txt)" -eq "$(sort -u order.txt | wc -l)" ] || (echo 'FILENAMES NOT UNIQUE!' ; exit 1)
24 | cd -
25 |
26 | echo '* reconstructing'
27 | # to reconstruct a single file schema:
28 | cd sql/schema && (echo 00 ; cat order.txt) | xargs cat > $RSCHEMAFN ;
29 | diff -Nuar $SCHEMAFN $RSCHEMAFN || (echo "DUMPS $SCHEMAFN && $RSCHEMAFN NOT EQUAL!" ; exit 2) &&
30 | echo '* erasing monodumps' && \
31 | rm $SCHEMAFN $RSCHEMAFN && \
32 | echo '* rewriting view creation' && \
33 | sed -E -i 's/^CREATE VIEW/CREATE OR REPLACE VIEW/g' *sql && \
34 | echo '* all done!'
35 | cd -
36 |
--------------------------------------------------------------------------------
/postgrest.conf:
--------------------------------------------------------------------------------
1 | db-uri="$(DBURI)"
2 | db-schema = "public" # this schema gets added to the search_path of every request
3 | db-anon-role = "anon"
4 | db-pool = 100
5 | db-pool-timeout = 10
6 |
7 | server-host = "!4"
8 | server-port = "$(POSTGRESTPORT)"
9 | jwt-secret="$(JWTSECRET)"
10 |
--------------------------------------------------------------------------------
/psql-admin.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
3 | source $DIR/.env
4 | #echo 'connecting to "'$DBURI'"'
5 | if [ -z "$DBADMINSUDO" ]
6 | then
7 | psql "$@" "$DBURIADMIN"
8 | else
9 | sudo -u $DBADMINSUDO psql "$@" "$DBURIADMIN"
10 | fi
11 |
--------------------------------------------------------------------------------
/psql.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
3 | source $DIR/.env
4 | #echo 'connecting to "'$DBURI'"'
5 | psql "$@" "$DBURI"
6 |
--------------------------------------------------------------------------------
/server/.env:
--------------------------------------------------------------------------------
1 | ../.env
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | import 'dotenv/config.js'
2 | import archiver from 'archiver'
3 | import pg from 'pg'
4 | import sirv from 'sirv'
5 | import polka from 'polka'
6 | import compression from 'compression'
7 | import Busboy from 'busboy'
8 | import path from 'path'
9 | import fs, { createReadStream } from 'fs'
10 | import { Server } from 'socket.io' // socketIo
11 | import http from 'http'
12 | import Cors from 'cors'
13 |
14 | const { NODE_ENV } = process.env
15 | const dev = NODE_ENV === 'development'
16 | const l = console.log
17 |
18 |
19 | const cors = { origin: '*', 'Access-Control-Allow-Origin': '*' } // FIXME!
20 |
21 | let isConnected = false
22 | const client = new pg.Client(process.env.DBURI)
23 | l('connstr is', process.env.DBURI)
24 | const server = http.createServer()
25 |
26 | async function getAuthUser(req) {
27 | //validate
28 | let token
29 | if (req.query.token) {
30 | token = req.query.token
31 | } else if (req.headers.authorization) {
32 | token = req.headers.authorization.split(' ')[1]
33 | } else if (req.headers.cookie)
34 | token = Object.fromEntries(
35 | req.headers.cookie.split('; ').map((c) => c.split('='))
36 | ).auth
37 | else {
38 | return null
39 | }
40 |
41 | //l('validating token',token);
42 | let vres = await client.query('select (verify_token($1)).*', [token])
43 | let { header, payload, valid } = vres.rows[0]
44 | //l('validate returned',header,payload,valid);
45 | if (valid !== true) {
46 | return null
47 | }
48 | let user = payload.email
49 | return user
50 | }
51 |
52 | async function deny(res) {
53 | res.writeHead(403)
54 | return res.end()
55 | }
56 | async function notfound(res) {
57 | res.writeHead(404)
58 | return res.end()
59 | }
60 |
61 | const P = '/server' //prefix
62 |
63 |
64 |
65 | polka({ server }) // You can also use Express
66 | .use(
67 | Cors(cors),
68 | compression({ threshold: 0 }),
69 | sirv('../app/static', { dev })
70 | )
71 | //.use('/app',proxy) // proxy is disabled in favor of standalone servers
72 | .listen(process.env.SERVER_PORT, (err) => {
73 | if (err) console.log('error', err)
74 | })
75 |
76 | // helper func to figure out if client is authorized for
77 | // action/resource access
78 | async function isAuthorized(o, user) {
79 | let rt = false
80 | return rt
81 | }
82 |
83 | async function pgconn() {
84 | if (!isConnected) {
85 | l('pgconn connecting.')
86 | isConnected = true
87 | await client.connect()
88 |
89 | for (let listen of listens)
90 | {
91 | l('listening on',listen);
92 | var query = await client.query(`LISTEN ${listen}`)
93 | }
94 |
95 | client.on('notification', async function (title) {
96 | l('NOTIFICATION', title)
97 | let o = JSON.parse(title.payload)
98 | for (let [sockid, socket] of Object.entries(sockets)) {
99 | //l('working socket',sockid,'for possible authorization to notify',o.owner_id,'regarding',o.id);
100 | const ia = await isAuthorized(o, socketUsers[socket.id]);
101 | l('ia=',ia,!!ia);
102 | if (ia) {
103 | l(
104 | 'notification',
105 | title.channel,
106 | title.payload,
107 | '=>',
108 | socketUsers[socket.id]
109 | )
110 | socket.emit('update', { message: title })
111 | }
112 | }
113 | })
114 | }
115 | //else l('pgconn ALREADY CONNECTED');
116 | }
117 | const io = new Server(server, { cors })
118 |
119 | // pg channels to listen on for events to pass on to connected clients
120 | const listens = [
121 | // INSERT LISTENED CHANNELS LIST HERE
122 | ]
123 | let sockets = {}
124 | let socketUsers = {}
125 | io.on('connection', async function (socket) {
126 | //const token = Object.keys(socket.handshake.query)[0];
127 | let cookies = Object.fromEntries(
128 | socket.handshake.headers.cookie
129 | ? socket.handshake.headers.cookie
130 | .split('; ')
131 | .map((x) => (x ? x.split('=') : [null, null]))
132 | : []
133 | )
134 | const token =
135 | cookies.auth || (socket.handshake.auth && socket.handshake.auth.token)
136 | //l('socket',socket.id,'connected with token',token);
137 | if (!token || token.length < 10) return socket.disconnect()
138 | //console.log('connected with token',token)
139 | pgconn()
140 | try {
141 | let vres = await client.query('select (verify_token($1)).*', [token])
142 | //l('vres',vres);
143 | let { header, payload, valid } = vres.rows[0]
144 | //l('validate returned',header,payload,valid);
145 | if (valid !== true) throw new Error('invalid token')
146 | let user = payload.email
147 | l('connected socketio user', user)
148 | socket.on('disconnect', () => {
149 | l('socket', socket.id, 'disconnecting')
150 | delete sockets[socket.id]
151 | delete socketUsers[socket.id]
152 | })
153 | sockets[socket.id] = socket
154 | socketUsers[socket.id] = user
155 | } catch (e) {
156 | l('error authenticating.', token, ':', e)
157 | socket.disconnect()
158 | }
159 | })
160 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "type": "module",
4 | "version": "1.0.0",
5 | "description": "",
6 | "main": "index.js",
7 | "scripts": {
8 | "test": "echo \"Error: no test specified\" && exit 1",
9 | "start": "node index.js"
10 | },
11 | "author": "",
12 | "license": "ISC",
13 | "dependencies": {
14 | "archiver": "^5.3.0",
15 | "busboy": "^0.3.1",
16 | "compression": "^1.7.4",
17 | "dotenv": "^10.0.0",
18 | "http-proxy-middleware": "^2.0.0",
19 | "nodemon": "^2.0.7",
20 | "pg": "^8.6.0",
21 | "polka": "^0.5.2",
22 | "sirv": "^1.0.11",
23 | "socket.io": "^4.1.2"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/setenv.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | echo '* removing old env (disregard error if no env set)' && \
3 | rm .env .env.sh
4 | echo '* symlinking .env.sh' && \
5 | ln -s envs/$1 ./.env.sh && \
6 | echo '* evaluating .env.sh into .env (node)' && \
7 | source .env.sh && \
8 | (
9 | IFS=$'\n'
10 | for L in $(cat .env.sh) ; do
11 | #echo 'L="'$L'"'
12 | N="$(echo "$L"|cut -f1 -d'=')"
13 | EV="$(eval 'echo $'$N)"
14 | #echo '# evaluating '$N
15 | echo "$N='"$EV"'"
16 | done
17 | echo ENV=$1
18 | ) > .env
19 |
20 |
--------------------------------------------------------------------------------
/sql/fixtures.sql:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/guyromm/svelte-postgrest-template/a105a8c9790f5cd26aa1405e5cbb60d9077454e0/sql/fixtures.sql
--------------------------------------------------------------------------------
/sql/pgjwt-nords.sql:
--------------------------------------------------------------------------------
1 | create extension pgcrypto;
2 | create extension pgjwt;
3 |
--------------------------------------------------------------------------------
/sql/pgjwt.sql:
--------------------------------------------------------------------------------
1 | create extension pgcrypto;
2 | drop role if exists admin;
3 | drop role if exists client;
4 | drop role if exists anon;
5 | create role admin;
6 | create role client;
7 | create role anon;
8 |
9 |
10 | CREATE OR REPLACE FUNCTION url_encode(data bytea) RETURNS text LANGUAGE sql AS $$
11 | SELECT translate(encode(data, 'base64'), E'+/=\n', '-_');
12 | $$;
13 |
14 |
15 | CREATE OR REPLACE FUNCTION url_decode(data text) RETURNS bytea LANGUAGE sql AS $$
16 | WITH t AS (SELECT translate(data, '-_', '+/') AS trans),
17 | rem AS (SELECT length(t.trans) % 4 AS remainder FROM t) -- compute padding size
18 | SELECT decode(
19 | t.trans ||
20 | CASE WHEN rem.remainder > 0
21 | THEN repeat('=', (4 - rem.remainder))
22 | ELSE '' END,
23 | 'base64') FROM t, rem;
24 | $$;
25 |
26 |
27 | CREATE OR REPLACE FUNCTION algorithm_sign(signables text, secret text, algorithm text)
28 | RETURNS text LANGUAGE sql AS $$
29 | WITH
30 | alg AS (
31 | SELECT CASE
32 | WHEN algorithm = 'HS256' THEN 'sha256'
33 | WHEN algorithm = 'HS384' THEN 'sha384'
34 | WHEN algorithm = 'HS512' THEN 'sha512'
35 | ELSE '' END AS id) -- hmac throws error
36 | SELECT public.url_encode(public.hmac(signables, secret, alg.id)) FROM alg;
37 | $$;
38 |
39 |
40 | CREATE OR REPLACE FUNCTION sign(payload json, secret text, algorithm text DEFAULT 'HS256')
41 | RETURNS text LANGUAGE sql AS $$
42 | WITH
43 | header AS (
44 | SELECT public.url_encode(convert_to('{"alg":"' || algorithm || '","typ":"JWT"}', 'utf8')) AS data
45 | ),
46 | payload AS (
47 | SELECT public.url_encode(convert_to(payload::text, 'utf8')) AS data
48 | ),
49 | signables AS (
50 | SELECT header.data || '.' || payload.data AS data FROM header, payload
51 | )
52 | SELECT
53 | signables.data || '.' ||
54 | public.algorithm_sign(signables.data, secret, algorithm) FROM signables;
55 | $$;
56 |
57 |
58 | CREATE OR REPLACE FUNCTION verify(token text, secret text, algorithm text DEFAULT 'HS256')
59 | RETURNS table(header json, payload json, valid boolean) LANGUAGE sql AS $$
60 | SELECT
61 | convert_from(public.url_decode(r[1]), 'utf8')::json AS header,
62 | convert_from(public.url_decode(r[2]), 'utf8')::json AS payload,
63 | r[3] = public.algorithm_sign(r[1] || '.' || r[2], secret, algorithm) AS valid
64 | FROM regexp_split_to_array(token, '\.') r;
65 | $$;
66 |
--------------------------------------------------------------------------------
/sql/schema/00:
--------------------------------------------------------------------------------
1 | --
2 | -- PostgreSQL database dump
3 | --
4 |
5 | -- Dumped from database version 14.5 (Ubuntu 14.5-0ubuntu0.22.04.1)
6 | -- Dumped by pg_dump version 14.5 (Ubuntu 14.5-0ubuntu0.22.04.1)
7 |
8 | SET statement_timeout = 0;
9 | SET lock_timeout = 0;
10 | SET idle_in_transaction_session_timeout = 0;
11 | SET client_encoding = 'UTF8';
12 | SET standard_conforming_strings = on;
13 | SELECT pg_catalog.set_config('search_path', '', false);
14 | SET check_function_bodies = false;
15 | SET xmloption = content;
16 | SET client_min_messages = warning;
17 | SET row_security = off;
18 |
19 | --
20 |
--------------------------------------------------------------------------------
/sql/schema/EXTENSION.pgcrypto.COMMENT.sql:
--------------------------------------------------------------------------------
1 | -- Name: EXTENSION pgcrypto; Type: COMMENT; Schema: -; Owner: -
2 | --
3 |
4 | COMMENT ON EXTENSION pgcrypto IS 'cryptographic functions';
5 |
6 |
7 | --
8 |
--------------------------------------------------------------------------------
/sql/schema/EXTENSION.pgjwt.COMMENT.sql:
--------------------------------------------------------------------------------
1 | -- Name: EXTENSION pgjwt; Type: COMMENT; Schema: -; Owner: -
2 | --
3 |
4 | COMMENT ON EXTENSION pgjwt IS 'JSON Web Token API for Postgresql';
5 |
6 |
7 | --
8 |
--------------------------------------------------------------------------------
/sql/schema/FUNCTION.role.email.cv.newrole.cv.ACL.sql:
--------------------------------------------------------------------------------
1 | -- Name: FUNCTION role(email character varying, newrole character varying); Type: ACL; Schema: public; Owner: -
2 | --
3 |
4 | GRANT ALL ON FUNCTION public.role(email character varying, newrole character varying) TO admin;
5 |
6 |
7 | --
8 |
--------------------------------------------------------------------------------
/sql/schema/FUNCTION.user_delete.email.cv.ACL.sql:
--------------------------------------------------------------------------------
1 | -- Name: FUNCTION user_delete(email character varying); Type: ACL; Schema: public; Owner: -
2 | --
3 |
4 | GRANT ALL ON FUNCTION public.user_delete(email character varying) TO admin;
5 |
6 |
7 | --
8 |
--------------------------------------------------------------------------------
/sql/schema/FUNCTION.user_insert.email.cv.ACL.sql:
--------------------------------------------------------------------------------
1 | -- Name: FUNCTION user_insert(email character varying); Type: ACL; Schema: public; Owner: -
2 | --
3 |
4 | GRANT ALL ON FUNCTION public.user_insert(email character varying) TO admin;
5 |
6 |
7 | --
8 |
--------------------------------------------------------------------------------
/sql/schema/FUNCTION.validate.email.cv.ACL.sql:
--------------------------------------------------------------------------------
1 | -- Name: FUNCTION validate(email character varying); Type: ACL; Schema: public; Owner: -
2 | --
3 |
4 | GRANT ALL ON FUNCTION public.validate(email character varying) TO admin;
5 |
6 |
7 | --
8 |
--------------------------------------------------------------------------------
/sql/schema/FUNCTION.verify_token.token.text,.algorithm.text.ACL.sql:
--------------------------------------------------------------------------------
1 | -- Name: FUNCTION verify_token(token text, algorithm text); Type: ACL; Schema: public; Owner: -
2 | --
3 |
4 | GRANT ALL ON FUNCTION public.verify_token(token text, algorithm text) TO admin;
5 |
6 |
7 | --
8 | -- PostgreSQL database dump complete
9 | --
10 |
11 |
--------------------------------------------------------------------------------
/sql/schema/basic_auth.SCHEMA.sql:
--------------------------------------------------------------------------------
1 | -- Name: basic_auth; Type: SCHEMA; Schema: -; Owner: -
2 | --
3 |
4 | CREATE SCHEMA basic_auth;
5 |
6 |
7 | --
8 |
--------------------------------------------------------------------------------
/sql/schema/check_role_exists.FUNCTION.sql:
--------------------------------------------------------------------------------
1 | -- Name: check_role_exists(); Type: FUNCTION; Schema: basic_auth; Owner: -
2 | --
3 |
4 | CREATE FUNCTION basic_auth.check_role_exists() RETURNS trigger
5 | LANGUAGE plpgsql
6 | AS $$
7 | begin
8 | if not exists (select 1 from pg_roles as r where r.rolname = new.role) then
9 | raise foreign_key_violation using message =
10 | 'unknown database role: ' || new.role;
11 | return null;
12 | end if;
13 | return new;
14 | end
15 | $$;
16 |
17 |
18 | --
19 |
--------------------------------------------------------------------------------
/sql/schema/encrypt_pass.FUNCTION.sql:
--------------------------------------------------------------------------------
1 | -- Name: encrypt_pass(); Type: FUNCTION; Schema: basic_auth; Owner: -
2 | --
3 |
4 | CREATE FUNCTION basic_auth.encrypt_pass() RETURNS trigger
5 | LANGUAGE plpgsql
6 | AS $$
7 | begin
8 | if tg_op = 'INSERT' or new.pass <> old.pass then
9 | new.pass = crypt(new.pass, gen_salt('bf'));
10 | end if;
11 | return new;
12 | end
13 | $$;
14 |
15 |
16 | --
17 |
--------------------------------------------------------------------------------
/sql/schema/invite.cv.FUNCTION.sql:
--------------------------------------------------------------------------------
1 | -- Name: invite(character varying); Type: FUNCTION; Schema: public; Owner: -
2 | --
3 |
4 | CREATE FUNCTION public.invite(email character varying) RETURNS text
5 | LANGUAGE sql SECURITY DEFINER
6 | AS $$
7 | -- jsonb_set(jsonb_set(coalesce(validation_info,'{}'::jsonb),'{token}'::text[],concat('"',md5(random()::text),'"')::jsonb),'{email_sent}','true'::jsonb)
8 | insert into basic_auth.users (email,role,validation_info) values(invite.email,'client',json_build_object('no_email',true,'token',md5(random()::text))) on conflict (email) do nothing returning email;
9 | $$;
10 |
11 |
12 | --
13 |
--------------------------------------------------------------------------------
/sql/schema/jwt_test.FUNCTION.sql:
--------------------------------------------------------------------------------
1 | -- Name: jwt_test(); Type: FUNCTION; Schema: public; Owner: -
2 | --
3 |
4 | CREATE FUNCTION public.jwt_test() RETURNS public.jwt_token
5 | LANGUAGE sql
6 | AS $$
7 | SELECT public.sign(
8 | row_to_json(r), current_setting('app.jwt_secret')
9 | ) AS token
10 | FROM (
11 | SELECT
12 | 'my_role'::text as role,
13 | extract(epoch from now())::integer + 300 AS exp
14 | ) r;
15 | $$;
16 |
17 |
18 | --
19 |
--------------------------------------------------------------------------------
/sql/schema/jwt_token.TYPE.sql:
--------------------------------------------------------------------------------
1 | -- Name: jwt_token; Type: TYPE; Schema: public; Owner: -
2 | --
3 |
4 | CREATE TYPE public.jwt_token AS (
5 | token text
6 | );
7 |
8 |
9 | --
10 |
--------------------------------------------------------------------------------
/sql/schema/login.text,.text.FUNCTION.sql:
--------------------------------------------------------------------------------
1 | -- Name: login(text, text); Type: FUNCTION; Schema: public; Owner: -
2 | --
3 |
4 | CREATE FUNCTION public.login(em text, ps text) RETURNS public.jwt_token
5 | LANGUAGE plpgsql SECURITY DEFINER
6 | AS $$
7 | declare
8 | _role name;
9 | _validated timestamptz;
10 | _approved timestamptz;
11 | result public.jwt_token;
12 | begin
13 | select basic_auth.user_role(em, ps) into _role;
14 | if _role is null then
15 | raise notice 'could not find user with email % pass %',em,ps;
16 | raise invalid_password using message = 'invalid user or password';
17 | end if;
18 | select basic_auth.user_validated(em,ps) into _validated;
19 | select approved INTO _approved from basic_auth.users u
20 | where u.email = em
21 | and u.pass = crypt(ps, u.pass);
22 |
23 | select sign(
24 | row_to_json(r), current_setting('app.jwt_secret')
25 | ) as token
26 | from (
27 | select
28 | concat(_role,(CASE WHEN _validated is null OR _approved IS NULL THEN '_unvalidated' ELSE '' END)) as role,
29 | login.em as email,
30 | _validated as validated,
31 | _approved as approved,
32 | extract(epoch from now())::integer + 60*60 as exp
33 | ) r
34 | into result;
35 | return result;
36 | end;
37 | $$;
38 |
39 |
40 | --
41 |
--------------------------------------------------------------------------------
/sql/schema/order.txt:
--------------------------------------------------------------------------------
1 |
2 | basic_auth.SCHEMA.sql
3 | pgcrypto.EXTENSION.sql
4 | EXTENSION.pgcrypto.COMMENT.sql
5 | pgjwt.EXTENSION.sql
6 | EXTENSION.pgjwt.COMMENT.sql
7 | jwt_token.TYPE.sql
8 | check_role_exists.FUNCTION.sql
9 | encrypt_pass.FUNCTION.sql
10 | user_role.text,.text.FUNCTION.sql
11 | user_validated.text,.text.FUNCTION.sql
12 | invite.cv.FUNCTION.sql
13 | jwt_test.FUNCTION.sql
14 | login.text,.text.FUNCTION.sql
15 | pass_reset.text.FUNCTION.sql
16 | pass_reset_new.text,.text.FUNCTION.sql
17 | refresh.FUNCTION.sql
18 | register.text,.text.FUNCTION.sql
19 | register.text,.text,.text.FUNCTION.sql
20 | role.cv.cv.FUNCTION.sql
21 | users.TABLE.sql
22 | users.VIEW.sql
23 | set_user_role.text,.text.FUNCTION.sql
24 | unvalidate.cv.FUNCTION.sql
25 | user_approved.text,.boolean.FUNCTION.sql
26 | user_delete.cv.FUNCTION.sql
27 | user_insert.cv.FUNCTION.sql
28 | user_validated.text,.boolean.FUNCTION.sql
29 | users_notify_trigger.FUNCTION.sql
30 | users_pass_reset_notify.FUNCTION.sql
31 | validate.cv.FUNCTION.sql
32 | validate_token.cv.FUNCTION.sql
33 | validation_reset.FUNCTION.sql
34 | verify_token.text,.text.FUNCTION.sql
35 | settings.TABLE.sql
36 | users.users_pkey.CONSTRAINT.sql
37 | settings.table_name_pk.CONSTRAINT.sql
38 | users.encrypt_pass.TRIGGER.sql
39 | users.ensure_user_role_exists.TRIGGER.sql
40 | users.users_notify.TRIGGER.sql
41 | users.users_pass_reset_notify_trig.TRIGGER.sql
42 | FUNCTION.role.email.cv.newrole.cv.ACL.sql
43 | FUNCTION.user_delete.email.cv.ACL.sql
44 | FUNCTION.user_insert.email.cv.ACL.sql
45 | FUNCTION.validate.email.cv.ACL.sql
46 | FUNCTION.verify_token.token.text,.algorithm.text.ACL.sql
47 |
--------------------------------------------------------------------------------
/sql/schema/pass_reset.text.FUNCTION.sql:
--------------------------------------------------------------------------------
1 | -- Name: pass_reset(text); Type: FUNCTION; Schema: public; Owner: -
2 | --
3 |
4 | CREATE FUNCTION public.pass_reset(email text) RETURNS json
5 | LANGUAGE plpgsql SECURITY DEFINER
6 | AS $$
7 | BEGIN
8 | UPDATE basic_auth.users u
9 | SET pass_reset_info = json_build_object(
10 | 'ts', NOW(),
11 | 'token', MD5(RANDOM()::text),
12 | 'email_sent', FALSE
13 | )
14 | WHERE u.email = pass_reset.email
15 | AND u.validated IS NOT NULL;
16 |
17 |
18 | IF NOT FOUND THEN
19 | RAISE EXCEPTION 'email not found';
20 | END IF;
21 |
22 | RETURN json_build_object('status', 'ok');
23 | END;
24 | $$;
25 |
26 |
27 | --
28 |
--------------------------------------------------------------------------------
/sql/schema/pass_reset_new.text,.text.FUNCTION.sql:
--------------------------------------------------------------------------------
1 | -- Name: pass_reset_new(text, text); Type: FUNCTION; Schema: public; Owner: -
2 | --
3 |
4 | CREATE FUNCTION public.pass_reset_new(token text, pass text) RETURNS json
5 | LANGUAGE plpgsql SECURITY DEFINER
6 | AS $$
7 | BEGIN
8 | UPDATE basic_auth.users u
9 | SET pass = pass_reset_new.pass,
10 | pass_reset_info = NULL
11 | WHERE u.pass_reset_info IS NOT NULL
12 | AND u.pass_reset_info ->> 'token' = pass_reset_new.token
13 | AND NOW() - (u.pass_reset_info ->> 'ts')::timestamptz < '1 HOUR'::interval;
14 |
15 | IF NOT FOUND THEN
16 | RAISE EXCEPTION 'token is not valid';
17 | END IF;
18 |
19 | RETURN json_build_object('status', 'ok');
20 | END;
21 | $$;
22 |
23 |
24 | --
25 |
--------------------------------------------------------------------------------
/sql/schema/pgcrypto.EXTENSION.sql:
--------------------------------------------------------------------------------
1 | -- Name: pgcrypto; Type: EXTENSION; Schema: -; Owner: -
2 | --
3 |
4 | CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA public;
5 |
6 |
7 | --
8 |
--------------------------------------------------------------------------------
/sql/schema/pgjwt.EXTENSION.sql:
--------------------------------------------------------------------------------
1 | -- Name: pgjwt; Type: EXTENSION; Schema: -; Owner: -
2 | --
3 |
4 | CREATE EXTENSION IF NOT EXISTS pgjwt WITH SCHEMA public;
5 |
6 |
7 | --
8 |
--------------------------------------------------------------------------------
/sql/schema/refresh.FUNCTION.sql:
--------------------------------------------------------------------------------
1 | -- Name: refresh(); Type: FUNCTION; Schema: public; Owner: -
2 | --
3 |
4 | CREATE FUNCTION public.refresh() RETURNS public.jwt_token
5 | LANGUAGE plpgsql SECURITY DEFINER
6 | AS $$
7 | declare
8 | result public.jwt_token;
9 | begin
10 | if current_setting('request.jwt.claims', true)::json->>'email' is null then
11 | raise invalid_password using message = 'not currently logged in!';
12 | end if;
13 |
14 | select sign(
15 | row_to_json(r), current_setting('app.jwt_secret')
16 | ) as token
17 | from (
18 | select
19 | concat(role,(CASE WHEN validated is null OR approved is NULL THEN '_unvalidated' ELSE '' END)) as role,
20 | email,
21 | validated,
22 | approved,
23 | extract(epoch from now())::integer + 60*60 as exp
24 | from basic_auth.users
25 | where email=current_setting('request.jwt.claims', true)::json->>'email'
26 | ) r
27 | into result;
28 | return result;
29 | end;
30 | $$;
31 |
32 |
33 | --
34 |
--------------------------------------------------------------------------------
/sql/schema/register.text,.text,.text.FUNCTION.sql:
--------------------------------------------------------------------------------
1 | -- Name: register(text, text, text); Type: FUNCTION; Schema: public; Owner: -
2 | --
3 |
4 | CREATE FUNCTION public.register(em text, ps text, vinfo text) RETURNS public.jwt_token
5 | LANGUAGE plpgsql SECURITY DEFINER
6 | AS $$
7 | declare
8 | result public.jwt_token;
9 | begin
10 | insert into basic_auth.users
11 | (email,pass,role,validated,validation_info)
12 | values(em,ps,'client',null,vinfo)
13 | on conflict (email) do
14 | update set
15 | validation_info=vinfo,
16 | validated=null,
17 | pass=ps
18 | where users.email=em;
19 | return login(em,ps);
20 | end;
21 | $$;
22 |
23 |
24 | --
25 |
--------------------------------------------------------------------------------
/sql/schema/register.text,.text.FUNCTION.sql:
--------------------------------------------------------------------------------
1 | -- Name: register(text, text); Type: FUNCTION; Schema: public; Owner: -
2 | --
3 |
4 | CREATE FUNCTION public.register(em text, ps text) RETURNS public.jwt_token
5 | LANGUAGE plpgsql SECURITY DEFINER
6 | AS $$
7 | DECLARE
8 | mode_invite_only_enabled bool = (
9 | SELECT COALESCE((SELECT value::bool
10 | FROM public.settings
11 | WHERE key = 'mode_invite_only'), FALSE));
12 | BEGIN
13 | INSERT INTO basic_auth.users (email, pass, role, validated, approved)
14 | VALUES (em,
15 | ps,
16 | 'client',
17 | NULL,
18 | CASE mode_invite_only_enabled WHEN FALSE THEN NOW() END);
19 | RETURN login(em, ps);
20 | END;
21 | $$;
22 |
23 |
24 | --
25 |
--------------------------------------------------------------------------------
/sql/schema/role.cv.cv.FUNCTION.sql:
--------------------------------------------------------------------------------
1 | -- Name: role(character varying, character varying); Type: FUNCTION; Schema: public; Owner: -
2 | --
3 |
4 | CREATE FUNCTION public.role(email character varying, newrole character varying) RETURNS void
5 | LANGUAGE sql SECURITY DEFINER
6 | AS $$
7 | update basic_auth.users set role=newrole where email=role.email and email<>current_setting('request.jwt.claims', true)::json->>'email' and role<>newrole;
8 | $$;
9 |
10 |
11 | SET default_tablespace = '';
12 |
13 | SET default_table_access_method = heap;
14 |
15 | --
16 |
--------------------------------------------------------------------------------
/sql/schema/set_user_role.text,.text.FUNCTION.sql:
--------------------------------------------------------------------------------
1 | -- Name: set_user_role(text, text); Type: FUNCTION; Schema: public; Owner: -
2 | --
3 |
4 | CREATE FUNCTION public.set_user_role(_email text, role text) RETURNS public.users
5 | LANGUAGE plpgsql SECURITY DEFINER
6 | AS $$
7 | BEGIN
8 | IF current_setting('request.jwt.claims', true)::json->>'role' != 'admin' THEN
9 | RAISE insufficient_privilege;
10 | END IF;
11 |
12 | IF set_user_role.role NOT IN ('client', 'admin') THEN
13 | RAISE NOTICE 'incorrect value of param role';
14 | END IF;
15 |
16 | UPDATE basic_auth.users SET role=set_user_role.role WHERE email = _email;
17 |
18 | IF NOT FOUND THEN
19 | RAISE EXCEPTION 'user not changed';
20 | END IF;
21 |
22 | RETURN (SELECT u FROM public.users u WHERE u.email = _email);
23 | END ;
24 | $$;
25 |
26 |
27 | --
28 |
--------------------------------------------------------------------------------
/sql/schema/settings.TABLE.sql:
--------------------------------------------------------------------------------
1 | -- Name: settings; Type: TABLE; Schema: public; Owner: -
2 | --
3 |
4 | CREATE TABLE public.settings (
5 | key text NOT NULL,
6 | title text NOT NULL,
7 | value jsonb NOT NULL
8 | );
9 |
10 |
11 | --
12 |
--------------------------------------------------------------------------------
/sql/schema/settings.table_name_pk.CONSTRAINT.sql:
--------------------------------------------------------------------------------
1 | -- Name: settings table_name_pk; Type: CONSTRAINT; Schema: public; Owner: -
2 | --
3 |
4 | ALTER TABLE ONLY public.settings
5 | ADD CONSTRAINT table_name_pk PRIMARY KEY (key);
6 |
7 |
8 | --
9 |
--------------------------------------------------------------------------------
/sql/schema/unvalidate.cv.FUNCTION.sql:
--------------------------------------------------------------------------------
1 | -- Name: unvalidate(character varying); Type: FUNCTION; Schema: public; Owner: -
2 | --
3 |
4 | CREATE FUNCTION public.unvalidate(email character varying) RETURNS void
5 | LANGUAGE sql SECURITY DEFINER
6 | AS $$
7 | update basic_auth.users set validated=null,validation_info=null where validated is not null and email=unvalidate.email;
8 | $$;
9 |
10 |
11 | --
12 |
--------------------------------------------------------------------------------
/sql/schema/user_approved.text,.boolean.FUNCTION.sql:
--------------------------------------------------------------------------------
1 | -- Name: user_approved(text, boolean); Type: FUNCTION; Schema: public; Owner: -
2 | --
3 |
4 | CREATE FUNCTION public.user_approved(_email text, is_approved boolean) RETURNS public.users
5 | LANGUAGE plpgsql SECURITY DEFINER
6 | AS $$
7 | BEGIN
8 | IF current_setting('request.jwt.claims', true)::json->>'role' != 'admin' THEN
9 | RAISE insufficient_privilege;
10 | END IF;
11 |
12 | UPDATE basic_auth.users SET approved=CASE WHEN is_approved IS TRUE THEN NOW() END
13 | WHERE email=_email
14 | AND ((is_approved IS TRUE AND approved IS NULL)
15 | OR (is_approved IS FALSE AND approved IS NOT NULL));
16 |
17 | IF NOT FOUND THEN
18 | RAISE EXCEPTION 'user not changed';
19 | END IF;
20 |
21 | RETURN (SELECT u FROM public.users u WHERE u.email = _email);
22 | END;
23 | $$;
24 |
25 |
26 | --
27 |
--------------------------------------------------------------------------------
/sql/schema/user_delete.cv.FUNCTION.sql:
--------------------------------------------------------------------------------
1 | -- Name: user_delete(character varying); Type: FUNCTION; Schema: public; Owner: -
2 | --
3 |
4 | CREATE FUNCTION public.user_delete(email character varying) RETURNS void
5 | LANGUAGE sql SECURITY DEFINER
6 | AS $$
7 | delete from basic_auth.users where email=user_delete.email and role<>'admin' and validated is null and email<>current_setting('request.jwt.claims', true)::json->>'email';
8 | $$;
9 |
10 |
11 | --
12 |
--------------------------------------------------------------------------------
/sql/schema/user_insert.cv.FUNCTION.sql:
--------------------------------------------------------------------------------
1 | -- Name: user_insert(character varying); Type: FUNCTION; Schema: public; Owner: -
2 | --
3 |
4 | CREATE FUNCTION public.user_insert(email character varying) RETURNS void
5 | LANGUAGE sql SECURITY DEFINER
6 | AS $$
7 | insert into basic_auth.users (email,pass,role) values(user_insert.email,md5(random()::text),'client');
8 | $$;
9 |
10 |
11 | --
12 |
--------------------------------------------------------------------------------
/sql/schema/user_role.text,.text.FUNCTION.sql:
--------------------------------------------------------------------------------
1 | -- Name: user_role(text, text); Type: FUNCTION; Schema: basic_auth; Owner: -
2 | --
3 |
4 | CREATE FUNCTION basic_auth.user_role(email text, pass text) RETURNS name
5 | LANGUAGE plpgsql
6 | AS $$
7 | begin
8 | return (
9 | select role from basic_auth.users
10 | where users.email = user_role.email
11 | and users.pass = crypt(user_role.pass, users.pass)
12 | );
13 | end;
14 | $$;
15 |
16 |
17 | --
18 |
--------------------------------------------------------------------------------
/sql/schema/user_validated.text,.boolean.FUNCTION.sql:
--------------------------------------------------------------------------------
1 | -- Name: user_validated(text, boolean); Type: FUNCTION; Schema: public; Owner: -
2 | --
3 |
4 | CREATE FUNCTION public.user_validated(_email text, is_valid boolean) RETURNS public.users
5 | LANGUAGE plpgsql SECURITY DEFINER
6 | AS $$
7 | BEGIN
8 | IF current_setting('request.jwt.claims', true)::json->>'role' != 'admin' THEN
9 | RAISE insufficient_privilege;
10 | END IF;
11 |
12 | UPDATE basic_auth.users SET validated=CASE WHEN is_valid IS TRUE THEN NOW() END
13 | WHERE email=_email
14 | AND ((is_valid IS TRUE AND validated IS NULL)
15 | OR (is_valid IS FALSE AND validated IS NOT NULL));
16 |
17 | IF NOT FOUND THEN
18 | RAISE EXCEPTION 'user not changed';
19 | END IF;
20 |
21 | RETURN (SELECT u FROM public.users u WHERE u.email = _email);
22 | END;
23 | $$;
24 |
25 |
26 | --
27 |
--------------------------------------------------------------------------------
/sql/schema/user_validated.text,.text.FUNCTION.sql:
--------------------------------------------------------------------------------
1 | -- Name: user_validated(text, text); Type: FUNCTION; Schema: basic_auth; Owner: -
2 | --
3 |
4 | CREATE FUNCTION basic_auth.user_validated(email text, pass text) RETURNS timestamp with time zone
5 | LANGUAGE plpgsql
6 | AS $$
7 | begin
8 | return (
9 | select validated from basic_auth.users
10 | where users.email = user_validated.email
11 | and users.pass = crypt(user_validated.pass, users.pass)
12 | );
13 | end;
14 | $$;
15 |
16 |
17 | --
18 |
--------------------------------------------------------------------------------
/sql/schema/users.TABLE.sql:
--------------------------------------------------------------------------------
1 | -- Name: users; Type: TABLE; Schema: basic_auth; Owner: -
2 | --
3 |
4 | CREATE TABLE basic_auth.users (
5 | email text NOT NULL,
6 | pass text,
7 | role name NOT NULL,
8 | validated timestamp with time zone,
9 | validation_info jsonb,
10 | pass_reset_info jsonb,
11 | ts timestamp with time zone DEFAULT now(),
12 | approved timestamp with time zone,
13 | CONSTRAINT users_email_check CHECK ((email ~* '^.+@.+\..+$'::text)),
14 | CONSTRAINT users_pass_check CHECK ((length(pass) < 512)),
15 | CONSTRAINT users_role_check CHECK ((length((role)::text) < 512))
16 | );
17 |
18 |
19 | --
20 |
--------------------------------------------------------------------------------
/sql/schema/users.VIEW.sql:
--------------------------------------------------------------------------------
1 | -- Name: users; Type: VIEW; Schema: public; Owner: -
2 | --
3 |
4 | CREATE OR REPLACE VIEW public.users AS
5 | SELECT u.ts,
6 | u.email,
7 | u.role,
8 | u.validated,
9 | u.pass_reset_info,
10 | u.validation_info,
11 | u.approved
12 | FROM basic_auth.users u;
13 |
14 |
15 | --
16 |
--------------------------------------------------------------------------------
/sql/schema/users.encrypt_pass.TRIGGER.sql:
--------------------------------------------------------------------------------
1 | -- Name: users encrypt_pass; Type: TRIGGER; Schema: basic_auth; Owner: -
2 | --
3 |
4 | CREATE TRIGGER encrypt_pass BEFORE INSERT OR UPDATE ON basic_auth.users FOR EACH ROW EXECUTE FUNCTION basic_auth.encrypt_pass();
5 |
6 |
7 | --
8 |
--------------------------------------------------------------------------------
/sql/schema/users.ensure_user_role_exists.TRIGGER.sql:
--------------------------------------------------------------------------------
1 | -- Name: users ensure_user_role_exists; Type: TRIGGER; Schema: basic_auth; Owner: -
2 | --
3 |
4 | CREATE CONSTRAINT TRIGGER ensure_user_role_exists AFTER INSERT OR UPDATE ON basic_auth.users NOT DEFERRABLE INITIALLY IMMEDIATE FOR EACH ROW EXECUTE FUNCTION basic_auth.check_role_exists();
5 |
6 |
7 | --
8 |
--------------------------------------------------------------------------------
/sql/schema/users.users_notify.TRIGGER.sql:
--------------------------------------------------------------------------------
1 | -- Name: users users_notify; Type: TRIGGER; Schema: basic_auth; Owner: -
2 | --
3 |
4 | CREATE TRIGGER users_notify AFTER INSERT OR UPDATE ON basic_auth.users FOR EACH ROW EXECUTE FUNCTION public.users_notify_trigger();
5 |
6 |
7 | --
8 |
--------------------------------------------------------------------------------
/sql/schema/users.users_pass_reset_notify_trig.TRIGGER.sql:
--------------------------------------------------------------------------------
1 | -- Name: users users_pass_reset_notify_trig; Type: TRIGGER; Schema: basic_auth; Owner: -
2 | --
3 |
4 | CREATE TRIGGER users_pass_reset_notify_trig AFTER UPDATE ON basic_auth.users FOR EACH ROW EXECUTE FUNCTION public.users_pass_reset_notify();
5 |
6 |
7 | --
8 |
--------------------------------------------------------------------------------
/sql/schema/users.users_pkey.CONSTRAINT.sql:
--------------------------------------------------------------------------------
1 | -- Name: users users_pkey; Type: CONSTRAINT; Schema: basic_auth; Owner: -
2 | --
3 |
4 | ALTER TABLE ONLY basic_auth.users
5 | ADD CONSTRAINT users_pkey PRIMARY KEY (email);
6 |
7 |
8 | --
9 |
--------------------------------------------------------------------------------
/sql/schema/users_notify_trigger.FUNCTION.sql:
--------------------------------------------------------------------------------
1 | -- Name: users_notify_trigger(); Type: FUNCTION; Schema: public; Owner: -
2 | --
3 |
4 | CREATE FUNCTION public.users_notify_trigger() RETURNS trigger
5 | LANGUAGE plpgsql
6 | AS $$
7 | begin
8 | perform pg_notify('users_insert', (jsonb_build_object('email',NEW.email))::text);
9 | return null;
10 | end;
11 | $$;
12 |
13 |
14 | --
15 |
--------------------------------------------------------------------------------
/sql/schema/users_pass_reset_notify.FUNCTION.sql:
--------------------------------------------------------------------------------
1 | -- Name: users_pass_reset_notify(); Type: FUNCTION; Schema: public; Owner: -
2 | --
3 |
4 | CREATE FUNCTION public.users_pass_reset_notify() RETURNS trigger
5 | LANGUAGE plpgsql
6 | AS $$
7 | BEGIN
8 | IF NEW.pass_reset_info IS NOT NULL AND OLD.pass_reset_info IS DISTINCT FROM NEW.pass_reset_info THEN
9 | PERFORM pg_notify('users_pass_reset', (json_build_object('email', NEW.email))::text);
10 | END IF;
11 |
12 | RETURN NEW;
13 | END;
14 | $$;
15 |
16 |
17 | --
18 |
--------------------------------------------------------------------------------
/sql/schema/validate.cv.FUNCTION.sql:
--------------------------------------------------------------------------------
1 | -- Name: validate(character varying); Type: FUNCTION; Schema: public; Owner: -
2 | --
3 |
4 | CREATE FUNCTION public.validate(email character varying) RETURNS void
5 | LANGUAGE sql SECURITY DEFINER
6 | AS $$
7 | update basic_auth.users set validated=now() where validated is null and email=validate.email;
8 | select pg_notify('validated',(json_build_object('email',validate.email))::text);
9 | $$;
10 |
11 |
12 | --
13 |
--------------------------------------------------------------------------------
/sql/schema/validate_token.cv.FUNCTION.sql:
--------------------------------------------------------------------------------
1 | -- Name: validate_token(character varying); Type: FUNCTION; Schema: public; Owner: -
2 | --
3 |
4 | CREATE FUNCTION public.validate_token(token character varying) RETURNS json
5 | LANGUAGE plpgsql SECURITY DEFINER
6 | AS $$
7 | declare
8 | u basic_auth.users;
9 | rttok public.jwt_token;
10 | status varchar;
11 | begin
12 | select into u * from basic_auth.users where validation_info->>'token'=validate_token.token and (validation_info->>'email_sent'='true' or validation_info->>'no_email'='true'); -- and validated is null into u;
13 | raise notice 'got unvalidated user %',u.email;
14 | if u.email is not null and u.validated is null then
15 | update basic_auth.users set validated=now(), validation_info=validation_info-'token' where email=u.email;
16 | select sign(
17 | row_to_json(r), current_setting('app.jwt_secret')
18 | ) as token
19 | from (
20 | select
21 | concat(role,(CASE WHEN validated is null OR approved IS NULL THEN '_unvalidated' ELSE '' END)) as role,
22 | email,
23 | validated,
24 | approved,
25 | extract(epoch from now())::integer + 60*60 as exp
26 | from basic_auth.users
27 | where email=u.email
28 | ) r into rttok;
29 | status:= 'signed 1 (initial) rttok with '||rttok;
30 | elsif u.email is not null and u.validated>=(now()-interval '1 hour') then
31 | -- let's respect a validation token for up to an hour after validation
32 | select sign(
33 | row_to_json(r), current_setting('app.jwt_secret')
34 | ) as token
35 | from (
36 | select
37 | concat(role,(CASE WHEN validated is null OR approved IS NULL THEN '_unvalidated' ELSE '' END)) as role,
38 | email,
39 | validated,
40 | approved,
41 | extract(epoch from now())::integer + 60*60 as exp
42 | from basic_auth.users
43 | where email=u.email
44 | ) r into rttok;
45 | status:= 'signed 2 (hour-window) rttok with '||rttok;
46 | elsif u.email is null then
47 | status:='no matching user';
48 | else
49 | status:='validated too long ago. token no longer valid';
50 | end if;
51 | return json_build_object('token',rttok,'validated',u.validated,'email',u.email,'status',status);
52 | end;
53 | $$;
54 |
55 |
56 | --
57 |
--------------------------------------------------------------------------------
/sql/schema/validation_reset.FUNCTION.sql:
--------------------------------------------------------------------------------
1 | -- Name: validation_reset(); Type: FUNCTION; Schema: public; Owner: -
2 | --
3 |
4 | CREATE FUNCTION public.validation_reset() RETURNS json
5 | LANGUAGE plpgsql SECURITY DEFINER
6 | AS $$
7 | DECLARE
8 | cur_email text = current_setting('request.jwt.claims', true)::json->>'email';
9 | BEGIN
10 | IF cur_email IS NULL THEN
11 | RAISE invalid_password USING MESSAGE = 'not currently logged in!';
12 | END IF;
13 |
14 | UPDATE basic_auth.users u
15 | SET validation_info=NULL
16 | WHERE u.email = cur_email
17 | AND u.validated IS NULL;
18 |
19 | IF NOT FOUND THEN
20 | RAISE EXCEPTION 'email not found or already validated';
21 | END IF;
22 |
23 | PERFORM pg_notify('users_insert', (jsonb_build_object('email', cur_email))::text);
24 |
25 | RETURN json_build_object('status', 'ok');
26 | END;
27 | $$;
28 |
29 |
30 | --
31 |
--------------------------------------------------------------------------------
/sql/schema/verify_token.text,.text.FUNCTION.sql:
--------------------------------------------------------------------------------
1 | -- Name: verify_token(text, text); Type: FUNCTION; Schema: public; Owner: -
2 | --
3 |
4 | CREATE FUNCTION public.verify_token(token text, algorithm text DEFAULT 'HS256'::text) RETURNS TABLE(header json, payload json, valid boolean)
5 | LANGUAGE sql
6 | AS $$
7 | SELECT
8 | convert_from(public.url_decode(r[1]), 'utf8')::json AS header,
9 | convert_from(public.url_decode(r[2]), 'utf8')::json AS payload,
10 | r[3] = public.algorithm_sign(r[1] || '.' || r[2], current_setting('app.jwt_secret'), algorithm) AS valid
11 | FROM regexp_split_to_array(token, '\.') r;
12 | $$;
13 |
14 |
15 | --
16 |
--------------------------------------------------------------------------------
/systemd.sample:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=AppName
3 | DefaultDependencies=no
4 | After=postgresql.service
5 |
6 | [Service]
7 | Type=simple
8 | WorkingDirectory=/path/to/app
9 | User=APPUSER
10 | Group=APPGROUP
11 | ExecStart=/path/to/startup/script/tmux.sh
12 | TimeoutStartSec=0
13 | RemainAfterExit=yes
14 |
15 | [Install]
16 | WantedBy=default.target
17 |
--------------------------------------------------------------------------------
/tmux.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
3 | source $DIR/.env
4 | session="$APPNAME"
5 |
6 | function ti() {
7 | #tmux -d "$@" -s $session
8 | tmux new-session -d -s $session
9 | }
10 |
11 | function t() {
12 | tmux "$@"
13 | }
14 | function ts() {
15 | tmux "$@" -t "$session"
16 | }
17 |
18 | function tsk() {
19 | wn="$1"
20 | shift
21 | t send-keys -t "$session:$wn" "$@" C-m
22 | }
23 |
24 | function tnw() {
25 | t new-window -a -t "$session" -n "$1"
26 | }
27 |
28 | function tsw() {
29 | wn="$1"
30 | shift
31 | t split-window "$@" -t "$session:$wn"
32 | }
33 |
34 | function destroy() {
35 | echo '* destroying previous session' && \
36 | ts kill-session -t "$session"
37 | }
38 | function create() {
39 | echo '* creating session' && \
40 | ti && \
41 | echo '* creating window postgrest' && \
42 | t rename-window -t "$session.0" "postgrest" && \
43 | tsk postgrest 'source .env ; export DBURI ; export POSTGRESTPORT ; export JWTSECRET ; [[ ! -z "$POSTGRESTPORT" ]] && postgrest postgrest.conf || echo "no POSTGRESTPORT provided"' && \
44 | echo '* app' && \
45 | tnw app && \
46 | tsk app "nvm use && cd app ; npm run dev -- --port=$APP_PORT" && \
47 | echo '* server' && \
48 | tnw server && \
49 | tsk server "nvm use && cd server ; PORT=$SERVER_PORT npm run start" && \
50 | echo '* email-worker' && \
51 | tnw email-worker && \
52 | tsk email-worker "cli/email_worker.js -l" && \
53 | ( [[ ! -z $POSTGREST_PROXY_PORT ]] && (
54 | echo '* postgrest_proxy' && \
55 | tnw pgproxy && \
56 | tsk pgproxy "source .env && mitmdump -p $POSTGREST_PROXY_PORT -w mitm.log --mode reverse:$(echo $POSTGREST_BASE_URI | sed -E 's/\/default\/testicle//g')"
57 | ) || echo '* postgrest proxy disabled')
58 | }
59 | function attach() {
60 | ts attach-ses
61 | }
62 |
63 |
64 | echo "0 = '$0'"
65 | [[ $0 =~ bash$ ]] && echo "tmux.sh sourced" ||
66 | {
67 | destroy
68 | create && \
69 | attach
70 | }
71 |
--------------------------------------------------------------------------------