complete registration and login
This commit is contained in:
@@ -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
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())
|
||||
email String @unique
|
||||
password String
|
||||
token String @default("")
|
||||
first_login Boolean
|
||||
}
|
||||
|
||||
123
server.ts
123
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<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,
|
||||
};
|
||||
|
||||
@@ -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
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
|
||||
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
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 { UIService } from "./services/ui-service";
|
||||
|
||||
if (!localStorage.getItem("token")) {
|
||||
location.replace("/login");
|
||||
}
|
||||
|
||||
// ENV
|
||||
const API_KEY = import.meta.env.VITE_MAPTILER_API_KEY || "";
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user