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;