complete registration and login
This commit is contained in:
@@ -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
9
logout.html
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<!doctype html>
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>Logout</h1>
|
||||||
|
|
||||||
|
<script type="module" src="/src/logout.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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 '';
|
||||||
@@ -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
123
server.ts
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
81
src/controllers/login.ts
Normal 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,
|
||||||
|
});
|
||||||
|
};
|
||||||
97
src/controllers/register.ts
Normal file
97
src/controllers/register.ts
Normal 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
33
src/login.ts
33
src/login.ts
@@ -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
5
src/logout.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
if (localStorage.getItem("token")) {
|
||||||
|
localStorage.removeItem("token");
|
||||||
|
|
||||||
|
location.replace("/login");
|
||||||
|
}
|
||||||
@@ -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 || "";
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
Reference in New Issue
Block a user