diff --git a/internal/http/routes/frontend.go b/internal/http/routes/frontend.go
index 364a5d2a..5947518c 100644
--- a/internal/http/routes/frontend.go
+++ b/internal/http/routes/frontend.go
@@ -48,12 +48,6 @@ type FrontendRoutes struct {
func (r *FrontendRoutes) Setup(e *gin.Engine) {
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) {
ctx.HTML(http.StatusOK, "index.html", gin.H{
"RootPath": r.cfg.Http.RootPath,
diff --git a/internal/http/routes/frontend_test.go b/internal/http/routes/frontend_test.go
index 8ba4f1ad..80934db8 100644
--- a/internal/http/routes/frontend_test.go
+++ b/internal/http/routes/frontend_test.go
@@ -31,13 +31,6 @@ func TestFrontendRoutes(t *testing.T) {
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) {
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/assets/css/style.css", nil)
diff --git a/internal/view/assets/js/component/login.js b/internal/view/assets/js/component/login.js
new file mode 100644
index 00000000..9150f0ee
--- /dev/null
+++ b/internal/view/assets/js/component/login.js
@@ -0,0 +1,152 @@
+const template = `
+
+`;
+
+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");
+
+ // 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();
+ }
+ });
+ },
+};
diff --git a/internal/view/index.html b/internal/view/index.html
index ab669557..e8f0be25 100644
--- a/internal/view/index.html
+++ b/internal/view/index.html
@@ -21,7 +21,9 @@
-