Improve signin messaging
This commit is contained in:
parent
b4e6bac566
commit
72a8ed5c16
7 changed files with 110 additions and 20 deletions
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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...)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue