diff --git a/index.html b/index.html index 0ab9b12..316b574 100644 --- a/index.html +++ b/index.html @@ -21,6 +21,7 @@ Login
+ Logout
Register
diff --git a/logout.html b/logout.html new file mode 100644 index 0000000..9ec08fc --- /dev/null +++ b/logout.html @@ -0,0 +1,9 @@ + + + + +

Logout

+ + + + diff --git a/prisma/migrations/20260126105358_add_user_token/migration.sql b/prisma/migrations/20260126105358_add_user_token/migration.sql new file mode 100644 index 0000000..c15f621 --- /dev/null +++ b/prisma/migrations/20260126105358_add_user_token/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `token` to the `User` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "token" TEXT NOT NULL DEFAULT ''; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8b92553..6ef6783 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -11,5 +11,6 @@ model User { id Int @id @default(autoincrement()) email String @unique password String + token String @default("") first_login Boolean } diff --git a/server.ts b/server.ts index 6001e51..cad5049 100644 --- a/server.ts +++ b/server.ts @@ -1,12 +1,8 @@ import { Hono } from "hono"; import { cors } from "hono/cors"; -import { upgradeWebSocket, websocket } from "hono/bun"; -import { DatabaseService } from "@/services/database-service"; -import { RegisterRequest, RegisterResponse } from "@/types/types"; -import { MESSAGES } from "@/auth/messages"; -import { validateEmail } from "@/utilities/email"; -import { Prisma } from "@/orm/generated/prisma/client"; -import { hashPassword } from "@/utilities/password"; +// import { upgradeWebSocket, websocket } from "hono/bun"; +import { RegisterController } from "@/controllers/register"; +import { LoginController } from "@/controllers/login"; const app = new Hono(); @@ -28,103 +24,30 @@ app.get("/", async (c) => { }); }); -app.post("/api/v1/register", async (c) => { - let body: RegisterRequest | undefined; - let errors = false; - let messages: Array = []; +app.post("/api/v1/register", RegisterController); - // Get the request body and handle malformed payload - try { - body = (await c.req.json()) as RegisterRequest; - } catch (error) { - console.error(`Received invalid payload: ${error}`); - return c.json({ - status: "error", - messages: [MESSAGES.INVALID_REQUEST], - } as RegisterResponse); - } +app.post("/api/v1/login", LoginController); - // ////////////////// - // Request validation - if (!body.email) { - errors = true; - messages.push(MESSAGES.MISSING_EMAIL); - } - - if (!validateEmail(body.email)) { - errors = true; - messages.push(MESSAGES.INVALID_EMAIL); - } - - if (!body.password) { - errors = true; - messages.push(MESSAGES.MISSING_PASSWORD); - } - // End: Request validation - // /////////////////////// - - if (errors) { - return c.json({ - status: "error", - messages: messages, - } as RegisterResponse); - } - - // Database - const database = new DatabaseService(); - - try { - // Sala la password - body.password = await hashPassword(body.password); - - await database.getClient().user.create({ - data: { - ...body, - first_login: true, - }, - }); - } catch (e) { - if ( - e instanceof Prisma.PrismaClientKnownRequestError && - e.code === "P2002" - ) { - return c.json({ - status: "error", - messages: [MESSAGES.USER_ALREADY_EXISTS], - } as RegisterResponse); - } - - return c.json({ - status: "error", - messages: [MESSAGES.UNKNOWN_DATABASE_ERROR], - } as RegisterResponse); - } - - return c.json({ - status: "success", - } as RegisterResponse); -}); - -app.get( - "/ws", - upgradeWebSocket((c) => { - return { - onOpen(e, ws) { - console.log("Server: Connection opened"); - ws.send("Hello!"); - }, - onClose: () => { - console.log("Server: Connection closed"); - }, - onMessage: (ev) => { - console.log(ev.data); - }, - }; - }), -); +// app.get( +// "/ws", +// upgradeWebSocket((c) => { +// return { +// onOpen(e, ws) { +// console.log("Server: Connection opened"); +// ws.send("Hello!"); +// }, +// onClose: () => { +// console.log("Server: Connection closed"); +// }, +// onMessage: (ev) => { +// console.log(ev.data); +// }, +// }; +// }), +// ); export default { port: Bun.env.SERVER_PORT, fetch: app.fetch, - websocket, + // websocket, }; diff --git a/src/constants.ts b/src/constants.ts index 71b0515..cbd9cdc 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -7,3 +7,12 @@ export const INITIAL_ZOOM = 17; * MapTiler style URL for the map */ export const MAP_STYLE = import.meta.env.VITE_MAPTILER_STYLE; + +export const STATUSES = { + SUCCESS: "success", + ERROR: "error", +}; + +export const DB_ERROR_CODES = { + P2002: "P2002", // "Unique constraint failed on the {constraint}" +}; diff --git a/src/controllers/login.ts b/src/controllers/login.ts new file mode 100644 index 0000000..6f77a3f --- /dev/null +++ b/src/controllers/login.ts @@ -0,0 +1,81 @@ +import { MESSAGES } from "@/auth/messages"; +import { STATUSES } from "@/constants"; +import { DatabaseService } from "@/services/database-service"; +import { RegisterRequest, RegisterResponse } from "@/types/types"; +import { validateEmail } from "@/utilities/email"; +import { generateToken, hashPassword } from "@/utilities/crypt"; +import { Context } from "hono"; + +export const LoginController = async (c: Context) => { + let body; + let errors: boolean = false; + let messages: Array = []; + + // Get the request body and handle malformed payload + try { + body = (await c.req.json()) as RegisterRequest; + } catch (error) { + console.error(`Received invalid payload: ${error}`); + return c.json({ + status: STATUSES.ERROR, + messages: [MESSAGES.INVALID_REQUEST], + } as RegisterResponse); + } + + if (!body.email) { + errors = true; + messages.push(MESSAGES.MISSING_EMAIL); + } + + if (!validateEmail(body.email)) { + errors = true; + messages.push(MESSAGES.INVALID_EMAIL); + } + + if (!body.password) { + errors = true; + messages.push(MESSAGES.MISSING_PASSWORD); + } + + if (errors) { + return c.json({ + status: STATUSES.ERROR, + messages: messages, + } as RegisterResponse); + } + + const database = new DatabaseService(); + const foundUser = await database.getClient().user.findUnique({ + where: { + email: body.email, + }, + }); + + if (foundUser) { + const passwordHash = await hashPassword(body.password); + if (passwordHash == foundUser.password) { + const token = generateToken(); + + // Setta il token per l'utente + await database.getClient().user.update({ + where: { + id: foundUser.id, + }, + data: { + token: token, + }, + }); + + return c.json({ + email: foundUser.email, + token: token, + }); + } + return c.json({ + message: MESSAGES.WRONG_PASSWORD, + }); + } + return c.json({ + message: MESSAGES.USER_NOT_FOUND, + }); +}; diff --git a/src/controllers/register.ts b/src/controllers/register.ts new file mode 100644 index 0000000..59a3517 --- /dev/null +++ b/src/controllers/register.ts @@ -0,0 +1,97 @@ +import { MESSAGES } from "@/auth/messages"; +import { STATUSES, DB_ERROR_CODES } from "@/constants"; +import { Prisma } from "@/orm/generated/prisma/client"; +import { DatabaseService } from "@/services/database-service"; +import { + ApiErrorResponse, + RegisterRequest, + RegisterResponse, +} from "@/types/types"; +import { validateEmail } from "@/utilities/email"; +import { hashPassword } from "@/utilities/crypt"; +import { Context } from "hono"; + +/** + * + * @param c Request context + * @returns JsonRespond a JSON response + */ +export const RegisterController = async (c: Context) => { + let body: RegisterRequest | undefined; + let errors = false; + let messages: Array = []; + + // Get the request body and handle malformed payload + try { + body = (await c.req.json()) as RegisterRequest; + } catch (error) { + console.error(`Received invalid payload: ${error}`); + return c.json({ + status: STATUSES.ERROR, + messages: [MESSAGES.INVALID_REQUEST], + } as ApiErrorResponse); + } + + // ////////////////// + // Request validation + if (!body.email) { + errors = true; + messages.push(MESSAGES.MISSING_EMAIL); + } + + if (!validateEmail(body.email)) { + errors = true; + messages.push(MESSAGES.INVALID_EMAIL); + } + + if (!body.password) { + errors = true; + messages.push(MESSAGES.MISSING_PASSWORD); + } + // End: Request validation + // /////////////////////// + + if (errors) { + return c.json({ + status: STATUSES.ERROR, + messages: messages, + } as ApiErrorResponse); + } + + // Database + const database = new DatabaseService(); + + try { + // Sala la password + body.password = await hashPassword(body.password); + + const createdUser = await database.getClient().user.create({ + data: { + ...body, + first_login: true, + }, + }); + + return c.json({ + status: STATUSES.SUCCESS, + user: { + email: createdUser.email, + }, + } as RegisterResponse); + } catch (e) { + if ( + e instanceof Prisma.PrismaClientKnownRequestError && + e.code === DB_ERROR_CODES.P2002 + ) { + return c.json({ + status: STATUSES.ERROR, + messages: [MESSAGES.USER_ALREADY_EXISTS], + } as ApiErrorResponse); + } + + return c.json({ + status: STATUSES.ERROR, + messages: [MESSAGES.UNKNOWN_DATABASE_ERROR], + } as ApiErrorResponse); + } +}; diff --git a/src/login.ts b/src/login.ts index 08e8a24..f7f8457 100644 --- a/src/login.ts +++ b/src/login.ts @@ -5,15 +5,34 @@ import "@/main.css"; // LIBRARIES import $ from "jquery"; -// import { FilterSpecification, Map, config } from "@maptiler/sdk"; -// import { createIcons, Locate, LocateFixed } from "lucide"; +import { LoginResponse } from "./types/types"; -console.log("login"); +if (localStorage.getItem("token")) { + location.replace("/"); +} -$("#login").on("submit", (e) => { +const API_SERVER = import.meta.env.VITE_API_SERVER!; + +$("#login").on("submit", async (e) => { e.preventDefault(); - const email = $("#email").val(); - const password = $("#password").val(); + const email = $("#email"); + const password = $("#password"); - console.log(email, password); + fetch(`${API_SERVER}/api/v1/login`, { + method: "POST", + body: JSON.stringify({ + email: email.val(), + password: password.val(), + }), + }).then(async (response) => { + if (response.ok) { + const loginResponse = (await response.json()) as LoginResponse; + if (loginResponse.token) { + localStorage.setItem("token", loginResponse.token); + location.replace("/"); + } else { + console.error("Login failed", loginResponse); + } + } + }); }); diff --git a/src/logout.ts b/src/logout.ts new file mode 100644 index 0000000..da1a8bb --- /dev/null +++ b/src/logout.ts @@ -0,0 +1,5 @@ +if (localStorage.getItem("token")) { + localStorage.removeItem("token"); + + location.replace("/login"); +} diff --git a/src/main.ts b/src/main.ts index 007537e..eea8d72 100644 --- a/src/main.ts +++ b/src/main.ts @@ -14,6 +14,10 @@ import { panToCurrentLocation } from "@/utilities/geo"; import { MapService } from "@/services/map-service"; import { UIService } from "./services/ui-service"; +if (!localStorage.getItem("token")) { + location.replace("/login"); +} + // ENV const API_KEY = import.meta.env.VITE_MAPTILER_API_KEY || ""; diff --git a/src/register.ts b/src/register.ts index 06e7d97..fc59680 100644 --- a/src/register.ts +++ b/src/register.ts @@ -5,6 +5,12 @@ import "@/main.css"; // LIBRARIES import $ from "jquery"; +import { RegisterResponse } from "./types/types"; +import { register } from "module"; + +if (localStorage.getItem("token")) { + location.replace("/"); +} const API_SERVER = import.meta.env.VITE_API_SERVER!; @@ -13,13 +19,26 @@ $("#register").on("submit", async (e) => { const email = $("#email"); const password = $("#password"); - const response = await fetch(`${API_SERVER}/api/v1/register`, { + fetch(`${API_SERVER}/api/v1/register`, { method: "POST", body: JSON.stringify({ email: email.val(), password: password.val(), }), - }); + }).then(async (response) => { + if (response.ok) { + const registerResponse = (await response.json()) as RegisterResponse; - console.log(await response.json()); + if (registerResponse.status == "error") { + console.error(`${registerResponse.messages}`); + } + + if (registerResponse.status == "success" && registerResponse.user) { + console.log(`Created user ${registerResponse.user.email}`); + location.replace("/login"); + } + } else { + console.error("Registration returned an error"); + } + }); }); diff --git a/src/services/map-service.ts b/src/services/map-service.ts index b8ba243..c8e1df1 100644 --- a/src/services/map-service.ts +++ b/src/services/map-service.ts @@ -55,10 +55,9 @@ export class MapService { const hovered = features[0]; const hoveredId = hovered.id; - // Aggiorna l'ID della feature nel container delle informazioni. - this.uiService.$info.html(hoveredId?.toString() || ""); - if (hoveredId) { + // Aggiorna l'ID della feature nel container delle informazioni. + this.uiService.$info.html(hoveredId?.toString() || "None"); this.map.setFilter( LAYERS.BUILDING_HIGHLIGHT.id, layerFilterEq(hoveredId), @@ -66,6 +65,7 @@ export class MapService { this.map.getCanvas().style.cursor = "pointer"; } } else { + this.uiService.$info.html("None"); this.map.getCanvas().style.cursor = "default"; this.map.setFilter(LAYERS.BUILDING_HIGHLIGHT.id, layerFilterEq()); } diff --git a/src/types/types.ts b/src/types/types.ts index f7a407a..fdb7391 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -98,7 +98,24 @@ export interface RegisterRequest { password: string; } -export interface RegisterResponse { +export interface ApiResponse { status: "success" | "error"; messages: Array | undefined; } + +export interface ApiErrorResponse extends ApiResponse { + status: "error"; +} + +export interface RegisterResponse extends ApiResponse { + status: "success" | "error"; + messages: Array | undefined; + user: { + email: string; + }; +} + +export interface LoginResponse extends ApiResponse { + status: "success"; + token: string; +} diff --git a/src/utilities/password.ts b/src/utilities/crypt.ts similarity index 61% rename from src/utilities/password.ts rename to src/utilities/crypt.ts index 753e5ad..41f3695 100644 --- a/src/utilities/password.ts +++ b/src/utilities/crypt.ts @@ -1,3 +1,11 @@ +export const generateToken = (): string => { + const CHARACTERS = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;:,.<>?"; + const array = new Uint8Array(40); + crypto.getRandomValues(array); + return Array.from(array, (b) => CHARACTERS[b % CHARACTERS.length]).join(""); +}; + export const hashPassword = async (password: string): Promise => { const PASSWORD_SALT = Bun.env.PASSWORD_SALT!; const saltedPassword = PASSWORD_SALT + password;