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>
<a href="/login">Login</a><br />
<a href="/logout">Logout</a><br />
<a href="/register">Register</a><br />
</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())
email String @unique
password String
token String @default("")
first_login Boolean
}

123
server.ts
View File

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

View File

@@ -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}"
};

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
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);
}
}
});
});

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 { UIService } from "./services/ui-service";
if (!localStorage.getItem("token")) {
location.replace("/login");
}
// ENV
const API_KEY = import.meta.env.VITE_MAPTILER_API_KEY || "";

View File

@@ -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");
}
});
});

View File

@@ -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());
}

View File

@@ -98,7 +98,24 @@ export interface RegisterRequest {
password: string;
}
export interface RegisterResponse {
export interface ApiResponse {
status: "success" | "error";
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> => {
const PASSWORD_SALT = Bun.env.PASSWORD_SALT!;
const saltedPassword = PASSWORD_SALT + password;