Added sign up button and improved logout flow.

Also added proper link to profile management.
This commit is contained in:
Harrison Deng 2021-07-12 03:06:13 -05:00
parent 9e55b459fc
commit c597d65256
11 changed files with 127 additions and 27 deletions

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 }),
});
userManager.signinSilentCallback();

View File

@ -0,0 +1,16 @@
<!-- Completely separate static html should improve silent login performance as we don't need to load entire SPA. -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>authentication</title>
<script type="module" src="callback-handler.js"></script>
</head>
<body>
<div>
Silently authenticating user. If you are seeing this page, you probably want to <a href="/">go back to the app</a>.
</div>
</body>
</html>

View File

@ -35,6 +35,10 @@
<ProfileDisplay>
</ProfileDisplay>
</li>
<li class="nav-item">
<ProfileSignUp>
</ProfileSignUp>
</li>
<li class="nav-item">
<ProfileLogIn>
</ProfileLogIn>
@ -57,14 +61,17 @@ import "./assets/scss/main.scss";
import ProfileDisplay from "./components/ProfileDisplay.vue";
import ProfileLogIn from "./components/ProfileLogIn.vue";
import ProfileLogOut from "./components/ProfileLogOut.vue";
import ProfileSignUp from "./components/ProfileSignUp.vue";
export default {
components: {
ProfileDisplay,
ProfileLogIn,
ProfileSignUp,
ProfileLogOut
},
mounted() {
this.$store.dispatch("attemptSilentAuthentication");
this.$store.dispatch("updatePublicApiSettings");
this.$store.dispatch("loadUser");
}
};
</script>

View File

@ -1,5 +1,5 @@
<template>
<a v-if="visible" href="authentication/profile" class="btn">
<a v-if="visible" href="Identity/Account/Manage" class="btn">
<slot v-if="username" :displayName="username" name="username">
{{ username }}
</slot>
@ -13,7 +13,7 @@
</template>
<script>
import WaitCircle from "@/components/WaitCircle.vue";
import WaitCircle from "./WaitCircle.vue";
export default {
components: {
WaitCircle
@ -32,18 +32,18 @@ export default {
computed: {
username() {
return this.$store.getters.username &&
!this.$store.state.identity.loading
!this.$store.getters.isIdentityLoading
? this.$store.getters.username
: null;
},
visible() {
return !this.showUnauthenticated
? this.$store.getters.isAuthenticated ||
this.$store.state.identity.loading
this.$store.getters.isIdentityLoading
: true;
},
isProfileLoading() {
return this.$store.state.identity.loading;
return this.$store.getters.isIdentityLoading;
}
}
};

View File

@ -6,10 +6,10 @@
<script>
export default {
name: "ProfileLogin",
name: "ProfileLogIn",
computed: {
visible() {
return !this.$store.getters.isAuthenticated && !this.$store.state.identity.loading;
return !this.$store.getters.isAuthenticated && !this.$store.getters.isIdentityLoading;
}
},
methods: {

View File

@ -9,7 +9,7 @@ export default {
name: "ProfileLogOut",
computed: {
visible() {
return this.$store.getters.isAuthenticated && !this.$store.state.identity.loading;
return this.$store.getters.isAuthenticated && !this.$store.getters.isIdentityLoading;
}
},
methods: {

View File

@ -0,0 +1,21 @@
<template>
<button v-if="visible" @click="onClick" type="button" class="btn">
<slot>Sign Up!</slot>
</button>
</template>
<script>
export default {
name: "ProfileSignUp",
computed: {
visible() {
return this.$store.state.identity.registrationEnabled && !this.$store.getters.isAuthenticated && !this.$store.getters.isIdentityLoading;
}
},
methods: {
onClick() {
this.$store.dispatch("beginRegistration");
}
}
};
</script>

View File

@ -10,4 +10,13 @@ const userManager = new UserManager({
userStore: new WebStorageStateStore({ store: window.localStorage }),
});
export { userManager };
const identityPaths = {
Manage: "Identity/Account/Manage",
Register: "/Identity/Account/Register"
};
const identityQueryParameters = {
ReturnUrl: "returnUrl",
};
export { userManager, identityPaths, identityQueryParameters };

View File

@ -1,24 +1,28 @@
import router from "../router";
import { userManager } from "../services/authentication";
import { addBearerTokenInterceptor, removeBearerTokenInterceptor } from "../services/http";
import { identityPaths, identityQueryParameters, userManager } from "../services/authentication";
import { addBearerTokenInterceptor, http, removeBearerTokenInterceptor } from "../services/http";
import { get, put } from "../services/persistence";
const identity = {
state: () => ({
user: null,
loading: true,
loadingLayers: 0,
callbackPath: null,
registrationEnabled: false,
}),
getters: {
isAuthenticated(state) {
return state.user ? !state.user.expired : false;
},
isIdentityLoading(state) {
return state.loadingLayers > 0;
},
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 }) {
@ -31,14 +35,20 @@ const identity = {
removeBearerTokenInterceptor();
},
beginAuthenticating(state) {
state.loading = true;
state.loading += 1;
},
endAuthenticating(state) {
state.loading = false;
state.loading -= 1;
},
authCallbackLocation(state) {
state.callbackPath = window.location.pathname;
put("callbackPath", state.callbackPath);
},
enableIdentificationRegistration(state) {
state.registrationEnabled = true;
},
disableIdentificationRegistration(state) {
state.registrationEnabled = false;
}
},
actions: {
@ -50,20 +60,24 @@ const identity = {
}
context.commit("endAuthenticating");
},
async attemptSilentAuthentication(context) {
async attemptSilentAuthentication(context, options) {
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"
redirect_uri: window.location.origin + "/authentication/silent-login-callback.html"
});
context.commit("login", { user });
} catch { }
context.commit("endAuthenticating");
}
if (options && options.redirect) {
router.replace(context.getters.authCallbackLocation);
}
},
async beginAuthentication(context) {
if (context.getters.isAuthenticated) return;
@ -78,20 +92,39 @@ const identity = {
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");
router.replace(context.getters.authCallbackLocation);
},
async completeDeauthentication(context) {
if (!context.getters.isAuthenticated) return;
context.commit("logout");
router.push(context.getters.authCallbackLocation);
try {
await userManager.removeUser();
} catch (error) {
console.error(error);
}
context.commit("endAuthenticating");
router.replace(context.getters.authCallbackLocation);
},
beginRegistration(context) {
context.commit("authCallbackLocation");
window.location.replace(window.location.origin + identityPaths.Register + "?" + identityQueryParameters.ReturnUrl + "=" + "/authentication/silent-login");
},
async updatePublicApiSettings(context) {
try {
const settings = (await http.get("PublicApiSettings")).data;
if (settings.RegistrationEnabled === "True") {
context.commit("enableIdentificationRegistration");
} else {
context.commit("disableIdentificationRegistration");
}
} catch (error) {
console.error(error);
}
}
},
};

View File

@ -14,7 +14,7 @@
</template>
<script>
import WaitCircle from "@/components/WaitCircle.vue";
import WaitCircle from "../components/WaitCircle.vue";
import { userManager } from "../services/authentication";
export default {
@ -36,14 +36,15 @@ export default {
},
methods: {
async completeCallback() {
console.log("Completing callback for " + this.action);
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 if (this.action === "silent-login") {
this.$store.dispatch("attemptSilentAuthentication", { redirect: true });
} else {
console.warn("Unknown callback: " + this.action);
}

View File

@ -37,9 +37,9 @@ namespace MultiShop
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>(
options => {
options.Clients.AddIdentityServerSPA("MultiShop", spa => {
spa.WithRedirectUri("/authentication/silent-login-callback");
spa.WithRedirectUri("/authentication/silent-login-callback.html");
spa.WithRedirectUri("/authentication/login-callback");
spa.WithRedirectUri("/authentication/logout-callback");
spa.WithLogoutRedirectUri("/authentication/logout-callback");
});
});