mirror of
https://github.com/go-shiori/shiori.git
synced 2025-09-27 15:26:36 +08:00
refactor: move login.html into a login component (#1017)
* feat: Add login component JavaScript file * feat: Create login component and refactor login view * refactor: Convert login to single-page application with dynamic component rendering * feat: Enhance session validation and login form display logic * fix: Resolve Vue app mounting and method duplication issues * fix: Prevent null reference error when focusing username input * fix: Initialize `isLoggedIn` to true to show login form during async check * refactor: Improve session validation and login flow logic * fix: Adjust login component visibility and initial login state * feat: Add login form template to login component * feat: Update login template to match original login.html design * fix: Resolve login view rendering and state management issues * refactor: Remove login route from frontend routes * refactor: Remove login-footer from login component template * fix: Modify logout to show login form without redirecting * refactor: Remove /login route test for SPA architecture * refactor: delete login.html file * style: Remove extra blank line in frontend_test.go * chore: run make style changes
This commit is contained in:
parent
617f5ddcd7
commit
fb51755e32
5 changed files with 217 additions and 187 deletions
|
@ -48,12 +48,6 @@ type FrontendRoutes struct {
|
||||||
|
|
||||||
func (r *FrontendRoutes) Setup(e *gin.Engine) {
|
func (r *FrontendRoutes) Setup(e *gin.Engine) {
|
||||||
group := e.Group("/")
|
group := e.Group("/")
|
||||||
group.GET("/login", func(ctx *gin.Context) {
|
|
||||||
ctx.HTML(http.StatusOK, "login.html", gin.H{
|
|
||||||
"RootPath": r.cfg.Http.RootPath,
|
|
||||||
"Version": model.BuildVersion,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
group.GET("/", func(ctx *gin.Context) {
|
group.GET("/", func(ctx *gin.Context) {
|
||||||
ctx.HTML(http.StatusOK, "index.html", gin.H{
|
ctx.HTML(http.StatusOK, "index.html", gin.H{
|
||||||
"RootPath": r.cfg.Http.RootPath,
|
"RootPath": r.cfg.Http.RootPath,
|
||||||
|
|
|
@ -31,13 +31,6 @@ func TestFrontendRoutes(t *testing.T) {
|
||||||
require.Equal(t, 200, w.Code)
|
require.Equal(t, 200, w.Code)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("/login", func(t *testing.T) {
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
req, _ := http.NewRequest("GET", "/login", nil)
|
|
||||||
g.ServeHTTP(w, req)
|
|
||||||
require.Equal(t, 200, w.Code)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("/css/style.css", func(t *testing.T) {
|
t.Run("/css/style.css", func(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
req, _ := http.NewRequest("GET", "/assets/css/style.css", nil)
|
req, _ := http.NewRequest("GET", "/assets/css/style.css", nil)
|
||||||
|
|
152
internal/view/assets/js/component/login.js
Normal file
152
internal/view/assets/js/component/login.js
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
const template = `
|
||||||
|
<div id="login-scene">
|
||||||
|
<p class="error-message" v-if="error !== ''">{{error}}</p>
|
||||||
|
<div id="login-box">
|
||||||
|
<form @submit.prevent="login">
|
||||||
|
<div id="logo-area">
|
||||||
|
<p id="logo">
|
||||||
|
<span>栞</span>shiori
|
||||||
|
</p>
|
||||||
|
<p id="tagline">simple bookmark manager</p>
|
||||||
|
</div>
|
||||||
|
<div id="input-area">
|
||||||
|
<label for="username">Username: </label>
|
||||||
|
<input id="username" type="text" name="username" placeholder="Username" tabindex="1" autofocus />
|
||||||
|
<label for="password">Password: </label>
|
||||||
|
<input id="password" type="password" name="password" placeholder="Password" tabindex="2"
|
||||||
|
@keyup.enter="login">
|
||||||
|
<label class="checkbox-field"><input type="checkbox" name="remember" v-model="remember"
|
||||||
|
tabindex="3">Remember me</label>
|
||||||
|
</div>
|
||||||
|
<div id="button-area">
|
||||||
|
<a v-if="loading">
|
||||||
|
<i class="fas fa-fw fa-spinner fa-spin"></i>
|
||||||
|
</a>
|
||||||
|
<a v-else class="button" tabindex="4" @click="login" @keyup.enter="login">Log In</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "login-view",
|
||||||
|
template,
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
error: "",
|
||||||
|
loading: false,
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
remember: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
emits: ["login-success"],
|
||||||
|
methods: {
|
||||||
|
async getErrorMessage(err) {
|
||||||
|
switch (err.constructor) {
|
||||||
|
case Error:
|
||||||
|
return err.message;
|
||||||
|
case Response:
|
||||||
|
var text = await err.text();
|
||||||
|
|
||||||
|
// Handle new error messages
|
||||||
|
if (text[0] == "{") {
|
||||||
|
var json = JSON.parse(text);
|
||||||
|
return json.message;
|
||||||
|
}
|
||||||
|
return `${text} (${err.status})`;
|
||||||
|
default:
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parseJWT(token) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(atob(token.split(".")[1]));
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
login() {
|
||||||
|
// Get values directly from the form
|
||||||
|
const usernameInput = document.querySelector("#username");
|
||||||
|
const passwordInput = document.querySelector("#password");
|
||||||
|
this.username = usernameInput ? usernameInput.value : this.username;
|
||||||
|
this.password = passwordInput ? passwordInput.value : this.password;
|
||||||
|
|
||||||
|
// Validate input
|
||||||
|
if (this.username === "") {
|
||||||
|
this.error = "Username must not empty";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove old cookie
|
||||||
|
document.cookie = `session-id=; Path=${
|
||||||
|
new URL(document.baseURI).pathname
|
||||||
|
}; Expires=Thu, 01 Jan 1970 00:00:00 GMT;`;
|
||||||
|
|
||||||
|
// Send request
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
fetch(new URL("api/v1/auth/login", document.baseURI), {
|
||||||
|
method: "post",
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: this.username,
|
||||||
|
password: this.password,
|
||||||
|
remember_me: this.remember == 1 ? true : false,
|
||||||
|
}),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.ok) throw response;
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
// Save session id
|
||||||
|
document.cookie = `session-id=${json.message.session}; Path=${
|
||||||
|
new URL(document.baseURI).pathname
|
||||||
|
}; Expires=${json.message.expires}`;
|
||||||
|
document.cookie = `token=${json.message.token}; Path=${
|
||||||
|
new URL(document.baseURI).pathname
|
||||||
|
}; Expires=${json.message.expires}`;
|
||||||
|
|
||||||
|
// Save account data
|
||||||
|
localStorage.setItem("shiori-token", json.message.token);
|
||||||
|
localStorage.setItem(
|
||||||
|
"shiori-account",
|
||||||
|
JSON.stringify(this.parseJWT(json.message.token).account),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.visible = false;
|
||||||
|
this.$emit("login-success");
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.loading = false;
|
||||||
|
this.getErrorMessage(err).then((msg) => {
|
||||||
|
this.error = msg;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
// Clear any existing cookies
|
||||||
|
document.cookie = `session-id=; Path=${
|
||||||
|
new URL(document.baseURI).pathname
|
||||||
|
}; Expires=Thu, 01 Jan 1970 00:00:00 GMT;`;
|
||||||
|
document.cookie = `token=; Path=${
|
||||||
|
new URL(document.baseURI).pathname
|
||||||
|
}; Expires=Thu, 01 Jan 1970 00:00:00 GMT;`;
|
||||||
|
|
||||||
|
// Clear local storage
|
||||||
|
localStorage.removeItem("shiori-account");
|
||||||
|
localStorage.removeItem("shiori-token");
|
||||||
|
|
||||||
|
// <input autofocus> wasn't working all the time, so I'm putting this here as a fallback
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const usernameInput = document.querySelector("#username");
|
||||||
|
if (usernameInput) {
|
||||||
|
usernameInput.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
|
@ -21,7 +21,9 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="main-scene">
|
<div id="app">
|
||||||
|
<login-view v-if="isLoggedIn === false" @login-success="onLoginSuccess"></login-view>
|
||||||
|
<div id="main-scene" v-else-if="isLoggedIn === true">
|
||||||
<div id="main-sidebar">
|
<div id="main-sidebar">
|
||||||
<a v-for="item in sidebarItems" :title="item.title" :class="{active: activePage === item.page}" @click="switchPage(item.page)">
|
<a v-for="item in sidebarItems" :title="item.title" :class="{active: activePage === item.page}" @click="switchPage(item.page)">
|
||||||
<i class="fas fa-fw" :class="item.icon"></i>
|
<i class="fas fa-fw" :class="item.icon"></i>
|
||||||
|
@ -35,10 +37,12 @@
|
||||||
<component :is="activePage" :active-account="activeAccount" :app-options="appOptions" @setting-changed="saveSetting"></component>
|
<component :is="activePage" :active-account="activeAccount" :app-options="appOptions" @setting-changed="saveSetting"></component>
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
<custom-dialog v-bind="dialog" />
|
<custom-dialog v-bind="dialog" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import basePage from "./assets/js/page/base.js";
|
import basePage from "./assets/js/page/base.js";
|
||||||
|
import LoginComponent from "./assets/js/component/login.js";
|
||||||
import pageHome from "./assets/js/page/home.js";
|
import pageHome from "./assets/js/page/home.js";
|
||||||
import pageSetting from "./assets/js/page/setting.js";
|
import pageSetting from "./assets/js/page/setting.js";
|
||||||
import customDialog from "./assets/js/component/dialog.js";
|
import customDialog from "./assets/js/component/dialog.js";
|
||||||
|
@ -46,14 +50,16 @@
|
||||||
Vue.prototype.$bus = EventBus;
|
Vue.prototype.$bus = EventBus;
|
||||||
|
|
||||||
var app = new Vue({
|
var app = new Vue({
|
||||||
el: '#main-scene',
|
el: '#app',
|
||||||
mixins: [basePage],
|
mixins: [basePage],
|
||||||
components: {
|
components: {
|
||||||
pageHome,
|
pageHome,
|
||||||
pageSetting,
|
pageSetting,
|
||||||
customDialog
|
customDialog,
|
||||||
|
'login-view': LoginComponent
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
|
isLoggedIn: false,
|
||||||
activePage: "page-home",
|
activePage: "page-home",
|
||||||
sidebarItems: [{
|
sidebarItems: [{
|
||||||
title: "Home",
|
title: "Home",
|
||||||
|
@ -72,8 +78,8 @@
|
||||||
url = new Url;
|
url = new Url;
|
||||||
|
|
||||||
if (page === 'page-home' && this.activePage === 'page-home') {
|
if (page === 'page-home' && this.activePage === 'page-home') {
|
||||||
Vue.prototype.$bus.$emit('clearHomePage', {});
|
Vue.prototype.$bus.$emit('clearHomePage', {});
|
||||||
}
|
}
|
||||||
url.hash = pageName;
|
url.hash = pageName;
|
||||||
this.activePage = page;
|
this.activePage = page;
|
||||||
history.pushState(state, page, url);
|
history.pushState(state, page, url);
|
||||||
|
@ -95,7 +101,8 @@
|
||||||
localStorage.removeItem("shiori-account");
|
localStorage.removeItem("shiori-account");
|
||||||
localStorage.removeItem("shiori-token");
|
localStorage.removeItem("shiori-token");
|
||||||
document.cookie = `session-id=; Path=${new URL(document.baseURI).pathname}; Expires=Thu, 01 Jan 1970 00:00:00 GMT;`;
|
document.cookie = `session-id=; Path=${new URL(document.baseURI).pathname}; Expires=Thu, 01 Jan 1970 00:00:00 GMT;`;
|
||||||
location.href = new URL("login", document.baseURI);
|
document.cookie = `token=; Path=${new URL(document.baseURI).pathname}; Expires=Thu, 01 Jan 1970 00:00:00 GMT;`;
|
||||||
|
this.isLoggedIn = false;
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
this.dialog.loading = false;
|
this.dialog.loading = false;
|
||||||
this.getErrorMessage(err).then(msg => {
|
this.getErrorMessage(err).then(msg => {
|
||||||
|
@ -133,7 +140,6 @@
|
||||||
MakePublic: MakePublic,
|
MakePublic: MakePublic,
|
||||||
};
|
};
|
||||||
this.themeSwitch(Theme)
|
this.themeSwitch(Theme)
|
||||||
|
|
||||||
},
|
},
|
||||||
loadAccount() {
|
loadAccount() {
|
||||||
var account = JSON.parse(localStorage.getItem("shiori-account")) || {},
|
var account = JSON.parse(localStorage.getItem("shiori-account")) || {},
|
||||||
|
@ -146,12 +152,60 @@
|
||||||
username: username,
|
username: username,
|
||||||
owner: owner,
|
owner: owner,
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
onLoginSuccess() {
|
||||||
|
this.loadSetting();
|
||||||
|
this.loadAccount();
|
||||||
|
this.isLoggedIn = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async validateSession() {
|
||||||
|
const token = localStorage.getItem("shiori-token");
|
||||||
|
const account = localStorage.getItem("shiori-account");
|
||||||
|
|
||||||
|
if (!(token && account)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(new URL("api/v1/auth/check", document.baseURI), {
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Invalid session');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
// Clear invalid session data
|
||||||
|
localStorage.removeItem("shiori-account");
|
||||||
|
localStorage.removeItem("shiori-token");
|
||||||
|
document.cookie = `session-id=; Path=${new URL(document.baseURI).pathname}; Expires=Thu, 01 Jan 1970 00:00:00 GMT;`;
|
||||||
|
document.cookie = `token=; Path=${new URL(document.baseURI).pathname}; Expires=Thu, 01 Jan 1970 00:00:00 GMT;`;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async checkLoginStatus() {
|
||||||
|
const isValid = await this.validateSession();
|
||||||
|
this.isLoggedIn = isValid;
|
||||||
|
|
||||||
|
if (isValid) {
|
||||||
|
this.loadSetting();
|
||||||
|
this.loadAccount();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
async mounted() {
|
||||||
// Load setting
|
await this.checkLoginStatus();
|
||||||
this.loadSetting();
|
if (this.isLoggedIn) {
|
||||||
this.loadAccount();
|
this.loadSetting();
|
||||||
|
this.loadAccount();
|
||||||
|
}
|
||||||
|
|
||||||
// Prepare history state watcher
|
// Prepare history state watcher
|
||||||
var stateWatcher = (e) => {
|
var stateWatcher = (e) => {
|
||||||
|
|
|
@ -1,163 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<base href="$$.RootPath$$">
|
|
||||||
<title>Login - Shiori</title>
|
|
||||||
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
|
|
||||||
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="assets/res/apple-touch-icon-152x152.png">
|
|
||||||
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="assets/res/apple-touch-icon-144x144.png">
|
|
||||||
<link rel="icon" type="image/png" href="assets/res/favicon-32x32.png" sizes="32x32">
|
|
||||||
<link rel="icon" type="image/png" href="assets/res/favicon-16x16.png" sizes="16x16">
|
|
||||||
<link rel="icon" type="image/x-icon" href="assets/res/favicon.ico">
|
|
||||||
|
|
||||||
<link href="assets/css/style.css" rel="stylesheet">
|
|
||||||
|
|
||||||
<script src="assets/js/vue.min.js"></script>
|
|
||||||
<script src="assets/js/url.min.js"></script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="login-scene">
|
|
||||||
<p class="error-message" v-if="error !== ''">{{error}}</p>
|
|
||||||
<div id="login-box">
|
|
||||||
<form @submit.prevent="login">
|
|
||||||
<div id="logo-area">
|
|
||||||
<p id="logo">
|
|
||||||
<span>栞</span>shiori
|
|
||||||
</p>
|
|
||||||
<p id="tagline">simple bookmark manager</p>
|
|
||||||
</div>
|
|
||||||
<div id="input-area">
|
|
||||||
<label for="username">Username: </label>
|
|
||||||
<input id="username" type="text" name="username" placeholder="Username" tabindex="1" autofocus />
|
|
||||||
<label for="password">Password: </label>
|
|
||||||
<input id="password" type="password" name="password" placeholder="Password" tabindex="2"
|
|
||||||
@keyup.enter="login">
|
|
||||||
<label class="checkbox-field"><input type="checkbox" name="remember" v-model="remember"
|
|
||||||
tabindex="3">Remember me</label>
|
|
||||||
</div>
|
|
||||||
<div id="button-area">
|
|
||||||
<a v-if="loading">
|
|
||||||
<i class="fas fa-fw fa-spinner fa-spin"></i>
|
|
||||||
</a>
|
|
||||||
<a v-else class="button" tabindex="4" @click="login" @keyup.enter="login">Log In</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<footer class="login-footer">
|
|
||||||
<p>$$.Version$$</p>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script type="module">
|
|
||||||
var app = new Vue({
|
|
||||||
el: "#login-scene",
|
|
||||||
data: {
|
|
||||||
error: "",
|
|
||||||
loading: false,
|
|
||||||
username: "",
|
|
||||||
password: "",
|
|
||||||
remember: false,
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async getErrorMessage(err) {
|
|
||||||
switch (err.constructor) {
|
|
||||||
case Error:
|
|
||||||
return err.message;
|
|
||||||
case Response:
|
|
||||||
var text = await err.text();
|
|
||||||
|
|
||||||
// Handle new error messages
|
|
||||||
if (text[0] == "{") {
|
|
||||||
var json = JSON.parse(text);
|
|
||||||
return json.message;
|
|
||||||
}
|
|
||||||
return `${text} (${err.status})`;
|
|
||||||
default:
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
login() {
|
|
||||||
function parseJWT(token) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(atob(token.split('.')[1]));
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// needed to work around autofill issue
|
|
||||||
// https://github.com/facebook/react/issues/1159#issuecomment-506584346
|
|
||||||
this.username = document.querySelector('#username').value;
|
|
||||||
this.password = document.querySelector('#password').value;
|
|
||||||
// Validate input
|
|
||||||
if (this.username === "") {
|
|
||||||
this.error = "Username must not empty";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove old cookie
|
|
||||||
document.cookie = `session-id=; Path=${new URL(document.baseURI).pathname}; Expires=Thu, 01 Jan 1970 00:00:00 GMT;`;
|
|
||||||
|
|
||||||
// Send request
|
|
||||||
this.loading = true;
|
|
||||||
|
|
||||||
fetch(new URL("api/v1/auth/login", document.baseURI), {
|
|
||||||
method: "post",
|
|
||||||
body: JSON.stringify({
|
|
||||||
username: this.username,
|
|
||||||
password: this.password,
|
|
||||||
remember_me: this.remember == 1 ? true : false,
|
|
||||||
}),
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
}).then(response => {
|
|
||||||
if (!response.ok) throw response;
|
|
||||||
return response.json();
|
|
||||||
}).then(json => {
|
|
||||||
// Save session id
|
|
||||||
document.cookie = `session-id=${json.message.session}; Path=${new URL(document.baseURI).pathname}; Expires=${json.message.expires}`;
|
|
||||||
document.cookie = `token=${json.message.token}; Path=${new URL(document.baseURI).pathname}; Expires=${json.message.expires}`;
|
|
||||||
|
|
||||||
// Save account data
|
|
||||||
localStorage.setItem("shiori-token", json.message.token);
|
|
||||||
localStorage.setItem("shiori-account", JSON.stringify(parseJWT(json.message.token).account));
|
|
||||||
|
|
||||||
// Go to destination page
|
|
||||||
var currentUrl = new Url(),
|
|
||||||
dstUrl = currentUrl.query.dst,
|
|
||||||
dstPage = currentUrl.hash || "home";
|
|
||||||
|
|
||||||
if (dstPage !== "home" && dstPage !== "setting") {
|
|
||||||
dstPage = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Improve this redirect logic
|
|
||||||
var newUrl = new Url(dstUrl || document.baseURI);
|
|
||||||
newUrl.hash = dstPage;
|
|
||||||
|
|
||||||
location.href = newUrl;
|
|
||||||
}).catch(err => {
|
|
||||||
this.loading = false;
|
|
||||||
this.getErrorMessage(err).then(msg => {
|
|
||||||
this.error = msg;
|
|
||||||
})
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
// Load setting
|
|
||||||
localStorage.removeItem("shiori-account");
|
|
||||||
localStorage.removeItem("shiori-token");
|
|
||||||
|
|
||||||
// <input autofocus> wasn't working all the time, so I'm putting this here as a fallback
|
|
||||||
document.querySelector('#username').focus()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
Loading…
Add table
Reference in a new issue