complete registration and login

This commit is contained in:
Matteo Rosati
2026-01-26 14:44:17 +01:00
parent 4fc146789f
commit c9db7c89d8
15 changed files with 315 additions and 114 deletions

View File

@@ -21,6 +21,7 @@
</div> </div>
</div> </div>
<a href="/login">Login</a><br /> <a href="/login">Login</a><br />
<a href="/logout">Logout</a><br />
<a href="/register">Register</a><br /> <a href="/register">Register</a><br />
</div> </div>

9
logout.html Normal file
View File

@@ -0,0 +1,9 @@
<!doctype html>
<html>
<body>
<h1>Logout</h1>
<script type="module" src="/src/logout.ts"></script>
</body>
</html>

View File

@@ -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 '';

View File

@@ -11,5 +11,6 @@ model User {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
email String @unique email String @unique
password String password String
token String @default("")
first_login Boolean first_login Boolean
} }

123
server.ts
View File

@@ -1,12 +1,8 @@
import { Hono } from "hono"; import { Hono } from "hono";
import { cors } from "hono/cors"; import { cors } from "hono/cors";
import { upgradeWebSocket, websocket } from "hono/bun"; // import { upgradeWebSocket, websocket } from "hono/bun";
import { DatabaseService } from "@/services/database-service"; import { RegisterController } from "@/controllers/register";
import { RegisterRequest, RegisterResponse } from "@/types/types"; import { LoginController } from "@/controllers/login";
import { MESSAGES } from "@/auth/messages";
import { validateEmail } from "@/utilities/email";
import { Prisma } from "@/orm/generated/prisma/client";
import { hashPassword } from "@/utilities/password";
const app = new Hono(); const app = new Hono();
@@ -28,103 +24,30 @@ app.get("/", async (c) => {
}); });
}); });
app.post("/api/v1/register", async (c) => { app.post("/api/v1/register", RegisterController);
let body: RegisterRequest | undefined;
let errors = false;
let messages: Array<string> = [];
// Get the request body and handle malformed payload app.post("/api/v1/login", LoginController);
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.get(
// Request validation // "/ws",
if (!body.email) { // upgradeWebSocket((c) => {
errors = true; // return {
messages.push(MESSAGES.MISSING_EMAIL); // onOpen(e, ws) {
} // console.log("Server: Connection opened");
// ws.send("Hello!");
if (!validateEmail(body.email)) { // },
errors = true; // onClose: () => {
messages.push(MESSAGES.INVALID_EMAIL); // console.log("Server: Connection closed");
} // },
// onMessage: (ev) => {
if (!body.password) { // console.log(ev.data);
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);
},
};
}),
);
export default { export default {
port: Bun.env.SERVER_PORT, port: Bun.env.SERVER_PORT,
fetch: app.fetch, fetch: app.fetch,
websocket, // websocket,
}; };

View File

@@ -7,3 +7,12 @@ export const INITIAL_ZOOM = 17;
* MapTiler style URL for the map * MapTiler style URL for the map
*/ */
export const MAP_STYLE = import.meta.env.VITE_MAPTILER_STYLE; 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}"
};

81
src/controllers/login.ts Normal file
View File

@@ -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<string> = [];
// 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,
});
};

View File

@@ -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<string> = [];
// 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);
}
};

View File

@@ -5,15 +5,34 @@ import "@/main.css";
// LIBRARIES // LIBRARIES
import $ from "jquery"; import $ from "jquery";
// import { FilterSpecification, Map, config } from "@maptiler/sdk"; import { LoginResponse } from "./types/types";
// import { createIcons, Locate, LocateFixed } from "lucide";
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(); e.preventDefault();
const email = $("#email").val(); const email = $("#email");
const password = $("#password").val(); 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);
}
}
});
}); });

5
src/logout.ts Normal file
View File

@@ -0,0 +1,5 @@
if (localStorage.getItem("token")) {
localStorage.removeItem("token");
location.replace("/login");
}

View File

@@ -14,6 +14,10 @@ import { panToCurrentLocation } from "@/utilities/geo";
import { MapService } from "@/services/map-service"; import { MapService } from "@/services/map-service";
import { UIService } from "./services/ui-service"; import { UIService } from "./services/ui-service";
if (!localStorage.getItem("token")) {
location.replace("/login");
}
// ENV // ENV
const API_KEY = import.meta.env.VITE_MAPTILER_API_KEY || ""; const API_KEY = import.meta.env.VITE_MAPTILER_API_KEY || "";

View File

@@ -5,6 +5,12 @@ import "@/main.css";
// LIBRARIES // LIBRARIES
import $ from "jquery"; 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!; const API_SERVER = import.meta.env.VITE_API_SERVER!;
@@ -13,13 +19,26 @@ $("#register").on("submit", async (e) => {
const email = $("#email"); const email = $("#email");
const password = $("#password"); const password = $("#password");
const response = await fetch(`${API_SERVER}/api/v1/register`, { fetch(`${API_SERVER}/api/v1/register`, {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
email: email.val(), email: email.val(),
password: password.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");
}
});
}); });

View File

@@ -55,10 +55,9 @@ export class MapService {
const hovered = features[0]; const hovered = features[0];
const hoveredId = hovered.id; const hoveredId = hovered.id;
// Aggiorna l'ID della feature nel container delle informazioni.
this.uiService.$info.html(hoveredId?.toString() || "");
if (hoveredId) { if (hoveredId) {
// Aggiorna l'ID della feature nel container delle informazioni.
this.uiService.$info.html(hoveredId?.toString() || "None");
this.map.setFilter( this.map.setFilter(
LAYERS.BUILDING_HIGHLIGHT.id, LAYERS.BUILDING_HIGHLIGHT.id,
layerFilterEq(hoveredId), layerFilterEq(hoveredId),
@@ -66,6 +65,7 @@ export class MapService {
this.map.getCanvas().style.cursor = "pointer"; this.map.getCanvas().style.cursor = "pointer";
} }
} else { } else {
this.uiService.$info.html("None");
this.map.getCanvas().style.cursor = "default"; this.map.getCanvas().style.cursor = "default";
this.map.setFilter(LAYERS.BUILDING_HIGHLIGHT.id, layerFilterEq()); this.map.setFilter(LAYERS.BUILDING_HIGHLIGHT.id, layerFilterEq());
} }

View File

@@ -98,7 +98,24 @@ export interface RegisterRequest {
password: string; password: string;
} }
export interface RegisterResponse { export interface ApiResponse {
status: "success" | "error"; status: "success" | "error";
messages: Array<string> | undefined; messages: Array<string> | undefined;
} }
export interface ApiErrorResponse extends ApiResponse {
status: "error";
}
export interface RegisterResponse extends ApiResponse {
status: "success" | "error";
messages: Array<string> | undefined;
user: {
email: string;
};
}
export interface LoginResponse extends ApiResponse {
status: "success";
token: string;
}

View File

@@ -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<string> => { export const hashPassword = async (password: string): Promise<string> => {
const PASSWORD_SALT = Bun.env.PASSWORD_SALT!; const PASSWORD_SALT = Bun.env.PASSWORD_SALT!;
const saltedPassword = PASSWORD_SALT + password; const saltedPassword = PASSWORD_SALT + password;