Add CORS config and auth with JWT
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
import { buildApiUrl } from "./url";
|
||||
|
||||
type AuthTokenResponse = {
|
||||
accessToken: string;
|
||||
email: string;
|
||||
tokenType: string;
|
||||
expiresIn: number;
|
||||
};
|
||||
|
||||
async function fetchApi<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const response = await fetch(buildApiUrl(path), {
|
||||
...init,
|
||||
@@ -21,6 +28,14 @@ async function fetchApi<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
|
||||
function getAccessToken() {
|
||||
if (typeof window === "undefined") {
|
||||
return "";
|
||||
}
|
||||
|
||||
return localStorage.getItem("session-token") ?? "";
|
||||
}
|
||||
|
||||
export type LokOpenHours = {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -74,6 +89,9 @@ export async function createLokOpenHours(
|
||||
|
||||
return await fetchApi<LokOpenHours>("/lok/open-hours", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${getAccessToken()}`,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
@@ -106,6 +124,9 @@ export async function updateLokOpenHours(
|
||||
|
||||
return await fetchApi<LokOpenHours>(`/lok/open-hours/${id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
Authorization: `Bearer ${getAccessToken()}`,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
@@ -117,6 +138,9 @@ export async function deleteLokOpenHours(id: number): Promise<void> {
|
||||
|
||||
await fetchApi<void>(`/lok/open-hours/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: `Bearer ${getAccessToken()}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -131,6 +155,22 @@ export async function setActiveLokOpenHours(
|
||||
`/lok/open-hours/${id}/active`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
Authorization: `Bearer ${getAccessToken()}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function requestAuthToken(
|
||||
email: string,
|
||||
password: string,
|
||||
): Promise<AuthTokenResponse> {
|
||||
return await fetchApi<AuthTokenResponse>("/auth/token", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
password,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,14 +3,10 @@ import { buildApiUrl } from "./url";
|
||||
|
||||
describe("buildApiUrl", () => {
|
||||
it("joins base url and relative path without duplicate slashes", () => {
|
||||
expect(buildApiUrl("/lok/open-hours")).toBe(
|
||||
"http://localhost:5013/lok/open-hours",
|
||||
);
|
||||
expect(buildApiUrl("/lok/open-hours")).toBe("/api/lok/open-hours");
|
||||
});
|
||||
|
||||
it("accepts path without leading slash", () => {
|
||||
expect(buildApiUrl("lok/open-hours/1")).toBe(
|
||||
"http://localhost:5013/lok/open-hours/1",
|
||||
);
|
||||
expect(buildApiUrl("lok/open-hours/1")).toBe("/api/lok/open-hours/1");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,12 +21,16 @@ function AppShell() {
|
||||
|
||||
useEffect(() => {
|
||||
const storedEmail = localStorage.getItem("session-email");
|
||||
if (!storedEmail) {
|
||||
const storedToken = localStorage.getItem("session-token");
|
||||
if (!storedEmail || !storedToken) {
|
||||
setSession(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setSession({ email: storedEmail });
|
||||
setSession({
|
||||
email: storedEmail,
|
||||
token: storedToken,
|
||||
});
|
||||
}, [setSession]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -13,6 +13,7 @@ export default function Nav() {
|
||||
const signOut = () => {
|
||||
setSession(null);
|
||||
localStorage.removeItem("session-email");
|
||||
localStorage.removeItem("session-token");
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
@@ -34,8 +35,8 @@ export default function Nav() {
|
||||
type="button"
|
||||
onClick={() => setLanguage("fi")}
|
||||
className={`rounded px-2 py-1 text-xs ${language === "fi"
|
||||
? "bg-[#E3A977] text-[#4C250E]"
|
||||
: "text-[#F5D1A9] hover:text-[#FFF7EE]"
|
||||
? "bg-[#E3A977] text-[#4C250E]"
|
||||
: "text-[#F5D1A9] hover:text-[#FFF7EE]"
|
||||
}`}
|
||||
>
|
||||
{t("nav.language.fi")}
|
||||
@@ -44,8 +45,8 @@ export default function Nav() {
|
||||
type="button"
|
||||
onClick={() => setLanguage("en")}
|
||||
className={`rounded px-2 py-1 text-xs ${language === "en"
|
||||
? "bg-[#E3A977] text-[#4C250E]"
|
||||
: "text-[#F5D1A9] hover:text-[#FFF7EE]"
|
||||
? "bg-[#E3A977] text-[#4C250E]"
|
||||
: "text-[#F5D1A9] hover:text-[#FFF7EE]"
|
||||
}`}
|
||||
>
|
||||
{t("nav.language.en")}
|
||||
|
||||
@@ -55,6 +55,7 @@ const translations = {
|
||||
"notFound.goHome": "Go Home",
|
||||
"error.title": "Error",
|
||||
"errors.requiredEmailPassword": "Email and password are required",
|
||||
"errors.invalidEmailOrPassword": "Invalid email or password",
|
||||
},
|
||||
fi: {
|
||||
"nav.home": "Etusivu",
|
||||
@@ -106,6 +107,7 @@ const translations = {
|
||||
"notFound.goHome": "Takaisin etusivulle",
|
||||
"error.title": "Virhe",
|
||||
"errors.requiredEmailPassword": "Sähköposti ja salasana vaaditaan",
|
||||
"errors.invalidEmailOrPassword": "Virheellinen sähköposti tai salasana",
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { FormEvent, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useRecoilState } from "recoil";
|
||||
import { requestAuthToken } from "~/api";
|
||||
import { useT } from "~/i18n";
|
||||
import { sessionAtom } from "~/state/appState";
|
||||
|
||||
@@ -21,7 +22,7 @@ export default function Login() {
|
||||
navigate("/");
|
||||
}, [session, navigate]);
|
||||
|
||||
const submit = (event: FormEvent) => {
|
||||
const submit = async (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!email.trim() || !password.trim()) {
|
||||
@@ -30,9 +31,22 @@ export default function Login() {
|
||||
}
|
||||
|
||||
const normalizedEmail = email.trim().toLowerCase();
|
||||
setSession({ email: normalizedEmail });
|
||||
localStorage.setItem("session-email", normalizedEmail);
|
||||
navigate("/");
|
||||
|
||||
try {
|
||||
const auth = await requestAuthToken(normalizedEmail, password);
|
||||
|
||||
setSession({
|
||||
email: auth.email,
|
||||
token: auth.accessToken,
|
||||
});
|
||||
|
||||
localStorage.setItem("session-email", auth.email);
|
||||
localStorage.setItem("session-token", auth.accessToken);
|
||||
setError("");
|
||||
navigate("/");
|
||||
} catch {
|
||||
setError(t("errors.invalidEmailOrPassword"));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -5,6 +5,7 @@ export type Language = "fi" | "en";
|
||||
|
||||
export type Session = {
|
||||
email: string;
|
||||
token: string;
|
||||
};
|
||||
|
||||
export type Toast = {
|
||||
|
||||
Reference in New Issue
Block a user