Browse Source

Changes from ljsthw-project-template brought over. The update method for this is kind of terrible so I'll have to sort out how to make it smooth for people.

master
Zed A. Shaw 1 month ago
parent
commit
7b6dd78cd0
  1. 277
      .eslintrc.json
  2. 106
      .stylelintrc.json
  3. 4
      api/admin/schema.js
  4. 15
      api/admin/table.js
  5. 11
      api/devtools/djenterator.js
  6. 9
      api/devtools/info.js
  7. 4
      api/login.js
  8. 11
      api/password_reset.js
  9. 28
      api/payments/btcpay.js
  10. 18
      api/payments/paypal.js
  11. 4
      api/register.js
  12. 33
      api/user/payments.js
  13. 11
      api/user/profile.js
  14. 35
      client/App.svelte
  15. 0
      client/Footer.svelte
  16. 26
      client/Header.svelte
  17. 66
      client/Layout.svelte
  18. 26
      client/api.js
  19. 1
      client/assert.js
  20. 28
      client/bando/Bandolier.svelte
  21. 114
      client/bando/Components.svelte
  22. 65
      client/bando/Djenterator.svelte
  23. 10
      client/bando/IconFinder.svelte
  24. 2
      client/bando/demos/Accordion.svelte
  25. 4
      client/bando/demos/Badge.svelte
  26. 7
      client/bando/demos/ButtonGroup.svelte
  27. 2
      client/bando/demos/Calendar.svelte
  28. 4
      client/bando/demos/Callout.svelte
  29. 4
      client/bando/demos/Cards.svelte
  30. 16
      client/bando/demos/Carousel.svelte
  31. 27
      client/bando/demos/Chat.svelte
  32. 52
      client/bando/demos/Code.svelte
  33. 2
      client/bando/demos/Countdown.svelte
  34. 2
      client/bando/demos/Darkmode.svelte
  35. 11
      client/bando/demos/DataTable.svelte
  36. 235
      client/bando/demos/FairPay.svelte
  37. 26
      client/bando/demos/Flipper.svelte
  38. 37
      client/bando/demos/Form.svelte
  39. 112
      client/bando/demos/Icon.svelte
  40. 56
      client/bando/demos/IconImage.svelte
  41. 34
      client/bando/demos/LiveStream.svelte
  42. 4
      client/bando/demos/LoggedIn.svelte
  43. 2
      client/bando/demos/Login.svelte
  44. 53
      client/bando/demos/Markdown.svelte
  45. 6
      client/bando/demos/Modal.svelte
  46. 3
      client/bando/demos/Pagination.svelte
  47. 2
      client/bando/demos/Panels.svelte
  48. 2
      client/bando/demos/PlaceHolder.svelte
  49. 2
      client/bando/demos/Progress.svelte
  50. 2
      client/bando/demos/Sidebar.svelte
  51. 2
      client/bando/demos/SnapImage.svelte
  52. 2
      client/bando/demos/Spinner.svelte
  53. 2
      client/bando/demos/Switch.svelte
  54. 2
      client/bando/demos/Tabs.svelte
  55. 2
      client/bando/demos/Tiles.svelte
  56. 2
      client/bando/demos/Toast.svelte
  57. 4
      client/bando/demos/Tooltip.svelte
  58. 9
      client/bando/demos/Video.svelte
  59. 55
      client/bando/demos/WTVideo.svelte
  60. 22
      client/components/BTCPay.svelte
  61. 47
      client/components/Calendar.svelte
  62. 11
      client/components/Carousel.svelte
  63. 202
      client/components/Chat.svelte
  64. 94
      client/components/Code.svelte
  65. 7
      client/components/Countdown.svelte
  66. 14
      client/components/DataTable.svelte
  67. 92
      client/components/Form.svelte
  68. 28
      client/components/FormField.svelte
  69. 6
      client/components/Icon.svelte
  70. 7
      client/components/IconImage.svelte
  71. 6
      client/components/LoggedIn.svelte
  72. 18
      client/components/Login.svelte
  73. 56
      client/components/Markdown.svelte
  74. 33
      client/components/Modal.svelte
  75. 17
      client/components/Paypal.svelte
  76. 2
      client/components/PlaceHolder.svelte
  77. 12
      client/components/Sidebar.svelte
  78. 4
      client/components/Tabs.svelte
  79. 60
      client/components/Video.svelte
  80. 24
      client/components/WTVideo.svelte
  81. 5
      client/lib/helpers.js
  82. 2
      client/main.js
  83. 102
      client/media.js
  84. 97
      client/pages/Home.svelte
  85. 27
      client/pages/Login.svelte
  86. 223
      client/pages/Purchase.svelte
  87. 85
      client/pages/Register.svelte
  88. 20
      client/pages/ResetPassword.svelte
  89. 92
      client/pages/UserProfile.svelte
  90. 11
      client/pages/admin/Create.svelte
  91. 35
      client/pages/admin/ReadUpdate.svelte
  92. 47
      client/pages/admin/Table.svelte
  93. 10
      client/pages/admin/index.svelte
  94. 2
      client/stores.js
  95. 12
      lib/auth.js
  96. 13
      lib/email.js
  97. 33
      lib/models.js
  98. 39
      lib/ormish.js
  99. 13
      lib/rollup.js
  100. 11
      lib/testing.js

277
.eslintrc.json

@ -0,0 +1,277 @@
{
"env": {
"browser": true,
"es6": true,
"node": true
},
"plugins": ["svelte3"],
"overrides": [
{"files": "*.svelte", "processor": "svelte3/svelte3"}
],
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 2021,
"sourceType": "module"
},
"rules": {
"accessor-pairs": "error",
"array-bracket-newline": "off",
"array-bracket-spacing": [ "warn", "never" ],
"array-callback-return": "error",
"array-element-newline": "off",
"arrow-body-style": "off",
"arrow-parens": "off",
"arrow-spacing": [
"error",
{
"after": true,
"before": true
}
],
"block-scoped-var": "error",
"block-spacing": "error",
"brace-style": [
"error",
"1tbs"
],
"callback-return": "off",
"camelcase": "off",
"capitalized-comments": "off",
"class-methods-use-this": "off",
"comma-dangle": "off",
"comma-spacing": [
"warn",
{
"after": true,
"before": false
}
],
"comma-style": [
"error",
"last"
],
"complexity": "error",
"computed-property-spacing": [
"error",
"never"
],
"consistent-return": "error",
"consistent-this": "error",
"curly": "off",
"default-case": "error",
"dot-location": "error",
"dot-notation": "off",
"eol-last": "error",
"eqeqeq": "off",
"func-call-spacing": "error",
"func-name-matching": "error",
"func-names": "error",
"func-style": [
"error",
"expression"
],
"function-paren-newline": "off",
"generator-star-spacing": "error",
"global-require": "off",
"guard-for-in": "error",
"handle-callback-err": "error",
"id-blacklist": "error",
"id-length": "off",
"id-match": "error",
"implicit-arrow-linebreak": [
"error",
"beside"
],
"indent": "off",
"indent-legacy": "off",
"init-declarations": "warn",
"jsx-quotes": "error",
"key-spacing": "warn",
"keyword-spacing": "off",
"line-comment-position": "off",
"linebreak-style": [
"error",
"unix"
],
"lines-around-comment": "off",
"lines-around-directive": "error",
"lines-between-class-members": "error",
"max-classes-per-file": "off",
"max-depth": "error",
"max-len": "off",
"max-lines": "off",
"max-lines-per-function": "off",
"max-nested-callbacks": "error",
"max-params": "off",
"max-statements": "off",
"max-statements-per-line": "error",
"multiline-comment-style": "off",
"new-cap": ["error", {"newIsCap": false, "capIsNew": true}],
"new-parens": "error",
"newline-after-var": "off",
"newline-before-return": "off",
"newline-per-chained-call": "off",
"no-alert": "error",
"no-array-constructor": "error",
"no-async-promise-executor": "error",
"no-await-in-loop": "off",
"no-bitwise": "error",
"no-buffer-constructor": "error",
"no-caller": "error",
"no-catch-shadow": "error",
"no-confusing-arrow": "error",
"no-console": "warn",
"no-continue": "error",
"no-div-regex": "error",
"no-duplicate-imports": "error",
"no-else-return": "off",
"no-empty-function": "error",
"no-eq-null": "off",
"no-eval": "error",
"no-extend-native": "error",
"no-extra-bind": "error",
"no-extra-label": "error",
"no-extra-parens": "off",
"no-floating-decimal": "error",
"no-implicit-coercion": "error",
"no-implicit-globals": "error",
"no-implied-eval": "error",
"no-inline-comments": "off",
"no-invalid-this": "error",
"no-iterator": "error",
"no-label-var": "error",
"no-labels": "error",
"no-lone-blocks": "error",
"no-lonely-if": "error",
"no-loop-func": "error",
"no-magic-numbers": "off",
"no-misleading-character-class": "error",
"no-mixed-operators": "error",
"no-mixed-requires": "error",
"no-multi-assign": "error",
"no-multi-spaces": "off",
"no-multi-str": "error",
"no-multiple-empty-lines": "warn",
"no-native-reassign": "error",
"no-negated-condition": "off",
"no-negated-in-lhs": "error",
"no-nested-ternary": "error",
"no-new": "error",
"no-new-func": "error",
"no-new-object": "error",
"no-new-require": "error",
"no-new-wrappers": "error",
"no-octal-escape": "error",
"no-param-reassign": "error",
"no-path-concat": "error",
"no-plusplus": ["error", { "allowForLoopAfterthoughts": true }],
"no-process-env": "off",
"no-process-exit": "off",
"no-proto": "error",
"no-prototype-builtins": "error",
"no-restricted-globals": "error",
"no-restricted-imports": "error",
"no-restricted-modules": "error",
"no-restricted-properties": "error",
"no-restricted-syntax": "error",
"no-return-assign": "warn",
"no-return-await": "off",
"no-script-url": "error",
"no-self-compare": "error",
"no-sequences": "error",
"no-shadow": "off",
"no-shadow-restricted-names": "error",
"no-spaced-func": "error",
"no-sync": "warn",
"no-tabs": "off",
"no-template-curly-in-string": "error",
"no-ternary": "off",
"no-throw-literal": "error",
"no-trailing-spaces": "off",
"no-undef-init": "off",
"no-undefined": "off",
"no-underscore-dangle": "off",
"no-unmodified-loop-condition": "error",
"no-unneeded-ternary": "error",
"no-unused-expressions": "error",
"no-use-before-define": ["error", {"functions": true, "classes": false}],
"no-useless-call": "error",
"no-useless-computed-key": "error",
"no-useless-concat": "error",
"no-useless-constructor": "error",
"no-useless-rename": "error",
"no-useless-return": "error",
"no-var": "error",
"no-void": "error",
"no-warning-comments": "off",
"no-whitespace-before-property": "error",
"no-with": "error",
"nonblock-statement-body-position": "error",
"object-curly-newline": "off",
"object-curly-spacing": "off",
"object-shorthand": "error",
"one-var": "off",
"one-var-declaration-per-line": "error",
"operator-assignment": "error",
"operator-linebreak": "error",
"padded-blocks": "off",
"padding-line-between-statements": "error",
"prefer-arrow-callback": "error",
"prefer-const": "off",
"prefer-destructuring": "off",
"prefer-numeric-literals": "error",
"prefer-object-spread": "error",
"prefer-promise-reject-errors": "error",
"prefer-reflect": "off",
"prefer-rest-params": "error",
"prefer-spread": "error",
"prefer-template": "error",
"quote-props": "off",
"quotes": "off",
"radix": "error",
"require-atomic-updates": "warn",
"require-await": "off",
"require-jsdoc": "off",
"require-unicode-regexp": "error",
"rest-spread-spacing": "error",
"semi": "off",
"semi-spacing": "error",
"semi-style": [
"error",
"last"
],
"sort-imports": "off",
"sort-keys": "off",
"sort-vars": "error",
"space-before-blocks": "error",
"space-before-function-paren": "off",
"space-in-parens": [
"error",
"never"
],
"space-infix-ops": "off",
"space-unary-ops": "error",
"spaced-comment": [
"error",
"always"
],
"strict": "error",
"switch-colon-spacing": "error",
"symbol-description": "error",
"template-curly-spacing": [ "off", "never" ],
"template-tag-spacing": "error",
"unicode-bom": [
"error",
"never"
],
"valid-jsdoc": "error",
"vars-on-top": "error",
"wrap-iife": "error",
"wrap-regex": "error",
"yield-star-spacing": "error",
"yoda": [
"error",
"never"
]
}
}

106
.stylelintrc.json

@ -0,0 +1,106 @@
{
"rules": {
"at-rule-empty-line-before": [ "always", { "except": ["blockless-after-same-name-blockless", "first-nested"], "ignore": ["after-comment"]}],
"at-rule-name-case": "lower",
"at-rule-name-space-after": "always-single-line",
"at-rule-no-unknown": true,
"at-rule-semicolon-newline-after": "always",
"block-closing-brace-empty-line-before": "never",
"block-closing-brace-newline-after": "always",
"block-closing-brace-newline-before": "always-multi-line",
"block-closing-brace-space-before": "always-single-line",
"block-no-empty": true,
"block-opening-brace-newline-after": "always-multi-line",
"block-opening-brace-space-after": "always-single-line",
"block-opening-brace-space-before": "always",
"color-hex-case": "lower",
"color-hex-length": "short",
"color-no-invalid-hex": true,
"comment-empty-line-before": [ "always", { "except": ["first-nested"], "ignore": ["stylelint-commands"]}],
"comment-no-empty": true,
"comment-whitespace-inside": "always",
"custom-property-empty-line-before": [ "always", { "except": ["after-custom-property", "first-nested"], "ignore": ["after-comment", "inside-single-line-block"]}],
"declaration-bang-space-after": "never",
"declaration-bang-space-before": "always",
"declaration-block-no-duplicate-custom-properties": true,
"declaration-block-no-duplicate-properties": [ true, { "ignore": ["consecutive-duplicates-with-different-values"]}],
"declaration-block-no-shorthand-property-overrides": true,
"declaration-block-semicolon-newline-after": "always-multi-line",
"declaration-block-semicolon-space-after": "always-single-line",
"declaration-block-semicolon-space-before": "never",
"declaration-block-single-line-max-declarations": 1,
"declaration-block-trailing-semicolon": "always",
"declaration-colon-newline-after": "always-multi-line",
"declaration-colon-space-after": "always-single-line",
"declaration-colon-space-before": "never",
"declaration-empty-line-before": [ "always", { "except": ["after-declaration", "first-nested"], "ignore": ["after-comment", "inside-single-line-block"]}],
"font-family-no-duplicate-names": true,
"font-family-no-missing-generic-family-keyword": true,
"function-calc-no-invalid": true,
"function-calc-no-unspaced-operator": true,
"function-comma-newline-after": "always-multi-line",
"function-comma-space-after": "always-single-line",
"function-comma-space-before": "never",
"function-linear-gradient-no-nonstandard-direction": true,
"function-max-empty-lines": 0,
"function-name-case": "lower",
"function-parentheses-newline-inside": "always-multi-line",
"function-parentheses-space-inside": "never-single-line",
"function-whitespace-after": "always",
"indentation": 2,
"keyframe-declaration-no-important": true,
"length-zero-no-unit": false,
"max-empty-lines": 10,
"media-feature-colon-space-after": "always",
"media-feature-colon-space-before": "never",
"media-feature-name-case": "lower",
"media-feature-name-no-unknown": true,
"media-feature-parentheses-space-inside": "never",
"media-feature-range-operator-space-after": "always",
"media-feature-range-operator-space-before": "always",
"media-query-list-comma-newline-after": "always-multi-line",
"media-query-list-comma-space-after": "always-single-line",
"media-query-list-comma-space-before": "never",
"named-grid-areas-no-invalid": true,
"no-descending-specificity": true,
"no-duplicate-at-import-rules": true,
"no-duplicate-selectors": true,
"no-empty-source": true,
"no-eol-whitespace": true,
"no-extra-semicolons": true,
"no-invalid-double-slash-comments": true,
"no-invalid-position-at-import-rule": true,
"no-irregular-whitespace": true,
"no-missing-end-of-source-newline": true,
"number-leading-zero": "always",
"number-no-trailing-zeros": true,
"property-case": "lower",
"property-no-unknown": true,
"rule-empty-line-before": [ "always-multi-line", { "except": ["first-nested"], "ignore": ["after-comment"]}],
"selector-attribute-brackets-space-inside": "never",
"selector-attribute-operator-space-after": "never",
"selector-attribute-operator-space-before": "never",
"selector-combinator-space-after": "always",
"selector-combinator-space-before": "always",
"selector-descendant-combinator-no-non-space": true,
"selector-list-comma-newline-after": "always",
"selector-list-comma-space-before": "never",
"selector-max-empty-lines": 0,
"selector-pseudo-class-case": "lower",
"selector-pseudo-class-no-unknown": true,
"selector-pseudo-class-parentheses-space-inside": "never",
"selector-pseudo-element-case": "lower",
"selector-pseudo-element-colon-notation": "double",
"selector-pseudo-element-no-unknown": true,
"selector-type-case": "lower",
"selector-type-no-unknown": [ true, { "ignore": ["custom-elements", "default-namespace"]}],
"string-no-newline": true,
"unit-case": "lower",
"unit-no-unknown": true,
"value-keyword-case": "lower",
"value-list-comma-newline-after": "always-multi-line",
"value-list-comma-space-after": "always-single-line",
"value-list-comma-space-before": "never",
"value-list-max-empty-lines": 0
}
}

4
api/admin/schema.js

@ -1,9 +1,9 @@
import { knex } from '../../lib/ormish.js';
import { API } from '../../lib/api.js';
import assert from 'assert';
export const get = async (req, res) => {
const api = new API(req, res);
if(!api.admin_authenticated) {
return api.error(401, "Admin rights required.");
} else {
@ -13,7 +13,7 @@ export const get = async (req, res) => {
table._columns = await knex(table.name).columnInfo();
}
api.reply(200, result);
return api.reply(200, result);
}
}

15
api/admin/table.js

@ -1,21 +1,22 @@
import { knex } from '../../lib/ormish.js';
import { developer_admin, API } from '../../lib/api.js';
import assert from 'assert';
export const get = async (req, res) => {
const api = new API(req, res);
const { name, search, row_id, currentPage } = req.query;
const page = parseInt(currentPage || 0);
const page = parseInt(currentPage || "0", 10);
if(!api.admin_authenticated) {
return api.error(401, "Admin rights required.");
} else if(name && search) {
// get the list of columns to search
let columns = await knex(name).columnInfo();
let query = knex(name);
const col_info = await knex(name).columnInfo();
const query = knex(name);
let columns = Object.getOwnPropertyNames(col_info);
for(let col in columns) {
for(let col of columns) {
// SECURITY: string interpolation but not sure how to do this without it
query.orWhere(col, "like", `%${search}%`);
}
@ -69,7 +70,7 @@ export const post = async (req, res) => {
let res = await knex(name).where({id: row_id}).update(data);
api.reply(200, {message: "OK", result: res});
return api.reply(200, {message: "OK", result: res});
} else {
return api.error(500, {message: "name and row_id required for delete"});
}
@ -88,7 +89,7 @@ export const put = async (req, res) => {
let res = await knex(name).insert(req.body);
api.reply(200, {message: "OK", id: res[0]});
return api.reply(200, {message: "OK", id: res[0]});
} else {
return api.error(500, {message: "name required for delete"});
}

11
api/devtools/djenterator.js

@ -1,21 +1,20 @@
import logging from '../../lib/logging.js';
import { API } from '../../lib/api.js';
import glob from "glob";
import assert from 'assert';
import path from "path";
const log = logging.create(import.meta.url);
const log = logging.create("/api/devtools/djenterator.js");
export const get = async (req, res) => {
const api = new API(req, res);
glob.glob("./static/djenterator/**/!(*.vars)", (err, files) => {
if(err) {
glob.glob("./static/djenterator/**/!(*.vars)", (error, files) => {
if(error) {
log.error(error);
api.error(500, error);
return api.error(500, error);
} else {
api.reply(200, files.map(f => path.basename(f)));
return api.reply(200, files.map(f => path.basename(f)));
}
});
}

9
api/devtools/info.js

@ -2,7 +2,6 @@ import devtools from '../../lib/devtools.js';
import fs from 'fs';
export const get = async (req, res) => {
// the devtools module contains all of the errors from the service/api.js for api and sockets
// but to get at the svelte errors we have to read debug/errors/svelte.json
@ -16,8 +15,8 @@ export const get = async (req, res) => {
console.error(error);
}
res.status(200).json({
api: devtools.api,
sockets: devtools.sockets,
errors: devtools.errors.concat(svelte_errors)});
return res.status(200).json({
api: devtools.api,
sockets: devtools.sockets,
errors: devtools.errors.concat(svelte_errors)});
}

4
api/login.js

@ -1,9 +1,5 @@
import { developer_admin } from "../lib/api.js";
/**
* Used only the DevTools to see if you're running as a developer
* and need the tools even if not logged in.
*/
export const options = (req, res) => {
res.status(200).json({developer: developer_admin, admin: req.user && req.user.admin});
}

11
api/password_reset.js

@ -5,7 +5,7 @@ import { User } from "../lib/models.js";
import { v4 as uuidv4} from "uuid";
import { send_reset, send_reset_finished } from '../lib/queues.js';
const log = logging.create(import.meta.url);
const log = logging.create("/api/password_reset.js");
export const post = async (req, res) => {
const api = new API(req, res);
@ -23,14 +23,14 @@ export const post = async (req, res) => {
// they have a code submitted, so check it
if(user.reset_code !== code) {
return res.status(400).json({message: "Reset code doesn't match."});
return api.error(400, {message: "Reset code doesn't match."});
} else {
user.password = User.encrypt_password(password);
await User.update({email}, {password: user.password, reset_code: null, reset_sent_at: null});
send_reset_finished(user);
return res.status(200).json({message: "OK"});
return api.reply(200, {message: "OK"});
}
} else {
// new request, send out a code
@ -39,9 +39,10 @@ export const post = async (req, res) => {
send_reset(user);
return res.status(200).json({message: "OK"});
return res.reply(200, {message: "OK"});
}
} catch(error) {
return res.status(400).json({message: error.message});
log.error(error);
return api.error(400, {message: error.message});
}
}

28
api/payments/btcpay.js

@ -1,22 +1,22 @@
import { Payment } from '../../lib/models.js';
import assert from 'assert';
import { Payment, UserPayment } from '../../lib/models.js';
import { log } from '../../lib/logging.js';
import { API } from "../../lib/api.js";
import fetch from 'node-fetch';
import { btcpay_private } from '../../secrets/payments.js';
import { product } from '../../client/config.js';
import { fake_payments, product } from '../../client/config.js';
import assert from "assert";
export const get = async (req, res) => {
const api = new API(req, res);
console.log("USER ID", api.user);
try {
const amount = parseInt(req.query.amount || product.price);
assert(!fake_payments, "You have fake_payments set so use /api/user/payment APIs.");
const amount = parseInt(req.query.amount || product.price, 10);
assert(amount >= 0 && amount <= 100, `invalid amount ${amount}`);
let internal_id = Payment.gen_internal_id();
const options = {
method: 'POST',
headers: {
@ -31,7 +31,7 @@ export const get = async (req, res) => {
}),
}
const response = await fetch(btcpay_private.url + '/invoices', options);
const response = await fetch(`${btcpay_private.url}/invoices`, options);
const text = await response.text();
if(response.status != 200) {
@ -42,7 +42,6 @@ export const get = async (req, res) => {
const sys_primary_id = data.data.id;
let payment = await Payment.insert({
user_id: api.user.id,
system: 'btcpay',
status: 'pending',
internal_id,
@ -53,6 +52,13 @@ export const get = async (req, res) => {
assert(payment, "Failed to store payment in the database. Email help@learnjsthehardway.com.");
const user_payment = await UserPayment.insert({
user_id: api.user.id,
payment_id: payment.id
});
assert(user_payment.user_id == api.user.id, "Wrong id for user_payment");
api.reply(200, {sys_primary_id, internal_id});
}
} catch(error) {
@ -78,7 +84,7 @@ export const post = async (req, res) => {
},
}
const response = await fetch(btcpay_private.url + `/invoices/${msg.sys_primary_id}`, options);
const response = await fetch(`${btcpay_private.url}/invoices/${msg.sys_primary_id}`, options);
const data = await response.json();
// TODO: what are the error conditions on this response?
@ -88,12 +94,12 @@ export const post = async (req, res) => {
if(status == 'paid') {
let count = await Payment.update({
sys_primary_id,
sys_primary_id,
internal_id: msg.internal_id
}, {status: 'complete'});
assert(count === 1, "Failed to update payment database.");
}
}
api.reply(200, {sys_primary_id, status, btc_due});
} catch(error) {

18
api/payments/paypal.js

@ -1,18 +1,21 @@
import { Payment, User } from '../../lib/models.js';
import assert from 'assert';
import { Payment, UserPayment } from '../../lib/models.js';
import { API } from "../../lib/api.js";
import { log } from '../../lib/logging.js';
import dayjs from 'dayjs';
import { fake_payments } from "../../client/config.js";
import assert from 'assert';
export const post = async (req, res) => {
const api = new API(req, res);
try{
assert(!fake_payments, "You have fake_payments set so use /api/user/payment APIs.");
const msg = req.body;
log.debug("Paypal recieved message", msg);
let internal_id = Payment.gen_internal_id();
// TODO: see if we can use date-fns instead of dayjs here
let payment = await Payment.insert({
system: 'paypal',
status: 'complete',
@ -24,16 +27,23 @@ export const post = async (req, res) => {
assert(payment && payment.internal_id, "Failed to save payment.");
const user_payment = await UserPayment.insert({
user_id: api.user.id,
payment_id: payment.id
});
assert(user_payment.user_id == api.user.id, "Wrong id for user_payment");
let result = {
sys_primary_id: payment.sys_primary_id,
status: payment.status,
internal_id: payment.internal_id
internal_id: payment.internal_id
}
log.debug(result);
api.reply(200, result);
} catch(error) {
log.error(error);
app.error(403, 'PayPal configuration failed');
api.error(403, 'PayPal configuration failed');
}
}

4
api/register.js

@ -19,9 +19,9 @@ export const post = async (req, res) => {
if(good) {
delete good.password;
queues.send_welcome(good);
res.status(200).json({message: "OK"});
return res.status(200).json({message: "OK"});
} else {
res.status(403).json({message: "Failed to regiser."});
return res.status(403).json({message: "Failed to regiser."});
}
}
}

33
api/user/payments.js

@ -1,12 +1,8 @@
import * as models from "../../lib/models.js";
import * as queues from "../../lib/queues.js";
import logging from '../../lib/logging.js';
import assert from 'assert';
import { API } from '../../lib/api.js';
import { Payment } from '../../lib/models.js';
const log = logging.create(import.meta.url);
import { Payment, UserPayment } from '../../lib/models.js';
import assert from "assert";
const log = logging.create("/api/user/payments.js");
export const get = async (req, res) => {
const api = new API(req, res);
@ -25,15 +21,14 @@ export const get = async (req, res) => {
get.authenticated = true;
export const post = async (req, res) => {
const api = new API(req, res);
const reply_data = {}
let reply_data = {}
try {
// BUG: this seriously needs some TRANSACTIONS ZED!
const payment = await Payment.insert({
user_id: api.user.id,
system: "free",
status: "complete",
internal_id: "",
@ -41,11 +36,23 @@ export const post = async (req, res) => {
sys_created_on: "",
});
assert(payment.id, "Payment returned didn't have id.");
const user_payment = await UserPayment.insert({
user_id: api.user.id,
payment_id: payment.id
});
assert(user_payment.user_id == api.user.id, "Wrong id for user_payment");
// TODO: this is weird, can I just use my clean method?
reply_data = Object.fromEntries(Object.entries(payment));
log.debug(reply_data);
api.reply(200, reply_data);
return api.reply(200, reply_data);
} catch (error) {
log.error(error);
api.error(500, error);
return api.error(500, error);
}
}

11
api/user/profile.js

@ -1,18 +1,15 @@
import { User } from "../../lib/models.js";
import * as queues from "../../lib/queues.js";
import logging from '../../lib/logging.js';
import assert from 'assert';
import { API } from '../../lib/api.js';
const log = logging.create(import.meta.url);
const log = logging.create("/api/user/profile.js");
export const post = async (req, res) => {
const api = new API(req, res);
let user = req.body;
console.log("USER COMING IN IS", user);
try {
// need to process the password, which is annoying
if(user.password && user.password === user.password_repeat) {
@ -30,15 +27,17 @@ export const post = async (req, res) => {
if(valid) {
// TODO: need a way to share the validation
let result = await User.update({id: req.user.id}, user);
// TODO: Improve this check
assert(result !== undefined, "Severe database error.");
api.reply(200, { message: "OK" });
return api.reply(200, { message: "OK" });
} else {
log.debug(errors);
return api.error(500, `Invalid User fields submitted. ${JSON.stringify(errors)}`);
}
} catch (error) {
log.error(error);
api.error(500, error);
return api.error(500, error);
}
}

35
client/App.svelte

@ -1,46 +1,15 @@
<style>
</style>
<script>
import { logout_user } from './api';
import { user } from './stores';
import { user } from '$/client/stores';
import Router from 'svelte-spa-router';
import routes from './routes.js';
import {push, link} from 'svelte-spa-router';
import Darkmode from './components/Darkmode.svelte';
import Icon from './components/Icon.svelte';
import LoggedIn from './components/LoggedIn.svelte';
import { onMount } from 'svelte';
import Footer from './components/Footer.svelte';
import routes from '$/client/routes.js';
/* #if process.env.DANGER_ADMIN
import Bandolier from './bando/Bandolier.svelte';
// #endif */
</script>
<header>
<nav>
<a href="/" use:link><Icon name="pen-tool" size="48" /></a>
<LoggedIn>
<ul slot="yes">
<li><a href="#" on:click|preventDefault={ logout_user } data-testid="logout-link">Logout</a></li>
<li><a href="/profile/" use:link><Icon name="settings" /></a></li>
<li><Darkmode /></li>
</ul>
<ul slot="no">
<li><a href="/register/" use:link>Register</a></li>
<li><a href="/login/" use:link>Login</a></li>
<li><Darkmode /></li>
</ul>
</LoggedIn>
</nav>
</header>
<Router {routes}/>
<Footer />
<!-- #if process.env.DANGER_ADMIN
{#if $user.show_devtools}
<Bandolier shown={ false }/>

0
client/components/Footer.svelte → client/Footer.svelte

26
client/Header.svelte

@ -0,0 +1,26 @@
<script>
import { logout_user } from '$/client/api';
import LoggedIn from '$/client/components/LoggedIn.svelte';
import Icon from '$/client/components/Icon.svelte';
import Darkmode from '$/client/components/Darkmode.svelte';
import { onMount } from 'svelte';
import {push, link} from 'svelte-spa-router';
</script>
<header>
<nav>
<a href="/" use:link><Icon name="home" size="48" /></a>
<LoggedIn>
<ul slot="yes">
<li><a on:click|preventDefault={ logout_user } data-testid="logout-link">Logout</a></li>
<li><a href="/profile/" use:link><Icon name="settings" /></a></li>
<li><Darkmode /></li>
</ul>
<ul slot="no">
<li><a href="/register/" use:link>Register</a></li>
<li><a href="/login/" use:link>Login</a></li>
<li><Darkmode /></li>
</ul>
</LoggedIn>
</nav>
</header>

66
client/Layout.svelte

@ -0,0 +1,66 @@
<script>
import LoggedIn from './components/LoggedIn.svelte';
import Footer from './Footer.svelte';
import Header from './Header.svelte';
export let footer = true;
export let header = true;
export let authenticated = false;
export let testid = "page";
export let centered = false;
export let fullscreen = false;
</script>
<style>
main {
display: flex;
flex-direction: column;
padding-top: 1rem;
}
main.centered {
display: flex;
flex-direction: column;
align-items: center;
padding-top: 3rem;
}
main.fullscreen {
display: flex;
flex-direction: row !important;
padding: 0px !important;
margin: 0px !important;
height: 100vh !important;
max-height: 100vh !important;
min-height: 100vh !important;
}
</style>
{#if authenticated}
{#if header}
<Header />
{/if}
<LoggedIn redirect="/login" show_required_page={ true }>
<main class:fullscreen={fullscreen} class:centered={ centered } slot="yes" data-testid={ testid }>
<slot></slot>
</main>
</LoggedIn>
{#if footer}
<Footer />
{/if}
{:else}
{#if header}
<Header />
{/if}
<main class:fullscreen={fullscreen} class:centered={ centered } data-testid={ testid }>
<slot></slot>
</main>
{#if footer}
<Footer />
{/if}
{/if}

26
client/api.js

@ -2,21 +2,15 @@ const MOCK_ROUTES = {};
import { user } from "./stores.js";
export const logout_user = async () => {
user.update(u => ({authenticated: false}));
let [status, data] = await get('/api/logout');
window.location.replace("/client/#/login");
}
export const mock = (config) => {
for(let route in config) {
MOCK_ROUTES[route] = config[route];
for(let [route, value] of Object.entries(config)) {
MOCK_ROUTES[route] = value;
}
}
export const raw_mock = (url, method, body, unauthed_action) => {
export const raw_mock = (url, raw_method, body, unauthed_action) => {
let error;
method = method.toLowerCase();
const method = raw_method.toLowerCase();
const config = MOCK_ROUTES[url];
const data = config[method];
@ -77,6 +71,7 @@ export const raw = async (url, method, body, unauthed_action) => {
return [res.status, JSON.parse(text)];
} catch(error) {
console.error(error, "Failed to parse reply body as JSON. Text is:", text, "error", error, "URL", url);
return [500, {"message": "Exception processing request. See console log."}];
}
}
}
@ -91,6 +86,17 @@ export const del = async (url) => await raw(url, "DELETE");
export const options = async (url) => await raw(url, "OPTIONS");
export const logout_user = async () => {
user.update(() => {
return {authenticated: false}
});
let [status, data] = await get('/api/logout');
if(status !== 200) console.error("Invalid status from logout", status, data);
window.location.replace("/client/#/login");
}
export default {
post, get, put, del, mock, options
}

1
client/assert.js

@ -4,6 +4,7 @@ class AssertionError extends Error {
/* This is from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
* which claims you have to do this weird stuff to make your error actually work.
*/
constructor(foo = 'bar', ...params) {
// Pass remaining arguments (including vendor specific ones) to parent constructor
super(...params);

28
client/bando/Bandolier.svelte

@ -1,11 +1,11 @@
<script>
import { push, link } from 'svelte-spa-router';
import Icon from '../components/Icon.svelte';
import { link } from 'svelte-spa-router';
import Icon from '$/client/components/Icon.svelte';
import IconFinder from "./IconFinder.svelte";
import Djenterator from './Djenterator.svelte';
import { onMount } from 'svelte';
import { fade } from "svelte/transition";
import api from '../api.js';
import api from '$/client/api.js';
// this is a unique string that you can grep for to make sure bando isn't in your build
const canary = '3a025dba-1a62-4169-ae9f-f7cd4104c4f4';
@ -35,7 +35,6 @@
let notice = "";
$: has_errors = errors.length > 0;
$: console.log("ERROR", error_selected);
const rephresh = async () => {
let [status, data] = await api.get('/api/admin/schema');
@ -83,7 +82,6 @@
}
const search_icons = () => {
console.log("ICONS!");
icon_finder = true;
reset_view(true, false, false, false, false, false);
}
@ -113,12 +111,10 @@
const copy_schema = () => {
const text = JSON.stringify(table_selected, null, 4);
console.log("Copying to clip", text);
navigator.clipboard.writeText(text).then(() => {
notice = "Schema Copied";
}, () => {
console.log("Failed to copy.");
notice = "Failed copying to clipboard.";
});
}
@ -150,26 +146,26 @@
}
panel listing {
padding-top: 0px;
padding-top: 0;
background-color: var(--color-bg-secondary);
display: flex;
flex-grow: 1;
flex-direction: column;
flex-basis: 300px;
margin: 0rem;
margin: 0;
border-right: 1px solid var(--color-border);
}
panel listing h1 {
padding-left: 0.5rem;
margin: 0px;
margin: 0;
font-size: 1.4em;
}
panel listing ul {
list-style-type: none;
line-height: 1.2;
margin-top: 0px;
margin-top: 0;
padding-inline-start: 10px;
}
@ -213,7 +209,7 @@
background-color: hsl(0, 20%, 30%);
}
buttons #listing-open {
buttons#listing-open {
position: absolute;
top: 0;
left: 0;
@ -322,7 +318,7 @@
<pre>
<code>
{#each show_code.code.split('\n') as line, i}
<span>{ line + "\n" }</span>
<span>{ `${line}\n` }</span>
{/each}
</code>
</pre>
@ -349,7 +345,7 @@
<pre>
<code>
{#each socket_selected.code.split('\n') as line, i}
<span>{ line + "\n" }</span>
<span>{ `${line}\n` }</span>
{/each}
</code>
</pre>
@ -397,11 +393,11 @@
<code>
{#if error_selected.plugin}
{#each error_selected.frame.split("\n") as line, i}
{ line += "\n" }
{ line }
{/each}
{:else if error_selected.code}
{#each error_selected.code.split('\n') as line, i}
<span class:error={ error_selected.line === i + 1}>{ line + "\n" }</span>
<span class:error={ error_selected.line === i + 1}>{ `${line}\n` }</span>
{/each}
{:else}
<span>No code for error.</span>

114
client/bando/Components.svelte

@ -1,18 +1,26 @@
<script>
import Accordion from "./demos/Accordion.svelte";
import Badge from "./demos/Badge.svelte";
import CodeDemo from "./demos/Code.svelte";
import ButtonGroup from "./demos/ButtonGroup.svelte";
import Calendar from "./demos/Calendar.svelte";
import Callout from "./demos/Callout.svelte";
import Cards from "./demos/Cards.svelte";
import Carousel from "./demos/Carousel.svelte";
import Chat from "./demos/Chat.svelte";
import Countdown from "./demos/Countdown.svelte";
import Darkmode from "./demos/Darkmode.svelte";
import DataTable from "./demos/DataTable.svelte";
import FairPay from "./demos/FairPay.svelte";
import Flipper from "./demos/Flipper.svelte";
import Form from "./demos/Form.svelte";
import Icon from "../components/Icon.svelte";
import IconFinder from "./IconFinder.svelte";
import IconDemo from "./demos/Icon.svelte";
import IconImage from "./demos/IconImage.svelte";
import LiveStream from "./demos/LiveStream.svelte";
import LoggedIn from "./demos/LoggedIn.svelte";
import Login from "./demos/Login.svelte";
import Markdown from "./demos/Markdown.svelte";
import Modal from "./demos/Modal.svelte";
import Pagination from "./demos/Pagination.svelte";
import PlaceHolder from "./demos/PlaceHolder.svelte";
@ -27,12 +35,16 @@
import Toast from "./demos/Toast.svelte";
import Tooltip from "./demos/Tooltip.svelte";
import Video from "./demos/Video.svelte";
import DataTable from "./demos/DataTable.svelte";
import Flipper from "./demos/Flipper.svelte";
import WTVideo from "./demos/WTVideo.svelte";
import Code from "../components/Code.svelte";
import {onMount} from "svelte";
import Layout from "../Layout.svelte";
import { link } from "svelte-spa-router";
export let panels = [
/* WARNING: If you put any component that uses /api/login into the first slot
* or set active: true on them it will require a login.
*/
let panels = [
{title: "Accordion", active: true, icon: "align-justify", component: Accordion},
{title: "Badge", active: false, icon: "award", component: Badge},
{title: "ButtonGroup", active: false, icon: "server", component: ButtonGroup},
@ -40,14 +52,20 @@
{title: "Callout", active: false, icon: "file-plus", component: Callout},
{title: "Cards", active: false, icon: "credit-card", component: Cards},
{title: "Carousel", active: false, icon: "repeat", component: Carousel},
{title: "Chat", active: false, icon: "message-circle", component: Chat},
{title: "Code", active: false, icon: "code", component: CodeDemo},
{title: "Countdown", active: false, icon: "clock", component: Countdown},
{title: "Darkmode", active: false, icon: "sunrise", component: Darkmode},
{title: "Data Table", active: false, icon: "grid", component: DataTable},
{title: "DataTable", active: false, icon: "grid", component: DataTable},
{title: "FairPay", active: false, icon: "dollar-sign", component: FairPay},
{title: "Flipper", active: false, icon: "layers", component: Flipper},
{title: "Form", active: false, icon: "database", component: Form},
{title: "Icon", active: false, icon: "feather", component: IconFinder},
{title: "Icon", active: false, icon: "feather", component: IconDemo},
{title: "IconImage", active: false, icon: "image", component: IconImage},
{title: "LiveStream", active: false, icon: "cast", component: LiveStream},
{title: "LoggedIn", active: false, icon: "log-out", component: LoggedIn},
{title: "Login", active: false, icon: "log-in", component: Login},
{title: "Markdown", active: false, icon: "file", component: Markdown},
{title: "Modal", active: false, icon: "maximize", component: Modal},
{title: "Pagination", active: false, icon: "skip-forward", component: Pagination},
{title: "PlaceHolder", active: false, icon: "image", component: PlaceHolder},
@ -60,17 +78,22 @@
{title: "Tiles", active: false, icon: "camera", component: Tiles},
{title: "Toast", active: false, icon: "message-square", component: Toast},
{title: "Tooltip", active: false, icon: "help-circle", component: Tooltip},
{title: "WTVideo", active: false, icon: "video", component: WTVideo},
{title: "Video", active: false, icon: "video", component: Video},
];
panels.forEach(p => (p.code = p.code || `/bando/demos/${p.title}.svelte`));
let selected = panels[0];
let show_code = false;
const sidebar_select = (event) => {
const {index, item} = event.detail;
show_code = false;
selected = item;
panels = panels.map((x,i) => {
panels = panels.map((x, i) => {
x.active = i == index;
return x;
});
@ -78,28 +101,23 @@
</script>
<style>
main {
display: flex;
flex-direction: row;
margin: 0px;
padding: 0px;
}
div[slot="top"] span {
display: none;
}
div[slot="bottom"] span {
display: none;
}
contents {
padding: 0.5rem;
width: 100%;
max-height: 100vh;
overflow-y: auto;
}
tabs {
margin-bottom: 1rem;
}
@media only screen and (max-width: 900px) {
div[slot="top"] h1 {
div[slot="top"] h3 {
display: none;
}
@ -111,29 +129,49 @@
div[slot="bottom"] {
display: none;
}
}
div[slot="bottom"] span {
display: inline-block;
padding-top: 0.3rem;
}
left {
max-height: 100vh;
overflow-y: auto;
overflow-x: hidden;
max-width: min-content;
width: min-content;
min-width: min-content;
}
</style>
<main>
<Sidebar on:select={ sidebar_select } menu={ panels }>
<div slot="top">
<h1>Components</h1>
<span><Icon name="home" size="36" /></span>
</div>
<Layout fullscreen={ true } header={false} footer={ false }>
<left>
<Sidebar on:select={ sidebar_select } menu={ panels }>
<div slot="top">
<h3><a href="/" use:link><Icon name="home" /> Components</h3>
<span><Icon name="home" size="36" /></span>
</div>
<div slot="bottom">
<p>Code is in <b>client/bando/demos</b></p>
</div>
</Sidebar>
<div slot="bottom">
<p>Code is in <b>client/bando/demos</b></p>
</div>
</Sidebar>
</left>
<contents>
<h1>{ selected.title }</h1>
<svelte:component this={selected.component} />
<tabs>
<a class:active={ !show_code} on:click={ () => show_code = false }>
<Icon name="eye" size="36px"/> Live Demo
</a>
<a class:active={ show_code } on:click={ () => show_code = true }>
<Icon name="code" size="36px" /> Code
</a>
</tabs>
<component>
{#if show_code}
<Code src={ selected.code } />
{:else}
<svelte:component this={selected.component} />
{/if}
</component>
</contents>
</main>
</Layout>

65
client/bando/Djenterator.svelte

@ -1,15 +1,11 @@
<script>
import { push, link } from 'svelte-spa-router';
import { onMount } from 'svelte';
import template from "lodash/template";
import { fade } from "svelte/transition";
import Icon from "../components/Icon.svelte";
import api from "../api.js";
import Icon from "$/client/components/Icon.svelte";
export let template_file = "";
let showing_rendered = false;
let user_info = {};
let results = "";
let source = "";
let variable_json = "{ }";
@ -19,16 +15,6 @@
let notice = "";
let last_good = "";
$: if(variable_json) render_template();
// this reload the templates when you click on a new one
$: if(template_file) re_render(template_file);
const re_render = async (what) => {
await load_variables(what);
await load_template(what);
}
const render_template = () => {
try {
// avoid rendering when the current template doesn't match the renderer
@ -37,7 +23,7 @@
results = renderer(variables);
notice = "";
last_good = results;
}
}
return true;
} catch(err) {
@ -52,7 +38,7 @@
if(res.status == 200) {
variable_json = await res.text();
} else {
} else {
variable_json = '{}';
notice = `No ${template_name}.vars file found. ${res.status}`;
}
@ -74,7 +60,7 @@
notice = `${error.message}`;
results = source;
}
} else {
} else {
notice = `Error loading ${template_name}: ${res.status}`;
}
}
@ -99,6 +85,17 @@
const canary = '3a025dba-1a62-4169-ae9f-f7cd4104c4f4';
console.log("Canary is compiled into build as", canary);
$: if(variable_json) render_template();
// this reload the templates when you click on a new one
const re_render = async (what) => {
await load_variables(what);
await load_template(what);