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) {
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")

View file

@ -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...)
}

View file

@ -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

View file

@ -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,
});

View file

@ -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) {

View file

@ -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<boolean>(false);
@ -32,23 +45,58 @@ export const useSessionStore = defineStore("session", () => {
});
// 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> {
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<void> {
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,

View file

@ -75,6 +75,9 @@
<a href="forgot-password.html">Forgot password?</a>
</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="alert alert-danger">{{ error }}</div>
</div>
@ -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<string>("");
const isLoginSuccess = ref<boolean>(false);
const loading = ref<boolean>(false);
const paramNext = useQueryParam("next");
const password = ref<string>("");
const session = useSessionStore();
const username = ref<string>("");
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);