Improve signin messaging

This commit is contained in:
Eli Ribble 2026-04-23 15:24:06 +00:00
parent b4e6bac566
commit 72a8ed5c16
No known key found for this signature in database
7 changed files with 110 additions and 20 deletions

View file

@ -18,19 +18,22 @@ type reqSignin struct {
func postSignin(ctx context.Context, r *http.Request, req reqSignin) (string, *nhttp.ErrorWithStatus) { func postSignin(ctx context.Context, r *http.Request, req reqSignin) (string, *nhttp.ErrorWithStatus) {
if req.Password == "" { if req.Password == "" {
return "", nhttp.NewErrorStatus(http.StatusBadRequest, "Empty password") return "", nhttp.NewBadRequest("Empty password")
} }
if req.Username == "" { if req.Username == "" {
return "", nhttp.NewErrorStatus(http.StatusBadRequest, "Empty username") return "", nhttp.NewBadRequest("Empty username")
} }
log.Info().Str("username", req.Username).Msg("API Signin") log.Info().Str("username", req.Username).Msg("API Signin")
_, err := auth.SigninUser(r, req.Username, req.Password) _, err := auth.SigninUser(r, req.Username, req.Password)
if err != nil { if err != nil {
if errors.Is(err, auth.InvalidCredentials{}) { if errors.Is(err, auth.InvalidCredentials{}) {
return "", nhttp.NewErrorStatus(http.StatusUnauthorized, "invalid credentials") return "", nhttp.NewUnauthorized("invalid credentials")
} }
if errors.Is(err, auth.InvalidUsername{}) { 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") log.Error().Err(err).Str("username", req.Username).Msg("Login server error")
return "", nhttp.NewError("login server error") return "", nhttp.NewError("login server error")

View file

@ -36,3 +36,6 @@ func NewErrorStatus(status int, mesg_format string, args ...any) *ErrorWithStatu
func NewForbidden(mesg_format string, args ...any) *ErrorWithStatus { func NewForbidden(mesg_format string, args ...any) *ErrorWithStatus {
return NewErrorStatus(http.StatusForbidden, mesg_format, args...) return NewErrorStatus(http.StatusForbidden, mesg_format, args...)
} }
func NewUnauthorized(mesg_format string, args ...any) *ErrorWithStatus {
return NewErrorStatus(http.StatusUnauthorized, mesg_format, args...)
}

View file

@ -23,6 +23,12 @@ import (
type NoUserError struct{} type NoUserError struct{}
func (e NoUserError) Error() string { return "That user does not exist" } 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 { type User struct {
Active bool Active bool

View file

@ -1,12 +1,15 @@
// src/api/axios.ts // src/api/axios.ts
import axios, { AxiosInstance, AxiosRequestConfig } from "axios"; import axios, { AxiosInstance, AxiosRequestConfig } from "axios";
export interface AxiosErrorJSON {
status: number;
}
class ApiClient { class ApiClient {
private client: AxiosInstance; private client: AxiosInstance;
constructor() { constructor() {
this.client = axios.create({ this.client = axios.create({
timeout: 60000, timeout: 10000,
withCredentials: true, withCredentials: true,
}); });

View file

@ -224,7 +224,10 @@ router.beforeEach(async (to, from) => {
const storeSession = useSessionStore(); const storeSession = useSessionStore();
try { try {
if (!storeSession.isLoading && !storeSession.isAuthenticated) { 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}`; return `/signin?next=${from.fullPath}`;
} }
} catch (error) { } catch (error) {

View file

@ -1,3 +1,4 @@
import * as axios from "axios";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { ref } from "vue"; import { ref } from "vue";
import { SSEManager, type SSEMessage } from "@/SSEManager"; import { SSEManager, type SSEMessage } from "@/SSEManager";
@ -8,8 +9,20 @@ import {
URLs, URLs,
User, User,
} from "@/type/api"; } 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", () => { export const useSessionStore = defineStore("session", () => {
// State // State
const hasSession = ref<boolean>(false); const hasSession = ref<boolean>(false);
@ -32,23 +45,58 @@ export const useSessionStore = defineStore("session", () => {
}); });
// Actions // Actions
async function doSignin(
password: string,
username: string,
): Promise<SigninResult> {
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<Session> { async function fetchSession(): Promise<Session> {
error.value = null; error.value = null;
try { try {
const data: Session = await apiClient.JSONGet("/api/session"); const data: Session = await apiClient.JSONGet("/api/session");
isAuthenticated.value = true; 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; impersonating.value = data.impersonating || null;
notification_counts.value = data.notification_counts; notification_counts.value = data.notification_counts;
organization.value = data.organization; organization.value = data.organization;
self.value = data.self; self.value = data.self;
urls.value = data.urls; urls.value = data.urls;
return data; return data;
} catch (e) { } catch (e: any) {
error.value = e instanceof Error ? e.message : "an error ocurred"; const data: AxiosErrorJSON =
console.error("Error fetching user:", e); e instanceof axios.AxiosError
throw new Error(error.value); ? (e.toJSON() as AxiosErrorJSON)
: { status: 0 };
if (data.status == 401) {
throw new ErrorNotSignedIn();
}
console.error("Error fetching session:", e);
throw e;
} finally { } finally {
hasSession.value = true; hasSession.value = true;
isLoading.value = false; isLoading.value = false;
@ -77,7 +125,7 @@ export const useSessionStore = defineStore("session", () => {
} }
async function signout(): Promise<void> { async function signout(): Promise<void> {
isAuthenticated.value = false; isAuthenticated.value = false;
console.log("set authenticated", isAuthenticated.value); console.log("set authenticated", isAuthenticated.value, "due to signout");
apiClient.JSONPost("/api/signout", {}); apiClient.JSONPost("/api/signout", {});
} }
return { return {
@ -93,6 +141,7 @@ export const useSessionStore = defineStore("session", () => {
self, self,
urls, urls,
// Actions // Actions
doSignin,
fetchSession, fetchSession,
get, get,
signout, signout,

View file

@ -75,6 +75,9 @@
<a href="forgot-password.html">Forgot password?</a> <a href="forgot-password.html">Forgot password?</a>
</div> </div>
<div class="mt-3 text-center" v-if="isLoginSuccess">
<div class="alert alert-success">Login successful</div>
</div>
<div class="mt-3 text-center" v-if="error"> <div class="mt-3 text-center" v-if="error">
<div class="alert alert-danger">{{ error }}</div> <div class="alert alert-danger">{{ error }}</div>
</div> </div>
@ -113,23 +116,43 @@ import { apiClient } from "@/client";
import ButtonLoading from "@/components/common/ButtonLoading.vue"; import ButtonLoading from "@/components/common/ButtonLoading.vue";
import { useQueryParam } from "@/composable/use-query-param"; import { useQueryParam } from "@/composable/use-query-param";
import { router } from "@/route/config"; import { router } from "@/route/config";
import { useSessionStore } from "@/store/session";
const error = ref<string>(""); const error = ref<string>("");
const isLoginSuccess = ref<boolean>(false);
const loading = ref<boolean>(false); const loading = ref<boolean>(false);
const paramNext = useQueryParam("next"); const paramNext = useQueryParam("next");
const password = ref<string>(""); const password = ref<string>("");
const session = useSessionStore();
const username = ref<string>(""); const username = ref<string>("");
async function doLogin() { 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; loading.value = true;
error.value = "";
try { try {
const resp = await apiClient.JSONPost("/api/signin", { const resp = await session.doSignin(password.value, username.value);
password: password.value, isLoginSuccess.value = resp.is_success;
username: username.value, if (resp.status == 200) {
}); if (paramNext.value.value && paramNext.value.value != "/signin") {
if (paramNext.value.value) { router.push(paramNext.value.value);
router.push(paramNext.value.value); } else {
router.push("/");
}
} else if (resp.status == 401) {
error.value = "Invalid credentials";
} else { } else {
router.push("/"); error.value = `Status ${resp.status}`;
} }
} catch (e) { } catch (e) {
console.log("login failed", e); console.log("login failed", e);