From 72a8ed5c165827bfaea9787b3a16aed2ac0e50d6 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Thu, 23 Apr 2026 15:24:06 +0000 Subject: [PATCH] Improve signin messaging --- api/signin.go | 11 ++++--- http/error_with_status.go | 3 ++ platform/user.go | 6 ++++ ts/client.ts | 5 +++- ts/route/config.ts | 5 +++- ts/store/session.ts | 63 ++++++++++++++++++++++++++++++++++----- ts/view/Signin.vue | 37 ++++++++++++++++++----- 7 files changed, 110 insertions(+), 20 deletions(-) diff --git a/api/signin.go b/api/signin.go index 319a2a44..c87148fe 100644 --- a/api/signin.go +++ b/api/signin.go @@ -18,19 +18,22 @@ type reqSignin struct { func postSignin(ctx context.Context, r *http.Request, req reqSignin) (string, *nhttp.ErrorWithStatus) { if req.Password == "" { - return "", nhttp.NewErrorStatus(http.StatusBadRequest, "Empty password") + return "", nhttp.NewBadRequest("Empty password") } if req.Username == "" { - return "", nhttp.NewErrorStatus(http.StatusBadRequest, "Empty username") + return "", nhttp.NewBadRequest("Empty username") } log.Info().Str("username", req.Username).Msg("API Signin") _, err := auth.SigninUser(r, req.Username, req.Password) if err != nil { if errors.Is(err, auth.InvalidCredentials{}) { - return "", nhttp.NewErrorStatus(http.StatusUnauthorized, "invalid credentials") + return "", nhttp.NewUnauthorized("invalid credentials") } if errors.Is(err, auth.InvalidUsername{}) { - return "", nhttp.NewErrorStatus(http.StatusUnauthorized, "invalid credentials") + return "", nhttp.NewUnauthorized("invalid credentials") + } + if errors.Is(err, platform.NoUserError{}) { + return "", nhttp.NewUnauthorized("invalid credentials") } log.Error().Err(err).Str("username", req.Username).Msg("Login server error") return "", nhttp.NewError("login server error") diff --git a/http/error_with_status.go b/http/error_with_status.go index 44fec097..5b877943 100644 --- a/http/error_with_status.go +++ b/http/error_with_status.go @@ -36,3 +36,6 @@ func NewErrorStatus(status int, mesg_format string, args ...any) *ErrorWithStatu func NewForbidden(mesg_format string, args ...any) *ErrorWithStatus { return NewErrorStatus(http.StatusForbidden, mesg_format, args...) } +func NewUnauthorized(mesg_format string, args ...any) *ErrorWithStatus { + return NewErrorStatus(http.StatusUnauthorized, mesg_format, args...) +} diff --git a/platform/user.go b/platform/user.go index 2f44bfe1..7acf75bf 100644 --- a/platform/user.go +++ b/platform/user.go @@ -23,6 +23,12 @@ import ( type NoUserError struct{} func (e NoUserError) Error() string { return "That user does not exist" } +func (e NoUserError) Is(target error) bool { + if _, ok := target.(NoUserError); ok { + return true + } + return false +} type User struct { Active bool diff --git a/ts/client.ts b/ts/client.ts index ee3f305a..87cb10e2 100644 --- a/ts/client.ts +++ b/ts/client.ts @@ -1,12 +1,15 @@ // src/api/axios.ts import axios, { AxiosInstance, AxiosRequestConfig } from "axios"; +export interface AxiosErrorJSON { + status: number; +} class ApiClient { private client: AxiosInstance; constructor() { this.client = axios.create({ - timeout: 60000, + timeout: 10000, withCredentials: true, }); diff --git a/ts/route/config.ts b/ts/route/config.ts index 32b9225b..633161fc 100644 --- a/ts/route/config.ts +++ b/ts/route/config.ts @@ -224,7 +224,10 @@ router.beforeEach(async (to, from) => { const storeSession = useSessionStore(); try { if (!storeSession.isLoading && !storeSession.isAuthenticated) { - console.log("sending to signin because we're not authenticated"); + console.log( + "sending to signin because we're not authenticated and user wanted", + to.fullPath, + ); return `/signin?next=${from.fullPath}`; } } catch (error) { diff --git a/ts/store/session.ts b/ts/store/session.ts index c72af0a1..2436d59a 100644 --- a/ts/store/session.ts +++ b/ts/store/session.ts @@ -1,3 +1,4 @@ +import * as axios from "axios"; import { defineStore } from "pinia"; import { ref } from "vue"; import { SSEManager, type SSEMessage } from "@/SSEManager"; @@ -8,8 +9,20 @@ import { URLs, User, } from "@/type/api"; -import { apiClient } from "@/client"; +import { apiClient, AxiosErrorJSON } from "@/client"; +export class ErrorNotSignedIn extends Error { + constructor() { + super("not signed in"); + this.name = "ErrorNotSignedIn"; + Object.setPrototypeOf(this, ErrorNotSignedIn.prototype); + } +} + +export interface SigninResult { + is_success: boolean; + status: number; +} export const useSessionStore = defineStore("session", () => { // State const hasSession = ref(false); @@ -32,23 +45,58 @@ export const useSessionStore = defineStore("session", () => { }); // Actions + async function doSignin( + password: string, + username: string, + ): Promise { + try { + await apiClient.JSONPost("/api/signin", { + password: password, + username: username, + }); + return { + is_success: true, + status: 200, + }; + } catch (e: any) { + const data: AxiosErrorJSON = + e instanceof axios.AxiosError + ? (e.toJSON() as AxiosErrorJSON) + : { status: 0 }; + if (!data) throw e; + return { + is_success: false, + status: data.status, + }; + } + } async function fetchSession(): Promise { error.value = null; try { const data: Session = await apiClient.JSONGet("/api/session"); isAuthenticated.value = true; - console.log("set authenticated", isAuthenticated.value); + console.log( + "set authenticated", + isAuthenticated.value, + "due to successful GET /api/session", + ); impersonating.value = data.impersonating || null; notification_counts.value = data.notification_counts; organization.value = data.organization; self.value = data.self; urls.value = data.urls; return data; - } catch (e) { - error.value = e instanceof Error ? e.message : "an error ocurred"; - console.error("Error fetching user:", e); - throw new Error(error.value); + } catch (e: any) { + const data: AxiosErrorJSON = + e instanceof axios.AxiosError + ? (e.toJSON() as AxiosErrorJSON) + : { status: 0 }; + if (data.status == 401) { + throw new ErrorNotSignedIn(); + } + console.error("Error fetching session:", e); + throw e; } finally { hasSession.value = true; isLoading.value = false; @@ -77,7 +125,7 @@ export const useSessionStore = defineStore("session", () => { } async function signout(): Promise { isAuthenticated.value = false; - console.log("set authenticated", isAuthenticated.value); + console.log("set authenticated", isAuthenticated.value, "due to signout"); apiClient.JSONPost("/api/signout", {}); } return { @@ -93,6 +141,7 @@ export const useSessionStore = defineStore("session", () => { self, urls, // Actions + doSignin, fetchSession, get, signout, diff --git a/ts/view/Signin.vue b/ts/view/Signin.vue index fb19ad44..ef181b0c 100644 --- a/ts/view/Signin.vue +++ b/ts/view/Signin.vue @@ -75,6 +75,9 @@ Forgot password? +
+
Login successful
+
{{ error }}
@@ -113,23 +116,43 @@ import { apiClient } from "@/client"; import ButtonLoading from "@/components/common/ButtonLoading.vue"; import { useQueryParam } from "@/composable/use-query-param"; import { router } from "@/route/config"; +import { useSessionStore } from "@/store/session"; const error = ref(""); +const isLoginSuccess = ref(false); const loading = ref(false); const paramNext = useQueryParam("next"); const password = ref(""); +const session = useSessionStore(); const username = ref(""); async function doLogin() { + if (username.value == "" && password.value == "") { + error.value = + "Slow down there partner, you should add a username and password first."; + return; + } else if (username.value == "") { + error.value = "Your username is empty - we've got to know who you are."; + return; + } else if (password.value == "") { + error.value = "Your password is empty - you've got to put something there."; + return; + } + loading.value = true; + error.value = ""; try { - const resp = await apiClient.JSONPost("/api/signin", { - password: password.value, - username: username.value, - }); - if (paramNext.value.value) { - router.push(paramNext.value.value); + const resp = await session.doSignin(password.value, username.value); + isLoginSuccess.value = resp.is_success; + if (resp.status == 200) { + if (paramNext.value.value && paramNext.value.value != "/signin") { + router.push(paramNext.value.value); + } else { + router.push("/"); + } + } else if (resp.status == 401) { + error.value = "Invalid credentials"; } else { - router.push("/"); + error.value = `Status ${resp.status}`; } } catch (e) { console.log("login failed", e);