Skip to content

Commit

Permalink
Properly handle session expiration
Browse files Browse the repository at this point in the history
  • Loading branch information
zobweyt committed Aug 21, 2024
1 parent 52f9d97 commit 3f62392
Show file tree
Hide file tree
Showing 25 changed files with 234 additions and 129 deletions.
4 changes: 2 additions & 2 deletions frontend/src/components/steps/verification/email.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createForm, email, required } from "@modular-forms/solid";
import { toast } from "solid-sonner";
import { Button, FormControl, Stepper } from "~/components";
import { userExists } from "~/lib/auth";
import { $userExists } from "~/lib/auth";
import { useI18n } from "~/lib/i18n";
import { Verifier } from ".";

Expand All @@ -19,7 +19,7 @@ export function EmailStep() {
return;
}

const exists = await userExists(data.email);
const exists = await $userExists(data.email);
if (!exists) {
toast.error(i18n.t.steps.verification.email.notExists());
return;
Expand Down
11 changes: 4 additions & 7 deletions frontend/src/lib/api/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,16 @@ import colors from "picocolors";

import { type Middleware } from "openapi-fetch";

import { getSession, resetSession } from "~/lib/auth/session";
import { getSession } from "~/lib/auth/session";
import logger from "~/lib/logging/console";

export const AUTH_MIDDLEWARE: Middleware = {
onRequest: async ({ request }) => {
const session = await getSession();
const auth = session.data.auth;

if (session.data.auth) {
if (Date.parse(session.data.auth.expires_at) <= Date.now()) {
await resetSession();
} else {
request.headers.set("Authorization", `Bearer ${session.data.auth.token}`);
}
if (auth && Date.parse(auth.expires_at) > Date.now()) {
request.headers.set("Authorization", `${auth.token_type} ${auth.access_token}`);
}

return request;
Expand Down
34 changes: 17 additions & 17 deletions frontend/src/lib/auth/actions.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,45 @@
import { action, redirect } from "@solidjs/router";
import { $login, $resetPassword } from "~/lib/auth";
import { changeSession, resetSession } from "~/lib/auth/session";
import { components } from "~/lib/api/schema";
import { $authenticate, $resetPassword, $unauthenticate } from "~/lib/auth";
import { updateAuth } from "~/lib/auth/session";

export type LoginForm = {
email: string;
password: string;
redirect: string | undefined;
};

export const login = action(async (form: LoginForm) => {
export const authenticate = action(async (form: LoginForm) => {
"use server";

const { data, error, response } = await $login(form.email, form.password);
const { data, error, response } = await $authenticate(form.email, form.password);

if (data) {
await changeSession({ token: data.access_token, expires_at: data.expires_at });
await updateAuth(data);
}
if (error) {
return { error, code: response.status };
}

throw redirect(form.redirect || "/settings");
throw redirect(form.redirect || "/");
});

export const resetPassword = action(async (body: components["schemas"]["UserPasswordReset"]) => {
export const unauthenticate = action(async () => {
"use server";

const data = await $resetPassword({ password: body.password, email: body.email, code: body.code});
await $unauthenticate();

if (data) {
await changeSession({ token: data.access_token, expires_at: data.expires_at });
} else {
return false;
}
return true;
throw redirect("/");
});

export const logout = action(async () => {
export const resetPassword = action(async (body: components["schemas"]["UserPasswordReset"]) => {
"use server";

await resetSession();
throw redirect("/");
const data = await $resetPassword(body);

if (data) {
await updateAuth(data);
return true;
}
return false;
});
4 changes: 2 additions & 2 deletions frontend/src/lib/auth/current-user-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { createAsync } from "@solidjs/router";
import { type Accessor, type ParentComponent, createContext, useContext } from "solid-js";
import { type components } from "~/lib/api/schema";
import { getCurrentUser } from "~/lib/auth";
import { $getCurrentUser } from "~/lib/auth";

export type CurrentUserContextValue = Accessor<components["schemas"]["CurrentUserResponse"] | undefined>;

export const CurrentUserContext = createContext<CurrentUserContextValue>(() => undefined);

export const CurrentUserProvider: ParentComponent = (props) => {
const currentUser = createAsync(() => getCurrentUser());
const currentUser = createAsync(() => $getCurrentUser());

return <CurrentUserContext.Provider value={currentUser}>{props.children}</CurrentUserContext.Provider>;
};
Expand Down
1 change: 0 additions & 1 deletion frontend/src/lib/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,3 @@ export * from "./actions";
export * from "./current-user-provider";
export * from "./server";
export * from "./types";
export * from "./utils";
65 changes: 44 additions & 21 deletions frontend/src/lib/auth/server.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { cache } from "@solidjs/router";
import { cache, revalidate } from "@solidjs/router";
import { formDataSerializer, getClient } from "~/lib/api";
import { components } from "~/lib/api/schema";
import { getSession, resetSession } from "./session";

export const $login = async (username: string, password: string) => {
export const $authenticate = async (username: string, password: string) => {
"use server";

const client = await getClient();
Expand All @@ -17,35 +18,39 @@ export const $login = async (username: string, password: string) => {
});
};

export const $unauthenticate = async () => {
"use server";

await resetSession();
};

export const $requestVerification = async (email: string) => {
"use server";

const client = await getClient();

return (
await client.POST("/api/v1/verification/request", {
body: {
email: email,
},
})
).data;
const { data } = await client.POST("/api/v1/verification/request", {
body: {
email: email,
},
});

return data;
};

export const $verifyCode = async (body: components["schemas"]["CodeVerify"]) => {
"use server";

const client = await getClient();

return (
(
await client.POST("/api/v1/verification/verify", {
body: body,
})
).response.status === 202
);
const { response } = await client.POST("/api/v1/verification/verify", {
body: body,
});

return response.status === 202;
};

export const $resetPassword = async (body: { password: string; email: string; code: number }) => {
export const $resetPassword = async (body: components["schemas"]["UserPasswordReset"]) => {
"use server";

const client = await getClient();
Expand All @@ -57,7 +62,7 @@ export const $resetPassword = async (body: { password: string; email: string; co
).data;
};

export const userExists = cache(async (email: string) => {
export const $userExists = cache(async (email: string) => {
"use server";

const client = await getClient();
Expand All @@ -69,14 +74,32 @@ export const userExists = cache(async (email: string) => {
});

return data;
}, "userExists");
}, "$userExists");

export const getCurrentUser = cache(async () => {
export const $getCurrentUser = cache(async () => {
"use server";

const client = await getClient();

const { data } = await client.GET("/api/v1/users/me");

return data;
}, "currentUser");
}, "$getCurrentUser");

export const $getIsAuthenticated = cache(async () => {
"use server";

const session = await getSession();
const auth = session.data.auth;

return auth !== undefined && Date.parse(auth.expires_at) > Date.now();
}, "$getIsAuthenticated");

export const $getSessionExpirationDate = cache(async () => {
"use server";

const session = await getSession();
const auth = session.data.auth;

return auth ? Date.parse(auth.expires_at) : Date.now();
}, "$getSessionExpiresAt");
16 changes: 12 additions & 4 deletions frontend/src/lib/auth/session.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
import { getRequestEvent } from "solid-js/web";
import { updateSession, useSession } from "vinxi/http";
import { type Auth, type SessionData, SESSION_COOKIE_OPTIONS } from "~/lib/auth";
import { type SessionData, Auth, SESSION_COOKIE_OPTIONS } from "~/lib/auth";

export const getSession = async () => {
return await useSession<SessionData>(SESSION_COOKIE_OPTIONS);
};

export const changeSession = async (auth: Auth) => {
export const changeSession = async (data: SessionData) => {
"use server";

const event = getRequestEvent();

if (event) {
await updateSession(event.nativeEvent, SESSION_COOKIE_OPTIONS, () => ({ auth: auth }));
await updateSession(event.nativeEvent, SESSION_COOKIE_OPTIONS, () => data);
}
};

export const updateAuth = async (auth: Auth) => {
"use server";

await changeSession({
auth: auth,
});
};

export const resetSession = async () => {
"use server";

await changeSession(undefined);
await updateAuth(undefined);
};
3 changes: 2 additions & 1 deletion frontend/src/lib/auth/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export type Auth =
| {
token: string;
access_token: string;
token_type: string;
expires_at: string;
}
| undefined;
Expand Down
50 changes: 0 additions & 50 deletions frontend/src/lib/auth/utils.ts

This file was deleted.

1 change: 1 addition & 0 deletions frontend/src/lib/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export const dict: DictionaryMap = {
enjoy: "Enjoy it",
explore: "Explore it",
},
sessionExpired: "Your session has expired!"
};

export default dict;
1 change: 1 addition & 0 deletions frontend/src/lib/i18n/locales/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export const dict: DictionaryMap = {
enjoy: "Наслаждайтесь",
explore: "Исследуйте",
},
sessionExpired: "Ваша сессия истекла!"
};

export default dict;
1 change: 1 addition & 0 deletions frontend/src/lib/i18n/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ export type DictionaryMap = {
enjoy: string;
explore: string;
};
sessionExpired: string;
};

export type LocalizedDictionary = i18n.Flatten<DictionaryMap>;
Expand Down
Loading

0 comments on commit 3f62392

Please sign in to comment.