Refactored user store & moved popup logic from App to store

Cleaned up bits of all the components that use these stores.

User store now focuses around keeping track of the authorization token
and the response from /login. When a sucessfull login request is made we
save our new token and username & admin data to the with login(). Since
cookies aren't implemented yet we keep track of the auth_token to make
authroized requests back to the api later.
The username and admin data from within the body of the token is saved
and only cleared on logout().
Since we haven't implemented cookies we persist storage with
localStorage. Whenever we successfully decode and save a token body we
also save the token to localStorage. This is later used by
initFromLocalStorage() to hydrate the store on first page load.

Popup module is for opening and closing the popup, and now moved away
from a inline plugin in App entry. Now handles loading from &
updating query parameters type=movie | show.
The route listens checks if open every navigation and closes popup if it
is.
This commit is contained in:
2022-01-12 22:20:12 +01:00
parent d1cbbfffd8
commit b021882013
14 changed files with 723 additions and 685 deletions

View File

@@ -3,26 +3,31 @@
<section class="not-found">
<h1 class="not-found__title">Page Not Found</h1>
</section>
<seasoned-button class="button" @click="goBack">go back to previous page</seasoned-button>
<seasoned-button class="button" @click="goBack"
>go back to previous page</seasoned-button
>
</div>
</template>
<script>
import store from '@/store'
import SeasonedButton from '@/components/ui/SeasonedButton'
import { mapActions, mapGetters } from "vuex";
import SeasonedButton from "@/components/ui/SeasonedButton";
export default {
components: { SeasonedButton },
computed: {
...mapGetters("popup", ["isOpen"])
},
methods: {
...mapActions("popup", ["close"]),
goBack() {
this.$router.go(-1)
this.$router.go(-1);
}
},
created() {
if (this.$popup.isOpen == true)
this.$popup.close()
if (this.isOpen) this.close();
}
}
};
</script>
<style lang="scss" scoped>
@@ -47,7 +52,7 @@ export default {
.not-found {
display: flex;
height: calc(100vh - var(--header-size));
background: url('~assets/pulp-fiction.jpg') no-repeat 50% 50%;
background: url("~assets/pulp-fiction.jpg") no-repeat 50% 50%;
background-size: cover;
align-items: center;
flex-direction: column;

View File

@@ -1,64 +1,58 @@
<template>
<div class="movie-popup" @click="$popup.close()">
<div v-if="isOpen" class="movie-popup" @click="close">
<div class="movie-popup__box" @click.stop>
<movie :id="id" :type="type"></movie>
<button class="movie-popup__close" @click="$popup.close()"></button>
<button class="movie-popup__close" @click="close"></button>
</div>
<i class="loader"></i>
</div>
</template>
<script>
import { mapActions, mapGetters } from "vuex";
import Movie from "./Movie";
export default {
props: {
id: {
type: Number,
required: true
},
type: {
type: String,
required: true
components: { Movie },
computed: {
...mapGetters("popup", ["isOpen", "id", "type"])
},
watch: {
isOpen(value) {
value
? document.getElementsByTagName("body")[0].classList.add("no-scroll")
: document
.getElementsByTagName("body")[0]
.classList.remove("no-scroll");
}
},
components: { Movie },
methods: {
...mapActions("popup", ["close", "open"]),
checkEventForEscapeKey(event) {
if (event.keyCode == 27) {
this.$popup.close();
}
},
updateQueryParams(id = false) {
const params = new URLSearchParams(window.location.search);
if (params.has("movie")) {
params.delete("movie");
}
if (id) {
params.append("movie", id);
}
window.history.replaceState(
{},
"search",
`${window.location.protocol}//${window.location.hostname}${
window.location.port ? `:${window.location.port}` : ""
}${window.location.pathname}${
params.toString().length ? `?${params}` : ""
}`
);
if (event.keyCode == 27) this.close();
}
},
created() {
this.updateQueryParams(this.id);
const params = new URLSearchParams(window.location.search);
let id = null;
let type = null;
if (params.has("movie")) {
id = Number(params.get("movie"));
type = "movie";
} else if (params.has("show")) {
id = Number(params.get("show"));
type = "show";
}
if (id && type) {
this.open({ id, type });
}
window.addEventListener("keyup", this.checkEventForEscapeKey);
document.getElementsByTagName("body")[0].classList.add("no-scroll");
},
beforeDestroy() {
this.updateQueryParams();
window.removeEventListener("keyup", this.checkEventForEscapeKey);
document.getElementsByTagName("body")[0].classList.remove("no-scroll");
}
};
</script>

View File

@@ -4,7 +4,7 @@
<img
class="movie-item__img"
ref="poster-image"
@click="openMoviePopup(movie.id, movie.type)"
@click="openMoviePopup"
:alt="posterAltText"
:data-src="poster"
src="~assets/placeholder.png"
@@ -27,6 +27,7 @@
</template>
<script>
import { mapActions } from "vuex";
import img from "../directives/v-image";
export default {
@@ -101,8 +102,12 @@ export default {
imageObserver.observe(poster);
},
methods: {
openMoviePopup(id, type) {
this.$popup.open(id, type);
...mapActions("popup", ["open"]),
openMoviePopup() {
this.open({
id: this.movie.id,
type: this.movie.type
});
}
}
};

View File

@@ -1,6 +1,6 @@
<template>
<section class="profile">
<div class="profile__content" v-if="userLoggedIn">
<div class="profile__content" v-if="loggedIn">
<header class="profile__header">
<h2 class="profile__title">{{ emoji }} Welcome {{ username }}</h2>
@@ -12,7 +12,7 @@
showActivity ? "hide activity" : "show activity"
}}</seasoned-button>
<seasoned-button @click="logOut">Log out</seasoned-button>
<seasoned-button @click="_logout">Log out</seasoned-button>
</div>
</header>
@@ -24,7 +24,7 @@
<results-list v-if="results" :results="results" />
</div>
<section class="not-found" v-if="!userLoggedIn">
<section class="not-found" v-if="!loggedIn">
<div class="not-found__content">
<h2 class="not-found__title">Authentication Request Failed</h2>
<router-link :to="{ name: 'signin' }" exact title="Sign in here">
@@ -36,21 +36,19 @@
</template>
<script>
import storage from "@/storage";
import store from "@/store";
import { mapGetters, mapActions } from "vuex";
import ListHeader from "@/components/ListHeader";
import ResultsList from "@/components/ResultsList";
import Settings from "@/components/Settings";
import Activity from "@/components/ActivityPage";
import SeasonedButton from "@/components/ui/SeasonedButton";
import { getEmoji, getUserRequests } from "@/api";
import { getEmoji, getUserRequests, getSettings } from "@/api";
export default {
components: { ListHeader, ResultsList, Settings, Activity, SeasonedButton },
data() {
return {
userLoggedIn: "",
emoji: "",
results: undefined,
totalResults: undefined,
@@ -59,16 +57,17 @@ export default {
};
},
computed: {
...mapGetters("user", ["loggedIn", "username", "settings"]),
resultCount() {
if (this.results === undefined) return;
const loadedResults = this.results.length;
const totalResults = this.totalResults < 10000 ? this.totalResults : "∞";
return `${loadedResults} of ${totalResults} results`;
},
username: () => store.getters["userModule/username"]
}
},
methods: {
...mapActions("user", ["logout", "updateSettings"]),
toggleSettings() {
this.showSettings = this.showSettings ? false : true;
@@ -98,16 +97,20 @@ export default {
this.showActivity = this.showActivity == true ? false : true;
this.updateQueryParams("activity", this.showActivity);
},
logOut() {
this.$router.push("logout");
_logout() {
this.logout();
this.$router.push("home");
}
},
created() {
if (!localStorage.getItem("token")) {
this.userLoggedIn = false;
} else {
this.userLoggedIn = true;
if (!this.settings) {
getSettings().then(resp => {
const { settings } = resp;
if (settings) updateSettings(settings);
});
}
if (this.loggedIn) {
this.showSettings = window.location.toString().includes("settings=true");
this.showActivity = window.location.toString().includes("activity=true");

View File

@@ -1,47 +1,77 @@
<template>
<section class="profile">
<div class="profile__content" v-if="userLoggedIn">
<section class='settings'>
<h3 class='settings__header'>Plex account</h3>
<div class="profile__content" v-if="loggedIn">
<section class="settings">
<h3 class="settings__header">Plex account</h3>
<div v-if="!hasPlexUser">
<span class="settings__info">Sign in to your plex account to get information about recently added movies and to see your watch history</span>
<div v-if="!plexId">
<span class="settings__info"
>Sign in to your plex account to get information about recently
added movies and to see your watch history</span
>
<form class="form">
<seasoned-input placeholder="plex username" icon="Email" :value.sync="plexUsername"/>
<seasoned-input placeholder="plex password" icon="Keyhole" type="password"
:value.sync="plexPassword" @submit="authenticatePlex" />
<seasoned-input
placeholder="plex username"
icon="Email"
:value.sync="plexUsername"
/>
<seasoned-input
placeholder="plex password"
icon="Keyhole"
type="password"
:value.sync="plexPassword"
@submit="authenticatePlex"
/>
<seasoned-button @click="authenticatePlex">link plex account</seasoned-button>
<seasoned-button @click="authenticatePlex"
>link plex account</seasoned-button
>
</form>
</div>
<div v-else>
<span class="settings__info">Awesome, your account is already authenticated with plex! Enjoy viewing your seasoned search history, plex watch history and real-time torrent download progress.</span>
<seasoned-button @click="unauthenticatePlex">un-link plex account</seasoned-button>
<span class="settings__info"
>Awesome, your account is already authenticated with plex! Enjoy
viewing your seasoned search history, plex watch history and
real-time torrent download progress.</span
>
<seasoned-button @click="unauthenticatePlex"
>un-link plex account</seasoned-button
>
</div>
<seasoned-messages :messages.sync="messages" />
<hr class='setting__divider'>
<hr class="setting__divider" />
<h3 class='settings__header'>Change password</h3>
<h3 class="settings__header">Change password</h3>
<form class="form">
<seasoned-input placeholder="new password" icon="Keyhole" type="password"
:value.sync="newPassword" />
<seasoned-input
placeholder="new password"
icon="Keyhole"
type="password"
:value.sync="newPassword"
/>
<seasoned-input placeholder="repeat new password" icon="Keyhole" type="password"
:value.sync="newPasswordRepeat" />
<seasoned-input
placeholder="repeat new password"
icon="Keyhole"
type="password"
:value.sync="newPasswordRepeat"
/>
<seasoned-button @click="changePassword">change password</seasoned-button>
<seasoned-button @click="changePassword"
>change password</seasoned-button
>
</form>
<hr class='setting__divider'>
<hr class="setting__divider" />
</section>
</div>
<section class="not-found" v-else>
<div class="not-found__content">
<h2 class="not-found__title">Authentication Request Failed</h2>
<router-link :to="{name: 'signin'}" exact title="Sign in here">
<router-link :to="{ name: 'signin' }" exact title="Sign in here">
<button class="not-found__button button">Sign In</button>
</router-link>
</div>
@@ -50,82 +80,68 @@
</template>
<script>
import store from '@/store'
import storage from '@/storage'
import SeasonedInput from '@/components/ui/SeasonedInput'
import SeasonedButton from '@/components/ui/SeasonedButton'
import SeasonedMessages from '@/components/ui/SeasonedMessages'
import { mapGetters, mapState, mapActions } from "vuex";
import storage from "@/storage";
import SeasonedInput from "@/components/ui/SeasonedInput";
import SeasonedButton from "@/components/ui/SeasonedButton";
import SeasonedMessages from "@/components/ui/SeasonedMessages";
import { getSettings, updateSettings, linkPlexAccount, unlinkPlexAccount } from '@/api'
import { linkPlexAccount, unlinkPlexAccount, getSettings } from "@/api";
export default {
components: { SeasonedInput, SeasonedButton, SeasonedMessages },
data(){
return{
userLoggedIn: '',
data() {
return {
messages: [],
plexUsername: null,
plexPassword: null,
newPassword: null,
newPasswordRepeat: null,
emoji: null
}
};
},
computed: {
hasPlexUser: function() {
return this.settings && this.settings['plex_userid']
},
settings: {
get: () => {
return store.getters['userModule/settings']
},
set: function(newSettings) {
store.dispatch('userModule/setSettings', newSettings)
}
}
...mapGetters("user", ["loggedIn", "plexId", "settings"])
},
methods: {
setValue(l, t) {
this[l] = t
},
...mapActions("user", ["login", "updateSettings"]),
changePassword() {
return
return;
},
created() {
if (!this.settings) {
console.log("settings does not exists.", this.settings);
getSettings().then(resp => {
const { settings } = resp;
if (settings) updateSettings(settings);
});
}
},
async authenticatePlex() {
let username = this.plexUsername
let password = this.plexPassword
let username = this.plexUsername;
let password = this.plexPassword;
const response = await linkPlexAccount(username, password)
const { success, message } = await linkPlexAccount(username, password);
this.messages.push({
type: response.success ? 'success' : 'error',
title: response.success ? 'Authenticated with plex' : 'Something went wrong',
message: response.message
})
if (response.success)
getSettings().then(settings => this.settings = settings)
type: success ? "success" : "error",
title: success ? "Authenticated with plex" : "Something went wrong",
message: message
});
},
async unauthenticatePlex() {
const response = await unlinkPlexAccount()
const response = await unlinkPlexAccount();
this.messages.push({
type: response.success ? 'success' : 'error',
title: response.success ? 'Unlinked plex account ' : 'Something went wrong',
type: response.success ? "success" : "error",
title: response.success
? "Unlinked plex account "
: "Something went wrong",
message: response.message
})
if (response.success)
getSettings().then(settings => this.settings = settings)
}
},
created(){
const token = localStorage.getItem('token') || false;
if (token){
this.userLoggedIn = true
});
}
}
}
};
</script>
<style lang="scss" scoped>
@@ -133,7 +149,7 @@ export default {
@import "./src/scss/media-queries";
a {
text-decoration: none;
text-decoration: none;
}
// DUPLICATE CODE
@@ -142,22 +158,22 @@ a {
margin-top: 1rem;
}
&__group{
justify-content: unset;
&__input-icon {
margin-top: 8px;
height: 22px;
width: 22px;
}
&-input {
padding: 10px 5px 10px 45px;
height: 40px;
font-size: 17px;
width: 75%;
@include desktop-min {
width: 400px;
}
}
&__group {
justify-content: unset;
&__input-icon {
margin-top: 8px;
height: 22px;
width: 22px;
}
&-input {
padding: 10px 5px 10px 45px;
height: 40px;
font-size: 17px;
width: 75%;
@include desktop-min {
width: 400px;
}
}
}
}
.settings {
@@ -167,32 +183,32 @@ a {
padding: 1rem;
}
&__header {
margin: 0;
line-height: 16px;
color: $text-color;
font-weight: 300;
margin-bottom: 20px;
text-transform: uppercase;
}
&__info {
display: block;
margin-bottom: 25px;
}
hr {
display: block;
height: 1px;
border: 0;
border-bottom: 1px solid $text-color-50;
margin-top: 30px;
margin-bottom: 70px;
margin-left: 20px;
width: 96%;
text-align: left;
}
span {
font-weight: 200;
size: 16px;
}
&__header {
margin: 0;
line-height: 16px;
color: $text-color;
font-weight: 300;
margin-bottom: 20px;
text-transform: uppercase;
}
&__info {
display: block;
margin-bottom: 25px;
}
hr {
display: block;
height: 1px;
border: 0;
border-bottom: 1px solid $text-color-50;
margin-top: 30px;
margin-bottom: 70px;
margin-left: 20px;
width: 96%;
text-align: left;
}
span {
font-weight: 200;
size: 16px;
}
}
</style>

View File

@@ -2,88 +2,94 @@
<section>
<h1>Sign in</h1>
<seasoned-input placeholder="username"
icon="Email"
type="email"
@enter="submit"
:value.sync="username" />
<seasoned-input placeholder="password" icon="Keyhole" type="password" :value.sync="password" @enter="submit"/>
<seasoned-input
placeholder="username"
icon="Email"
type="email"
@enter="submit"
:value.sync="username"
/>
<seasoned-input
placeholder="password"
icon="Keyhole"
type="password"
:value.sync="password"
@enter="submit"
/>
<seasoned-button @click="submit">sign in</seasoned-button>
<router-link class="link" to="/register">Don't have a user? Register here</router-link>
<router-link class="link" to="/register"
>Don't have a user? Register here</router-link
>
<seasoned-messages :messages.sync="messages"></seasoned-messages>
</section>
</template>
<script>
import { login } from '@/api'
import storage from '../storage'
import SeasonedInput from '@/components/ui/SeasonedInput'
import SeasonedButton from '@/components/ui/SeasonedButton'
import SeasonedMessages from '@/components/ui/SeasonedMessages'
import { parseJwt } from '@/utils'
import { mapActions } from "vuex";
import { login } from "@/api";
import storage from "../storage";
import SeasonedInput from "@/components/ui/SeasonedInput";
import SeasonedButton from "@/components/ui/SeasonedButton";
import SeasonedMessages from "@/components/ui/SeasonedMessages";
export default {
components: { SeasonedInput, SeasonedButton, SeasonedMessages },
data(){
return{
data() {
return {
messages: [],
username: null,
password: null
}
};
},
methods: {
setValue(l, t) {
this[l] = t
},
...mapActions("user", ["login"]),
submit() {
this.messages = [];
let username = this.username;
let password = this.password;
let { username, password } = this;
if (username == null || username.length == 0) {
this.messages.push({ type: 'error', title: 'Missing username' })
return
if (!username || username.length == 0) {
this.messages.push({ type: "error", title: "Missing username" });
return;
}
if (password == null || password.length == 0) {
this.messages.push({ type: 'error', title: 'Missing password' })
return
if (!password || password.length == 0) {
this.messages.push({ type: "error", title: "Missing password" });
return;
}
this.signin(username, password)
this.signin(username, password);
},
signin(username, password) {
login(username, password, true)
.then(data => {
if (data.success){
const jwtData = parseJwt(data.token)
localStorage.setItem('token', data.token);
localStorage.setItem('username', jwtData['username']);
localStorage.setItem('admin', jwtData['admin'] || false);
eventHub.$emit('setUserStatus');
this.$router.push({ name: 'profile' })
if (data.success && this.login(data.token)) {
this.$router.push({ name: "profile" });
}
})
.catch(error => {
if (error.status === 401) {
this.messages.push({ type: 'error', title: 'Access denied', message: 'Incorrect username or password' })
}
else {
this.messages.push({ type: 'error', title: 'Unexpected error', message: error.message })
this.messages.push({
type: "error",
title: "Access denied",
message: "Incorrect username or password"
});
} else {
this.messages.push({
type: "error",
title: "Unexpected error",
message: error.message
});
}
});
}
},
created(){
document.title = 'Sign in' + storage.pageTitlePostfix;
created() {
document.title = "Sign in" + storage.pageTitlePostfix;
storage.backTitle = document.title;
}
}
};
</script>
<style lang="scss" scoped>