initial commit, have most of the categories crud methods
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
@@ -0,0 +1,5 @@
|
||||
[*.{js,jsx,ts,tsx,vue}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
@@ -0,0 +1,34 @@
|
||||
const ALWAYS = 'always'
|
||||
const NEVER = 'never'
|
||||
const ERROR = 2
|
||||
const WARN = 1
|
||||
const OFF = 0
|
||||
const PROD = process.env.NODE_ENV === 'production'
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true
|
||||
},
|
||||
extends: [
|
||||
'plugin:vue/vue3-strongly-recommended',
|
||||
'@vue/standard'
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020
|
||||
},
|
||||
rules: {
|
||||
'no-unused-vars': PROD ? ERROR : WARN,
|
||||
'no-console': PROD ? ERROR : WARN,
|
||||
'no-debugger': PROD ? ERROR : WARN,
|
||||
'space-before-function-paren': [WARN, NEVER],
|
||||
'vue/max-attributes-per-line': [ERROR, {
|
||||
singleline: 5,
|
||||
multiline: {
|
||||
max: 1,
|
||||
allowFirstLine: true
|
||||
}
|
||||
}],
|
||||
'vue/html-self-closing': OFF
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "npm run serve",
|
||||
"name": "Start Client",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
declare module "*.vue" {
|
||||
import Vue from 'vue';
|
||||
export default Vue.defineComponent();
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
CodeWorks Vue Starter
|
||||
=====================
|
||||
This template is designed to help get students started building vue applications
|
||||
|
||||
## Legal Overview
|
||||
|
||||
The content under the CodeWorks®, LLC Organization and all of the individual repos are solely intended for use by CodeWorks Instruction to deliver Educational content to CodeWorks Students.
|
||||
|
||||
---
|
||||
|
||||
## Copyright
|
||||
|
||||
© CodeWorks® LLC, 2021. Unauthorized use and/or duplication of this material without express and written permission from CodeWorks, LLC is strictly prohibited.
|
||||
|
||||
|
||||
<img src="https://bcw.blob.core.windows.net/public/img/7815839041305055" width="125">
|
||||
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<!-- <link rel="icon" href="https://bcw.blob.core.windows.net/public/img/9977764104160066" /> -->
|
||||
<title>
|
||||
budgit.client
|
||||
</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but budgit.client doesn't work properly without JavaScript
|
||||
enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
<script type="module" src="./src/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"include": [
|
||||
"./src/**/*"
|
||||
]
|
||||
}
|
||||
Generated
+6269
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "cw-vue-template",
|
||||
"version": "0.1.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vite",
|
||||
"build": "vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bcwdev/auth0provider-client": "^0.0.12",
|
||||
"@mdi/font": "^6.3.95",
|
||||
"@popperjs/core": "^2.10.2",
|
||||
"axios": "^0.23.0",
|
||||
"bootstrap": "^5.1.3",
|
||||
"socket.io-client": "^4.1.2",
|
||||
"sweetalert2": "^11.1.9",
|
||||
"vue": "^3.2.11",
|
||||
"vue-router": "^4.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "1.9.3",
|
||||
"@vue/compiler-sfc": "3.2.20",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"eslint": "8.0.1",
|
||||
"eslint-plugin-import": "2.25.2",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"eslint-plugin-vue": "7.19.1",
|
||||
"postcss": "^8.4.14",
|
||||
"sass": "1.32.4",
|
||||
"tailwindcss": "^3.1.4",
|
||||
"vite": "2.6.10"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<header>
|
||||
<Navbar />
|
||||
</header>
|
||||
<main>
|
||||
<router-view />
|
||||
</main>
|
||||
<footer>
|
||||
<div>
|
||||
Made with 💖 by CodeWorks
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from 'vue'
|
||||
import { AppState } from './AppState'
|
||||
export default {
|
||||
name: 'App',
|
||||
setup() {
|
||||
return {
|
||||
appState: computed(() => AppState)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="postcss">
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,7 @@
|
||||
import { reactive } from 'vue'
|
||||
|
||||
// NOTE AppState is a reactive object to contain app level data
|
||||
export const AppState = reactive({
|
||||
user: {},
|
||||
account: {}
|
||||
})
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 8.8 KiB |
@@ -0,0 +1,79 @@
|
||||
.skeleton-loader {
|
||||
box-shadow: 0 4px 10px 0 rgba(33, 33, 33, 0.15);
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background-color: #dcdcdc;
|
||||
pointer-events: none;
|
||||
&.dark{
|
||||
background-color: #9d9c9c;
|
||||
}
|
||||
|
||||
&.card{
|
||||
background-color: #fff;
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
background: linear-gradient(to right, transparent 0%, #f6f6f6 50%, transparent 100%);
|
||||
animation: load 1.5s cubic-bezier(0.4, 0.0, 0.2, 1) infinite;
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
&.avatar{
|
||||
border-radius: 50%;
|
||||
height: 45px;
|
||||
width: 45px;
|
||||
&.tile{
|
||||
border-radius: 5%;
|
||||
}
|
||||
&.md{
|
||||
height: 65px;
|
||||
width: 65px;
|
||||
}
|
||||
&.lg{
|
||||
height: 85px;
|
||||
width: 85px;
|
||||
}
|
||||
}
|
||||
&.text{
|
||||
width: clamp(50%, 160px, 100%);
|
||||
height: 1rem;
|
||||
line-height: 1.5rem;
|
||||
margin: .5rem 0;
|
||||
&.sm{
|
||||
width: clamp(25%, 80px, 100%);
|
||||
}
|
||||
&.lg{
|
||||
width: clamp(75%, 250px, 100%);
|
||||
}
|
||||
&.block{
|
||||
width: clamp(100%, 500px, 100%);
|
||||
}
|
||||
}
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 50px;
|
||||
background: linear-gradient(to right, transparent 0%, #E8E8E8 50%, transparent 100%);
|
||||
animation: load 1s cubic-bezier(0.4, 0.0, 0.2, 1) infinite;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@keyframes load {
|
||||
from {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
.form-control {
|
||||
background-color: #f7f5f5;
|
||||
&:focus {
|
||||
background-color: #fbf9f9;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Transitions
|
||||
|
||||
// [+] ROUTER TRANSITION
|
||||
// intentially empty by default
|
||||
// if you want to enable router
|
||||
// transtions change the settings below
|
||||
.route-enter-active,
|
||||
.route-leave-active {
|
||||
transition: all 0s linear; // try changing 0s to .25s
|
||||
}
|
||||
.route-enter-from,
|
||||
.route-leave-to {
|
||||
opacity: 1; // try setting to 0
|
||||
}
|
||||
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
.slideY-enter-active,
|
||||
.slideY-leave-active {
|
||||
transform: translateY(0);
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
.slideY-enter-from,
|
||||
.slideY-leave-to {
|
||||
transform: translateY(100vh);
|
||||
}
|
||||
.slideX-enter-active,
|
||||
.slideX-leave-active {
|
||||
transform: translateX(0);
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
.slideX-enter-from,
|
||||
.slideX-leave-to {
|
||||
transform: translateX(100vw);
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
@function color-yiq($background) {
|
||||
$r: red($background);
|
||||
$g: green($background);
|
||||
$b: blue($background);
|
||||
|
||||
$yiq: (($r * 299) + ($g * 587) + ($b * 114))/1000;
|
||||
|
||||
@if ($yiq >= 128) {
|
||||
@return #111;
|
||||
} @else {
|
||||
@return #fff;
|
||||
}
|
||||
}
|
||||
|
||||
@function fade($background, $alpha) {
|
||||
$r: red($background);
|
||||
$g: green($background);
|
||||
$b: blue($background);
|
||||
@return rgba($r, $g, $b, $alpha);
|
||||
}
|
||||
|
||||
@each $color, $value in $colors {
|
||||
.bg-#{$color} {
|
||||
color: color-yiq($value);
|
||||
background-color: $value !important;
|
||||
}
|
||||
.text-#{$color} {
|
||||
color: $value !important;
|
||||
}
|
||||
$num: 5;
|
||||
@while $num < 100 {
|
||||
$darker: darken($value, $num);
|
||||
$lighter: lighten($value, $num);
|
||||
.bg-#{$color} {
|
||||
&.lighten-#{$num} {
|
||||
color: color-yiq($lighter);
|
||||
background-color: $lighter !important;
|
||||
}
|
||||
&.darken-#{$num} {
|
||||
color: color-yiq($darker);
|
||||
background-color: $darker !important;
|
||||
}
|
||||
&.fade-#{$num} {
|
||||
background-color: fade($value, $num / 100) !important;
|
||||
}
|
||||
&.fade-lighten-#{$num} {
|
||||
background-color: fade($lighter, $num / 100) !important;
|
||||
}
|
||||
&.fade-darken-#{$num} {
|
||||
background-color: fade($darker, $num / 100) !important;
|
||||
}
|
||||
}
|
||||
.text-#{$color}.darken-#{$num} {
|
||||
color: $darker !important;
|
||||
}
|
||||
|
||||
.text-#{$color}.lighten-#{$num} {
|
||||
color: $lighter !important;
|
||||
}
|
||||
$num: $num + 5;
|
||||
}
|
||||
}
|
||||
|
||||
.clip-text {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
.action {
|
||||
cursor: pointer;
|
||||
}
|
||||
.no-select {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.scrollable-x {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.scrollable-y {
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
// opacity: 0.65;
|
||||
&::after {
|
||||
cursor: not-allowed;
|
||||
pointer-events: all;
|
||||
position: absolute;
|
||||
content: "";
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0.25rem;
|
||||
text-shadow: 1px 1px 0 #555, -1px -1px 0 #555, 1px -1px 0 #555, -1px 1px 0 #555, 1px 1px 0 #555;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: rgba(85, 85, 85, 0.37);
|
||||
&.text-disabled {
|
||||
content: "DISABLED";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.on-hover {
|
||||
transition: all 0.2s linear;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
*:hover > .on-hover {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.square {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
.square-left {
|
||||
border-top-left-radius: 0 !important;
|
||||
border-bottom-left-radius: 0 !important;
|
||||
}
|
||||
.square-right {
|
||||
border-top-right-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
}
|
||||
.square-top {
|
||||
border-top-right-radius: 0 !important;
|
||||
border-top-left-radius: 0 !important;
|
||||
}
|
||||
.square-bottom {
|
||||
border-bottom-right-radius: 0 !important;
|
||||
border-bottom-left-radius: 0 !important;
|
||||
}
|
||||
.square-bottom-left {
|
||||
border-bottom-left-radius: 0 !important;
|
||||
}
|
||||
.square-bottom-right {
|
||||
border-bottom-right-radius: 0 !important;
|
||||
}
|
||||
.square-top-left {
|
||||
border-top-left-radius: 0 !important;
|
||||
}
|
||||
.square-top-right {
|
||||
border-top-right-radius: 0 !important;
|
||||
}
|
||||
|
||||
.selectable {
|
||||
position: relative;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
&:after {
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
&:after {
|
||||
border-radius: inherit;
|
||||
bottom: 0;
|
||||
color: inherit;
|
||||
content: "";
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
transition: opacity 0.2s cubic-bezier(0.4, 0, 0.6, 1);
|
||||
background-color: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
.elevation-1 {
|
||||
box-shadow: 0 2px 1px -1px rgba(0, 0, 0, 0.2), 0 1px 1px 0 rgba(0, 0, 0, 0.14), 0 1px 3px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.elevation-2 {
|
||||
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.elevation-3 {
|
||||
box-shadow: 0 3px 3px -2px rgba(0, 0, 0, 0.2), 0 3px 4px 0 rgba(0, 0, 0, 0.14), 0 1px 8px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.elevation-4 {
|
||||
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2), 0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.elevation-5 {
|
||||
box-shadow: 0 2px 5px -1px rgba(0, 0, 0, 0.2), 0 5px 6px 0 rgba(0, 0, 0, 0.14), 0 1px 12px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.f-8 {
|
||||
font-size: 8pt !important;
|
||||
}
|
||||
.f-10 {
|
||||
font-size: 10pt !important;
|
||||
}
|
||||
.f-12 {
|
||||
font-size: 12pt !important;
|
||||
}
|
||||
.f-14 {
|
||||
font-size: 14pt !important;
|
||||
}
|
||||
.f-16 {
|
||||
font-size: 16pt !important;
|
||||
}
|
||||
.f-18 {
|
||||
font-size: 18pt !important;
|
||||
}
|
||||
.f-20 {
|
||||
font-size: 20pt !important;
|
||||
}
|
||||
.f-22 {
|
||||
font-size: 22pt !important;
|
||||
}
|
||||
.f-24 {
|
||||
font-size: 24pt !important;
|
||||
}
|
||||
.f-26 {
|
||||
font-size: 26pt !important;
|
||||
}
|
||||
.f-28 {
|
||||
font-size: 28pt !important;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.f-8 {
|
||||
font-size: 0.65rem !important;
|
||||
}
|
||||
|
||||
.f-10 {
|
||||
font-size: 0.75rem !important;
|
||||
}
|
||||
|
||||
.f-12 {
|
||||
font-size: 0.85rem !important;
|
||||
}
|
||||
|
||||
.f-14 {
|
||||
font-size: 0.95rem !important;
|
||||
}
|
||||
|
||||
.f-16 {
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
|
||||
.f-18 {
|
||||
font-size: 1.15rem !important;
|
||||
}
|
||||
|
||||
.f-20 {
|
||||
font-size: 1.25rem !important;
|
||||
}
|
||||
|
||||
.f-22 {
|
||||
font-size: 1.34rem !important;
|
||||
}
|
||||
|
||||
.f-24 {
|
||||
font-size: 1.45rem !important;
|
||||
}
|
||||
|
||||
.f-26 {
|
||||
font-size: 1.65rem !important;
|
||||
}
|
||||
|
||||
.f-28 {
|
||||
font-size: 1.75rem !important;
|
||||
}
|
||||
|
||||
.f-30 {
|
||||
font-size: 2rem !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
$body-bg: #e5e5e5;
|
||||
$primary: #3ba5dc;
|
||||
$secondary: #8392ab;
|
||||
$success: #00f867;
|
||||
$info: #5d0149;
|
||||
$warning: #fbcf33;
|
||||
$danger: #ea0606;
|
||||
$light: #e9ecef;
|
||||
$dark: #111927;
|
||||
$grey: #cbcbcb;
|
||||
|
||||
$font-sans-serif: Open Sans;
|
||||
$font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
$gradient: linear-gradient(180deg, hsla(0, 0%, 100%, 0.15), hsla(0, 0%, 100%, 0));
|
||||
|
||||
$colors: (
|
||||
"primary": $primary,
|
||||
"secondary": $secondary,
|
||||
"danger": $danger,
|
||||
"success": $success,
|
||||
"warning": $warning,
|
||||
"info": $info,
|
||||
"dark": $dark,
|
||||
"grey": $grey,
|
||||
);
|
||||
|
||||
$color-ranges: (
|
||||
0: 0,
|
||||
5: 5,
|
||||
10: 10,
|
||||
15: 15,
|
||||
20: 20,
|
||||
25: 25,
|
||||
30: 30,
|
||||
35: 35,
|
||||
40: 40,
|
||||
);
|
||||
@@ -0,0 +1,37 @@
|
||||
@import "./_variables.scss";
|
||||
@import "./_overrides.scss";
|
||||
@import "./utilities.scss";
|
||||
@import "./loaders.scss";
|
||||
|
||||
#app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
background-color: $body-bg;
|
||||
}
|
||||
|
||||
main {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
&:not(:placeholder-shown) {
|
||||
&:valid {
|
||||
border: 1px solid $success;
|
||||
}
|
||||
&:invalid {
|
||||
border: 1px solid $danger;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
form:invalid button[type="submit"] {
|
||||
filter: grayscale(1);
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<span class="navbar-text">
|
||||
<button
|
||||
class="btn selectable text-success lighten-30 text-uppercase my-2 my-lg-0"
|
||||
@click="login"
|
||||
v-if="!user.isAuthenticated"
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
|
||||
<div class="dropdown my-2 my-lg-0" v-else>
|
||||
<div
|
||||
class="dropdown-toggle selectable"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
id="authDropdown"
|
||||
>
|
||||
<div v-if="account.picture">
|
||||
<img
|
||||
:src="account.picture"
|
||||
alt="account photo"
|
||||
height="40"
|
||||
class="rounded"
|
||||
/>
|
||||
<span class="mx-3 text-success lighten-30">{{ account.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="dropdown-menu p-0 list-group w-100"
|
||||
aria-labelledby="authDropdown"
|
||||
>
|
||||
<router-link :to="{ name: 'Account' }">
|
||||
<div class="list-group-item list-group-item-action hoverable">
|
||||
Manage Account
|
||||
</div>
|
||||
</router-link>
|
||||
<div
|
||||
class="list-group-item list-group-item-action hoverable text-danger"
|
||||
@click="logout"
|
||||
>
|
||||
<i class="mdi mdi-logout"></i>
|
||||
logout
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import { computed } from "@vue/reactivity";
|
||||
import { AppState } from "../AppState";
|
||||
import { AuthService } from "../services/AuthService";
|
||||
export default {
|
||||
setup() {
|
||||
return {
|
||||
user: computed(() => AppState.user),
|
||||
account: computed(() => AppState.account),
|
||||
async login() {
|
||||
AuthService.loginWithPopup();
|
||||
},
|
||||
async logout() {
|
||||
AuthService.logout({ returnTo: window.location.origin });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dropdown-menu {
|
||||
user-select: none;
|
||||
display: block;
|
||||
transform: scale(0);
|
||||
transition: all 0.15s linear;
|
||||
}
|
||||
.dropdown-menu.show {
|
||||
transform: scale(1);
|
||||
}
|
||||
.hoverable {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark px-3">
|
||||
<router-link class="navbar-brand d-flex" :to="{ name: 'Home' }">
|
||||
<div class="d-flex flex-column align-items-center">
|
||||
<img alt="logo" src="../assets/img/cw-logo.png" height="45" />
|
||||
</div>
|
||||
</router-link>
|
||||
<button
|
||||
class="navbar-toggler"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarText"
|
||||
aria-controls="navbarText"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<span class="navbar-toggler-icon" />
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarText">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li>
|
||||
<router-link
|
||||
:to="{ name: 'About' }"
|
||||
class="btn text-success lighten-30 selectable text-uppercase"
|
||||
>
|
||||
About
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- LOGIN COMPONENT HERE -->
|
||||
<Login />
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.nav-link {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.navbar-nav .router-link-exact-active {
|
||||
border-bottom: 2px solid var(--bs-success);
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,6 @@
|
||||
export const dev = window.location.origin.includes('localhost')
|
||||
export const baseURL = dev ? 'http://localhost:3000' : ''
|
||||
export const useSockets = false
|
||||
export const domain = 'dev-h9kk6tgd.us.auth0.com'
|
||||
export const clientId = 'C1FImfihED3KA1bZk7F3JsGg5CyjwrdX'
|
||||
export const audience = 'https://AnniDev.com'
|
||||
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -0,0 +1,14 @@
|
||||
import '@mdi/font/css/materialdesignicons.css'
|
||||
import './index.css'
|
||||
import { createApp } from 'vue'
|
||||
// @ts-ignore
|
||||
import App from './App.vue'
|
||||
import { registerGlobalComponents } from './registerGlobalComponents'
|
||||
import { router } from './router'
|
||||
|
||||
const root = createApp(App)
|
||||
registerGlobalComponents(root)
|
||||
|
||||
root
|
||||
.use(router)
|
||||
.mount('#app')
|
||||
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div class="about">
|
||||
<h1>This is the about page</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AboutPage'
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div class="about text-center">
|
||||
<h1>Welcome {{ account.name }}</h1>
|
||||
<img class="rounded" :src="account.picture" alt="" />
|
||||
<p>{{ account.email }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from 'vue'
|
||||
import { AppState } from '../AppState'
|
||||
export default {
|
||||
name: 'Account',
|
||||
setup() {
|
||||
return {
|
||||
account: computed(() => AppState.account)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
img {
|
||||
max-width: 100px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="home flex-grow-1 d-flex flex-column align-items-center justify-content-center">
|
||||
<div class="home-card p-5 bg-white rounded elevation-3">
|
||||
<img src="https://bcw.blob.core.windows.net/public/img/8600856373152463" alt="CodeWorks Logo" class="rounded-circle">
|
||||
<h1 class="my-5 bg-dark text-white p-3 rounded text-center">
|
||||
Vue 3 Starter
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Home'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.home{
|
||||
display: grid;
|
||||
height: 80vh;
|
||||
place-content: center;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
.home-card{
|
||||
width: 50vw;
|
||||
> img{
|
||||
height: 200px;
|
||||
max-width: 200px;
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,12 @@
|
||||
export function registerGlobalComponents(root) {
|
||||
// https://webpack.js.org/guides/dependency-management/#require-context
|
||||
// @ts-ignore
|
||||
const components = import.meta.globEager('./components/*.vue')
|
||||
Object.entries(components).forEach(([fileName, component]) => {
|
||||
const componentName = component.name || fileName
|
||||
.substr(fileName.lastIndexOf('/') + 1)
|
||||
.replace(/\.\w+$/, '')
|
||||
// Register component on this Vue instance
|
||||
root.component(componentName, component.default)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import { authGuard } from '@bcwdev/auth0provider-client'
|
||||
|
||||
function loadPage(page) {
|
||||
return () => import(`./pages/${page}.vue`)
|
||||
}
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: loadPage('HomePage')
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'About',
|
||||
component: loadPage('AboutPage')
|
||||
},
|
||||
{
|
||||
path: '/account',
|
||||
name: 'Account',
|
||||
component: loadPage('AccountPage'),
|
||||
beforeEnter: authGuard
|
||||
}
|
||||
]
|
||||
|
||||
export const router = createRouter({
|
||||
linkActiveClass: 'router-link-active',
|
||||
linkExactActiveClass: 'router-link-exact-active',
|
||||
history: createWebHashHistory(),
|
||||
routes
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
import { AppState } from '../AppState'
|
||||
import { logger } from '../utils/Logger'
|
||||
import { api } from './AxiosService'
|
||||
|
||||
class AccountService {
|
||||
async getAccount() {
|
||||
try {
|
||||
const res = await api.get('/account')
|
||||
AppState.account = res.data
|
||||
} catch (err) {
|
||||
logger.error('HAVE YOU STARTED YOUR SERVER YET???', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const accountService = new AccountService()
|
||||
@@ -0,0 +1,45 @@
|
||||
import { initialize } from '@bcwdev/auth0provider-client'
|
||||
import { AppState } from '../AppState'
|
||||
import { audience, clientId, domain } from '../env'
|
||||
import { router } from '../router'
|
||||
import { accountService } from './AccountService'
|
||||
import { api } from './AxiosService'
|
||||
import { socketService } from './SocketService'
|
||||
|
||||
export const AuthService = initialize({
|
||||
domain,
|
||||
clientId,
|
||||
audience,
|
||||
useRefreshTokens: true,
|
||||
onRedirectCallback: appState => {
|
||||
router.push(
|
||||
appState && appState.targetUrl
|
||||
? appState.targetUrl
|
||||
: window.location.pathname
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
AuthService.on(AuthService.AUTH_EVENTS.AUTHENTICATED, async function() {
|
||||
api.defaults.headers.authorization = AuthService.bearer
|
||||
api.interceptors.request.use(refreshAuthToken)
|
||||
AppState.user = AuthService.user
|
||||
await accountService.getAccount()
|
||||
socketService.authenticate(AuthService.bearer)
|
||||
// NOTE if there is something you want to do once the user is authenticated, place that here
|
||||
})
|
||||
|
||||
async function refreshAuthToken(config) {
|
||||
if (!AuthService.isAuthenticated) { return config }
|
||||
const expires = AuthService.identity.exp * 1000
|
||||
const expired = expires < Date.now()
|
||||
const needsRefresh = expires < Date.now() + (1000 * 60 * 60 * 12)
|
||||
if (expired) {
|
||||
await AuthService.loginWithPopup()
|
||||
} else if (needsRefresh) {
|
||||
await AuthService.getTokenSilently()
|
||||
api.defaults.headers.authorization = AuthService.bearer
|
||||
socketService.authenticate(AuthService.bearer)
|
||||
}
|
||||
return config
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import Axios from 'axios'
|
||||
import { baseURL } from '../env'
|
||||
export const api = Axios.create({
|
||||
baseURL,
|
||||
timeout: 8000
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
import Pop from '../utils/Pop'
|
||||
import { SocketHandler } from '../utils/SocketHandler'
|
||||
|
||||
class SocketService extends SocketHandler {
|
||||
constructor() {
|
||||
super()
|
||||
this
|
||||
.on('error', this.onError)
|
||||
}
|
||||
|
||||
onError(e) {
|
||||
Pop.toast(e.message, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
export const socketService = new SocketService()
|
||||
@@ -0,0 +1,35 @@
|
||||
import { dev } from '../env'
|
||||
|
||||
function log(type, content) {
|
||||
if (dev) {
|
||||
// eslint-disable-next-line no-console
|
||||
console[type](`[${type}] :: ${new Date().toLocaleTimeString()} :: `, ...content)
|
||||
} else {
|
||||
switch (type) {
|
||||
case 'log':
|
||||
case 'assert':
|
||||
return
|
||||
}
|
||||
// TODO SEND LOGS TO EXTERNAL SERVICE
|
||||
// eslint-disable-next-line no-console
|
||||
console[type](`[${type}] :: ${new Date().toLocaleTimeString()} :: `, ...content)
|
||||
}
|
||||
}
|
||||
|
||||
export const logger = {
|
||||
log() {
|
||||
log('log', arguments)
|
||||
},
|
||||
error() {
|
||||
log('error', arguments)
|
||||
},
|
||||
warn() {
|
||||
log('warn', arguments)
|
||||
},
|
||||
assert() {
|
||||
log('assert', arguments)
|
||||
},
|
||||
trace() {
|
||||
log('trace', arguments)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import Swal from 'sweetalert2'
|
||||
import 'sweetalert2/dist/sweetalert2.min.css'
|
||||
|
||||
export default class Pop {
|
||||
/**
|
||||
*
|
||||
* @param {string} title The title text.
|
||||
* @param {string} text The body text.
|
||||
* @param {string} icon 'success', 'error', 'info', 'warning', or 'question'.
|
||||
* @param {string} confirmButtonText The text of your confirm button.
|
||||
* -----------------------------------
|
||||
* {@link https://sweetalert2.github.io/#configuration|Check out Sweet Alerts}
|
||||
*/
|
||||
static async confirm(title = 'Are you sure?', text = "You won't be able to revert this!", icon = 'warning', confirmButtonText = 'Yes, delete it!') {
|
||||
try {
|
||||
const res = await Swal.fire({
|
||||
title: title,
|
||||
text: text,
|
||||
icon: icon,
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#3085d6',
|
||||
cancelButtonColor: '#d33',
|
||||
confirmButtonText: confirmButtonText
|
||||
})
|
||||
if (res.isConfirmed) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} title The title text
|
||||
* @param {string} display 'success', 'error', 'info', 'warning', or 'question'.
|
||||
* @param {string} position 'top', 'top-start', 'top-end', 'center', 'center-start', 'center-end', 'bottom', 'bottom-start', or 'bottom-end'.
|
||||
* @param {number} timer Time in milliseconds.
|
||||
* @param {boolean} progressBar Show progress bar or not respectively.
|
||||
* -----------------------------------
|
||||
* {@link https://sweetalert2.github.io/#configuration|Check out Sweet Alerts}
|
||||
*/
|
||||
static toast(title = 'Warning!', display = 'warning', position = 'top-end', timer = 3000, progressBar = true) {
|
||||
Swal.fire({
|
||||
title: title,
|
||||
icon: display,
|
||||
position: position,
|
||||
timer: timer,
|
||||
timerProgressBar: progressBar,
|
||||
toast: true,
|
||||
showConfirmButton: false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('axios').AxiosError | Error | String } Error An Error Object.
|
||||
*/
|
||||
static error(error) {
|
||||
if (error.isAxiosError) {
|
||||
const { response } = error
|
||||
this.toast(response.data.error?.message || response.data.message, 'error')
|
||||
} else {
|
||||
this.toast(error.message || error, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param { String } message The message to display. If not provided, will display a generic message.
|
||||
*/
|
||||
static success(message = 'Success!') {
|
||||
this.toast(message, 'success')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { io } from 'socket.io-client'
|
||||
import { baseURL, useSockets } from '../env.js'
|
||||
import { logger } from './Logger.js'
|
||||
|
||||
const SOCKET_EVENTS = {
|
||||
connection: 'connection',
|
||||
connected: 'connected',
|
||||
disconnect: 'disconnect',
|
||||
authenticate: 'authenticate',
|
||||
authenticated: 'authenticated',
|
||||
userConnected: 'userConnected',
|
||||
userDisconnected: 'userDisconnected',
|
||||
error: 'error'
|
||||
}
|
||||
|
||||
export class SocketHandler {
|
||||
/**
|
||||
* @param {String} url
|
||||
*/
|
||||
constructor(requiresAuth = false, url = baseURL) {
|
||||
if (!useSockets) { return }
|
||||
this.socket = io(url || baseURL)
|
||||
this.requiresAuth = requiresAuth
|
||||
this.queue = []
|
||||
this.authenticated = false
|
||||
this
|
||||
.on(SOCKET_EVENTS.connected, this.onConnected)
|
||||
.on(SOCKET_EVENTS.authenticated, this.onAuthenticated)
|
||||
.on(SOCKET_EVENTS.error, this.onError)
|
||||
}
|
||||
|
||||
on(event, fn) {
|
||||
this.socket?.on(event, fn.bind(this))
|
||||
return this
|
||||
}
|
||||
|
||||
onConnected(connection) {
|
||||
logger.log('[SOCKET_CONNECTION]', connection)
|
||||
this.connected = true
|
||||
this.playback()
|
||||
}
|
||||
|
||||
onAuthenticated(auth) {
|
||||
logger.log('[SOCKET_AUTHENTICATED]', auth)
|
||||
this.authenticated = true
|
||||
this.playback()
|
||||
}
|
||||
|
||||
authenticate(bearerToken) {
|
||||
this.socket?.emit(SOCKET_EVENTS.authenticate, bearerToken)
|
||||
}
|
||||
|
||||
onError(error) {
|
||||
logger.error('[SOCKET_ERROR]', error)
|
||||
}
|
||||
|
||||
enqueue(action, payload) {
|
||||
logger.log('[ENQUEING_ACTION]', { action, payload })
|
||||
this.queue.push({ action, payload })
|
||||
}
|
||||
|
||||
playback() {
|
||||
logger.log('[SOCKET_PLAYBACK]')
|
||||
const playback = [...this.queue]
|
||||
this.queue = []
|
||||
playback.forEach(e => {
|
||||
this.emit(e.action, e.payload)
|
||||
})
|
||||
}
|
||||
|
||||
emit(action, payload = undefined) {
|
||||
if (this.requiresAuth && !this.authenticated) {
|
||||
return this.enqueue(action, payload)
|
||||
}
|
||||
if (!this.connected) {
|
||||
return this.enqueue(action, payload)
|
||||
}
|
||||
this.socket.emit(action, payload)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.vue"
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
build: {
|
||||
outDir: 'docs',
|
||||
sourcemap: false
|
||||
},
|
||||
server: {
|
||||
port: 8080
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,44 @@
|
||||
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "budgit.client",
|
||||
"name": "client"
|
||||
},
|
||||
{
|
||||
"path": "budgit",
|
||||
"name": "server"
|
||||
},
|
||||
{
|
||||
"name": "budgit",
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"editor.suggestSelection": "first",
|
||||
"git.enableSmartCommit": true,
|
||||
"git.confirmSync": false,
|
||||
"git.autofetch": true,
|
||||
"javascript.referencesCodeLens.enabled": true,
|
||||
"javascript.referencesCodeLens.showOnAllFunctions": true,
|
||||
"typescript.implementationsCodeLens.enabled": true,
|
||||
"typescript.referencesCodeLens.enabled": true,
|
||||
"typescript.referencesCodeLens.showOnAllFunctions": true,
|
||||
"js/ts.implicitProjectConfig.checkJs": true,
|
||||
"files.exclude": {
|
||||
"**/.nyc_*": true,
|
||||
"**/coverage": true,
|
||||
"**/node_modules": true
|
||||
},
|
||||
"javascript.updateImportsOnFileMove.enabled": "always",
|
||||
"explorer.confirmDragAndDrop": false,
|
||||
"vetur.format.defaultFormatter.js": "vscode-typescript",
|
||||
"eslint.alwaysShowStatus": true,
|
||||
"eslint.format.enable": true,
|
||||
"eslint.lintTask.enable": true,
|
||||
"eslint.codeActionsOnSave.mode": "all",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
[*.{js,jsx,ts,tsx,vue}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
@@ -0,0 +1,25 @@
|
||||
const NEVER = 'never'
|
||||
const ERROR = 2
|
||||
const WARN = 1
|
||||
const PROD = process.env.NODE_ENV === 'production'
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true
|
||||
},
|
||||
parser: 'babel-eslint',
|
||||
extends: [
|
||||
'standard'
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
requireConfigFile: false
|
||||
},
|
||||
rules: {
|
||||
'no-unused-vars': PROD ? ERROR : WARN,
|
||||
'no-console': PROD ? ERROR : WARN,
|
||||
'no-debugger': PROD ? ERROR : WARN,
|
||||
'space-before-function-paren': [WARN, NEVER]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
.env
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Start Server",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"program": "${workspaceFolder}/index.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import BaseController from './server/utils/BaseController'
|
||||
import { logger } from './server/utils/Logger'
|
||||
|
||||
export class Paths {
|
||||
static get Public() {
|
||||
return path.join(__dirname, 'client')
|
||||
}
|
||||
|
||||
static get Server() {
|
||||
return path.join(__dirname, 'server')
|
||||
}
|
||||
|
||||
static get Controllers() {
|
||||
return this.Server + '/controllers'
|
||||
}
|
||||
|
||||
static get Handlers() {
|
||||
return this.Server + '/handlers'
|
||||
}
|
||||
}
|
||||
|
||||
export function RegisterControllers(router) {
|
||||
const controllers = fs.readdirSync(Paths.Controllers)
|
||||
controllers.forEach(loadController)
|
||||
async function loadController(controllerName) {
|
||||
try {
|
||||
if (!controllerName.endsWith('.js')) return
|
||||
const fileHandler = await import(Paths.Controllers + '/' + controllerName)
|
||||
let ControllerClass = fileHandler[controllerName.slice(0, -3)]
|
||||
if (!ControllerClass) {
|
||||
throw new Error(`${controllerName} The exported class does not match the filename`)
|
||||
}
|
||||
if (ControllerClass.default) {
|
||||
ControllerClass = ControllerClass.default
|
||||
}
|
||||
const controller = new ControllerClass()
|
||||
if (controller instanceof BaseController) {
|
||||
router.use(controller.mount, controller.router)
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
'[CONTROLLER ERROR] unable to load controller, potential duplication, review mount path and controller class name, and see error below',
|
||||
controllerName,
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const HANDLERS = []
|
||||
|
||||
export async function RegisterSocketHandlers() {
|
||||
const directory = Paths.Handlers
|
||||
const handlers = fs.readdirSync(directory)
|
||||
handlers.forEach(async(handlerName) => {
|
||||
try {
|
||||
if (!handlerName.endsWith('.js')) { return }
|
||||
const fileHandler = await import(directory + '/' + handlerName)
|
||||
let HandlerClass = fileHandler[handlerName.slice(0, -3)]
|
||||
if (!HandlerClass) {
|
||||
throw new Error(`${handlerName} The exported class does not match the filename`)
|
||||
}
|
||||
if (HandlerClass.default) {
|
||||
HandlerClass = HandlerClass.default
|
||||
}
|
||||
HANDLERS.push(HandlerClass)
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
'[SOCKET_HANDLER_ERROR] unable to attach socket handler, potential duplication, review mount path and controller class name, and see error below',
|
||||
handlerName,
|
||||
e
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function attachHandlers(io, socket, user, profile) {
|
||||
if (socket._handlers && user && profile) {
|
||||
return socket._handlers.forEach(handler => handler.attachUser(user, profile))
|
||||
}
|
||||
socket._handlers = HANDLERS.map(Handler => new Handler(io, socket))
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
class EventEmitter {
|
||||
constructor() {
|
||||
this._listeners = {}
|
||||
}
|
||||
|
||||
on(event: string | number, fn: function, thisContext: any = null): void { }
|
||||
|
||||
off(event: string | number, fn: function): void { }
|
||||
|
||||
emit(event: string, payload: any): void { }
|
||||
}
|
||||
|
||||
class AuthPlugin extends EventEmitter {
|
||||
AUTH_EVENTS = {
|
||||
LOADING: 'LOADING',
|
||||
LOADED: 'LOADED',
|
||||
AUTHENTICATED: 'AUTHENTICATED'
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
options: AuthServiceMethodOptions
|
||||
loading: boolean
|
||||
isAuthenticated: boolean
|
||||
user: Object
|
||||
userInfo: Object
|
||||
identity: Object
|
||||
bearer: string
|
||||
|
||||
/** Authenticates the user using a popup window */
|
||||
async loginWithPopup(o?: AuthServiceMethodOptions): Promise<void> { }
|
||||
/** Handles the callback when logging in using a redirect */
|
||||
async handleRedirectCallback(): void { }
|
||||
/** Authenticates the user using the redirect method */
|
||||
loginWithRedirect(o?: AuthServiceMethodOptions = {}): void { }
|
||||
|
||||
/**
|
||||
* Returns the access token. If the token is invalid or missing, a new one is retrieved
|
||||
* @param {AuthServiceMethodOptions} [o]
|
||||
*/
|
||||
getTokenSilently(o: AuthServiceMethodOptions): void { }
|
||||
/**
|
||||
* @param {string[] | string} permissions
|
||||
*/
|
||||
hasPermissions(permissions: string[] | string): boolean { }
|
||||
|
||||
/**
|
||||
* @param {string[] | string} roles
|
||||
*/
|
||||
hasRoles(roles: string[] | string): boolean { }
|
||||
|
||||
/** Logs the user out and removes their session on the authorization server */
|
||||
logout(o?: AuthServiceMethodOptions): void { }
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const Auth0Provider = {
|
||||
/**
|
||||
* @param {{ onRedirectCallback: () => void; domain: string, audience: string, clientId: string, useRefreshTokens?: boolean }} options
|
||||
*/
|
||||
initialize(options: { onRedirectCallback: () => void; domain: string; audience: string; clientId: string; }): AuthPlugin { return new AuthPlugin(options) },
|
||||
async authGuard(to: RouterItem, from: RouterItem, next: () => Promise<void>): Promise<void> { }
|
||||
}
|
||||
|
||||
interface RouterItem {
|
||||
fullPath: string
|
||||
}
|
||||
|
||||
interface AuthServiceMethodOptions {
|
||||
display?: 'page' | 'popup' | 'touch' | 'wap',
|
||||
prompt?: 'none' | 'login' | 'consent' | 'select_account',
|
||||
max_age?: string | number,
|
||||
ui_locales?: string,
|
||||
id_token_hint?: string,
|
||||
login_hint?: string,
|
||||
acr_values?: string,
|
||||
scope?: string,
|
||||
audience?: string,
|
||||
connection?: string,
|
||||
[key: string]: any
|
||||
}
|
||||
Vendored
+32
@@ -0,0 +1,32 @@
|
||||
class Axios {
|
||||
defaults: AxiosRequestOptions
|
||||
create(options: AxiosConfig): Axios { }
|
||||
get(url: string, options?: AxiosRequestOptions): Promise<AxiosResponse>
|
||||
post(url: string, payload: any, options?: AxiosRequestOptions): Promise<AxiosResponse>
|
||||
put(url: string, payload: any, options?: AxiosRequestOptions): Promise<AxiosResponse>
|
||||
delete(url: string, options?: AxiosRequestOptions): Promise<AxiosResponse>
|
||||
}
|
||||
|
||||
declare const axios = new Axios
|
||||
|
||||
interface AxiosConfig extends AxiosRequestOptions {
|
||||
baseURL: string
|
||||
timeout?: number = 8000
|
||||
withCredentials?: boolean = false
|
||||
}
|
||||
|
||||
interface AxiosRequestOptions {
|
||||
headers?: any
|
||||
params?: any
|
||||
query?: any
|
||||
}
|
||||
|
||||
interface AxiosResponse {
|
||||
data: any
|
||||
}
|
||||
|
||||
interface AxiosError {
|
||||
status: number
|
||||
message: string
|
||||
response: AxiosResponse
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { dev } from './env.js'
|
||||
import { EventEmitter } from './Utils/EventEmitter.js'
|
||||
import { isValidProp } from './Utils/isValidProp.js'
|
||||
|
||||
class AppState extends EventEmitter {
|
||||
user = {}
|
||||
account = {}
|
||||
/** @type {import('./Models/Value').Value[]} */
|
||||
values = []
|
||||
socketData = []
|
||||
}
|
||||
|
||||
export const ProxyState = new Proxy(new AppState(), {
|
||||
get(target, prop) {
|
||||
isValidProp(target, prop)
|
||||
return target[prop]
|
||||
},
|
||||
set(target, prop, value) {
|
||||
isValidProp(target, prop)
|
||||
target[prop] = value
|
||||
target.emit(prop, value)
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
if (dev) {
|
||||
// @ts-ignore
|
||||
window.ProxyState = ProxyState
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { ProxyState } from '../AppState.js'
|
||||
import { audience, clientId, domain } from '../env.js'
|
||||
import { AuthService } from '../Services/AuthService.js'
|
||||
import { logger } from '../Utils/Logger.js'
|
||||
|
||||
function drawUser() {
|
||||
const user = ProxyState.user
|
||||
const account = ProxyState.account
|
||||
const userAvatar = avatarTemplate(account)
|
||||
const button = authButton(user)
|
||||
const template = /* html */ `
|
||||
${userAvatar}
|
||||
${button}
|
||||
`
|
||||
document.getElementById('authstate').innerHTML = template
|
||||
}
|
||||
|
||||
function _drawAuthSettings() {
|
||||
const elem = document.getElementById('auth-settings')
|
||||
if (!elem) { return }
|
||||
elem.innerHTML = /* html */`
|
||||
<div class="card p-2 elevation-4">
|
||||
<div class="card-title p-2">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="avatar">
|
||||
<img src="https://avatars.githubusercontent.com/u/2824157?s=280&v=4" alt="user" height="45" class="rounded-circle">
|
||||
</div>
|
||||
<div class="text mx-2">
|
||||
<b>Auth0 Settings</b>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body border-top">
|
||||
<div class="text block"><b>Domain:</b> ${domain}</div>
|
||||
<div class="text block"><b>Audience:</b> ${audience}</div>
|
||||
<div class="text block"><b>Client Id:</b> ${clientId}</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
export class AuthController {
|
||||
constructor() {
|
||||
ProxyState.on('account', drawUser)
|
||||
AuthService.on(AuthService.AUTH_EVENTS.LOADED, drawUser)
|
||||
AuthService.on(AuthService.AUTH_EVENTS.LOADED, _drawAuthSettings)
|
||||
drawUser()
|
||||
}
|
||||
|
||||
async login() {
|
||||
try {
|
||||
await AuthService.loginWithPopup()
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
logout() {
|
||||
try {
|
||||
AuthService.logout()
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function authButton(user) {
|
||||
if (AuthService.loading) { return '' }
|
||||
return user.isAuthenticated
|
||||
? /* html */ `
|
||||
<button class="btn btn-small btn-dark text-muted selectable" onclick="app.authController.logout()">✖</button>
|
||||
`
|
||||
: /* html */ `
|
||||
<button class="btn btn-dark selectable" onclick="app.authController.login()">login</button>
|
||||
`
|
||||
}
|
||||
|
||||
function avatarTemplate(account) {
|
||||
return account.picture
|
||||
? /* html */ `
|
||||
<div class="mr-2">
|
||||
<img class="rounded-circle" src="${account.picture}" alt="${account.name}" height="45"/>
|
||||
<span class="mx-1">${account.name}</span>
|
||||
</div>`
|
||||
: AuthService.loading
|
||||
? /* html */ `
|
||||
<div class="skeleton-loader dark avatar"></div>
|
||||
<div class="skeleton-loader dark text sm mx-2"></div>`
|
||||
: /* html */`
|
||||
<div></div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { ProxyState } from '../AppState.js'
|
||||
import { socketService } from '../Services/SocketService.js'
|
||||
import { logger } from '../Utils/Logger.js'
|
||||
|
||||
function _drawSocketData() {
|
||||
logger.log('draw socket data', ProxyState.socketData)
|
||||
}
|
||||
|
||||
export class SocketTestController {
|
||||
constructor() {
|
||||
ProxyState.on('socketData', _drawSocketData)
|
||||
}
|
||||
|
||||
testSocket() {
|
||||
socketService.emit('SOCKET_TEST', { name: 'SOCKET_TEST', id: ~~(Math.random() * 99999) })
|
||||
}
|
||||
|
||||
testAuthSocket() {
|
||||
socketService.emit('AUTH_TEST', { name: 'AUTH_TEST', id: ~~(Math.random() * 99999) })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ProxyState } from '../AppState.js'
|
||||
import { valuesService } from '../Services/ValuesService.js'
|
||||
import { logger } from '../Utils/Logger.js'
|
||||
|
||||
// Private
|
||||
function _draw() {
|
||||
const values = ProxyState.values
|
||||
logger.log(values)
|
||||
}
|
||||
|
||||
// Public
|
||||
export class ValuesController {
|
||||
constructor() {
|
||||
ProxyState.on('values', _draw)
|
||||
}
|
||||
|
||||
addValue() {
|
||||
valuesService.addValue()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export class Value {
|
||||
constructor(data) {
|
||||
this.title = data.title
|
||||
}
|
||||
|
||||
get Template() {
|
||||
return this.title
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { ProxyState } from '../AppState.js'
|
||||
import { logger } from '../Utils/Logger.js'
|
||||
import { api } from './AxiosService.js'
|
||||
|
||||
class AccountService {
|
||||
async getAccount() {
|
||||
try {
|
||||
const res = await api.get('/account')
|
||||
ProxyState.account = res.data
|
||||
} catch (err) {
|
||||
logger.error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const accountService = new AccountService()
|
||||
@@ -0,0 +1,45 @@
|
||||
import { ProxyState } from '../AppState.js'
|
||||
import { audience, clientId, domain } from '../env.js'
|
||||
import { accountService } from './AccountService.js'
|
||||
import { api } from './AxiosService.js'
|
||||
// import { socketService } from './SocketService.js'
|
||||
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line no-undef
|
||||
export const AuthService = Auth0Provider.initialize({
|
||||
domain,
|
||||
clientId,
|
||||
audience,
|
||||
useRefreshTokens: true,
|
||||
onRedirectCallback: appState => {
|
||||
window.location.replace(
|
||||
appState && appState.targetUrl
|
||||
? appState.targetUrl
|
||||
: window.location.pathname
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
AuthService.on(AuthService.AUTH_EVENTS.AUTHENTICATED, async() => {
|
||||
api.defaults.headers.authorization = AuthService.bearer
|
||||
api.interceptors.request.use(refreshAuthToken)
|
||||
ProxyState.user = AuthService.user
|
||||
await accountService.getAccount()
|
||||
// socketService.authenticate(AuthService.bearer)
|
||||
})
|
||||
|
||||
async function refreshAuthToken(config) {
|
||||
if (!AuthService.isAuthenticated) { return config }
|
||||
const expires = AuthService.identity.exp * 1000
|
||||
const expired = expires < Date.now()
|
||||
const needsRefresh = expires < Date.now() + (1000 * 60 * 60 * 12)
|
||||
if (expired) {
|
||||
await AuthService.loginWithPopup()
|
||||
// socketService.authenticate(AuthService.bearer)
|
||||
} else if (needsRefresh) {
|
||||
await AuthService.getTokenSilently()
|
||||
// socketService.authenticate(AuthService.bearer)
|
||||
}
|
||||
api.defaults.headers.authorization = AuthService.bearer
|
||||
return config
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { baseURL } from '../env.js'
|
||||
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line no-undef
|
||||
export const api = axios.create({
|
||||
baseURL: baseURL,
|
||||
timeout: 8000,
|
||||
withCredentials: true
|
||||
})
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ProxyState } from '../AppState.js'
|
||||
import { SocketHandler } from '../utils/SocketHandler.js'
|
||||
class SocketService extends SocketHandler {
|
||||
constructor() {
|
||||
super()
|
||||
this
|
||||
.on('IS_TESTED', this.onTested)
|
||||
.on('IS_AUTHED', this.onIsAuthed)
|
||||
}
|
||||
|
||||
onTested(payload) {
|
||||
ProxyState.socketData = [...ProxyState.socketData, payload]
|
||||
}
|
||||
|
||||
onIsAuthed(payload) {
|
||||
ProxyState.socketData = [...ProxyState.socketData, payload]
|
||||
}
|
||||
}
|
||||
|
||||
export const socketService = new SocketService()
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ProxyState } from '../AppState.js'
|
||||
import { Value } from '../Models/Value.js'
|
||||
|
||||
class ValuesService {
|
||||
addValue() {
|
||||
ProxyState.values = [...ProxyState.values, new Value({ title: Math.random() })]
|
||||
}
|
||||
}
|
||||
|
||||
export const valuesService = new ValuesService()
|
||||
@@ -0,0 +1,49 @@
|
||||
import { logger } from './Logger.js'
|
||||
|
||||
export class EventEmitter {
|
||||
_listeners = {}
|
||||
|
||||
/**
|
||||
* @param {string | number} event
|
||||
* @param {function} fn
|
||||
* @param {any} thisContext
|
||||
*/
|
||||
on(event, fn, thisContext = null) {
|
||||
if (typeof fn !== 'function') { return }
|
||||
if (!(event in this)) {
|
||||
logger.error(`Unable to register listener for '${event}'`)
|
||||
return
|
||||
}
|
||||
this._listeners[event] = Array.isArray(this._listeners[event]) ? this._listeners[event] : []
|
||||
this._listeners[event] = this._listeners[event] || []
|
||||
// @ts-ignore
|
||||
fn.ctx = thisContext
|
||||
this._listeners[event].push(fn)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | number} event
|
||||
* @param {function} fn
|
||||
*/
|
||||
off(event, fn) {
|
||||
this._listeners[event] = Array.isArray(this._listeners[event]) ? this._listeners[event] : []
|
||||
const i = this._listeners[event].indexOf(fn)
|
||||
if (i === -1) { return }
|
||||
this._listeners[event].splice(i, 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | number | symbol} event
|
||||
* @param {any} [payload]
|
||||
*/
|
||||
emit(event, payload) {
|
||||
this._listeners[event] = this._listeners[event] || []
|
||||
const length = this._listeners[event].length
|
||||
for (let i = 0; i < length; i++) {
|
||||
const fn = this._listeners[event][i]
|
||||
fn.ctx
|
||||
? fn.call(fn.ctx, payload)
|
||||
: fn(payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { dev } from '../env.js'
|
||||
|
||||
function log(type, content) {
|
||||
if (dev) {
|
||||
// eslint-disable-next-line no-console
|
||||
console[type](`[${type}] :: ${new Date().toLocaleTimeString()} :: `, ...content)
|
||||
} else {
|
||||
switch (type) {
|
||||
case 'log':
|
||||
case 'assert':
|
||||
return
|
||||
}
|
||||
// TODO SEND LOGS TO EXTERNAL SERVICE
|
||||
// eslint-disable-next-line no-console
|
||||
console[type](`[${type}] :: ${new Date().toLocaleTimeString()} :: `, ...content)
|
||||
}
|
||||
}
|
||||
|
||||
export const logger = {
|
||||
log() {
|
||||
log('log', arguments)
|
||||
},
|
||||
error() {
|
||||
log('error', arguments)
|
||||
},
|
||||
warn() {
|
||||
log('warn', arguments)
|
||||
},
|
||||
assert() {
|
||||
log('assert', arguments)
|
||||
},
|
||||
trace() {
|
||||
log('trace', arguments)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { logger } from './Logger.js'
|
||||
import { baseURL } from '../env.js'
|
||||
|
||||
let connected = false
|
||||
let queue = []
|
||||
|
||||
const SOCKET_EVENTS = {
|
||||
connection: 'connection',
|
||||
connected: 'connected',
|
||||
disconnect: 'disconnect',
|
||||
authenticate: 'authenticate',
|
||||
authenticated: 'authenticated',
|
||||
userConnected: 'userConnected',
|
||||
userDisconnected: 'userDisconnected',
|
||||
error: 'error'
|
||||
}
|
||||
|
||||
export class SocketHandler {
|
||||
/**
|
||||
* @param {String} url
|
||||
*/
|
||||
constructor(url = baseURL) {
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line no-undef
|
||||
this.socket = io(url || baseURL)
|
||||
this
|
||||
.on(SOCKET_EVENTS.connected, this.onConnected)
|
||||
.on(SOCKET_EVENTS.authenticated, this.onAuthenticated)
|
||||
.on(SOCKET_EVENTS.error, this.onError)
|
||||
}
|
||||
|
||||
on(event, fn) {
|
||||
this.socket.on(event, fn.bind(this))
|
||||
return this
|
||||
}
|
||||
|
||||
onConnected(connection) {
|
||||
logger.log('[SOCKET_CONNECTION]', connection)
|
||||
connected = true
|
||||
}
|
||||
|
||||
onAuthenticated(auth) {
|
||||
logger.log('[SOCKET_AUTHENTICATED]', auth)
|
||||
const playback = [...queue]
|
||||
queue = []
|
||||
playback.forEach(e => {
|
||||
this.emit(e.action, e.payload)
|
||||
})
|
||||
}
|
||||
|
||||
authenticate(bearerToken) {
|
||||
this.socket.emit(SOCKET_EVENTS.authenticate, bearerToken)
|
||||
}
|
||||
|
||||
onError(error) {
|
||||
logger.error('[SOCKET_ERROR]', error)
|
||||
}
|
||||
|
||||
emit(action, payload = undefined) {
|
||||
if (!connected) {
|
||||
logger.log('[ENQUEING_ACTION]', { action, payload })
|
||||
return queue.push({ action, payload })
|
||||
}
|
||||
this.socket.emit(action, payload)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export function isValidProp(target, prop) {
|
||||
if (typeof target[prop] === 'function') { return }
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (!target.hasOwnProperty(prop)) {
|
||||
throw new Error(`[BAD PROP]:[${prop}] Invalid Property Access via Proxy State`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export const dev = window.location.origin.includes('localhost')
|
||||
export const baseURL = dev ? 'http://localhost:3000' : ''
|
||||
export const domain = ''
|
||||
export const audience = ''
|
||||
export const clientId = ''
|
||||
@@ -0,0 +1,10 @@
|
||||
import { AuthController } from './Controllers/AuthController.js'
|
||||
import { ValuesController } from './Controllers/ValuesController.js'
|
||||
|
||||
class App {
|
||||
authController = new AuthController();
|
||||
valuesController = new ValuesController();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
window.app = new App()
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
@@ -0,0 +1,37 @@
|
||||
$body-bg: #e5e5e5;
|
||||
$primary: #3ba5dc;
|
||||
$secondary: #8392ab;
|
||||
$success: #00f867;
|
||||
$info: #cb0c9f;
|
||||
$warning: #fbcf33;
|
||||
$danger: #ea0606;
|
||||
$light: #e9ecef;
|
||||
$dark: #111927;
|
||||
$grey: #cbcbcb;
|
||||
|
||||
$font-sans-serif: Open Sans;
|
||||
$font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
$gradient: linear-gradient(180deg, hsla(0, 0%, 100%, 0.15), hsla(0, 0%, 100%, 0));
|
||||
|
||||
$colors: (
|
||||
"primary": $primary,
|
||||
"secondary": $secondary,
|
||||
"danger": $danger,
|
||||
"success": $success,
|
||||
"warning": $warning,
|
||||
"info": $info,
|
||||
"dark": $dark,
|
||||
"grey": $grey,
|
||||
);
|
||||
|
||||
$color-ranges: (
|
||||
0: 0,
|
||||
5: 5,
|
||||
10: 10,
|
||||
15: 15,
|
||||
20: 20,
|
||||
25: 25,
|
||||
30: 30,
|
||||
35: 35,
|
||||
40: 40,
|
||||
);
|
||||
@@ -0,0 +1,259 @@
|
||||
@each $color, $value in $colors {
|
||||
.bg-#{$color} {
|
||||
color: color-yiq($value);
|
||||
background-color: $value !important;
|
||||
}
|
||||
.text-#{$color} {
|
||||
color: $value !important;
|
||||
}
|
||||
}
|
||||
|
||||
@each $color, $value in $colors {
|
||||
@each $num, $val in $color-ranges {
|
||||
.bg-#{$color}.darken-#{$num} {
|
||||
color: color-yiq(darken($value, $num));
|
||||
background-color: darken($value, $num) !important;
|
||||
}
|
||||
.text-#{$color}.darken-#{$num} {
|
||||
color: darken($value, $num) !important;
|
||||
}
|
||||
.bg-#{$color}.lighten-#{$num} {
|
||||
color: color-yiq(lighten($value, $num));
|
||||
background-color: lighten($value, $num) !important;
|
||||
}
|
||||
.text-#{$color}.lighten-#{$num} {
|
||||
color: lighten($value, $num) !important;
|
||||
}
|
||||
.bg-#{$color}.darken-#{$num}0 {
|
||||
color: color-yiq(darken($value, $num * 0.25));
|
||||
background-color: darken($value, $num * 0.25) !important;
|
||||
}
|
||||
.text-#{$color}.darken-#{$num}0 {
|
||||
color: darken($value, $num * 0.25) !important;
|
||||
}
|
||||
.bg-#{$color}.lighten-#{$num}0 {
|
||||
color: color-yiq(lighten($value, $num * 0.25));
|
||||
background-color: lighten($value, $num * 0.25) !important;
|
||||
}
|
||||
.text-#{$color}.lighten-#{$num}0 {
|
||||
color: lighten($value, $num * 0.25) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.clip-text {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
.action {
|
||||
cursor: pointer;
|
||||
}
|
||||
.no-select {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.scrollable-x {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.scrollable-y {
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
// opacity: 0.65;
|
||||
&::after {
|
||||
cursor: not-allowed;
|
||||
pointer-events: all;
|
||||
position: absolute;
|
||||
content: "";
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0.25rem;
|
||||
text-shadow: 1px 1px 0 #555, -1px -1px 0 #555, 1px -1px 0 #555, -1px 1px 0 #555, 1px 1px 0 #555;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: rgba(85, 85, 85, 0.37);
|
||||
&.text-disabled {
|
||||
content: "DISABLED";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.on-hover {
|
||||
transition: all 0.2s linear;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
*:hover > .on-hover {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.square {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
.square-left {
|
||||
border-top-left-radius: 0 !important;
|
||||
border-bottom-left-radius: 0 !important;
|
||||
}
|
||||
.square-right {
|
||||
border-top-right-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
}
|
||||
.square-top {
|
||||
border-top-right-radius: 0 !important;
|
||||
border-top-left-radius: 0 !important;
|
||||
}
|
||||
.square-bottom {
|
||||
border-bottom-right-radius: 0 !important;
|
||||
border-bottom-left-radius: 0 !important;
|
||||
}
|
||||
.square-bottom-left {
|
||||
border-bottom-left-radius: 0 !important;
|
||||
}
|
||||
.square-bottom-right {
|
||||
border-bottom-right-radius: 0 !important;
|
||||
}
|
||||
.square-top-left {
|
||||
border-top-left-radius: 0 !important;
|
||||
}
|
||||
.square-top-right {
|
||||
border-top-right-radius: 0 !important;
|
||||
}
|
||||
|
||||
.selectable {
|
||||
position: relative;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
&:after {
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
&:after {
|
||||
border-radius: inherit;
|
||||
bottom: 0;
|
||||
color: inherit;
|
||||
content: "";
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
transition: opacity 0.2s cubic-bezier(0.4, 0, 0.6, 1);
|
||||
background-color: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
.elevation-1 {
|
||||
box-shadow: 0 2px 1px -1px rgba(0, 0, 0, 0.2), 0 1px 1px 0 rgba(0, 0, 0, 0.14), 0 1px 3px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.elevation-2 {
|
||||
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.elevation-3 {
|
||||
box-shadow: 0 3px 3px -2px rgba(0, 0, 0, 0.2), 0 3px 4px 0 rgba(0, 0, 0, 0.14), 0 1px 8px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.elevation-4 {
|
||||
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2), 0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.elevation-5 {
|
||||
box-shadow: 0 2px 5px -1px rgba(0, 0, 0, 0.2), 0 5px 6px 0 rgba(0, 0, 0, 0.14), 0 1px 12px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.f-8 {
|
||||
font-size: 8pt !important;
|
||||
}
|
||||
.f-10 {
|
||||
font-size: 10pt !important;
|
||||
}
|
||||
.f-12 {
|
||||
font-size: 12pt !important;
|
||||
}
|
||||
.f-14 {
|
||||
font-size: 14pt !important;
|
||||
}
|
||||
.f-16 {
|
||||
font-size: 16pt !important;
|
||||
}
|
||||
.f-18 {
|
||||
font-size: 18pt !important;
|
||||
}
|
||||
.f-20 {
|
||||
font-size: 20pt !important;
|
||||
}
|
||||
.f-22 {
|
||||
font-size: 22pt !important;
|
||||
}
|
||||
.f-24 {
|
||||
font-size: 24pt !important;
|
||||
}
|
||||
.f-26 {
|
||||
font-size: 26pt !important;
|
||||
}
|
||||
.f-28 {
|
||||
font-size: 28pt !important;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.f-8 {
|
||||
font-size: 0.65rem !important;
|
||||
}
|
||||
|
||||
.f-10 {
|
||||
font-size: 0.75rem !important;
|
||||
}
|
||||
|
||||
.f-12 {
|
||||
font-size: 0.85rem !important;
|
||||
}
|
||||
|
||||
.f-14 {
|
||||
font-size: 0.95rem !important;
|
||||
}
|
||||
|
||||
.f-16 {
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
|
||||
.f-18 {
|
||||
font-size: 1.15rem !important;
|
||||
}
|
||||
|
||||
.f-20 {
|
||||
font-size: 1.25rem !important;
|
||||
}
|
||||
|
||||
.f-22 {
|
||||
font-size: 1.34rem !important;
|
||||
}
|
||||
|
||||
.f-24 {
|
||||
font-size: 1.45rem !important;
|
||||
}
|
||||
|
||||
.f-26 {
|
||||
font-size: 1.65rem !important;
|
||||
}
|
||||
|
||||
.f-28 {
|
||||
font-size: 1.75rem !important;
|
||||
}
|
||||
|
||||
.f-30 {
|
||||
font-size: 2rem !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
.skeleton-loader {
|
||||
box-shadow: 0 4px 10px 0 rgba(33, 33, 33, 0.15);
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background-color: #dcdcdc;
|
||||
pointer-events: none;
|
||||
&.dark{
|
||||
background-color: #9d9c9c;
|
||||
}
|
||||
|
||||
&.card{
|
||||
background-color: #fff;
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
background: linear-gradient(to right, transparent 0%, #f6f6f6 50%, transparent 100%);
|
||||
animation: load 1.5s cubic-bezier(0.4, 0.0, 0.2, 1) infinite;
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
&.avatar{
|
||||
border-radius: 50%;
|
||||
height: 45px;
|
||||
width: 45px;
|
||||
&.tile{
|
||||
border-radius: 5%;
|
||||
}
|
||||
&.md{
|
||||
height: 65px;
|
||||
width: 65px;
|
||||
}
|
||||
&.lg{
|
||||
height: 85px;
|
||||
width: 85px;
|
||||
}
|
||||
}
|
||||
&.text{
|
||||
width: clamp(50%, 160px, 100%);
|
||||
height: 1rem;
|
||||
line-height: 1.5rem;
|
||||
margin: .5rem 0;
|
||||
&.sm{
|
||||
width: clamp(25%, 80px, 100%);
|
||||
}
|
||||
&.lg{
|
||||
width: clamp(75%, 250px, 100%);
|
||||
}
|
||||
&.block{
|
||||
width: clamp(100%, 500px, 100%);
|
||||
}
|
||||
}
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 50px;
|
||||
background: linear-gradient(to right, transparent 0%, #E8E8E8 50%, transparent 100%);
|
||||
animation: load 1s cubic-bezier(0.4, 0.0, 0.2, 1) infinite;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@keyframes load {
|
||||
from {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
@import "./_variables.scss";
|
||||
@import "../../../node_modules/bootstrap/scss/bootstrap";
|
||||
@import "./_overrides.scss";
|
||||
@import "./custom.scss";
|
||||
@import "./loaders.scss";
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
background-color: $body-bg;
|
||||
}
|
||||
|
||||
main {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,77 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<link rel="icon" href="https://bcw.blob.core.windows.net/public/img/9977764104160066">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="assets/style/main.css">
|
||||
<title>budgit</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<nav class="navbar navbar-dark navbar-expand bg-dark text-light">
|
||||
<a class="navbar-brand" href="#">
|
||||
<img src="https://codeworks.blob.core.windows.net/public/assets/img/cw-mark.png" height="45" alt=""
|
||||
loading="lazy">
|
||||
<span class="ml-2">budgit</span>
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavAltMarkup"
|
||||
aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
|
||||
<div class="navbar-nav">
|
||||
<!-- NAVLINKS -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1 justify-content-end align-items-center d-flex" id="authstate"></div>
|
||||
</nav>
|
||||
</header>
|
||||
<main id="app">
|
||||
<div class="container-fluid">
|
||||
<div class="row my-3">
|
||||
<div class="col-lg-5" id="auth-settings">
|
||||
<div class="card p-2 skeleton-loader">
|
||||
<div class="card-title p-2">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="skeleton-loader avatar"></div>
|
||||
<div class="skeleton-loader text mx-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body border-top">
|
||||
<div class="skeleton-loader text block"></div>
|
||||
<div class="skeleton-loader text block"></div>
|
||||
<div class="skeleton-loader text block"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="container-fluid bg-dark text-light">
|
||||
<div class="row text-center">
|
||||
<div class="col">
|
||||
<p><small>Made with 💖 by CodeWorks</small></p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script src="https://cdn.auth0.com/js/auth0-spa-js/1.15/auth0-spa-js.production.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/gh/jakeoverall/auth0provider-client/browser/AuthProvider.browser.js"></script>
|
||||
<script src="https://cdn.socket.io/4.1.1/socket.io.min.js"
|
||||
integrity="sha384-cdrFIqe3RasCMNE0jeFG9xJHog/tgOVC1E9Lzve8LQN1g5WUHo0Kvk1mawWjxX7a"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-U1DAWAznBHeqEIlVSCgzq+c9gqGAJn5c/t99JyeKa9xxaYpSvHU5awsuZVVFIhvj"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
<script src="app/main.js" type="module"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,5 @@
|
||||
require('dotenv').config()
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line no-global-assign
|
||||
require = require('esm')(module)
|
||||
module.exports = require('./server/main')
|
||||
Generated
+6912
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "codeworks-auth-starter",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"debug": "node index.js",
|
||||
"start": "node index.js",
|
||||
"dev": "node index.js",
|
||||
"setup": "npm install",
|
||||
"sass": "sass --watch client/assets/scss/main.scss:client/assets/style/main.css"
|
||||
},
|
||||
"author": "Codeworks",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@bcwdev/auth0provider": "1.0.13",
|
||||
"cors": "2.8.5",
|
||||
"dotenv": "16.0.0",
|
||||
"esm": "3.2.25",
|
||||
"express": "4.17.3",
|
||||
"helmet": "4.6.0",
|
||||
"mongoose": "6.3.1",
|
||||
"socket.io": "4.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.5.4",
|
||||
"eslint": "8.14.0",
|
||||
"eslint-config-standard": "17.0.0",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"eslint-plugin-promise": "6.0.0",
|
||||
"sass": "1.50.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
# Auth0 Server Setup
|
||||
|
||||
This template is designed to help kickstart a project that utilizes <a href="https://auth0.com/" target="_blank">Auth0.</a> The bulk of the structure has been setup and requires a few pieces of configuration.
|
||||
|
||||
The first thing you will need to provide is in the `.env` file. You will need to supply the port, Auth0 credentials, and mongoDb connectionstring. These environment variables are used throughout the template, so be sure to add them in when moving into production as well.
|
||||
|
||||
**_.env_**
|
||||
|
||||
```
|
||||
NODE_ENV=dev
|
||||
PORT=
|
||||
CONNECTION_STRING=
|
||||
AUTH_DOMAIN=
|
||||
AUTH_AUDIENCE=
|
||||
AUTH_CLIENT_ID=
|
||||
```
|
||||
|
||||
### MVC - Controllers
|
||||
|
||||
This template will automatically register all of the controllers found in the controllers folder of the server. This opinionated workflow should help provide a structure on how to build your api. Generally speaking every controller method should start with a `try catch block` and utilize the default error handler setup in Startup.js This means if a request ever fails the controller should call the next function with the error provided.
|
||||
|
||||
### MVC - Services
|
||||
|
||||
Services are responsible for implementing and enforcing your business rules. Be sure to use them wisely and do not put your business logic in controllers. Services should be usable by both controllers and sockets and potentally other services. Never directly access the `DbContext` outside of a service.
|
||||
|
||||
### MVC - (Models, Collections & DbContext)
|
||||
|
||||
Models are defined as mongoose schemas and then imported into a central location called the DbContext. All access to the database should be limited to the DbContext. `Collections.js` is a file purely designed to avoid the common problem of magic strings. This means when you register your models and have dependencies or relationships between one or more models you should import from Collections so you know the naming is always the same.
|
||||
|
||||
### Working with Auth0
|
||||
|
||||
This library provides easily configured middleware that will validate user auth tokens, roles, permissions and provides a simple approach to get userInfo associted with a user account. Each middleware will call next with an error on any failure so be sure to setup a default error handler. Also note that we extend the express request object with
|
||||
|
||||
- req.user: `{ UserIdentity }`
|
||||
- req.userInfo: `{ UserInfo }`
|
||||
|
||||
### Enable RBAC or Extended Rules (required)
|
||||
|
||||
In your auth0 dashboard be sure to enable RBAC or add in this custom rule
|
||||
|
||||
```javascript
|
||||
//AUTH0 RULE
|
||||
/**
|
||||
* Add common namespaced properties to userInfo,
|
||||
* note auth0 will strip any non namespaced properties
|
||||
*/
|
||||
function extendUserInfo(user, context, callback) {
|
||||
const namespace = 'https://YOURDOMAINHERE.auth0.com';
|
||||
context.idToken = context.idToken || {};
|
||||
context.authorization = context.authorization || {};
|
||||
user.app_metadata = user.app_metadata || { };
|
||||
user.app_metadata.new = user.app_metadata.id ? false : true;
|
||||
user.app_metadata.id = user.app_metadata.id || generateId();
|
||||
|
||||
for (const key in user.app_metadata) {
|
||||
context.idToken[`${namespace}/${key}`] = user.app_metadata[key];
|
||||
}
|
||||
context.idToken[`${namespace}/roles`] = context.authorization.roles;
|
||||
context.idToken[`${namespace}/permissions`] = context.authorization.permissions;
|
||||
context.idToken[`${namespace}/user_metadata`] = user.user_metadata;
|
||||
|
||||
if(!user.app_metadata.new){
|
||||
return callback(null, user, context);
|
||||
}
|
||||
delete user.app_metadata.new;
|
||||
auth0.users.updateAppMetadata(user.user_id, user.app_metadata)
|
||||
.then(function () {
|
||||
callback(null, user, context);
|
||||
})
|
||||
.catch(function (err) {
|
||||
callback(err);
|
||||
});
|
||||
|
||||
function generateId() {
|
||||
let timestamp = (new Date().getTime() / 1000 | 0).toString(16);
|
||||
return timestamp + 'xxxxxxxxxxxxxxxx'.replace(/[x]/g, () => (
|
||||
Math.random() * 16 | 0).toString(16)).toLowerCase();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Example of how to use and configure auth0Provider, You can configure the auth0Provider anywhere in your application and then import it and use the middleware anywhere
|
||||
|
||||
```javascript
|
||||
import { auth0Provider } from "@bcw/auth0-server";
|
||||
|
||||
auth0Provider.configure({
|
||||
domain: process.env.AUTH_DOMAIN,
|
||||
clientId: process.env.AUTH_CLIENT_ID,
|
||||
audience: process.env.AUTH_AUDIENCE
|
||||
});
|
||||
|
||||
// validates a request has a Bearer auth token in req.headers.authentication
|
||||
app.use("/authenticated", auth0Provider.isAuthenticated, (req, res, next) => {
|
||||
res.send({ userIdentity: req.user });
|
||||
});
|
||||
|
||||
// validates the request token and extracts the userInfo saved in auth0
|
||||
app.use("/user-profile", getAuthorizedUserInfo, (req, res, next) => {
|
||||
res.send({ userIdentity: req.user, userInfo: req.userInfo });
|
||||
});
|
||||
|
||||
// validates the request token, extracts the userIdentity and userInfo
|
||||
// fails if role is not found in the token
|
||||
// Enable RBAC or Extended Rules
|
||||
app.use(
|
||||
"/admins-only",
|
||||
auth0Provider.hasRoles("admin"),
|
||||
(req, res, next) => {}
|
||||
);
|
||||
|
||||
// validates the request token, extracts the userIdentity and userInfo
|
||||
// fails if any permission is not found in the token
|
||||
// Enable RBAC or Extended Rules
|
||||
app.use(
|
||||
"/messages",
|
||||
auth0Provider.hasPermissions(["read:messages", "write:messages"]),
|
||||
(req, res, next) => {}
|
||||
);
|
||||
|
||||
//recommended default error handler
|
||||
app.use((error, req, res, next) => {
|
||||
if (error.status == 500 || !error.status) {
|
||||
error.message = console.error(error); // should write to external
|
||||
}
|
||||
error = error || {
|
||||
status: 400,
|
||||
message: "An unexpected error occured please try again later"
|
||||
};
|
||||
res.status(error.status).send({ ...error, url: req.url });
|
||||
});
|
||||
```
|
||||
|
||||
Using chained methods with express.Router()
|
||||
|
||||
```javascript
|
||||
express
|
||||
.Router()
|
||||
.get("", this.getAll)
|
||||
.use(AuthorizationService.isAuthorized)
|
||||
// everything below this point requires authorization
|
||||
.get("/:id", this.getById);
|
||||
.put("/:id", this.updateById);
|
||||
.use(AuthorizationService.hasPermission("delete:blog"))
|
||||
// requires permission to reach this point
|
||||
.delete("/:id", this.deleteById);
|
||||
```
|
||||
|
||||
## Legal Overview
|
||||
|
||||
The content under the CodeWorks®, LLC Organization and all of the individual repos are solely intended for use by CodeWorks Instruction to deliver Educational content to CodeWorks Students.
|
||||
|
||||
---
|
||||
|
||||
## Copyright
|
||||
|
||||
© CodeWorks® LLC, 2021. Unauthorized use and/or duplication of this material without express and written permission from CodeWorks, LLC is strictly prohibited.
|
||||
|
||||
|
||||
<img src="https://bcw.blob.core.windows.net/public/img/7815839041305055" width="125">
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { logger } from './utils/Logger'
|
||||
import { attachHandlers } from '../Setup'
|
||||
|
||||
const SOCKET_EVENTS = {
|
||||
connection: 'connection',
|
||||
connected: 'connected',
|
||||
disconnect: 'disconnect',
|
||||
userConnected: 'userConnected',
|
||||
userDisconnected: 'userDisconnected',
|
||||
error: 'error'
|
||||
}
|
||||
|
||||
class SocketProvider {
|
||||
/**
|
||||
* @type {Server}
|
||||
*/
|
||||
io = null
|
||||
initialize(httpServer) {
|
||||
try {
|
||||
this.io = new Server(httpServer, {
|
||||
cors: {
|
||||
origin: process.env.NODE_ENV === 'dev' ? '*' : ''
|
||||
}
|
||||
})
|
||||
this.io.on(SOCKET_EVENTS.connection, (socket) => this.onConnect(socket))
|
||||
} catch (e) {
|
||||
logger.error('[SOCKETSTORE ERROR]', e)
|
||||
}
|
||||
}
|
||||
|
||||
onConnect(socket) {
|
||||
attachHandlers(this.io, socket)
|
||||
socket.emit(SOCKET_EVENTS.connected, {
|
||||
socket: socket.id,
|
||||
message: 'Successfully Connected'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a direct message to a user
|
||||
* @param {string} userId
|
||||
* @param {string} eventName
|
||||
* @param {any} payload
|
||||
*/
|
||||
messageUser(userId, eventName, payload) {
|
||||
try {
|
||||
this.io.to(userId).emit(eventName, payload)
|
||||
} catch (e) {
|
||||
logger.error('[SOCKET_ERROR] messageUser', e, { userId, eventName, payload })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to all sockets in a room
|
||||
* @param {string} room
|
||||
* @param {string} eventName
|
||||
* @param {any} payload
|
||||
*/
|
||||
messageRoom(room, eventName, payload) {
|
||||
this.io.to(room).emit(eventName, payload)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to all connected sockets
|
||||
* @param {string} eventName
|
||||
* @param {any} payload
|
||||
*/
|
||||
message(eventName, payload) {
|
||||
this.io.emit(eventName, payload)
|
||||
}
|
||||
}
|
||||
|
||||
export const socketProvider = new SocketProvider()
|
||||
@@ -0,0 +1,81 @@
|
||||
import cors from 'cors'
|
||||
import express from 'express'
|
||||
import helmet from 'helmet'
|
||||
import { json } from 'body-parser'
|
||||
import { RegisterControllers, Paths, RegisterSocketHandlers } from '../Setup'
|
||||
import { Auth0Provider } from '@bcwdev/auth0provider'
|
||||
import { logger } from './utils/Logger'
|
||||
import { AccountValidator } from './utils/AccountValidator'
|
||||
|
||||
export class Startup {
|
||||
static ConfigureGlobalMiddleware(app) {
|
||||
// NOTE Configure and Register Middleware
|
||||
Startup.configureCors(app)
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: false
|
||||
}))
|
||||
app.use(json({ limit: '50mb' }))
|
||||
|
||||
// NOTE Configures auth0 middleware that is used throughout controllers
|
||||
Auth0Provider.configure({
|
||||
domain: process.env.AUTH_DOMAIN,
|
||||
clientId: process.env.AUTH_CLIENT_ID,
|
||||
audience: process.env.AUTH_AUDIENCE
|
||||
})
|
||||
}
|
||||
|
||||
static configureCors(app) {
|
||||
const allowedDomains = []
|
||||
const corsOptions = {
|
||||
origin(origin, callback) {
|
||||
if (process.env.NODE_ENV === 'dev') {
|
||||
return callback(null, true)
|
||||
}
|
||||
const originIsWhitelisted = allowedDomains.indexOf(origin) !== -1
|
||||
callback(null, originIsWhitelisted)
|
||||
},
|
||||
credentials: true
|
||||
}
|
||||
|
||||
app.use(cors(corsOptions))
|
||||
}
|
||||
|
||||
static ConfigureRoutes(app) {
|
||||
const router = express.Router()
|
||||
app.use(AccountValidator)
|
||||
RegisterControllers(router)
|
||||
RegisterSocketHandlers()
|
||||
app.use(router)
|
||||
|
||||
app.use('', express.static(Paths.Public))
|
||||
|
||||
Startup.registerErrorHandlers(app)
|
||||
}
|
||||
|
||||
static registerErrorHandlers(app) {
|
||||
// NOTE SEND JSON 404 for any unknown routes
|
||||
app.use('/api', (_, res, next) => {
|
||||
res.status(404).send({ status: 404, message: 'Not Found' })
|
||||
})
|
||||
|
||||
// NOTE SEND HTML 404 for any unknown routes
|
||||
app.use(
|
||||
'*',
|
||||
(_, res, next) => {
|
||||
res.status(404)
|
||||
next()
|
||||
},
|
||||
express.static(Paths.Public + '404')
|
||||
)
|
||||
// NOTE Default Error Handler
|
||||
app.use((error, req, res, next) => {
|
||||
if (!error.status) {
|
||||
error.status = 400
|
||||
}
|
||||
if (error.status === 500) {
|
||||
logger.error(error)
|
||||
}
|
||||
res.status(error.status).send({ error: { message: error.toString(), status: error.status }, url: req.url })
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Auth0Provider } from '@bcwdev/auth0provider'
|
||||
import { accountService } from '../services/AccountService'
|
||||
import BaseController from '../utils/BaseController'
|
||||
|
||||
export class AccountController extends BaseController {
|
||||
constructor() {
|
||||
super('account')
|
||||
this.router
|
||||
.use(Auth0Provider.getAuthorizedUserInfo)
|
||||
.get('', this.getUserAccount)
|
||||
}
|
||||
|
||||
async getUserAccount(req, res, next) {
|
||||
try {
|
||||
const account = await accountService.getAccount(req.userInfo)
|
||||
res.send(account)
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Auth0Provider } from '@bcwdev/auth0provider'
|
||||
import { accountService } from '../services/AccountService'
|
||||
import { categoriesService } from '../services/CategoriesService'
|
||||
import BaseController from '../utils/BaseController'
|
||||
|
||||
|
||||
export class CategoriesController extends BaseController {
|
||||
constructor() {
|
||||
super('api/categories')
|
||||
this.router
|
||||
.use(Auth0Provider.getAuthorizedUserInfo)
|
||||
.post('', this.create)
|
||||
.get('', this.getAll)
|
||||
.get('/:id', this.getById)
|
||||
.put('/:id', this.modify)
|
||||
}
|
||||
|
||||
async create(req, res, next) {
|
||||
try {
|
||||
req.body.accountId = req.userInfo.id
|
||||
const data = await categoriesService.create(req.body)
|
||||
return res.send(data)
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
async getAll(req, res, next) {
|
||||
try {
|
||||
const data = await categoriesService.getAll(req.userInfo.id)
|
||||
return res.send(data)
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
async getById(req, res, next) {
|
||||
try {
|
||||
const data = await categoriesService.getById(req.params.id, req.userInfo.id)
|
||||
return res.send(data)
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO modify()
|
||||
async modify(req, res, next) {
|
||||
try {
|
||||
const data = await categoriesService.modify(res.data, req.params.id, req.userInfo.id)
|
||||
return res.send(data)
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO delete()
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { profileService } from '../services/ProfileService.js'
|
||||
import BaseController from '../utils/BaseController'
|
||||
|
||||
export class ProfilesController extends BaseController {
|
||||
constructor() {
|
||||
super('api/profiles')
|
||||
this.router
|
||||
.get('', this.getProfiles)
|
||||
.get('/:id', this.getProfile)
|
||||
}
|
||||
|
||||
async getProfiles(req, res, next) {
|
||||
try {
|
||||
const profiles = await profileService.findProfiles(req.query.name, req.query.offset)
|
||||
res.send(profiles)
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
async getProfile(req, res, next) {
|
||||
try {
|
||||
const profile = await profileService.getProfileById(req.params.id)
|
||||
res.send(profile)
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import BaseController from '../utils/BaseController'
|
||||
import { Auth0Provider } from '@bcwdev/auth0provider'
|
||||
|
||||
export class ValuesController extends BaseController {
|
||||
constructor() {
|
||||
super('api/values')
|
||||
this.router
|
||||
.get('', this.getAll)
|
||||
// NOTE: Beyond this point all routes require Authorization tokens (the user must be logged in)
|
||||
.use(Auth0Provider.getAuthorizedUserInfo)
|
||||
.post('', this.create)
|
||||
}
|
||||
|
||||
async getAll(req, res, next) {
|
||||
try {
|
||||
return res.send(['value1', 'value2'])
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
async create(req, res, next) {
|
||||
try {
|
||||
// NOTE NEVER TRUST THE CLIENT TO ADD THE CREATOR ID
|
||||
req.body.creatorId = req.userInfo.id
|
||||
res.send(req.body)
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import mongoose from 'mongoose'
|
||||
import { logger } from '../utils/Logger'
|
||||
|
||||
mongoose.connection.on('error', err => {
|
||||
logger.error('[DATABASE ERROR]:', err)
|
||||
})
|
||||
mongoose.connection.on('connection', () => {
|
||||
logger.log('DbConnection Successful')
|
||||
})
|
||||
|
||||
export class DbConnection {
|
||||
static async connect(connectionstring = process.env.CONNECTION_STRING || '') {
|
||||
const status = 0
|
||||
try {
|
||||
const status = await mongoose.connect(connectionstring)
|
||||
logger.log('[CONNECTION TO DB SUCCESSFUL]')
|
||||
return status
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
'[MONGOOSE CONNECTION ERROR]:',
|
||||
'Invalid connection string'
|
||||
)
|
||||
return status
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import mongoose from 'mongoose'
|
||||
import { AccountSchema } from '../models/Account'
|
||||
import { CategorySchema } from '../models/Category';
|
||||
import { TransactionSchema } from '../models/Transaction';
|
||||
import { ValueSchema } from '../models/Value'
|
||||
|
||||
class DbContext {
|
||||
Values = mongoose.model('Value', ValueSchema);
|
||||
Account = mongoose.model('Account', AccountSchema);
|
||||
Transactions = mongoose.model('Transactions', TransactionSchema)
|
||||
Categories = mongoose.model('Categories', CategorySchema)
|
||||
}
|
||||
|
||||
export const dbContext = new DbContext()
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Auth0Provider } from '@bcwdev/auth0provider'
|
||||
import { attachHandlers } from '../../Setup'
|
||||
import { accountService } from '../services/AccountService'
|
||||
import { SocketHandler } from '../utils/SocketHandler'
|
||||
|
||||
export class AuthHandler extends SocketHandler {
|
||||
/**
|
||||
* @param {import("socket.io").Server} io
|
||||
* @param {import("socket.io").Socket} socket
|
||||
*/
|
||||
constructor(io, socket) {
|
||||
super(io, socket)
|
||||
this
|
||||
.on('authenticate', this.onAuthenticate)
|
||||
.on('disconnect', this.onDisconnect)
|
||||
}
|
||||
|
||||
async onAuthenticate(bearerToken) {
|
||||
try {
|
||||
const user = await Auth0Provider.getUserInfoFromBearerToken(bearerToken)
|
||||
const profile = await accountService.getAccount(user)
|
||||
const limitedProfile = {
|
||||
id: profile.id,
|
||||
email: profile.email,
|
||||
picture: profile.picture
|
||||
}
|
||||
this.socket.join(user.id)
|
||||
attachHandlers(this.io, this.socket, user, limitedProfile)
|
||||
this.socket.emit('authenticated', limitedProfile)
|
||||
this.io.emit('userConnected', limitedProfile)
|
||||
} catch (e) {
|
||||
this.socket.emit('error', e)
|
||||
}
|
||||
}
|
||||
|
||||
async onDisconnect() {
|
||||
this.io.emit('userDisconnected', this.profile)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { SocketHandler } from '../utils/SocketHandler'
|
||||
|
||||
export class AuthTestHandler extends SocketHandler {
|
||||
/**
|
||||
* @param {import("socket.io").Server} io
|
||||
* @param {import("socket.io").Socket} socket
|
||||
*/
|
||||
constructor(io, socket) {
|
||||
super(io, socket, true)
|
||||
this
|
||||
.on('AUTH_TEST', this.testAuthEvent)
|
||||
}
|
||||
|
||||
async testAuthEvent(payload) {
|
||||
this.socket.emit('IS_AUTHED', {
|
||||
user: this.user,
|
||||
profile: this.profile,
|
||||
payload
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { SocketHandler } from '../utils/SocketHandler'
|
||||
|
||||
export class TestHandler extends SocketHandler {
|
||||
/**
|
||||
* @param {import("socket.io").Server} io
|
||||
* @param {import("socket.io").Socket} socket
|
||||
*/
|
||||
constructor(io, socket) {
|
||||
super(io, socket)
|
||||
this
|
||||
.on('SOCKET_TEST', this.testEvent)
|
||||
}
|
||||
|
||||
async testEvent(payload) {
|
||||
this.socket.emit('IS_TESTED', payload)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import express from 'express'
|
||||
import { socketProvider } from './SocketProvider'
|
||||
import { Startup } from './Startup'
|
||||
import { DbConnection } from './db/DbConfig'
|
||||
import { logger } from './utils/Logger'
|
||||
import { createServer } from 'http'
|
||||
|
||||
// create server & socketServer
|
||||
const app = express()
|
||||
const port = process.env.PORT || 3000
|
||||
|
||||
const httpServer = createServer(app)
|
||||
Startup.ConfigureGlobalMiddleware(app)
|
||||
Startup.ConfigureRoutes(app)
|
||||
|
||||
// Establish Socket
|
||||
socketProvider.initialize(httpServer)
|
||||
|
||||
// Connect to Atlas MongoDB
|
||||
DbConnection.connect()
|
||||
|
||||
// Start Server
|
||||
httpServer.listen(port, () => {
|
||||
logger.log(`[SERVING ON PORT: ${port}]`)
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
import mongoose from 'mongoose'
|
||||
const Schema = mongoose.Schema
|
||||
|
||||
export const AccountSchema = new Schema(
|
||||
{
|
||||
subs: [{ type: String, unique: true }],
|
||||
email: { type: String, lowercase: true, unique: true },
|
||||
name: { type: String, required: true },
|
||||
picture: { type: String }
|
||||
// NOTE If you wish to add additional properties do so here
|
||||
},
|
||||
{ timestamps: true, toJSON: { virtuals: true } }
|
||||
)
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import mongoose from 'mongoose'
|
||||
import { TransactionSchema } from './Transaction'
|
||||
const Schema = mongoose.Schema
|
||||
|
||||
|
||||
export const CategorySchema = new Schema(
|
||||
{
|
||||
/** @property {ObjectId} [accountId] -ID of the account this transaction belongs to */
|
||||
accountId: {type: Schema.Types.ObjectId, required: true},
|
||||
|
||||
/** @property {String} [name] -name of the category */
|
||||
name: {type: String, required: true},
|
||||
|
||||
/** @property {Number} [budgeted] -amount currently budgeted towards this category */
|
||||
budgeted: {type: Number, default: 0},
|
||||
|
||||
/** @property {Number} [targetAmt] -target amount to be budgeted */
|
||||
// TODO default?
|
||||
targetAmt: {type: Number, default: 0},
|
||||
|
||||
/** @property {String} [targetStart] -start date for target cycle, in ISO format ('YYYY-MM-DD') */
|
||||
// TODO default?
|
||||
targetStart: {type: String},
|
||||
|
||||
/** @property {Number} [targetFreq] -frequency of target refill, in targetUnits */
|
||||
// TODO default?
|
||||
targetFreq: {type: String},
|
||||
|
||||
/** @property {String} [targetUnit] -unit of refill frequency */
|
||||
targetUnit: {
|
||||
type: String,
|
||||
enum: [
|
||||
'day', // days
|
||||
'month', // calendar months (always lands on same day of the month)
|
||||
'year', // calendar years (always lands on same date)
|
||||
'once', // target is needed only once, ie savings goal
|
||||
'balance' // target is always needed, ie emergency fund
|
||||
]
|
||||
// TODO default?
|
||||
}
|
||||
},
|
||||
{timestamps: true, toJSON: {virtuals: true}}
|
||||
)
|
||||
|
||||
// TODO this probably won't work
|
||||
CategorySchema.virtual('transactions', {
|
||||
localField: '_id',
|
||||
foreignField: 'categoryId',
|
||||
ref: 'Transactions'
|
||||
// ,justOne: true
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
import mongoose from 'mongoose'
|
||||
const Schema = mongoose.Schema
|
||||
|
||||
|
||||
export const TransactionSchema = new Schema(
|
||||
{
|
||||
/** @property {ObjectId} [accountId] -ID of the account this transaction belongs to */
|
||||
accountId: {type: Schema.Types.ObjectId, required: true},
|
||||
|
||||
/** @property {ObjectId} [categoryId] -ID of the spending category this transaction belongs to */
|
||||
categoryId: {type: Schema.Types.ObjectId},
|
||||
|
||||
/** @property {Number} [amount] - number of cents spent (negative is income) */
|
||||
amount: {type: Number, required: true},
|
||||
|
||||
/** @property {String} [date] -ISO date this transaction occured ('YYYY-MM-DD') */
|
||||
date: {type: String, required: true},
|
||||
|
||||
/** @property {String} [payee] -name of who this transaction was paid to/from */
|
||||
payee: {type: String},
|
||||
|
||||
/** @property {String} [comment] -user comments about transaction */
|
||||
comment: {type: String}
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,18 @@
|
||||
import mongoose from 'mongoose'
|
||||
const Schema = mongoose.Schema
|
||||
|
||||
export const ValueSchema = new Schema(
|
||||
{
|
||||
title: { type: String, required: true },
|
||||
description: { type: String, required: true },
|
||||
creatorId: { type: Schema.Types.ObjectId, ref: 'Profile', required: true }
|
||||
},
|
||||
{ timestamps: true, toJSON: { virtuals: true } }
|
||||
)
|
||||
|
||||
ValueSchema.virtual('creator', {
|
||||
localField: 'creatorId',
|
||||
foreignField: '_id',
|
||||
justOne: true,
|
||||
ref: 'Profile'
|
||||
})
|
||||
@@ -0,0 +1,78 @@
|
||||
import { dbContext } from '../db/DbContext'
|
||||
|
||||
// Private Methods
|
||||
|
||||
/**
|
||||
* Creates account if one does not exist
|
||||
* @param {any} account
|
||||
* @param {any} user
|
||||
*/
|
||||
async function createAccountIfNeeded(account, user) {
|
||||
if (!account) {
|
||||
user._id = user.id
|
||||
account = await dbContext.Account.create({
|
||||
...user,
|
||||
subs: [user.sub]
|
||||
})
|
||||
}
|
||||
return account
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds sub to account if not already on account
|
||||
* @param {any} account
|
||||
* @param {any} user
|
||||
*/
|
||||
async function mergeSubsIfNeeded(account, user) {
|
||||
if (!account.subs.includes(user.sub)) {
|
||||
// @ts-ignore
|
||||
account.subs.push(user.sub)
|
||||
await account.save()
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Restricts changes to the body of the account object
|
||||
* @param {any} body
|
||||
*/
|
||||
function sanitizeBody(body) {
|
||||
const writable = {
|
||||
name: body.name,
|
||||
picture: body.picture
|
||||
}
|
||||
return writable
|
||||
}
|
||||
|
||||
class AccountService {
|
||||
/**
|
||||
* Returns a user account from the Auth0 user object
|
||||
*
|
||||
* Creates user if none exists
|
||||
*
|
||||
* Adds sub of Auth0 account to account if not currently on account
|
||||
* @param {any} user
|
||||
*/
|
||||
async getAccount(user) {
|
||||
let account = await dbContext.Account.findOne({
|
||||
_id: user.id
|
||||
})
|
||||
account = await createAccountIfNeeded(account, user)
|
||||
await mergeSubsIfNeeded(account, user)
|
||||
return account
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates account with the request body, will only allow changes to editable fields
|
||||
* @param {any} user Auth0 user object
|
||||
* @param {any} body Updates to apply to user object
|
||||
*/
|
||||
async updateAccount(user, body) {
|
||||
const update = sanitizeBody(body)
|
||||
const account = await dbContext.Account.findOneAndUpdate(
|
||||
{ _id: user.id },
|
||||
{ $set: update },
|
||||
{ runValidators: true, setDefaultsOnInsert: true, new: true }
|
||||
)
|
||||
return account
|
||||
}
|
||||
}
|
||||
export const accountService = new AccountService()
|
||||
@@ -0,0 +1,52 @@
|
||||
import { dbContext } from '../db/DbContext'
|
||||
import { BadRequest } from '../utils/Errors'
|
||||
|
||||
|
||||
class CategoriesService {
|
||||
async create(newCategory) {
|
||||
const dbCategory = await dbContext.Categories.create(newCategory)
|
||||
if (!dbCategory) {
|
||||
throw new BadRequest('Category does not exist')
|
||||
}
|
||||
return dbCategory
|
||||
}
|
||||
|
||||
async getAll(accountId) {
|
||||
if (!accountId) {
|
||||
throw new BadRequest('you must be logged in')
|
||||
}
|
||||
const categories = await dbContext.Categories.find({accountId: accountId})
|
||||
return categories
|
||||
}
|
||||
|
||||
async getById(categoryId, accountId) {
|
||||
const category = await dbContext.Categories.find({accountId: accountId, _id: categoryId})
|
||||
if (!category){
|
||||
throw new BadRequest('Invalid Id')
|
||||
}
|
||||
// if (accountId != category.accountId) {
|
||||
// // TODO this still is a data leak, because it tells the attacker that the category exists
|
||||
// throw new BadRequest('das nacho category')
|
||||
// }
|
||||
return category
|
||||
}
|
||||
|
||||
// TODO modify()
|
||||
async modify(update, categoryId, accountId) {
|
||||
let original = await this.getById(categoryId, accountId)
|
||||
if (!original) {
|
||||
throw new BadRequest('category does not exist')
|
||||
}
|
||||
original.name = update.name || original.name
|
||||
original.budgeted = update.budgeted || original.budgeted
|
||||
original.targetAmt = update.targetAmt || original.targetAmt
|
||||
original.targetStart = update.targetStart || original.targetStart
|
||||
original.targetFreq = update.targetFreq || original.targetFreq
|
||||
original.targetUnit = update.targetUnit || original.targetUnit
|
||||
update = await dbContext.Categories.findByIdAndUpdate(categoryId, original)
|
||||
}
|
||||
|
||||
// TODO delete()
|
||||
}
|
||||
|
||||
export const categoriesService = new CategoriesService
|
||||
@@ -0,0 +1,33 @@
|
||||
import { dbContext } from '../db/DbContext.js'
|
||||
|
||||
// IMPORTANT profiles should not be updated or modified in any way here. Use the AccountService
|
||||
|
||||
class ProfileService {
|
||||
/**
|
||||
* Returns a user profile from its id
|
||||
* @param {string} id
|
||||
*/
|
||||
async getProfileById(id) {
|
||||
const profile = await dbContext.Profiles.findById(id)
|
||||
return profile
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list user profiles from a query search of name likeness
|
||||
* limits to first 20 without offset
|
||||
* @param {string} name
|
||||
*/
|
||||
async findProfiles(name = '', offset = 0) {
|
||||
const filter = new RegExp(name, 'ig')
|
||||
return await dbContext.Profiles
|
||||
.aggregate([{
|
||||
$match: { name: filter }
|
||||
}])
|
||||
.collation({ locale: 'en_US', strength: 1 })
|
||||
.skip(Number(offset))
|
||||
.limit(20)
|
||||
.exec()
|
||||
}
|
||||
}
|
||||
|
||||
export const profileService = new ProfileService()
|
||||
@@ -0,0 +1,19 @@
|
||||
import { dbContext } from '../db/DbContext'
|
||||
import { BadRequest } from '../utils/Errors'
|
||||
|
||||
class ValuesService {
|
||||
async find(query = {}) {
|
||||
const values = await dbContext.Values.find(query)
|
||||
return values
|
||||
}
|
||||
|
||||
async findById(id) {
|
||||
const value = await dbContext.Values.findById(id)
|
||||
if (!value) {
|
||||
throw new BadRequest('Invalid Id')
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
export const valuesService = new ValuesService()
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Auth0Provider } from '@bcwdev/auth0provider'
|
||||
import { accountService } from '../services/AccountService'
|
||||
|
||||
export async function AccountValidator(req, res, next) {
|
||||
try {
|
||||
const bearer = req.headers.Authorization || req.headers.authorization
|
||||
if (!bearer) {
|
||||
return next()
|
||||
}
|
||||
const userInfo = await Auth0Provider.getUserInfoFromBearerToken(bearer)
|
||||
if (!userInfo.id) {
|
||||
throw new Error('[MISSING_AUTH0_RULE] Unable to create account: Missing Extend UserInfo rule in Auth0 account')
|
||||
}
|
||||
req.account = await accountService.getAccount(userInfo)
|
||||
next()
|
||||
} catch (e) {
|
||||
next(e)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import express from 'express'
|
||||
|
||||
export default class BaseController {
|
||||
constructor(mount) {
|
||||
if (typeof mount !== 'string') {
|
||||
throw new Error('Unable to register controller no mount path specified')
|
||||
}
|
||||
if (mount[0] !== '/') {
|
||||
mount = '/' + mount
|
||||
}
|
||||
this.mount = mount
|
||||
this.router = express.Router({ mergeParams: true })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
export class NotFound extends Error {
|
||||
constructor(message = 'Not Found') {
|
||||
super(message)
|
||||
this.status = 404
|
||||
}
|
||||
}
|
||||
export class Forbidden extends Error {
|
||||
constructor(message = 'Access Forbidden') {
|
||||
super(message)
|
||||
this.status = 403
|
||||
}
|
||||
}
|
||||
export class UnAuthorized extends Error {
|
||||
constructor(message = 'Unauthorized') {
|
||||
super(message)
|
||||
this.status = 401
|
||||
}
|
||||
}
|
||||
export class BadRequest extends Error {
|
||||
constructor(message = 'Bad Request') {
|
||||
super(message)
|
||||
this.status = 400
|
||||
}
|
||||
}
|
||||
export class Unexpected extends Error {
|
||||
constructor(message = 'Unexpected Error') {
|
||||
super(message)
|
||||
this.status = 500
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user