Began transition to Vue3.

Implemented logging in and logging out.

Implemented authenticated http client.

Laid some groundwork for SCSS.
This commit is contained in:
Harrison Deng 2021-07-09 21:47:40 -05:00
parent 54b1565537
commit 3d3c43b944
284 changed files with 20487 additions and 38338 deletions

11
.vscode/launch.json vendored
View File

@ -1,11 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch and Debug Standalone Blazor WebAssembly App",
"type": "blazorwasm",
"request": "launch",
"cwd": "${workspaceFolder}/src/MultiShop/Server"
}
]
}

View File

@ -2,7 +2,7 @@
<ItemGroup>
<ProjectReference Include="..\Framework\MultiShop.Shop.Framework.csproj" />
<ProjectReference Include="..\..\..\SimpleLogger\SimpleLogger.csproj" />
<ProjectReference Include="..\..\SimpleLogger\SimpleLogger.csproj" />
</ItemGroup>
<PropertyGroup>

View File

@ -2,7 +2,7 @@
<ItemGroup>
<ProjectReference Include="..\Framework\MultiShop.Shop.Framework.csproj" />
<ProjectReference Include="..\..\..\SimpleLogger\SimpleLogger.csproj" />
<ProjectReference Include="..\..\SimpleLogger\SimpleLogger.csproj" />
</ItemGroup>
<ItemGroup>

43
MultiShop/.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,43 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: Current File",
"type": "python",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal"
},
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
"name": ".NET Core Launch (web)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/server/bin/Debug/net5.0/MultiShop.dll",
"args": [],
"cwd": "${workspaceFolder}/server",
"stopAtEntry": false,
// Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
}
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach",
"processId": "${command:pickProcess}"
},
]
}

11
MultiShop/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,11 @@
{
"eslint.workingDirectories": [
"./client"
],
"eslint.validate": [
"vue",
"javascript",
],
"javascript.preferences.quoteStyle": "double",
"vetur.format.options.tabSize": 4
}

View File

@ -7,7 +7,7 @@
"type": "process",
"args": [
"build",
"${workspaceFolder}/src/MultiShop/Server/MultiShop.Server.csproj",
"${workspaceFolder}/server/MultiShop.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
@ -19,24 +19,20 @@
"type": "process",
"args": [
"publish",
"${workspaceFolder}/src/MultiShop/Server/MultiShop.Server.csproj",
"${workspaceFolder}/server/MultiShop.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "watch",
"command": "dotnet",
"label": "watch all",
"command": "py",
"type": "process",
"args": [
"watch",
"run",
"${workspaceFolder}/src/MultiShop/Server/MultiShop.Server.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
"scripts/watch_all.py"
],
"problemMatcher": "$msCompile"
}
"problemMatcher": ["$msCompile", "$node-sass", "$jshint"],
},
]
}

View File

@ -0,0 +1,3 @@
> 1%
last 2 versions
not dead

View 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

View 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
View 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?

View 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/).

View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

View File

@ -0,0 +1,5 @@
{
"include": [
"./src/**/*"
]
}

18567
MultiShop/client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View 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

View 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>

View File

@ -0,0 +1,2 @@
User-agent: *
Disallow:

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View 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;
}
}

View 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;
}

View 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>

View 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>

View 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>

View 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>

View 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>

View 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");

View 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);
}
});
}

View 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 };

View 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;

View 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 };

View 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 };

View 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 };

View 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 };

View 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 };

View File

@ -0,0 +1,16 @@
import { createStore } from "vuex";
import { identity } from "./identity";
export default createStore({
state: {
},
getters: {
},
mutations: {
},
actions: {
},
modules: {
identity: identity,
},
});

View File

@ -0,0 +1,5 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>

View 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>

View 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>

View File

@ -0,0 +1,10 @@
module.exports = {
plugins: ['wdio'],
extends: 'plugin:wdio/recommended',
env: {
mocha: true
},
rules: {
strict: 'off'
}
}

View File

@ -0,0 +1,15 @@
class App {
/**
* elements
*/
get heading () { return $('h1') }
/**
* methods
*/
open (path = '/') {
browser.url(path)
}
}
module.exports = new App()

View 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')
})
})

View 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)
})
})

View 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']
: []
}
}
]
}

View 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}`
}
}]
}

View 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) {
}
}

3
MultiShop/package-lock.json generated Normal file
View File

@ -0,0 +1,3 @@
{
"lockfileVersion": 1
}

View File

@ -0,0 +1,28 @@
import os
import shutil
SERVER_DIR = "src/MultiShop/Server"
DATA_DIR = "Data"
DB_MIGRATE_CMD = "dotnet ef migrations add InitialCreate -o {0}"
DB_UPDATE_CMD = "dotnet ef database update"
os.chdir(os.path.dirname(os.path.realpath(__file__)))
os.chdir("..")
os.chdir(SERVER_DIR)
print("Working in: " + os.getcwd())
migrationsDir = os.path.join(DATA_DIR, "Migrations")
print("Deleting current migrations directory if it exists.")
shutil.rmtree(migrationsDir, ignore_errors=True)
print("Deleting old app.db if it exists.")
if os.path.exists("app.db"):
os.remove("app.db")
print("Creating migration.")
os.system(DB_MIGRATE_CMD.format(migrationsDir))
print("Updating database.")
os.system(DB_UPDATE_CMD)

View File

@ -0,0 +1,29 @@
import os
import asyncio
import sys
SERVER_CSPROJ_DIR = "server"
CLIENT_PACKAGE_DIR = "client"
async def exec(cmd, path):
os.chdir(os.path.dirname(os.path.realpath(__file__)))
os.chdir(os.pardir)
os.chdir(path)
print("Executing \"{0}\" in \"{1}\".".format(cmd, path))
proc = await asyncio.create_subprocess_shell(
cmd,
stdout=sys.stdout,
stderr=sys.stderr,
)
await proc.wait()
async def main():
print("Beginning development servers.")
await asyncio.gather(
exec("dotnet watch run", SERVER_CSPROJ_DIR),
exec("npm run serve", CLIENT_PACKAGE_DIR))
asyncio.run(main())

231
MultiShop/server/.gitignore vendored Normal file
View File

@ -0,0 +1,231 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
build/
bld/
bin/
Bin/
obj/
Obj/
# Visual Studio 2015 cache/options directory
.vs/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# TODO: Comment the next line if you want to checkin your web deploy settings
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Microsoft Azure ApplicationInsights config file
ApplicationInsights.config
# Windows Store app package directory
AppPackages/
BundleArtifacts/
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.pfx
*.publishsettings
orleans.codegen.cs
/node_modules
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
# FAKE - F# Make
.fake/

View File

@ -2,7 +2,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace MultiShop.Server.Controllers
namespace MultiShop.Controllers
{
public class OidcConfigurationController : Controller
{

View File

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace MultiShop.Controllers
{
[Authorize]
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
var rng = new Random();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToArray();
}
}
}

View File

@ -0,0 +1,21 @@
using MultiShop.Models;
using IdentityServer4.EntityFramework.Options;
using Microsoft.AspNetCore.ApiAuthorization.IdentityServer;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace MultiShop.Data
{
public class ApplicationDbContext : ApiAuthorizationDbContext<ApplicationUser>
{
public ApplicationDbContext(
DbContextOptions options,
IOptions<OperationalStoreOptions> operationalStoreOptions) : base(options, operationalStoreOptions)
{
}
}
}

View File

@ -1,22 +1,86 @@
// <auto-generated />
using System;
using MultiShop.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using MultiShop.Server.Data;
namespace MultiShop.Server.Data.Migrations
namespace MultiShop.Data.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20210531175621_InitialCreate")]
partial class InitialCreate
[Migration("00000000000000_CreateIdentitySchema")]
partial class CreateIdentitySchema
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "5.0.6");
.HasAnnotation("ProductVersion", "5.0.0-rc.1.20417.2");
modelBuilder.Entity("MultiShop.Models.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b =>
{
@ -253,180 +317,6 @@ namespace MultiShop.Server.Data.Migrations
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("MultiShop.Server.Models.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("MultiShop.Shared.Models.ApplicationProfile", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ApplicationUserId")
.HasColumnType("TEXT");
b.Property<bool>("CacheCommonSearches")
.HasColumnType("INTEGER");
b.Property<bool>("DarkMode")
.HasColumnType("INTEGER");
b.Property<bool>("EnableSearchHistory")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ApplicationUserId")
.IsUnique();
b.ToTable("ApplicationProfile");
});
modelBuilder.Entity("MultiShop.Shared.Models.ResultsProfile", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ApplicationUserId")
.HasColumnType("TEXT");
b.Property<string>("Order")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ApplicationUserId")
.IsUnique();
b.ToTable("ResultsProfile");
});
modelBuilder.Entity("MultiShop.Shared.Models.SearchProfile", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ApplicationUserId")
.HasColumnType("TEXT");
b.Property<int>("Currency")
.HasColumnType("INTEGER");
b.Property<bool>("EnableMaxShippingFee")
.HasColumnType("INTEGER");
b.Property<bool>("EnableUpperPrice")
.HasColumnType("INTEGER");
b.Property<bool>("KeepUnknownPurchaseCount")
.HasColumnType("INTEGER");
b.Property<bool>("KeepUnknownRatingCount")
.HasColumnType("INTEGER");
b.Property<bool>("KeepUnknownShipping")
.HasColumnType("INTEGER");
b.Property<bool>("KeepUnrated")
.HasColumnType("INTEGER");
b.Property<int>("LowerPrice")
.HasColumnType("INTEGER");
b.Property<int>("MaxResults")
.HasColumnType("INTEGER");
b.Property<int>("MaxShippingFee")
.HasColumnType("INTEGER");
b.Property<int>("MinPurchases")
.HasColumnType("INTEGER");
b.Property<float>("MinRating")
.HasColumnType("REAL");
b.Property<int>("MinReviews")
.HasColumnType("INTEGER");
b.Property<string>("ShopStates")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("UpperPrice")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ApplicationUserId")
.IsUnique();
b.ToTable("SearchProfile");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
@ -438,7 +328,7 @@ namespace MultiShop.Server.Data.Migrations
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("MultiShop.Server.Models.ApplicationUser", null)
b.HasOne("MultiShop.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
@ -447,7 +337,7 @@ namespace MultiShop.Server.Data.Migrations
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("MultiShop.Server.Models.ApplicationUser", null)
b.HasOne("MultiShop.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
@ -462,7 +352,7 @@ namespace MultiShop.Server.Data.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("MultiShop.Server.Models.ApplicationUser", null)
b.HasOne("MultiShop.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
@ -471,45 +361,12 @@ namespace MultiShop.Server.Data.Migrations
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("MultiShop.Server.Models.ApplicationUser", null)
b.HasOne("MultiShop.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("MultiShop.Shared.Models.ApplicationProfile", b =>
{
b.HasOne("MultiShop.Server.Models.ApplicationUser", null)
.WithOne("ApplicationProfile")
.HasForeignKey("MultiShop.Shared.Models.ApplicationProfile", "ApplicationUserId");
});
modelBuilder.Entity("MultiShop.Shared.Models.ResultsProfile", b =>
{
b.HasOne("MultiShop.Server.Models.ApplicationUser", null)
.WithOne("ResultsProfile")
.HasForeignKey("MultiShop.Shared.Models.ResultsProfile", "ApplicationUserId");
});
modelBuilder.Entity("MultiShop.Shared.Models.SearchProfile", b =>
{
b.HasOne("MultiShop.Server.Models.ApplicationUser", null)
.WithOne("SearchProfile")
.HasForeignKey("MultiShop.Shared.Models.SearchProfile", "ApplicationUserId");
});
modelBuilder.Entity("MultiShop.Server.Models.ApplicationUser", b =>
{
b.Navigation("ApplicationProfile")
.IsRequired();
b.Navigation("ResultsProfile")
.IsRequired();
b.Navigation("SearchProfile")
.IsRequired();
});
#pragma warning restore 612, 618
}
}

View File

@ -1,9 +1,9 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace MultiShop.Server.Data.Migrations
namespace MultiShop.Data.Migrations
{
public partial class InitialCreate : Migration
public partial class CreateIdentitySchema : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
@ -106,28 +106,6 @@ namespace MultiShop.Server.Data.Migrations
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ApplicationProfile",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ApplicationUserId = table.Column<string>(type: "TEXT", nullable: true),
DarkMode = table.Column<bool>(type: "INTEGER", nullable: false),
CacheCommonSearches = table.Column<bool>(type: "INTEGER", nullable: false),
EnableSearchHistory = table.Column<bool>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ApplicationProfile", x => x.Id);
table.ForeignKey(
name: "FK_ApplicationProfile_AspNetUsers_ApplicationUserId",
column: x => x.ApplicationUserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "AspNetUserClaims",
columns: table => new
@ -213,66 +191,6 @@ namespace MultiShop.Server.Data.Migrations
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ResultsProfile",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ApplicationUserId = table.Column<string>(type: "TEXT", nullable: true),
Order = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ResultsProfile", x => x.Id);
table.ForeignKey(
name: "FK_ResultsProfile_AspNetUsers_ApplicationUserId",
column: x => x.ApplicationUserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "SearchProfile",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ApplicationUserId = table.Column<string>(type: "TEXT", nullable: true),
Currency = table.Column<int>(type: "INTEGER", nullable: false),
MaxResults = table.Column<int>(type: "INTEGER", nullable: false),
MinRating = table.Column<float>(type: "REAL", nullable: false),
KeepUnrated = table.Column<bool>(type: "INTEGER", nullable: false),
EnableUpperPrice = table.Column<bool>(type: "INTEGER", nullable: false),
UpperPrice = table.Column<int>(type: "INTEGER", nullable: false),
LowerPrice = table.Column<int>(type: "INTEGER", nullable: false),
MinPurchases = table.Column<int>(type: "INTEGER", nullable: false),
KeepUnknownPurchaseCount = table.Column<bool>(type: "INTEGER", nullable: false),
MinReviews = table.Column<int>(type: "INTEGER", nullable: false),
KeepUnknownRatingCount = table.Column<bool>(type: "INTEGER", nullable: false),
EnableMaxShippingFee = table.Column<bool>(type: "INTEGER", nullable: false),
MaxShippingFee = table.Column<int>(type: "INTEGER", nullable: false),
KeepUnknownShipping = table.Column<bool>(type: "INTEGER", nullable: false),
ShopStates = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SearchProfile", x => x.Id);
table.ForeignKey(
name: "FK_SearchProfile_AspNetUsers_ApplicationUserId",
column: x => x.ApplicationUserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_ApplicationProfile_ApplicationUserId",
table: "ApplicationProfile",
column: "ApplicationUserId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_AspNetRoleClaims_RoleId",
table: "AspNetRoleClaims",
@ -335,25 +253,10 @@ namespace MultiShop.Server.Data.Migrations
name: "IX_PersistedGrants_SubjectId_SessionId_Type",
table: "PersistedGrants",
columns: new[] { "SubjectId", "SessionId", "Type" });
migrationBuilder.CreateIndex(
name: "IX_ResultsProfile_ApplicationUserId",
table: "ResultsProfile",
column: "ApplicationUserId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_SearchProfile_ApplicationUserId",
table: "SearchProfile",
column: "ApplicationUserId",
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ApplicationProfile");
migrationBuilder.DropTable(
name: "AspNetRoleClaims");
@ -375,12 +278,6 @@ namespace MultiShop.Server.Data.Migrations
migrationBuilder.DropTable(
name: "PersistedGrants");
migrationBuilder.DropTable(
name: "ResultsProfile");
migrationBuilder.DropTable(
name: "SearchProfile");
migrationBuilder.DropTable(
name: "AspNetRoles");

View File

@ -1,11 +1,11 @@
// <auto-generated />
using System;
using MultiShop.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using MultiShop.Server.Data;
namespace MultiShop.Server.Data.Migrations
namespace MultiShop.Data.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
@ -14,7 +14,71 @@ namespace MultiShop.Server.Data.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "5.0.6");
.HasAnnotation("ProductVersion", "5.0.0-rc.1.20417.2");
modelBuilder.Entity("MultiShop.Models.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b =>
{
@ -251,180 +315,6 @@ namespace MultiShop.Server.Data.Migrations
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("MultiShop.Server.Models.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("MultiShop.Shared.Models.ApplicationProfile", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ApplicationUserId")
.HasColumnType("TEXT");
b.Property<bool>("CacheCommonSearches")
.HasColumnType("INTEGER");
b.Property<bool>("DarkMode")
.HasColumnType("INTEGER");
b.Property<bool>("EnableSearchHistory")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ApplicationUserId")
.IsUnique();
b.ToTable("ApplicationProfile");
});
modelBuilder.Entity("MultiShop.Shared.Models.ResultsProfile", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ApplicationUserId")
.HasColumnType("TEXT");
b.Property<string>("Order")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ApplicationUserId")
.IsUnique();
b.ToTable("ResultsProfile");
});
modelBuilder.Entity("MultiShop.Shared.Models.SearchProfile", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ApplicationUserId")
.HasColumnType("TEXT");
b.Property<int>("Currency")
.HasColumnType("INTEGER");
b.Property<bool>("EnableMaxShippingFee")
.HasColumnType("INTEGER");
b.Property<bool>("EnableUpperPrice")
.HasColumnType("INTEGER");
b.Property<bool>("KeepUnknownPurchaseCount")
.HasColumnType("INTEGER");
b.Property<bool>("KeepUnknownRatingCount")
.HasColumnType("INTEGER");
b.Property<bool>("KeepUnknownShipping")
.HasColumnType("INTEGER");
b.Property<bool>("KeepUnrated")
.HasColumnType("INTEGER");
b.Property<int>("LowerPrice")
.HasColumnType("INTEGER");
b.Property<int>("MaxResults")
.HasColumnType("INTEGER");
b.Property<int>("MaxShippingFee")
.HasColumnType("INTEGER");
b.Property<int>("MinPurchases")
.HasColumnType("INTEGER");
b.Property<float>("MinRating")
.HasColumnType("REAL");
b.Property<int>("MinReviews")
.HasColumnType("INTEGER");
b.Property<string>("ShopStates")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("UpperPrice")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ApplicationUserId")
.IsUnique();
b.ToTable("SearchProfile");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
@ -436,7 +326,7 @@ namespace MultiShop.Server.Data.Migrations
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("MultiShop.Server.Models.ApplicationUser", null)
b.HasOne("MultiShop.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
@ -445,7 +335,7 @@ namespace MultiShop.Server.Data.Migrations
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("MultiShop.Server.Models.ApplicationUser", null)
b.HasOne("MultiShop.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
@ -460,7 +350,7 @@ namespace MultiShop.Server.Data.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("MultiShop.Server.Models.ApplicationUser", null)
b.HasOne("MultiShop.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
@ -469,45 +359,12 @@ namespace MultiShop.Server.Data.Migrations
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("MultiShop.Server.Models.ApplicationUser", null)
b.HasOne("MultiShop.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("MultiShop.Shared.Models.ApplicationProfile", b =>
{
b.HasOne("MultiShop.Server.Models.ApplicationUser", null)
.WithOne("ApplicationProfile")
.HasForeignKey("MultiShop.Shared.Models.ApplicationProfile", "ApplicationUserId");
});
modelBuilder.Entity("MultiShop.Shared.Models.ResultsProfile", b =>
{
b.HasOne("MultiShop.Server.Models.ApplicationUser", null)
.WithOne("ResultsProfile")
.HasForeignKey("MultiShop.Shared.Models.ResultsProfile", "ApplicationUserId");
});
modelBuilder.Entity("MultiShop.Shared.Models.SearchProfile", b =>
{
b.HasOne("MultiShop.Server.Models.ApplicationUser", null)
.WithOne("SearchProfile")
.HasForeignKey("MultiShop.Shared.Models.SearchProfile", "ApplicationUserId");
});
modelBuilder.Entity("MultiShop.Server.Models.ApplicationUser", b =>
{
b.Navigation("ApplicationProfile")
.IsRequired();
b.Navigation("ResultsProfile")
.IsRequired();
b.Navigation("SearchProfile")
.IsRequired();
});
#pragma warning restore 612, 618
}
}

View File

@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Identity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace MultiShop.Models
{
public class ApplicationUser : IdentityUser
{
}
}

View File

@ -0,0 +1,64 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
<IsPackable>false</IsPackable>
<SpaRoot>../client/</SpaRoot>
<DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules\**</DefaultItemExcludes>
<!-- Set this to true if you enable server-side prerendering -->
<BuildServerSideRenderer>false</BuildServerSideRenderer>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="5.0.5" />
<PackageReference Include="Microsoft.AspNetCore.ApiAuthorization.IdentityServer" Version="5.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="5.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="5.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="5.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.5" />
</ItemGroup>
<ItemGroup>
<None Update="app.db" CopyToOutputDirectory="PreserveNewest" ExcludeFromSingleFile="true" />
</ItemGroup>
<ItemGroup>
<!-- Don't publish the SPA source files, but do show them in the project files list -->
<Content Remove="$(SpaRoot)**" />
<None Remove="$(SpaRoot)**" />
<None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**" />
</ItemGroup>
<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">
<!-- Ensure Node.js is installed -->
<Exec Command="node --version" ContinueOnError="true">
<Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
</Exec>
<Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." />
<Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." />
<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
</Target>
<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
<Exec WorkingDirectory="$(SpaRoot)" Command="npm run build -- --prod" />
<Exec WorkingDirectory="$(SpaRoot)" Command="npm run build:ssr -- --prod" Condition=" '$(BuildServerSideRenderer)' == 'true' " />
<!-- Include the newly-built files in the publish output -->
<ItemGroup>
<DistFiles Include="$(SpaRoot)dist\**; $(SpaRoot)dist-server\**" />
<DistFiles Include="$(SpaRoot)node_modules\**" Condition="'$(BuildServerSideRenderer)' == 'true'" />
<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
<RelativePath>%(DistFiles.Identity)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</ResolvedFileToPublish>
</ItemGroup>
</Target>
</Project>

View File

@ -0,0 +1,26 @@
@page
@model ErrorModel
@{
ViewData["Title"] = "Error";
}
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (Model.ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
@ -7,16 +7,11 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
namespace MultiShop.Server.Pages
namespace MultiShop.Pages
{
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
public string RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
private readonly ILogger<ErrorModel> _logger;
public ErrorModel(ILogger<ErrorModel> logger)
@ -24,6 +19,10 @@ namespace MultiShop.Server.Pages
_logger = logger;
}
public string RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
public void OnGet()
{
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;

View File

@ -0,0 +1,36 @@
@using Microsoft.AspNetCore.Identity
@using MultiShop.Models;
@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager
@{
string returnUrl = null;
var query = ViewContext.HttpContext.Request.Query;
if (query.ContainsKey("returnUrl"))
{
returnUrl = query["returnUrl"];
}
}
<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
<li class="nav-item">
<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">Hello @User.Identity.Name!</a>
</li>
<li class="nav-item">
<form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="/">
<button type="submit" class="nav-link btn btn-link text-dark">Logout</button>
</form>
</li>
}
else
{
<li class="nav-item">
<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Register" asp-route-returnUrl="@returnUrl">Register</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Login" asp-route-returnUrl="@returnUrl">Login</a>
</li>
}
</ul>

View File

@ -0,0 +1,3 @@
@using MultiShop
@namespace MultiShop.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View File

@ -1,10 +1,13 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace MultiShop.Server
namespace MultiShop
{
public class Program
{
@ -15,11 +18,6 @@ namespace MultiShop.Server
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostingContext, config) =>
config.AddInMemoryCollection(new Dictionary<string, string>() {
{"IdentityServer:Clients:MultiShop.Client:Profile", "IdentityServerSPA"}
})
)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();

View File

@ -1,26 +1,23 @@
{
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:5738",
"sslPort": 44353
"applicationUrl": "http://localhost:61909",
"sslPort": 44392
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"MultiShop": {
"commandName": "Project",
"dotnetRunMessages": "true",
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"

View File

@ -1,37 +1,36 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.SpaServices.AngularCli;
using Microsoft.EntityFrameworkCore;
using MultiShop.Data;
using MultiShop.Models;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Linq;
using MultiShop.Server.Data;
using MultiShop.Server.Models;
using Microsoft.AspNetCore.Identity;
using System.Security.Claims;
namespace MultiShop.Server
namespace MultiShop
{
public class Startup
{
public Startup(IConfiguration configuration)
public Startup(IConfiguration configuration, IHostEnvironment environment)
{
Configuration = configuration;
Environment = environment;
}
public IConfiguration Configuration { get; }
public IHostEnvironment Environment { get; }
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options => {
options.UseLazyLoadingProxies();
options.UseSqlite(Configuration.GetConnectionString("DefaultConnection"));
});
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDatabaseDeveloperPageExceptionFilter();
@ -39,15 +38,23 @@ namespace MultiShop.Server
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
services.Configure<IdentityOptions>(Options => Options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier); //Note: Despite default, doesn't work without this.
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>(
options => {
options.Clients.AddIdentityServerSPA("MultiShop", spa => {
spa.WithRedirectUri("/authentication/silent-login-callback");
spa.WithRedirectUri("/authentication/login-callback");
spa.WithRedirectUri("/authentication/logout-callback");
});
});
services.AddAuthentication()
.AddIdentityServerJwt();
services.AddControllersWithViews();
services.AddRazorPages();
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "client/dist";
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@ -57,7 +64,6 @@ namespace MultiShop.Server
{
app.UseDeveloperExceptionPage();
app.UseMigrationsEndPoint();
app.UseWebAssemblyDebugging();
}
else
{
@ -67,20 +73,33 @@ namespace MultiShop.Server
}
app.UseHttpsRedirection();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
if (!env.IsDevelopment())
{
app.UseSpaStaticFiles();
}
app.UseRouting();
app.UseIdentityServer();
app.UseAuthentication();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
endpoints.MapRazorPages();
endpoints.MapControllers();
endpoints.MapFallbackToFile("index.html");
});
app.UseSpa(spa =>
{
spa.Options.SourcePath = "../client"; // "May not exist in published applications" - https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.spaservices.spaoptions.sourcepath?view=aspnetcore-5.0#Microsoft_AspNetCore_SpaServices_SpaOptions_SourcePath
if (env.IsDevelopment())
{
spa.UseProxyToSpaDevelopmentServer(Configuration["SPA:BaseUri"]);
}
});
}
}

View File

@ -0,0 +1,15 @@
using System;
namespace MultiShop
{
public class WeatherForecast
{
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string Summary { get; set; }
}
}

View File

@ -1,10 +1,12 @@
{
"SPA": {
"BaseUri": "http://localhost:8080"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"MultiShop": "Debug"
"Microsoft.Hosting.Lifetime": "Information"
}
},
"IdentityServer": {

View File

@ -0,0 +1,13 @@
{
"ConnectionStrings": {
"DefaultConnection": "DataSource=app.db;Cache=Shared"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

14
MultiShop/vetur.config.js Normal file
View File

@ -0,0 +1,14 @@
module.exports = {
// **optional** default: `{}`
// override vscode settings
// Notice: It only affects the settings used by Vetur.
settings: {
"vetur.useWorkspaceDependencies": true,
"vetur.experimental.templateInterpolationService": true
},
// **optional** default: `[{ root: './' }]`
// support monorepos
projects: [
"./client"
]
};

View File

@ -1,39 +0,0 @@
<CascadingAuthenticationState>
<CascadingDependencies Dependencies="@dependencies">
<Content>
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" Context="authState">
<NotAuthorized>
@if (!authState.User.Identity.IsAuthenticated)
{
<RedirectToLogin />
}
else
{
<p>You are not authorized to access this resource.</p>
}
</NotAuthorized>
</AuthorizeRouteView>
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</Content>
<LoadingContent Context="Status">
<div class="d-flex flex-column align-items-center justify-content-center" style="width: 100vw; height: 100vh;">
<div class="my-2">
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
<div class="my-2">
Loading @Status...
</div>
</div>
</LoadingContent>
</CascadingDependencies>
</CascadingAuthenticationState>

View File

@ -1,53 +0,0 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.Logging;
using Microsoft.JSInterop;
using MultiShop.Client.Module;
using MultiShop.Shared.Models;
using MultiShop.Shop.Framework;
namespace MultiShop.Client
{
public partial class App
{
[Inject]
private IJSRuntime JS { get; set; }
[Inject]
private IHttpClientFactory HttpClientFactory {get; set;}
private ICollection<RuntimeDependencyManager.Dependency> dependencies = new List<RuntimeDependencyManager.Dependency>();
protected override void OnInitialized()
{
base.OnInitialized();
dependencies.Add(new RuntimeDependencyManager.Dependency(typeof(IReadOnlyDictionary<string, IShop>), "Shops", async (publicHttp, authenticatedHttp, auth, logger) => await (new ShopModuleLoader(publicHttp, logger)).GetShops()));
dependencies.Add(new RuntimeDependencyManager.Dependency(typeof(ApplicationProfile), "Application Profile", DownloadApplicationProfile));
}
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
}
private async ValueTask<object> DownloadApplicationProfile(HttpClient publicHttp, HttpClient http, AuthenticationState authState, ILogger logger)
{
if (authState.User.Identity.IsAuthenticated)
{
logger.LogDebug($"User is logged in. Attempting to fetch application profile.");
HttpResponseMessage response = await http.GetAsync("Profile/Application");
if (response.IsSuccessStatusCode)
{
return await response.Content.ReadFromJsonAsync<ApplicationProfile>();
}
}
ApplicationProfile profile = await JS.InvokeAsync<ApplicationProfile>("MultiShop.LocalStorageManager.retrieve", "ApplicationProfile");
if (profile != null) return profile;
return new ApplicationProfile();
}
}
}

View File

@ -1,47 +0,0 @@
using System;
using MultiShop.Shared;
using MultiShop.Shared.Models;
namespace MultiShop.Client.Extensions
{
public static class ResultProfileExtensions
{
public static int? CompareListings(this ResultsProfile.Category category, ProductListingInfo a, ProductListingInfo b)
{
switch (category)
{
case ResultsProfile.Category.RatingPriceRatio:
float? dealDiff = a.RatingToPriceRatio - b.RatingToPriceRatio;
if (!dealDiff.HasValue) return null;
int dealCeil = (int)Math.Ceiling(Math.Abs(dealDiff.Value));
return dealDiff < 0 ? -dealCeil : dealCeil;
case ResultsProfile.Category.Price:
float priceDiff = b.Listing.UpperPrice - a.Listing.UpperPrice;
int priceCeil = (int)Math.Ceiling(Math.Abs(priceDiff));
return priceDiff < 0 ? -priceCeil : priceCeil;
case ResultsProfile.Category.Purchases:
return a.Listing.PurchaseCount - b.Listing.PurchaseCount;
case ResultsProfile.Category.Reviews:
return a.Listing.ReviewCount - b.Listing.ReviewCount;
}
throw new ArgumentException($"{category} does not have a defined comparison.");
}
public static string FriendlyName(this ResultsProfile.Category category)
{
switch (category)
{
case ResultsProfile.Category.RatingPriceRatio:
return "Best rating to price ratio first";
case ResultsProfile.Category.Price:
return "Lowest price first";
case ResultsProfile.Category.Purchases:
return "Most purchases first";
case ResultsProfile.Category.Reviews:
return "Most reviews first";
}
throw new ArgumentException($"{category} does not have a friendly name defined.");
}
}
}

View File

@ -1,9 +0,0 @@
using MultiShop.Shared.Models;
namespace MultiShop.Client.Extensions
{
public static class SearchProfileExtensions
{
}
}

View File

@ -1,22 +0,0 @@
using MultiShop.Shared.Models;
namespace MultiShop.Client.Extensions
{
public static class applicationProfileExtensions
{
public static string GetButtonCssClass(this ApplicationProfile applicationProfile, string otherClasses = "", bool outline = false) {
if (outline) {
return otherClasses + (applicationProfile.DarkMode ? " btn btn-outline-light" : " btn btn-outline-dark");
}
return otherClasses + (applicationProfile.DarkMode ? " btn btn-light" : " btn btn-dark");
}
public static string GetPageCssClass(this ApplicationProfile applicationProfile, string otherClasses = "") {
return otherClasses + (applicationProfile.DarkMode ? " text-white bg-dark" : " text-dark bg-white");
}
public static string GetNavCssClass(this ApplicationProfile applicationProfile, string otherClasses = "") {
return otherClasses + (applicationProfile.DarkMode ? " navbar-dark bg-dark" : " navbar-light bg-light");
}
}
}

Some files were not shown because too many files have changed in this diff Show More