diff --git a/.env.example b/.env.example index 304a913..f6d25ac 100644 --- a/.env.example +++ b/.env.example @@ -11,7 +11,8 @@ GRAPH_PORT=3000 ELK_PORT=5601 -GOOGLE_CALLBACK_URL=https://localhost:8443/api/v1 +# the url to which the user will be redirected when it logs with google +CALLBACK_REDIR=http://localhost:3000 GOOGLE_CLIENT_SECRET=susAF GOOGLE_CLIENT_ID=Really @@ -25,4 +26,6 @@ SMTP_AUTH_USERNAME= SMTP_AUTH_PASSWORD= EMAIL_TO= +USER_URL= +AUTH_URL= CORS_ORIGIN= diff --git a/doc/auth/logout.md b/doc/auth/logout.md new file mode 100644 index 0000000..98b6edc --- /dev/null +++ b/doc/auth/logout.md @@ -0,0 +1,24 @@ +# Logout + +Available endpoints: +- GET `/logout` + +Common return: +- 500 with response +```json +{ + "error": "Internal server error" +} +``` + +## GET `/logout` + +Used to logout the client (it just delete the cookie) + +Returns: +- 200 with response and clear cookie +```json +{ + "msg": "Logout successful" +} +``` diff --git a/doc/auth/me.md b/doc/auth/me.md index 4c907e2..26a2e15 100644 --- a/doc/auth/me.md +++ b/doc/auth/me.md @@ -3,3 +3,9 @@ GET `/me` Inputs : just need the JWT cookie Returns the user of the account + +``` +{ + user: ":userId" +} +``` diff --git a/docker/api-base/compose.yml b/docker/api-base/compose.yml index fa6d469..bed06c8 100644 --- a/docker/api-base/compose.yml +++ b/docker/api-base/compose.yml @@ -30,7 +30,7 @@ services: - back environment: - TZ=Europe/Paris - - GOOGLE_CALLBACK_URL=${GOOGLE_CALLBACK_URL} + - GOOGLE_CALLBACK_URL=${AUTH_URL} - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID} - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET} - API_TARGET=auth diff --git a/docker/front/Dockerfile b/docker/front/Dockerfile index b8f2ce2..36fc0e5 100644 --- a/docker/front/Dockerfile +++ b/docker/front/Dockerfile @@ -13,6 +13,9 @@ RUN cd /build \ FROM node:lts-alpine AS builder-vite +ARG VITE_USER_URL +ARG VITE_AUTH_URL + RUN npm install -g pnpm WORKDIR /app @@ -24,8 +27,8 @@ RUN pnpm install --frozen-lockfile COPY vite.config.js tailwind.config.js ./ COPY src ./src -RUN pnpm vite build - +RUN VITE_USER_URL=${VITE_USER_URL} VITE_AUTH_URL=${VITE_AUTH_URL}\ + pnpm vite build FROM alpine:3.22 diff --git a/docker/front/compose.yml b/docker/front/compose.yml index 72b2b67..134b29d 100644 --- a/docker/front/compose.yml +++ b/docker/front/compose.yml @@ -4,6 +4,9 @@ services: build: dockerfile: docker/front/Dockerfile context: ../../ + args: + - VITE_USER_URL=${USER_URL} + - VITE_AUTH_URL=${AUTH_URL} environment: - TZ=Europe/Paris networks: diff --git a/src/api/auth/default.js b/src/api/auth/default.js index 63b9888..56ebf62 100644 --- a/src/api/auth/default.js +++ b/src/api/auth/default.js @@ -11,6 +11,7 @@ import { gRegisterCallback } from './gRegisterCallback.js'; import { totpSetup } from './totpSetup.js'; import { totpDelete } from './totpDelete.js'; import { totpVerify } from './totpVerify.js'; +import { logout } from './logout.js'; const saltRounds = 10; export const appName = process.env.APP_NAME || 'knl_meowscendence'; @@ -114,4 +115,6 @@ export default async function(fastify, options) { } } }, async (request, reply) => { return register(request, reply, saltRounds, fastify); }); + + fastify.get('/logout', {}, async (request, reply) => { return logout(reply, fastify); }) } diff --git a/src/api/auth/gLogCallback.js b/src/api/auth/gLogCallback.js index 975b7d6..6b3c78b 100644 --- a/src/api/auth/gLogCallback.js +++ b/src/api/auth/gLogCallback.js @@ -37,7 +37,7 @@ export async function gLogCallback(request, reply, fastify) { return reply.code(400).send({ error: "User does not exist" }); } - const token = fastify.jwt.sign(user); + const token = fastify.jwt.sign({ user: user.username}); return reply .setCookie('token', token, { @@ -45,9 +45,7 @@ export async function gLogCallback(request, reply, fastify) { path: '/', secure: env !== 'development', sameSite: 'lax', - }) - .code(200) - .send({ msg: "Login successful" }); + }).redirect(process.env.CALLBACK_REDIR); } catch (error) { fastify.log.error(error); reply.code(500).send({ error: 'Internal server error' }); diff --git a/src/api/auth/gRegisterCallback.js b/src/api/auth/gRegisterCallback.js index f79542f..217fae5 100644 --- a/src/api/auth/gRegisterCallback.js +++ b/src/api/auth/gRegisterCallback.js @@ -1,6 +1,7 @@ import axios from 'axios' import authDB from '../../utils/authDB.js'; +import { authUserCreate } from '../../utils/authUserCreate.js'; var env = process.env.NODE_ENV || 'development'; @@ -46,7 +47,9 @@ export async function gRegisterCallback(request, reply, fastify) { authDB.addUser(user.username, ''); - const token = fastify.jwt.sign(user); + authUserCreate(user.username, fastify) + + const token = fastify.jwt.sign({ user: user.username}); return reply .setCookie('token', token, { @@ -54,9 +57,7 @@ export async function gRegisterCallback(request, reply, fastify) { path: '/', secure: env !== 'development', sameSite: 'lax', - }) - .code(200) - .send({ msg: "Register successful" }); + }).redirect(process.env.CALLBACK_REDIR); } catch (error) { fastify.log.error(error); reply.code(500).send({ error: 'Internal server error' }); diff --git a/src/api/auth/logout.js b/src/api/auth/logout.js new file mode 100644 index 0000000..b3c57ef --- /dev/null +++ b/src/api/auth/logout.js @@ -0,0 +1,18 @@ +/** + * @async + * @param {import("fastify").FastifyReply} reply + * @param {import("fastify").FastifyInstance} fastify + * + * @returns {import("fastify").FastifyReply} + */ +export async function logout(reply, fastify) { + try { + return reply + .code(200) + .clearCookie("token") + .send({ msg: "Logout successful" }); + } catch { + fastify.log.error(err); + return reply.code(500).send({ error: "Internal server error" }); + } +} diff --git a/src/api/auth/register.js b/src/api/auth/register.js index 7463452..5e10ff1 100644 --- a/src/api/auth/register.js +++ b/src/api/auth/register.js @@ -2,6 +2,7 @@ import bcrypt from 'bcrypt'; import { isValidString } from '../../utils/authUtils.js'; import authDB from '../../utils/authDB.js'; +import { authUserCreate } from '../../utils/authUserCreate.js'; var env = process.env.NODE_ENV || 'development'; @@ -36,6 +37,8 @@ export async function register(request, reply, saltRounds, fastify) { const hash = await bcrypt.hash(password, saltRounds); authDB.addUser(user, hash); + authUserCreate(user, fastify) + const token = fastify.jwt.sign({ user }); return reply diff --git a/src/front/index.html b/src/front/index.html index 6471bae..de47004 100644 --- a/src/front/index.html +++ b/src/front/index.html @@ -36,6 +36,7 @@
+
-
- 12:37 +
+ +
+ 12:37 +
diff --git a/src/front/static/ts/main.ts b/src/front/static/ts/main.ts index b18641f..6abfa27 100644 --- a/src/front/static/ts/main.ts +++ b/src/front/static/ts/main.ts @@ -1,6 +1,8 @@ import { oneko } from "./oneko.ts"; import ProfileMenu from "./views/ProfileMenu.ts"; +import FriendsMenu from "./views/Friends.ts"; let profile_view = new ProfileMenu; +let friends_view = new FriendsMenu; export async function isLogged(): Promise { let uuid_req = await fetch("http://localhost:3001/me", { @@ -78,6 +80,10 @@ document.addEventListener("DOMContentLoaded", () => { profile_view.open = false; document.getElementById("taskbar-menu").innerHTML = ""; } + if (e.target.matches("#friends-btn")) { + friends_view.open = !friends_view.open; + friends_view.run(); + } if (e.target.matches("[data-link]")) { e.preventDefault(); navigationManager(e.target.href); @@ -96,7 +102,10 @@ document.addEventListener("DOMContentLoaded", () => { router(); }); -function updateClock() { +oneko(); + +function updateClock() +{ const days = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']; const months = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']; const clock = document.getElementById("taskbar-clock"); diff --git a/src/front/static/ts/views/Friends.ts b/src/front/static/ts/views/Friends.ts new file mode 100644 index 0000000..7e11e4e --- /dev/null +++ b/src/front/static/ts/views/Friends.ts @@ -0,0 +1,200 @@ +import Aview from "./Aview.ts" +import { setOnekoState } from "../oneko.ts" +import { dragElement } from "./drag.ts"; +import { isLogged, navigationManager } from "../main.ts" + +export default class extends Aview { + + open: Boolean = false; + + constructor() { + super(); + this.setTitle("friends list"); + setOnekoState("default"); + } + + async getHTML() { + return ` +
+
+ friends.ts +
+ +
+
+ + + + +
+ + +
+ +
+
+
+ `; + } + + async run() { + if (!await isLogged()) + navigationManager("/"); + + if (this.open === true) { + document.getElementById("friends-menu").innerHTML = await this.getHTML(); + } else { + document.getElementById("friends-menu").innerHTML = ""; + return; + } + + let uuid: String; + uuid = document.cookie.match(new RegExp('(^| )' + "uuid" + '=([^;]+)'))[2]; + const friends_error_message = (document.getElementById("friends-error-message") as HTMLParagraphElement); + const friends_list = (document.getElementById("friends_list") as HTMLUListElement); + const new_friend = (document.getElementById("new-friend") as HTMLInputElement); + const add_friend_err = (document.getElementById("add-friend-err") as HTMLParagraphElement); + const add_friend_msg = (document.getElementById("add-friend-msg") as HTMLParagraphElement); + + async function removeFriend(name: String) { + const data_req = await fetch("http://localhost:3002/users/" + uuid + "/friends/" + name, { + method: "DELETE", + credentials: "include", + }); + if (data_req.status === 200) { + console.log("friends removed successfully"); + } else { + console.log("could not remove friend"); + } + list_friends(); + list_friends(); + list_friends(); + list_friends(); + list_friends(); + list_friends(); + } + + async function isFriendLogged(name: string): Promise { + const data_req = await fetch("http://localhost:3002/ping/" + name, { + method: "GET", + credentials: "include", + }); + + if (data_req.status === 404) + return false; + + return (await data_req.json()).isLogged + } + + const list_friends = async () => { + const data_req = await fetch("http://localhost:3002/users/" + uuid + "/friends/count", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + }); + if (data_req.status === 404) { + } + let data = await data_req.json(); + while (friends_list.firstChild) { + friends_list.removeChild(friends_list.firstChild); + } + + if (data.n_friends > 0) { + const list_req = await fetch("http://localhost:3002/users/" + uuid + "/friends?iStart=0&iEnd=2147483647", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + }); + if (list_req.status == 404) { + friends_list.classList.add("hidden") + return; + } else if (list_req.status === 200) { + friends_list.classList.remove("hidden") + + let list = (await list_req.json()).friends as JSON; + + for (let i = 0; i < data.n_friends; i++) { + let new_friends = document.createElement('li'); + + const activitySpan = document.createElement('span'); + const isLogged = await isFriendLogged(list[i].friendName) + activitySpan.textContent = "•"; + if (isLogged == true) + activitySpan.className = "px-0 text-green-500"; + else + activitySpan.className = "px-0 text-red-500"; + + + const span = document.createElement('span'); + span.className = "px-3"; + span.textContent = list[i].friendName; + + const but = document.createElement('button'); + but.textContent = "-"; + but.classList.add("px-0", "py-0", "taskbar-button"); + but.onclick = function() { + removeFriend(list[i].friendName); + }; + + new_friends.appendChild(activitySpan); + new_friends.appendChild(span); + new_friends.appendChild(but); + friends_list.appendChild(new_friends); + } + } else { + friends_error_message.innerHTML = (await list_req.json()).error; + friends_error_message.classList.remove("hidden"); + } + } else { + friends_list.classList.add("hidden") + } + } + + const add_friend = async () => { + const data_req = await fetch("http://localhost:3002/users/" + uuid + "/friends/" + new_friend.value, { + method: "POST", + credentials: "include", + }); + let data = await data_req.json() + if (data_req.status === 200) { + add_friend_msg.innerHTML = data.msg; + add_friend_msg.classList.remove("hidden"); + if (!add_friend_err.classList.contains("hidden")) + add_friend_err.classList.add("hidden") + } else { + add_friend_err.innerHTML = data.error; + add_friend_err.classList.remove("hidden"); + if (!add_friend_msg.classList.contains("hidden")) + add_friend_msg.classList.add("hidden") + list_friends() + return + } + list_friends() + new_friend.value = ''; + } + + try { + const data_req = await fetch("http://localhost:3002/users/" + uuid + "/friends/count", { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + }); + if (data_req.status === 200) { + // let data = await data_req.json(); + list_friends() + } + } catch (error) { + friends_error_message.innerHTML = "failed to fetch friends"; + friends_error_message.classList.remove("hidden"); + } + document.getElementById("add-friends-button")?.addEventListener("click", add_friend); + setInterval(list_friends, 30000); + } +} diff --git a/src/front/static/ts/views/ProfileMenu.ts b/src/front/static/ts/views/ProfileMenu.ts index 0113c08..c2937d5 100644 --- a/src/front/static/ts/views/ProfileMenu.ts +++ b/src/front/static/ts/views/ProfileMenu.ts @@ -2,7 +2,6 @@ import Aview from "./Aview.ts" import { isLogged, navigationManager } from "../main.ts" export default class extends Aview { - async getHTML() { return `
@@ -66,6 +65,7 @@ export default class extends Aview {
profile settings + friends `; } diff --git a/src/utils/authUserCreate.js b/src/utils/authUserCreate.js new file mode 100644 index 0000000..e8a6771 --- /dev/null +++ b/src/utils/authUserCreate.js @@ -0,0 +1,24 @@ +import axios from 'axios'; + +/** + * @param {string} username + * @param {import('fastify').FastifyInstance} fastify + */ +export async function authUserCreate(username, fastify) { + const payload = { + displayName: username, + }; + const cookie = fastify.jwt.sign({ user: "admin" }); + + const url = process.env.USER_URL || "http://localhost:3002" + + await axios.post( + url + "/users/" + username, + payload, + { + headers: { + 'Cookie': 'token=' + cookie, + }, + } + ); +}