Skip to content

Commit fb51755

Browse files
authored
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
1 parent 617f5dd commit fb51755

File tree

5 files changed

+217
-187
lines changed

5 files changed

+217
-187
lines changed

internal/http/routes/frontend.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,6 @@ type FrontendRoutes struct {
4848

4949
func (r *FrontendRoutes) Setup(e *gin.Engine) {
5050
group := e.Group("/")
51-
group.GET("/login", func(ctx *gin.Context) {
52-
ctx.HTML(http.StatusOK, "login.html", gin.H{
53-
"RootPath": r.cfg.Http.RootPath,
54-
"Version": model.BuildVersion,
55-
})
56-
})
5751
group.GET("/", func(ctx *gin.Context) {
5852
ctx.HTML(http.StatusOK, "index.html", gin.H{
5953
"RootPath": r.cfg.Http.RootPath,

internal/http/routes/frontend_test.go

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,6 @@ func TestFrontendRoutes(t *testing.T) {
3131
require.Equal(t, 200, w.Code)
3232
})
3333

34-
t.Run("/login", func(t *testing.T) {
35-
w := httptest.NewRecorder()
36-
req, _ := http.NewRequest("GET", "/login", nil)
37-
g.ServeHTTP(w, req)
38-
require.Equal(t, 200, w.Code)
39-
})
40-
4134
t.Run("/css/style.css", func(t *testing.T) {
4235
w := httptest.NewRecorder()
4336
req, _ := http.NewRequest("GET", "/assets/css/style.css", nil)
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
const template = `
2+
<div id="login-scene">
3+
<p class="error-message" v-if="error !== ''">{{error}}</p>
4+
<div id="login-box">
5+
<form @submit.prevent="login">
6+
<div id="logo-area">
7+
<p id="logo">
8+
<span>栞</span>shiori
9+
</p>
10+
<p id="tagline">simple bookmark manager</p>
11+
</div>
12+
<div id="input-area">
13+
<label for="username">Username: </label>
14+
<input id="username" type="text" name="username" placeholder="Username" tabindex="1" autofocus />
15+
<label for="password">Password: </label>
16+
<input id="password" type="password" name="password" placeholder="Password" tabindex="2"
17+
@keyup.enter="login">
18+
<label class="checkbox-field"><input type="checkbox" name="remember" v-model="remember"
19+
tabindex="3">Remember me</label>
20+
</div>
21+
<div id="button-area">
22+
<a v-if="loading">
23+
<i class="fas fa-fw fa-spinner fa-spin"></i>
24+
</a>
25+
<a v-else class="button" tabindex="4" @click="login" @keyup.enter="login">Log In</a>
26+
</div>
27+
</form>
28+
</div>
29+
</div>
30+
`;
31+
32+
export default {
33+
name: "login-view",
34+
template,
35+
data() {
36+
return {
37+
error: "",
38+
loading: false,
39+
username: "",
40+
password: "",
41+
remember: false,
42+
};
43+
},
44+
emits: ["login-success"],
45+
methods: {
46+
async getErrorMessage(err) {
47+
switch (err.constructor) {
48+
case Error:
49+
return err.message;
50+
case Response:
51+
var text = await err.text();
52+
53+
// Handle new error messages
54+
if (text[0] == "{") {
55+
var json = JSON.parse(text);
56+
return json.message;
57+
}
58+
return `${text} (${err.status})`;
59+
default:
60+
return err;
61+
}
62+
},
63+
parseJWT(token) {
64+
try {
65+
return JSON.parse(atob(token.split(".")[1]));
66+
} catch (e) {
67+
return null;
68+
}
69+
},
70+
login() {
71+
// Get values directly from the form
72+
const usernameInput = document.querySelector("#username");
73+
const passwordInput = document.querySelector("#password");
74+
this.username = usernameInput ? usernameInput.value : this.username;
75+
this.password = passwordInput ? passwordInput.value : this.password;
76+
77+
// Validate input
78+
if (this.username === "") {
79+
this.error = "Username must not empty";
80+
return;
81+
}
82+
83+
// Remove old cookie
84+
document.cookie = `session-id=; Path=${
85+
new URL(document.baseURI).pathname
86+
}; Expires=Thu, 01 Jan 1970 00:00:00 GMT;`;
87+
88+
// Send request
89+
this.loading = true;
90+
91+
fetch(new URL("api/v1/auth/login", document.baseURI), {
92+
method: "post",
93+
body: JSON.stringify({
94+
username: this.username,
95+
password: this.password,
96+
remember_me: this.remember == 1 ? true : false,
97+
}),
98+
headers: { "Content-Type": "application/json" },
99+
})
100+
.then((response) => {
101+
if (!response.ok) throw response;
102+
return response.json();
103+
})
104+
.then((json) => {
105+
// Save session id
106+
document.cookie = `session-id=${json.message.session}; Path=${
107+
new URL(document.baseURI).pathname
108+
}; Expires=${json.message.expires}`;
109+
document.cookie = `token=${json.message.token}; Path=${
110+
new URL(document.baseURI).pathname
111+
}; Expires=${json.message.expires}`;
112+
113+
// Save account data
114+
localStorage.setItem("shiori-token", json.message.token);
115+
localStorage.setItem(
116+
"shiori-account",
117+
JSON.stringify(this.parseJWT(json.message.token).account),
118+
);
119+
120+
this.visible = false;
121+
this.$emit("login-success");
122+
})
123+
.catch((err) => {
124+
this.loading = false;
125+
this.getErrorMessage(err).then((msg) => {
126+
this.error = msg;
127+
});
128+
});
129+
},
130+
},
131+
mounted() {
132+
// Clear any existing cookies
133+
document.cookie = `session-id=; Path=${
134+
new URL(document.baseURI).pathname
135+
}; Expires=Thu, 01 Jan 1970 00:00:00 GMT;`;
136+
document.cookie = `token=; Path=${
137+
new URL(document.baseURI).pathname
138+
}; Expires=Thu, 01 Jan 1970 00:00:00 GMT;`;
139+
140+
// Clear local storage
141+
localStorage.removeItem("shiori-account");
142+
localStorage.removeItem("shiori-token");
143+
144+
// <input autofocus> wasn't working all the time, so I'm putting this here as a fallback
145+
this.$nextTick(() => {
146+
const usernameInput = document.querySelector("#username");
147+
if (usernameInput) {
148+
usernameInput.focus();
149+
}
150+
});
151+
},
152+
};

internal/view/index.html

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
</head>
2222

2323
<body>
24-
<div id="main-scene">
24+
<div id="app">
25+
<login-view v-if="isLoggedIn === false" @login-success="onLoginSuccess"></login-view>
26+
<div id="main-scene" v-else-if="isLoggedIn === true">
2527
<div id="main-sidebar">
2628
<a v-for="item in sidebarItems" :title="item.title" :class="{active: activePage === item.page}" @click="switchPage(item.page)">
2729
<i class="fas fa-fw" :class="item.icon"></i>
@@ -35,25 +37,29 @@
3537
<component :is="activePage" :active-account="activeAccount" :app-options="appOptions" @setting-changed="saveSetting"></component>
3638
</keep-alive>
3739
<custom-dialog v-bind="dialog" />
40+
</div>
3841
</div>
3942

4043
<script type="module">
4144
import basePage from "./assets/js/page/base.js";
45+
import LoginComponent from "./assets/js/component/login.js";
4246
import pageHome from "./assets/js/page/home.js";
4347
import pageSetting from "./assets/js/page/setting.js";
4448
import customDialog from "./assets/js/component/dialog.js";
4549
import EventBus from "./assets/js/component/eventBus.js";
4650
Vue.prototype.$bus = EventBus;
4751

4852
var app = new Vue({
49-
el: '#main-scene',
53+
el: '#app',
5054
mixins: [basePage],
5155
components: {
5256
pageHome,
5357
pageSetting,
54-
customDialog
58+
customDialog,
59+
'login-view': LoginComponent
5560
},
5661
data: {
62+
isLoggedIn: false,
5763
activePage: "page-home",
5864
sidebarItems: [{
5965
title: "Home",
@@ -72,8 +78,8 @@
7278
url = new Url;
7379

7480
if (page === 'page-home' && this.activePage === 'page-home') {
75-
Vue.prototype.$bus.$emit('clearHomePage', {});
76-
}
81+
Vue.prototype.$bus.$emit('clearHomePage', {});
82+
}
7783
url.hash = pageName;
7884
this.activePage = page;
7985
history.pushState(state, page, url);
@@ -95,7 +101,8 @@
95101
localStorage.removeItem("shiori-account");
96102
localStorage.removeItem("shiori-token");
97103
document.cookie = `session-id=; Path=${new URL(document.baseURI).pathname}; Expires=Thu, 01 Jan 1970 00:00:00 GMT;`;
98-
location.href = new URL("login", document.baseURI);
104+
document.cookie = `token=; Path=${new URL(document.baseURI).pathname}; Expires=Thu, 01 Jan 1970 00:00:00 GMT;`;
105+
this.isLoggedIn = false;
99106
}).catch(err => {
100107
this.dialog.loading = false;
101108
this.getErrorMessage(err).then(msg => {
@@ -133,7 +140,6 @@
133140
MakePublic: MakePublic,
134141
};
135142
this.themeSwitch(Theme)
136-
137143
},
138144
loadAccount() {
139145
var account = JSON.parse(localStorage.getItem("shiori-account")) || {},
@@ -146,12 +152,60 @@
146152
username: username,
147153
owner: owner,
148154
};
155+
},
156+
157+
onLoginSuccess() {
158+
this.loadSetting();
159+
this.loadAccount();
160+
this.isLoggedIn = true;
161+
},
162+
163+
async validateSession() {
164+
const token = localStorage.getItem("shiori-token");
165+
const account = localStorage.getItem("shiori-account");
166+
167+
if (!(token && account)) {
168+
return false;
169+
}
170+
171+
try {
172+
const response = await fetch(new URL("api/v1/auth/check", document.baseURI), {
173+
headers: {
174+
"Authorization": `Bearer ${token}`
175+
}
176+
});
177+
178+
if (!response.ok) {
179+
throw new Error('Invalid session');
180+
}
181+
182+
return true;
183+
} catch (err) {
184+
// Clear invalid session data
185+
localStorage.removeItem("shiori-account");
186+
localStorage.removeItem("shiori-token");
187+
document.cookie = `session-id=; Path=${new URL(document.baseURI).pathname}; Expires=Thu, 01 Jan 1970 00:00:00 GMT;`;
188+
document.cookie = `token=; Path=${new URL(document.baseURI).pathname}; Expires=Thu, 01 Jan 1970 00:00:00 GMT;`;
189+
return false;
190+
}
191+
},
192+
193+
async checkLoginStatus() {
194+
const isValid = await this.validateSession();
195+
this.isLoggedIn = isValid;
196+
197+
if (isValid) {
198+
this.loadSetting();
199+
this.loadAccount();
200+
}
149201
}
150202
},
151-
mounted() {
152-
// Load setting
153-
this.loadSetting();
154-
this.loadAccount();
203+
async mounted() {
204+
await this.checkLoginStatus();
205+
if (this.isLoggedIn) {
206+
this.loadSetting();
207+
this.loadAccount();
208+
}
155209

156210
// Prepare history state watcher
157211
var stateWatcher = (e) => {

0 commit comments

Comments
 (0)