initial commit, have most of the categories crud methods

This commit is contained in:
Annika
2022-07-01 16:02:45 -06:00
commit a943a20d25
104 changed files with 34031 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
> 1%
last 2 versions
+5
View File
@@ -0,0 +1,5 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true
+34
View File
@@ -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
}
}
+20
View File
@@ -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?
+14
View File
@@ -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"
}
]
}
+4
View File
@@ -0,0 +1,4 @@
declare module "*.vue" {
import Vue from 'vue';
export default Vue.defineComponent();
}
+16
View File
@@ -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">
+24
View File
@@ -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>
+5
View File
@@ -0,0 +1,5 @@
{
"include": [
"./src/**/*"
]
}
+6269
View File
File diff suppressed because it is too large Load Diff
+33
View File
@@ -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"
}
}
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
+29
View File
@@ -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>
+7
View File
@@ -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,
);
+37
View File
@@ -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;
}
+84
View File
@@ -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>
+56
View File
@@ -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>
+6
View File
@@ -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'
+3
View File
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
+14
View File
@@ -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')
+11
View File
@@ -0,0 +1,11 @@
<template>
<div class="about">
<h1>This is the about page</h1>
</div>
</template>
<script>
export default {
name: 'AboutPage'
}
</script>
+26
View File
@@ -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>
+36
View File
@@ -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)
})
}
+32
View File
@@ -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()
+45
View File
@@ -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()
+35
View File
@@ -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)
}
}
+74
View File
@@ -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')
}
}
+80
View File
@@ -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)
}
}
+11
View File
@@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./index.html",
"./src/**/*.vue"
],
theme: {
extend: {},
},
plugins: [],
}
+14
View File
@@ -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
}
})
+44
View File
@@ -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
}
}
}
+5
View File
@@ -0,0 +1,5 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true
+25
View File
@@ -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]
}
}
+2
View File
@@ -0,0 +1,2 @@
node_modules
.env
+17
View File
@@ -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"
}
]
}
+84
View File
@@ -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
View File
@@ -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
}
+32
View File
@@ -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
}
+29
View File
@@ -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()
}
}
+9
View File
@@ -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()
+45
View File
@@ -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()
+49
View File
@@ -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)
}
}
}
+35
View File
@@ -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)
}
}
+66
View File
@@ -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)
}
}
+7
View File
@@ -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`)
}
}
+5
View File
@@ -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 = ''
+10
View File
@@ -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

+37
View File
@@ -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,
);
+259
View File
@@ -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;
}
}
+79
View File
@@ -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%;
}
}
+19
View File
@@ -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
+77
View File
@@ -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>
+5
View File
@@ -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')
+6912
View File
File diff suppressed because it is too large Load Diff
+34
View File
@@ -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"
}
}
+161
View File
@@ -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">
+74
View File
@@ -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()
+81
View File
@@ -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)
}
}
}
+26
View File
@@ -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
}
}
}
+14
View File
@@ -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()
+39
View File
@@ -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)
}
}
+21
View File
@@ -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
})
}
}
+17
View File
@@ -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)
}
}
+25
View File
@@ -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}]`)
})
+14
View File
@@ -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 } }
)
+51
View File
@@ -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
})
+25
View File
@@ -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}
}
)
+18
View File
@@ -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'
})
+78
View File
@@ -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
+33
View File
@@ -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()
+19
View File
@@ -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()
+19
View File
@@ -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)
}
}
+14
View File
@@ -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 })
}
}
+30
View File
@@ -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