Began transition to Vue3.
Implemented logging in and logging out. Implemented authenticated http client. Laid some groundwork for SCSS.
3
MultiShop/client/.browserslistrc
Normal file
@@ -0,0 +1,3 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
6
MultiShop/client/.editorconfig
Normal file
@@ -0,0 +1,6 @@
|
||||
[*.{js,jsx,ts,tsx,vue}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
quote_type = double
|
33
MultiShop/client/.eslintrc.js
Normal file
@@ -0,0 +1,33 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true
|
||||
},
|
||||
extends: [
|
||||
'plugin:vue/vue3-essential',
|
||||
'@vue/standard'
|
||||
],
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint'
|
||||
},
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'quotes': ['error', 'double', 'avoid-escape'],
|
||||
'semi': ['error', 'always'],
|
||||
'indent': ['error', 4],
|
||||
'comma-dangle': ['error', 'only-multiline'],
|
||||
'space-before-function-paren': ['error', 'never']
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
'**/__tests__/*.{j,t}s?(x)',
|
||||
'**/tests/unit/**/*.spec.{j,t}s?(x)'
|
||||
],
|
||||
env: {
|
||||
mocha: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
25
MultiShop/client/.gitignore
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
|
||||
/tests/e2e/logs/
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
34
MultiShop/client/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# multishop
|
||||
|
||||
## Project setup
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Run your unit tests
|
||||
```
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
### Run your end-to-end tests
|
||||
```
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
5
MultiShop/client/babel.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
5
MultiShop/client/jsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"include": [
|
||||
"./src/**/*"
|
||||
]
|
||||
}
|
18567
MultiShop/client/package-lock.json
generated
Normal file
48
MultiShop/client/package.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "multishop",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"test:unit": "vue-cli-service test:unit",
|
||||
"test:e2e": "vue-cli-service test:e2e",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.21.1",
|
||||
"bootstrap": "^5.0.2",
|
||||
"bootstrap-icons": "^1.5.0",
|
||||
"core-js": "^3.6.5",
|
||||
"oidc-client": "^1.11.5",
|
||||
"register-service-worker": "^1.7.1",
|
||||
"vue": "^3.0.0",
|
||||
"vue-router": "^4.0.0-0",
|
||||
"vuex": "^4.0.0-0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
"@vue/cli-plugin-e2e-webdriverio": "~4.5.0",
|
||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||
"@vue/cli-plugin-pwa": "~4.5.0",
|
||||
"@vue/cli-plugin-router": "~4.5.0",
|
||||
"@vue/cli-plugin-unit-mocha": "~4.5.0",
|
||||
"@vue/cli-plugin-vuex": "~4.5.0",
|
||||
"@vue/cli-service": "~4.5.0",
|
||||
"@vue/compiler-sfc": "^3.0.0",
|
||||
"@vue/eslint-config-standard": "^5.1.2",
|
||||
"@vue/test-utils": "^2.0.0-0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"chai": "^4.1.2",
|
||||
"chromedriver": "91",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-import": "^2.20.2",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"eslint-plugin-vue": "^7.0.0",
|
||||
"sass": "^1.35.1",
|
||||
"sass-loader": "^8.0.2",
|
||||
"wdio-chromedriver-service": "^6.0.3"
|
||||
}
|
||||
}
|
BIN
MultiShop/client/public/favicon.ico
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
MultiShop/client/public/img/icons/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
MultiShop/client/public/img/icons/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 29 KiB |
After Width: | Height: | Size: 6.3 KiB |
After Width: | Height: | Size: 22 KiB |
BIN
MultiShop/client/public/img/icons/apple-touch-icon-120x120.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
MultiShop/client/public/img/icons/apple-touch-icon-152x152.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
MultiShop/client/public/img/icons/apple-touch-icon-180x180.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
MultiShop/client/public/img/icons/apple-touch-icon-60x60.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
MultiShop/client/public/img/icons/apple-touch-icon-76x76.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
MultiShop/client/public/img/icons/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
MultiShop/client/public/img/icons/favicon-16x16.png
Normal file
After Width: | Height: | Size: 799 B |
BIN
MultiShop/client/public/img/icons/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
MultiShop/client/public/img/icons/msapplication-icon-144x144.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
MultiShop/client/public/img/icons/mstile-150x150.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
3
MultiShop/client/public/img/icons/safari-pinned-tab.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.00251 14.9297L0 1.07422H6.14651L8.00251 4.27503L9.84583 1.07422H16L8.00251 14.9297Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 215 B |
17
MultiShop/client/public/index.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<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="<%= BASE_URL %>favicon.ico">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
2
MultiShop/client/public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow:
|
70
MultiShop/client/src/App.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div class="content light">
|
||||
<nav class="navbar" id="nav">
|
||||
<div class="container-fluid">
|
||||
<router-link class="navbar-brand" to="/">MultiShop</router-link>
|
||||
<button
|
||||
class="navbar-toggler"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarContent"
|
||||
>
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarContent">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<router-link
|
||||
active-class="active"
|
||||
class="nav-link"
|
||||
to="/"
|
||||
>Home</router-link
|
||||
>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<router-link
|
||||
active-class="active"
|
||||
class="nav-link"
|
||||
to="/about"
|
||||
>About</router-link
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav ms-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<ProfileDisplay>
|
||||
</ProfileDisplay>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<ProfileLogIn>
|
||||
</ProfileLogIn>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<ProfileLogOut>
|
||||
</ProfileLogOut>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import "bootstrap/js/dist/collapse";
|
||||
import "./assets/scss/main.scss";
|
||||
import ProfileDisplay from "./components/ProfileDisplay.vue";
|
||||
import ProfileLogIn from "./components/ProfileLogIn.vue";
|
||||
import ProfileLogOut from "./components/ProfileLogOut.vue";
|
||||
export default {
|
||||
components: {
|
||||
ProfileDisplay,
|
||||
ProfileLogIn,
|
||||
ProfileLogOut
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch("attemptSilentAuthentication");
|
||||
}
|
||||
};
|
||||
</script>
|
BIN
MultiShop/client/src/assets/images/logo.png
Normal file
After Width: | Height: | Size: 6.7 KiB |
10
MultiShop/client/src/assets/scss/_light.scss
Normal file
@@ -0,0 +1,10 @@
|
||||
@use "../../../node_modules/bootstrap/scss/bootstrap";
|
||||
|
||||
#app .content.light {
|
||||
// Add light theme specific scss here.
|
||||
nav.navbar {
|
||||
@extend .navbar-expand-lg;
|
||||
@extend .navbar-light;
|
||||
@extend .bg-light;
|
||||
}
|
||||
}
|
12
MultiShop/client/src/assets/scss/main.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
@use "light";
|
||||
@import "../../../node_modules/bootstrap-icons/font/bootstrap-icons.css";
|
||||
|
||||
html, body, #app, #app > .content {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
50
MultiShop/client/src/components/ProfileDisplay.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<a v-if="visible" href="authentication/profile" class="btn">
|
||||
<slot v-if="username" :displayName="username" name="username">
|
||||
{{ username }}
|
||||
</slot>
|
||||
<slot v-else-if="isProfileLoading" name="loading">
|
||||
<WaitCircle class="spinner-grow-sm"></WaitCircle>
|
||||
</slot>
|
||||
<slot v-else name="unauthenticated">
|
||||
<span>Not Logged In</span>
|
||||
</slot>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WaitCircle from "@/components/WaitCircle.vue";
|
||||
export default {
|
||||
components: {
|
||||
WaitCircle
|
||||
},
|
||||
name: "ProfileDisplay",
|
||||
props: {
|
||||
showUnauthenticated: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
username() {
|
||||
return this.$store.getters.username &&
|
||||
!this.$store.state.identity.loading
|
||||
? this.$store.getters.username
|
||||
: null;
|
||||
},
|
||||
visible() {
|
||||
return !this.showUnauthenticated
|
||||
? this.$store.getters.isAuthenticated ||
|
||||
this.$store.state.identity.loading
|
||||
: true;
|
||||
},
|
||||
isProfileLoading() {
|
||||
return this.$store.state.identity.loading;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
21
MultiShop/client/src/components/ProfileLogIn.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<button v-if="visible" @click="onClick" type="button" class="btn">
|
||||
<slot>Log In</slot>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ProfileLogin",
|
||||
computed: {
|
||||
visible() {
|
||||
return !this.$store.getters.isAuthenticated && !this.$store.state.identity.loading;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
this.$store.dispatch("beginAuthentication");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
21
MultiShop/client/src/components/ProfileLogOut.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<button v-if="visible" @click="onClick" type="button" class="btn">
|
||||
<slot>Log Out</slot>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ProfileLogOut",
|
||||
computed: {
|
||||
visible() {
|
||||
return this.$store.getters.isAuthenticated && !this.$store.state.identity.loading;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
this.$store.dispatch("beginDeauthentication");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
39
MultiShop/client/src/components/SearchBar.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<form autocomplete="off">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" :placeholder="placeholder" v-model="query" @keyup.enter="onEnter">
|
||||
<slot name="append"></slot>
|
||||
<button class="btn btn-outline-primary" type="button" @click="onClick">
|
||||
<i class="bi bi-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
placeholder: String,
|
||||
searchCallback: Function
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
query: "",
|
||||
disabled: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
this.disabled = true;
|
||||
this.searchCallback(this.query);
|
||||
this.disabled = false;
|
||||
},
|
||||
onEnter() {
|
||||
this.disabled = true;
|
||||
this.searchCallback(this.query);
|
||||
this.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
</script>
|
24
MultiShop/client/src/components/WaitCircle.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="mx-auto" id="spinner-container">
|
||||
<div class="spinner-grow" role="status" v-bind="$attrs">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
inheritAttrs: false
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#spinner-container {
|
||||
width: fit-content;
|
||||
}
|
||||
</style>
|
13
MultiShop/client/src/main.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
import "./registerServiceWorker";
|
||||
import router from "./router";
|
||||
import store from "./store";
|
||||
import "./assets/scss/main.scss";
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(store);
|
||||
app.use(router);
|
||||
|
||||
app.mount("#app");
|
32
MultiShop/client/src/registerServiceWorker.js
Normal file
@@ -0,0 +1,32 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import { register } from "register-service-worker";
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
register(`${process.env.BASE_URL}service-worker.js`, {
|
||||
ready() {
|
||||
console.log(
|
||||
"App is being served from cache by a service worker.\n" +
|
||||
"For more details, visit https://goo.gl/AFskqB"
|
||||
);
|
||||
},
|
||||
registered() {
|
||||
console.log("Service worker has been registered.");
|
||||
},
|
||||
cached() {
|
||||
console.log("Content has been cached for offline use.");
|
||||
},
|
||||
updatefound() {
|
||||
console.log("New content is downloading.");
|
||||
},
|
||||
updated() {
|
||||
console.log("New content is available; please refresh.");
|
||||
},
|
||||
offline() {
|
||||
console.log("No internet connection found. App is running in offline mode.");
|
||||
},
|
||||
error(error) {
|
||||
console.error("Error during service worker registration:", error);
|
||||
}
|
||||
});
|
||||
}
|
13
MultiShop/client/src/router/guards.js
Normal file
@@ -0,0 +1,13 @@
|
||||
function authenticationGuard(app, to, from, next) {
|
||||
if (to.authenticationRequired) {
|
||||
if (app.$store.getters.identity.isAuthenticated) {
|
||||
next();
|
||||
} else {
|
||||
app.$store.dispatch("beginAuthentication");
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
export { authenticationGuard };
|
11
MultiShop/client/src/router/index.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import { authenticationGuard } from "./guards";
|
||||
import { routes } from "./routes";
|
||||
const router = createRouter({
|
||||
history: createWebHistory(process.env.BASE_URL),
|
||||
routes
|
||||
});
|
||||
|
||||
router.beforeEach((to, from, next) => authenticationGuard(router, to, from, next));
|
||||
|
||||
export default router;
|
25
MultiShop/client/src/router/routes.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import Home from "../views/Home.vue";
|
||||
import Authentication from "../views/Authentication.vue";
|
||||
const routes = [
|
||||
{
|
||||
path: "/",
|
||||
name: "Home",
|
||||
component: Home
|
||||
},
|
||||
{
|
||||
path: "/about",
|
||||
name: "About",
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (about.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import(/* webpackChunkName: "about" */ "../views/About.vue")
|
||||
},
|
||||
{
|
||||
path: "/authentication/:action?",
|
||||
name: "Authentication",
|
||||
component: Authentication,
|
||||
props: true,
|
||||
}
|
||||
];
|
||||
|
||||
export { routes };
|
13
MultiShop/client/src/services/authentication.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { UserManager, WebStorageStateStore } from "oidc-client";
|
||||
|
||||
const userManager = new UserManager({
|
||||
authority: window.location.origin,
|
||||
client_id: "MultiShop",
|
||||
redirect_uri: window.location.origin + "/authentication/login-callback",
|
||||
post_logout_redirect_uri: window.location.origin + "/authentication/logout-callback",
|
||||
response_type: "code",
|
||||
scope: "openid profile",
|
||||
userStore: new WebStorageStateStore({ store: window.localStorage }),
|
||||
});
|
||||
|
||||
export { userManager };
|
23
MultiShop/client/src/services/http.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import axios from "axios";
|
||||
|
||||
let currentAuthorizationInterceptorID = null;
|
||||
|
||||
const http = axios.create({
|
||||
baseURL: window.location.origin + "/api",
|
||||
timeout: 2000,
|
||||
});
|
||||
|
||||
function addBearerTokenInterceptor(token) {
|
||||
currentAuthorizationInterceptorID = http.interceptors.request.use((config) => {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
return config;
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
function removeBearerTokenInterceptor() {
|
||||
http.interceptors.request.eject(currentAuthorizationInterceptorID);
|
||||
}
|
||||
|
||||
export { http, addBearerTokenInterceptor, removeBearerTokenInterceptor };
|
31
MultiShop/client/src/services/persistence.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const prefix = "MultiShop";
|
||||
function put(key, value) {
|
||||
if (value == null) return false;
|
||||
try {
|
||||
localStorage.setItem(prefix + ":" + key, JSON.stringify(value));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function get(key) {
|
||||
if (!exists(key)) return null;
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(prefix + ":" + key));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function remove(key) {
|
||||
localStorage.removeItem(prefix + ":" + key);
|
||||
}
|
||||
|
||||
function exists(key) {
|
||||
return localStorage.getItem(prefix + ":" + key) != null;
|
||||
}
|
||||
|
||||
export { put, get, remove, exists };
|
99
MultiShop/client/src/store/identity.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import router from "../router";
|
||||
import { userManager } from "../services/authentication";
|
||||
import { addBearerTokenInterceptor, removeBearerTokenInterceptor } from "../services/http";
|
||||
import { get, put } from "../services/persistence";
|
||||
|
||||
const identity = {
|
||||
state: () => ({
|
||||
user: null,
|
||||
loading: true,
|
||||
callbackPath: null,
|
||||
}),
|
||||
getters: {
|
||||
isAuthenticated(state) {
|
||||
return state.user ? !state.user.expired : false;
|
||||
},
|
||||
username(state) {
|
||||
return (state.user && state.user.profile.name) ? state.user.profile.name : null;
|
||||
},
|
||||
authCallbackLocation(state) {
|
||||
return state.callbackPath ? state.callbackPath : get("callbackPath");
|
||||
}
|
||||
},
|
||||
mutations: {
|
||||
login(state, { user }) {
|
||||
if (!user) return;
|
||||
state.user = user;
|
||||
addBearerTokenInterceptor(user.access_token);
|
||||
},
|
||||
logout(state) {
|
||||
state.user = null;
|
||||
removeBearerTokenInterceptor();
|
||||
},
|
||||
beginAuthenticating(state) {
|
||||
state.loading = true;
|
||||
},
|
||||
endAuthenticating(state) {
|
||||
state.loading = false;
|
||||
},
|
||||
authCallbackLocation(state) {
|
||||
state.callbackPath = window.location.pathname;
|
||||
put("callbackPath", state.callbackPath);
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async loadUser(context) {
|
||||
context.commit("beginAuthenticating");
|
||||
const user = await userManager.getUser();
|
||||
if (user != null) {
|
||||
context.commit("login", { user });
|
||||
}
|
||||
context.commit("endAuthenticating");
|
||||
},
|
||||
async attemptSilentAuthentication(context) {
|
||||
if (context.getters.isAuthenticated) return;
|
||||
context.commit("beginAuthenticating");
|
||||
context.dispatch("loadUser");
|
||||
if (!context.getters.isAuthenticated) {
|
||||
try {
|
||||
const user = await userManager.signinSilent({
|
||||
redirect_uri: window.location.origin + "/authentication/silent-login-callback"
|
||||
});
|
||||
|
||||
context.commit("login", { user });
|
||||
} catch { }
|
||||
context.commit("endAuthenticating");
|
||||
}
|
||||
},
|
||||
async beginAuthentication(context) {
|
||||
if (context.getters.isAuthenticated) return;
|
||||
context.commit("beginAuthenticating");
|
||||
context.dispatch("loadUser");
|
||||
if (!context.getters.isAuthenticated) {
|
||||
context.commit("authCallbackLocation");
|
||||
userManager.signinRedirect();
|
||||
}
|
||||
},
|
||||
async beginDeauthentication(context) {
|
||||
if (!context.getters.isAuthenticated) return;
|
||||
context.commit("beginAuthenticating");
|
||||
context.commit("authCallbackLocation");
|
||||
await userManager.removeUser();
|
||||
userManager.signoutRedirect();
|
||||
},
|
||||
async completeAuthentication(context, { user }) {
|
||||
if (!user) return;
|
||||
context.commit("login", { user });
|
||||
router.push(context.getters.authCallbackLocation);
|
||||
context.commit("endAuthenticating");
|
||||
},
|
||||
async completeDeauthentication(context) {
|
||||
if (!context.getters.isAuthenticated) return;
|
||||
context.commit("logout");
|
||||
router.push(context.getters.authCallbackLocation);
|
||||
context.commit("endAuthenticating");
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export { identity };
|
16
MultiShop/client/src/store/index.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { createStore } from "vuex";
|
||||
import { identity } from "./identity";
|
||||
|
||||
export default createStore({
|
||||
state: {
|
||||
},
|
||||
getters: {
|
||||
},
|
||||
mutations: {
|
||||
},
|
||||
actions: {
|
||||
},
|
||||
modules: {
|
||||
identity: identity,
|
||||
},
|
||||
});
|
5
MultiShop/client/src/views/About.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="about">
|
||||
<h1>This is an about page</h1>
|
||||
</div>
|
||||
</template>
|
68
MultiShop/client/src/views/Authentication.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div class="info">
|
||||
<WaitCircle class="text-primary">
|
||||
<span class="my-1 text-muted"
|
||||
>Hang on just a sec! Waiting for authentication stuff to
|
||||
finish.</span
|
||||
>
|
||||
</WaitCircle>
|
||||
<div class="my-1 text-muted" v-show="longTimePassed">
|
||||
This seems to be taking longer than expected...
|
||||
<router-link to="/">Return home</router-link>?
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WaitCircle from "@/components/WaitCircle.vue";
|
||||
import { userManager } from "../services/authentication";
|
||||
|
||||
export default {
|
||||
name: "Authentication",
|
||||
props: {
|
||||
action: String
|
||||
},
|
||||
components: {
|
||||
WaitCircle
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
longTimePassed: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
setTimeout(() => (this.longTimePassed = true), 5000);
|
||||
this.completeCallback();
|
||||
},
|
||||
methods: {
|
||||
async completeCallback() {
|
||||
if (this.action === "login-callback") {
|
||||
const user = await userManager.signinRedirectCallback();
|
||||
this.$store.dispatch("completeAuthentication", { user });
|
||||
} else if (this.action === "silent-login-callback") {
|
||||
await userManager.signinSilentCallback();
|
||||
} else if (this.action === "logout-callback") {
|
||||
await userManager.signoutRedirectCallback();
|
||||
this.$store.dispatch("completeDeauthentication");
|
||||
} else {
|
||||
console.warn("Unknown callback: " + this.action);
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
async action() {
|
||||
this.completeCallback();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
13
MultiShop/client/src/views/Home.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div class="home">
|
||||
<img alt="Vue logo" src="../assets/images/logo.png">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Home",
|
||||
components: {
|
||||
}
|
||||
};
|
||||
</script>
|
10
MultiShop/client/tests/e2e/.eslintrc.js
Normal file
@@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
plugins: ['wdio'],
|
||||
extends: 'plugin:wdio/recommended',
|
||||
env: {
|
||||
mocha: true
|
||||
},
|
||||
rules: {
|
||||
strict: 'off'
|
||||
}
|
||||
}
|
15
MultiShop/client/tests/e2e/pageobjects/app.page.js
Normal file
@@ -0,0 +1,15 @@
|
||||
class App {
|
||||
/**
|
||||
* elements
|
||||
*/
|
||||
get heading () { return $('h1') }
|
||||
|
||||
/**
|
||||
* methods
|
||||
*/
|
||||
open (path = '/') {
|
||||
browser.url(path)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new App()
|
8
MultiShop/client/tests/e2e/specs/app.spec.js
Normal file
@@ -0,0 +1,8 @@
|
||||
const App = require('../pageobjects/app.page')
|
||||
|
||||
describe('Vue.js app', () => {
|
||||
it('should open and render', () => {
|
||||
App.open()
|
||||
expect(App.heading).toHaveText('Welcome to Your Vue.js App')
|
||||
})
|
||||
})
|
13
MultiShop/client/tests/unit/example.spec.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { expect } from 'chai'
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import HelloWorld from '@/components/HelloWorld.vue'
|
||||
|
||||
describe('HelloWorld.vue', () => {
|
||||
it('renders props.msg when passed', () => {
|
||||
const msg = 'new message'
|
||||
const wrapper = shallowMount(HelloWorld, {
|
||||
props: { msg }
|
||||
})
|
||||
expect(wrapper.text()).to.include(msg)
|
||||
})
|
||||
})
|
24
MultiShop/client/wdio.local.conf.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const { config } = require('./wdio.shared.conf')
|
||||
|
||||
exports.config = {
|
||||
/**
|
||||
* base config
|
||||
*/
|
||||
...config,
|
||||
/**
|
||||
* config for local testing
|
||||
*/
|
||||
maxInstances: 1,
|
||||
services: ['chromedriver'],
|
||||
capabilities: [
|
||||
{
|
||||
browserName: 'chrome',
|
||||
acceptInsecureCerts: true,
|
||||
'goog:chromeOptions': {
|
||||
args: process.argv.includes('--headless')
|
||||
? ['--headless', '--disable-gpu']
|
||||
: []
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
41
MultiShop/client/wdio.sauce.conf.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const { config } = require('./wdio.shared.conf')
|
||||
|
||||
const BUILD_ID = Math.ceil(Date.now() / 1000)
|
||||
|
||||
exports.config = {
|
||||
/**
|
||||
* base config
|
||||
*/
|
||||
...config,
|
||||
/**
|
||||
* config for testing on Sauce Labs
|
||||
*/
|
||||
user: process.env.SAUCE_USERNAME,
|
||||
key: process.env.SAUCE_ACCESS_KEY,
|
||||
region: 'us',
|
||||
headless: process.argv.includes('--headless'),
|
||||
|
||||
services: [
|
||||
['sauce', {
|
||||
sauceConnect: true,
|
||||
tunnelIdentifier: 'Vue.js Integration tests'
|
||||
}]
|
||||
],
|
||||
|
||||
maxInstances: 10,
|
||||
capabilities: [{
|
||||
browserName: 'firefox',
|
||||
browserVersion: 'latest',
|
||||
platformName: 'Windows 10',
|
||||
'sauce:options': {
|
||||
build: `Build ${BUILD_ID}`
|
||||
}
|
||||
}, {
|
||||
browserName: 'chrome',
|
||||
browserVersion: 'latest',
|
||||
platformName: 'Windows 10',
|
||||
'sauce:options': {
|
||||
build: `Build ${BUILD_ID}`
|
||||
}
|
||||
}]
|
||||
}
|
210
MultiShop/client/wdio.shared.conf.js
Normal file
@@ -0,0 +1,210 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
const path = require('path')
|
||||
|
||||
exports.config = {
|
||||
// ==================
|
||||
// Specify Test Files
|
||||
// ==================
|
||||
// Define which test specs should run. The pattern is relative to the directory
|
||||
// from which `wdio` was called. Notice that, if you are calling `wdio` from an
|
||||
// NPM script (see https://docs.npmjs.com/cli/run-script) then the current working
|
||||
// directory is where your package.json resides, so `wdio` will be called from there.
|
||||
//
|
||||
specs: [
|
||||
path.join(__dirname, '/tests/e2e/**/*.spec.js')
|
||||
],
|
||||
// Patterns to exclude.
|
||||
exclude: [
|
||||
// 'test/spec/multibrowser/**',
|
||||
// 'test/spec/mobile/**'
|
||||
],
|
||||
//
|
||||
// ===================
|
||||
// Test Configurations
|
||||
// ===================
|
||||
// Define all options that are relevant for the WebdriverIO instance here
|
||||
//
|
||||
// Level of logging verbosity: trace | debug | info | warn | error | silent
|
||||
logLevel: 'trace',
|
||||
//
|
||||
// Set directory to store all logs into
|
||||
outputDir: path.join(__dirname, 'tests/e2e/logs'),
|
||||
//
|
||||
// If you only want to run your tests until a specific amount of tests have failed use
|
||||
// bail (default is 0 - don't bail, run all tests).
|
||||
bail: 0,
|
||||
//
|
||||
// Default timeout for all waitFor* commands.
|
||||
waitforTimeout: 1000,
|
||||
//
|
||||
// Framework you want to run your specs with.
|
||||
// The following are supported: Mocha, Jasmine, and Cucumber
|
||||
// see also: https://webdriver.io/docs/frameworks.html
|
||||
//
|
||||
// Make sure you have the wdio adapter package for the specific framework
|
||||
// installed before running any tests.
|
||||
framework: 'mocha',
|
||||
//
|
||||
// The number of times to retry the entire specfile when it fails as a whole
|
||||
specFileRetries: 1,
|
||||
//
|
||||
// Retried specfiles are inserted at the beginning of the queue and retried immediately
|
||||
specFileRetriesDeferred: false,
|
||||
//
|
||||
// Test reporter for stdout.
|
||||
// The only one supported by default is 'dot'
|
||||
// see also: https://webdriver.io/docs/dot-reporter.html
|
||||
reporters: ['spec'],
|
||||
//
|
||||
// Options to be passed to Mocha.
|
||||
// See the full list at http://mochajs.org/
|
||||
mochaOpts: {
|
||||
ui: 'bdd',
|
||||
timeout: 30000
|
||||
},
|
||||
//
|
||||
// =====
|
||||
// Hooks
|
||||
// =====
|
||||
// WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance
|
||||
// it and to build services around it. You can either apply a single function or an array of
|
||||
// methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got
|
||||
// resolved to continue.
|
||||
//
|
||||
/**
|
||||
* Gets executed once before all workers get launched.
|
||||
* @param {Object} config wdio configuration object
|
||||
* @param {Array.<Object>} capabilities list of capabilities details
|
||||
*/
|
||||
onPrepare: function (config, capabilities) {
|
||||
},
|
||||
/**
|
||||
* Gets executed before a worker process is spawned and can be used to initialise specific service
|
||||
* for that worker as well as modify runtime environments in an async fashion.
|
||||
* @param {String} cid capability id (e.g 0-0)
|
||||
* @param {[type]} caps object containing capabilities for session that will be spawn in the worker
|
||||
* @param {[type]} specs specs to be run in the worker process
|
||||
* @param {[type]} args object that will be merged with the main configuration once worker is initialised
|
||||
* @param {[type]} execArgv list of string arguments passed to the worker process
|
||||
*/
|
||||
onWorkerStart: function (cid, caps, specs, args, execArgv) {
|
||||
},
|
||||
/**
|
||||
* Gets executed just before initialising the webdriver session and test framework. It allows you
|
||||
* to manipulate configurations depending on the capability or spec.
|
||||
* @param {Object} config wdio configuration object
|
||||
* @param {Array.<Object>} capabilities list of capabilities details
|
||||
* @param {Array.<String>} specs List of spec file paths that are to be run
|
||||
*/
|
||||
beforeSession: function (config, capabilities, specs) {
|
||||
},
|
||||
/**
|
||||
* Gets executed before test execution begins. At this point you can access to all global
|
||||
* variables like `browser`. It is the perfect place to define custom commands.
|
||||
* @param {Array.<Object>} capabilities list of capabilities details
|
||||
* @param {Array.<String>} specs List of spec file paths that are to be run
|
||||
*/
|
||||
before: function (capabilities, specs) {
|
||||
},
|
||||
/**
|
||||
* Hook that gets executed before the suite starts
|
||||
* @param {Object} suite suite details
|
||||
*/
|
||||
beforeSuite: function (suite) {
|
||||
},
|
||||
/**
|
||||
* Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling
|
||||
* beforeEach in Mocha)
|
||||
* stepData and world are Cucumber framework specific
|
||||
*/
|
||||
beforeHook: function (test, context/*, stepData, world */) {
|
||||
},
|
||||
/**
|
||||
* Hook that gets executed _after_ a hook within the suite ends (e.g. runs after calling
|
||||
* afterEach in Mocha)
|
||||
* stepData and world are Cucumber framework specific
|
||||
*/
|
||||
afterHook: function (test, context, { error, result, duration, passed, retries }/*, stepData, world */) {
|
||||
},
|
||||
/**
|
||||
* Function to be executed before a test (in Mocha/Jasmine) starts.
|
||||
*/
|
||||
beforeTest: function (test, context) {
|
||||
},
|
||||
//
|
||||
/**
|
||||
* Runs before a WebdriverIO command gets executed.
|
||||
* @param {String} commandName command name
|
||||
* @param {Array} args arguments that command would receive
|
||||
*/
|
||||
beforeCommand: function (commandName, args) {
|
||||
},
|
||||
/**
|
||||
* Runs after a WebdriverIO command gets executed.
|
||||
* @param {String} commandName hook command name
|
||||
* @param {Array} args arguments that command would receive
|
||||
* @param {Number} result 0 - command success, 1 - command error
|
||||
* @param {Object} error error object if any
|
||||
*/
|
||||
afterCommand: function (commandName, args, result, error) {
|
||||
},
|
||||
/**
|
||||
* Function to be executed after a test (in Mocha/Jasmine) ends.
|
||||
*/
|
||||
afterTest: function (test, context, { error, result, duration, passed, retries }) {
|
||||
},
|
||||
/**
|
||||
* Hook that gets executed after the suite has ended
|
||||
* @param {Object} suite suite details
|
||||
*/
|
||||
afterSuite: function (suite) {
|
||||
},
|
||||
/**
|
||||
* Gets executed after all tests are done. You still have access to all global variables from
|
||||
* the test.
|
||||
* @param {Number} result 0 - test pass, 1 - test fail
|
||||
* @param {Array.<Object>} capabilities list of capabilities details
|
||||
* @param {Array.<String>} specs List of spec file paths that ran
|
||||
*/
|
||||
after: function (result, capabilities, specs) {
|
||||
},
|
||||
/**
|
||||
* Gets executed right after terminating the webdriver session.
|
||||
* @param {Object} config wdio configuration object
|
||||
* @param {Array.<Object>} capabilities list of capabilities details
|
||||
* @param {Array.<String>} specs List of spec file paths that ran
|
||||
*/
|
||||
afterSession: function (config, capabilities, specs) {
|
||||
},
|
||||
/**
|
||||
* Gets executed after all workers got shut down and the process is about to exit. An error
|
||||
* thrown in the onComplete hook will result in the test run failing.
|
||||
* @param {Object} exitCode 0 - success, 1 - fail
|
||||
* @param {Object} config wdio configuration object
|
||||
* @param {Array.<Object>} capabilities list of capabilities details
|
||||
* @param {<Object>} results object containing test results
|
||||
*/
|
||||
onComplete: function (exitCode, config, capabilities, results) {
|
||||
},
|
||||
/**
|
||||
* Gets executed when a refresh happens.
|
||||
* @param {String} oldSessionId session ID of the old session
|
||||
* @param {String} newSessionId session ID of the new session
|
||||
*/
|
||||
onReload: function (oldSessionId, newSessionId) {
|
||||
},
|
||||
//
|
||||
// Cucumber specific hooks
|
||||
beforeFeature: function (uri, feature, scenarios) {
|
||||
},
|
||||
beforeScenario: function (uri, feature, scenario, sourceLocation) {
|
||||
},
|
||||
beforeStep: function ({ uri, feature, step }, context) {
|
||||
},
|
||||
afterStep: function ({ uri, feature, step }, context, { error, result, duration, passed, retries }) {
|
||||
},
|
||||
afterScenario: function (uri, feature, scenario, result, sourceLocation) {
|
||||
},
|
||||
afterFeature: function (uri, feature, scenarios) {
|
||||
}
|
||||
}
|