Compare commits
19 commits
eplace-ste
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
206de2c05e | ||
|
|
77a6d7fa6c | ||
|
|
34e5527244 | ||
|
|
b7d988048b | ||
|
|
3914fbdba8 | ||
|
|
e84731e8de | ||
|
|
1e4cd71e06 | ||
|
|
3d1a427dcf | ||
|
|
c6f5ab3438 | ||
|
|
50d46d0f0c | ||
|
|
8cab4ac558 | ||
|
|
814e559ccd | ||
|
|
02ac670fe1 | ||
|
|
2b7b6b8684 | ||
|
|
a0b5e97058 | ||
|
|
441f00507d | ||
|
|
0114214995 | ||
|
|
0fb4513b4b | ||
|
|
414aec97e5 |
12 changed files with 716 additions and 148 deletions
10
README.md
10
README.md
|
|
@ -0,0 +1,10 @@
|
||||||
|
# E/Place
|
||||||
|
|
||||||
|
> **Note** This is a school project, therefore it probably won't interest you if you are looking for something useful.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The goal of this rush was to implement a client for a r/place-like canvas. For those who don't know what I'm taliking about, r/place was an event on reddit where you had a world map where you could place a pixel of color every 5 minutes where you wanted.
|
||||||
|
This is basically the same thing: It first authenticates the user via OpenID, then connects to the server using Sockets.IO and a REST API simultaneously. Then it allows the user to place pixels, choose the color, view other people pixels, create or join rooms or customize profile, all with real time map update.
|
||||||
|
We had approximately 2 days to implement the client. The server socket, the API, and the OpenID server were already present, as well as the base HTML/CSS files and the madatory structure.
|
||||||
|
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
import * as auth from "../../../utils/auth"
|
import * as auth from "../../../utils/auth";
|
||||||
|
|
||||||
let code;
|
let code;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams(window.location.search)
|
const params = new URLSearchParams(window.location.search);
|
||||||
code = params.get("code")
|
|
||||||
}
|
code = params.get("code");
|
||||||
catch {
|
} catch {
|
||||||
console.error("Unable to retrieve code")
|
console.error("Unable to retrieve code");
|
||||||
alert("Unable to retrieve code")
|
alert("Unable to retrieve code");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! await auth.getToken(code)) {
|
if (!(await auth.getToken(code))) {
|
||||||
console.error("Unable to retrieve token")
|
console.error("Unable to retrieve token");
|
||||||
alert("Unable to retrieve token")
|
alert("Unable to retrieve token");
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug("Redirecting...")
|
console.debug("Redirecting...");
|
||||||
window.location = import.meta.env.VITE_URL
|
window.location = import.meta.env.VITE_URL;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// FIXME: File that provide utils function for the debug page
|
|
||||||
import $ from "jquery";
|
import $ from "jquery";
|
||||||
import jwt_decode from "jwt-decode";
|
import jwt_decode from "jwt-decode";
|
||||||
import { createAlert } from "../../utils/notify";
|
import { createAlert } from "../../utils/notify";
|
||||||
|
import { authedAPIRequest } from "../../utils/auth";
|
||||||
|
|
||||||
export async function displayStudentProfile() {
|
export async function displayStudentProfile() {
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
|
|
@ -10,7 +10,9 @@ export async function displayStudentProfile() {
|
||||||
const _uid = decoded.uid;
|
const _uid = decoded.uid;
|
||||||
|
|
||||||
// You have to write a request to fetch your informations
|
// You have to write a request to fetch your informations
|
||||||
const request_result = null;
|
const request_result = await authedAPIRequest(`/students/${_uid}`, {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
if (request_result === null) {
|
if (request_result === null) {
|
||||||
createAlert(
|
createAlert(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,111 @@
|
||||||
// FIXME: This is the entry point of the application, write your code here
|
import { initSocket, socket, subscribe } from "../utils/streams";
|
||||||
|
|
||||||
import { calculateLayout } from "./utils";
|
import { calculateLayout } from "./utils";
|
||||||
|
import { authenticate, refreshToken } from "../utils/auth";
|
||||||
|
import {
|
||||||
|
getPlacementData,
|
||||||
|
initCanvas,
|
||||||
|
renderCanvasUpdate,
|
||||||
|
} from "../rooms/canvas/utils";
|
||||||
|
import { getCanvas, placePixel } from "../rooms/canvas/index";
|
||||||
|
import { fetchRoomConfig, getCurrentRoomConfig } from "../rooms/index";
|
||||||
|
|
||||||
// Initialize the layout
|
// Initialize the layout
|
||||||
calculateLayout();
|
calculateLayout();
|
||||||
|
|
||||||
|
// Auth
|
||||||
|
await authenticate();
|
||||||
|
|
||||||
|
////////////////////////////////////
|
||||||
|
// Sockets
|
||||||
|
//
|
||||||
|
let room = window.location.pathname.split("/")[1];
|
||||||
|
const update_waitlist = [];
|
||||||
|
let initialized = false;
|
||||||
|
|
||||||
|
if (!room) {
|
||||||
|
room = "epi-place";
|
||||||
|
}
|
||||||
|
|
||||||
|
await initSocket();
|
||||||
|
await subscribe(room, "pixel-update");
|
||||||
|
// Main event
|
||||||
|
socket.on("message", async (response) => {
|
||||||
|
console.debug("Received server event on message: ");
|
||||||
|
console.debug(response);
|
||||||
|
if (response.error) {
|
||||||
|
console.error("Got server error: " + response.error.json.message);
|
||||||
|
|
||||||
|
if (response.error.json.data.httpStatus === 401) {
|
||||||
|
await refreshToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.result.type === "started") {
|
||||||
|
await fetchRoomConfig(room);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pixels = await getCanvas(room);
|
||||||
|
|
||||||
|
if (!pixels) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
initCanvas(await getCurrentRoomConfig(room), pixels);
|
||||||
|
initialized = true;
|
||||||
|
while (update_waitlist.length > 0) {
|
||||||
|
const obj = update_waitlist.pop();
|
||||||
|
|
||||||
|
if (obj) {
|
||||||
|
renderCanvasUpdate(obj.color, obj.posX, obj.posY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug("Loaded canvas");
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("pixel-update", async (msg) => {
|
||||||
|
// console.debug("Received server event on pixel-update: ");
|
||||||
|
// console.debug(msg);
|
||||||
|
if (msg.error) {
|
||||||
|
console.error("Got server error: " + msg.error.json.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.debug("Here is msg.data")
|
||||||
|
// console.debug(msg.result.data.json)
|
||||||
|
const {
|
||||||
|
// roomSlug,
|
||||||
|
posX,
|
||||||
|
posY,
|
||||||
|
color,
|
||||||
|
} = msg.result.data.json;
|
||||||
|
|
||||||
|
if (!initialized) {
|
||||||
|
update_waitlist.push({ posX, posY, color });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCanvasUpdate(color, posX, posY);
|
||||||
|
});
|
||||||
|
|
||||||
|
////////////////////////////////////
|
||||||
|
// Buttons
|
||||||
|
//
|
||||||
|
async function placePixelButton() {
|
||||||
|
const { color, posX, posY } = getPlacementData();
|
||||||
|
|
||||||
|
await placePixel(room, posX, posY, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////
|
||||||
|
// HTML
|
||||||
|
//
|
||||||
|
const placeButtonElt = document.getElementById("color-place-button");
|
||||||
|
|
||||||
|
placeButtonElt.addEventListener("click", () => {
|
||||||
|
placePixelButton();
|
||||||
|
});
|
||||||
|
|
||||||
|
export { room };
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,164 @@
|
||||||
// FIXME: This file should handle the room canvas API
|
import { authedAPIRequest } from "../../utils/auth";
|
||||||
// Link buttons to their respective functions
|
|
||||||
// Functions may include:
|
/**
|
||||||
// - getCanvas (get the canvas of a room and deserialize it)
|
* @param {string} room
|
||||||
// - subscribeToRoom (subscribe to the stream of a room)
|
* @return {Promise<Object?>} The response Json object
|
||||||
// - getPixelInfo (get the pixel info of a room)
|
*/
|
||||||
// - placePixel (place a pixel in a room)
|
async function fetchCanvas(room) {
|
||||||
|
if (!room) {
|
||||||
|
console.error("Cannot fetch canvas of an undefined room");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await authedAPIRequest("/rooms/" + room + "/canvas", {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response || !response.ok) {
|
||||||
|
// console.error(
|
||||||
|
// "Could not retrieve room canvas: " + response && response.statusText
|
||||||
|
// ? response.statusText
|
||||||
|
// : "no more informations",
|
||||||
|
// );
|
||||||
|
console.error(
|
||||||
|
"Could not retrieve room canvas: nique ta mère la moulinette",
|
||||||
|
);
|
||||||
|
|
||||||
|
// console.debug(await response.text());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await response.json();
|
||||||
|
|
||||||
|
console.debug(`Retrieved canvas for room ${room}:`);
|
||||||
|
// console.debug(res);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Splits a string into fixed length substrings
|
||||||
|
String.prototype.chunk = function (size) {
|
||||||
|
return [].concat.apply(
|
||||||
|
[],
|
||||||
|
this.split("").map(function (x, i) {
|
||||||
|
return i % size ? [] : this.slice(i, i + size);
|
||||||
|
}, this),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// get the canvas of a room and deserialize it
|
||||||
|
async function getCanvas(room) {
|
||||||
|
const raw_pixels = await fetchCanvas(room);
|
||||||
|
|
||||||
|
if (!raw_pixels || !raw_pixels.pixels) {
|
||||||
|
console.error("Aborting canvas deserialization");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to a an array of strings representing binary (beurk)
|
||||||
|
let raw_binary_str = "";
|
||||||
|
|
||||||
|
for (let i = 0; i < raw_pixels.pixels.length; i++) {
|
||||||
|
raw_binary_str = raw_binary_str.concat(
|
||||||
|
raw_pixels.pixels.charCodeAt(i).toString(2).padStart(8, "0"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// console.debug(raw_binary_str)
|
||||||
|
|
||||||
|
// Chunk it to an array of fixed-length string (5)
|
||||||
|
let pixel_array = raw_binary_str.chunk(5);
|
||||||
|
|
||||||
|
if (pixel_array[pixel_array.length - 1].length < 5) {
|
||||||
|
pixel_array.pop();
|
||||||
|
}
|
||||||
|
// console.debug(pixel_array)
|
||||||
|
|
||||||
|
// Convert into numbers
|
||||||
|
pixel_array = pixel_array.map((pixel) => parseInt(pixel, 2));
|
||||||
|
// console.debug(pixel_array)
|
||||||
|
|
||||||
|
return pixel_array;
|
||||||
|
}
|
||||||
|
|
||||||
|
// subscribe to the stream of a room
|
||||||
|
function subscribeToRoom() {}
|
||||||
|
|
||||||
|
// get the pixel info of a room
|
||||||
|
async function getPixelInfo(room, posX, posY) {
|
||||||
|
if (!room) {
|
||||||
|
console.error("Cannot place pixel on an undefined room");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
posX,
|
||||||
|
posY,
|
||||||
|
});
|
||||||
|
const response = await authedAPIRequest(
|
||||||
|
"/rooms/" + room + "/canvas/pixels" + "?" + params,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response || !response.ok) {
|
||||||
|
console.error(
|
||||||
|
"Could not retrieve pixel infos: " + response && response.statusText
|
||||||
|
? response.statusText
|
||||||
|
: "no more informations",
|
||||||
|
);
|
||||||
|
// console.debug(await response.text());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await response.json();
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// place a pixel in a room
|
||||||
|
async function placePixel(room, posX, posY, color) {
|
||||||
|
if (!room) {
|
||||||
|
console.error("Cannot place pixel on an undefined room");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await authedAPIRequest(
|
||||||
|
"/rooms/" + room + "/canvas/pixels",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
posX,
|
||||||
|
posY,
|
||||||
|
color,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response || !response.ok) {
|
||||||
|
console.error(
|
||||||
|
"Could not place pixel on map: " + response && response.statusText
|
||||||
|
? response.statusText
|
||||||
|
: "no more informations",
|
||||||
|
);
|
||||||
|
// console.debug(await response.text());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await response.json();
|
||||||
|
|
||||||
|
console.debug(
|
||||||
|
`Placed pixel on map at ${posX}:${posY} (${color}) in room ${room}:`,
|
||||||
|
);
|
||||||
|
// console.debug(res);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getCanvas, subscribeToRoom, getPixelInfo, placePixel };
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@
|
||||||
// - toggleTooltip (toggle the tooltip and display the pixel's information)
|
// - toggleTooltip (toggle the tooltip and display the pixel's information)
|
||||||
|
|
||||||
import $ from "jquery";
|
import $ from "jquery";
|
||||||
|
import { getPixelInfo } from "./index";
|
||||||
|
import { getStudent } from "../../students";
|
||||||
|
|
||||||
const canvasContainer = $("#canvas-container")?.[0];
|
const canvasContainer = $("#canvas-container")?.[0];
|
||||||
const canvas = $("#canvas")?.[0];
|
const canvas = $("#canvas")?.[0];
|
||||||
|
|
@ -39,7 +41,7 @@ let isDrag = false;
|
||||||
* Get the placement data, i.e. the color the user has selected and the
|
* Get the placement data, i.e. the color the user has selected and the
|
||||||
* coordinates of the pixel he is focusing on.
|
* coordinates of the pixel he is focusing on.
|
||||||
*
|
*
|
||||||
* @returns {{color: number, posX: number, posX: number}} the data
|
* @returns {{color: number, posX: number, posY: number}} the data
|
||||||
*/
|
*/
|
||||||
export const getPlacementData = () => ({
|
export const getPlacementData = () => ({
|
||||||
color: selectedColorIdx,
|
color: selectedColorIdx,
|
||||||
|
|
@ -58,8 +60,22 @@ export const toggleTooltip = async (state = false) => {
|
||||||
tooltip.style.display = state ? "flex" : "none";
|
tooltip.style.display = state ? "flex" : "none";
|
||||||
|
|
||||||
if (state) {
|
if (state) {
|
||||||
// FIXME: You should implement or call a function to get the pixel's information
|
// Get pixel
|
||||||
// and display it. Make use of target.x and target.y to get the pixel's position.
|
const room = window.location.pathname.split("/")[1] || "epi-place";
|
||||||
|
const response = await getPixelInfo(room, target.x, target.y);
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display
|
||||||
|
const date = new Date(response.timestamp);
|
||||||
|
|
||||||
|
document.getElementById("tooltip-date").innerText =
|
||||||
|
date.toLocaleDateString("fr-fr");
|
||||||
|
document.getElementById("tooltip-time").innerText =
|
||||||
|
date.toLocaleTimeString("fr-fr");
|
||||||
|
getStudent(response.placedByUid);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
import { authedAPIRequest } from "../utils/auth";
|
||||||
|
import { subscribe } from "../utils/streams";
|
||||||
|
import { resetValues } from "./canvas/utils";
|
||||||
// FIXME: This file should handle the rooms API
|
// FIXME: This file should handle the rooms API
|
||||||
// Functions may include:
|
// Functions may include:
|
||||||
// - fetchRoomConfig (get the configuration of a room)
|
// - fetchRoomConfig (get the configuration of a room)
|
||||||
|
|
@ -8,3 +11,111 @@
|
||||||
// - createRoom (create a room)
|
// - createRoom (create a room)
|
||||||
// - updateRoom (update a room's configuration)
|
// - updateRoom (update a room's configuration)
|
||||||
// - deleteRoom (delete a room)
|
// - deleteRoom (delete a room)
|
||||||
|
|
||||||
|
let roomConfig = null;
|
||||||
|
|
||||||
|
function setCurrentRoomConfig(cfg) {
|
||||||
|
roomConfig = cfg;
|
||||||
|
}
|
||||||
|
async function getCurrentRoomConfig(room) {
|
||||||
|
if (!roomConfig) {
|
||||||
|
await fetchRoomConfig(room);
|
||||||
|
}
|
||||||
|
|
||||||
|
return roomConfig;
|
||||||
|
}
|
||||||
|
async function joinRoom(room) {
|
||||||
|
// socket.on('connection', (sockett) => {
|
||||||
|
// sockett.join(room);
|
||||||
|
// broadcast to all connected clients in the room
|
||||||
|
// socket.to('some room').emit('hello', 'world');
|
||||||
|
// broadcast to all connected clients except those in the room
|
||||||
|
// socket.except('some room').emit('hello', 'world');
|
||||||
|
// });
|
||||||
|
|
||||||
|
resetValues();
|
||||||
|
subscribe(room);
|
||||||
|
}
|
||||||
|
|
||||||
|
// async function leaveRoom(room) {
|
||||||
|
// socket.on('connection', (socket) => {
|
||||||
|
// socket.leave(room);
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
async function listRooms() {
|
||||||
|
const response = await authedAPIRequest("/rooms/", { method: "GET" });
|
||||||
|
|
||||||
|
if (!response || !response.ok) {
|
||||||
|
console.error(
|
||||||
|
"Could not retrieve rooms list: " + response && response.statusText
|
||||||
|
? response.statusText
|
||||||
|
: "null",
|
||||||
|
);
|
||||||
|
console.debug(await response.text());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await response.json();
|
||||||
|
|
||||||
|
console.debug("Retrieved rooms list");
|
||||||
|
console.debug(res);
|
||||||
|
|
||||||
|
// Update HTML
|
||||||
|
// const roomNameElt = document.getElementById("room-name");
|
||||||
|
// roomNameElt.innerText = res.metadata.name;
|
||||||
|
}
|
||||||
|
// async function createRoom() {
|
||||||
|
|
||||||
|
// }
|
||||||
|
// async function updateRoom() {
|
||||||
|
|
||||||
|
// }
|
||||||
|
// async function deleteRoom() {
|
||||||
|
|
||||||
|
// }
|
||||||
|
|
||||||
|
async function fetchRoomConfig(room) {
|
||||||
|
if (!room) {
|
||||||
|
console.error("Cannot fetch an undefined room");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await authedAPIRequest("/rooms/" + room + "/config", {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response || !response.ok) {
|
||||||
|
console.error(
|
||||||
|
"Could not retrieve room config" + response && response.statusText
|
||||||
|
? response.statusText
|
||||||
|
: "null",
|
||||||
|
);
|
||||||
|
console.debug(await response.text());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await response.json();
|
||||||
|
|
||||||
|
console.debug(`Retrieved config for room ${room}:`);
|
||||||
|
console.debug(res);
|
||||||
|
|
||||||
|
setCurrentRoomConfig(res);
|
||||||
|
|
||||||
|
// Update HTML
|
||||||
|
document.getElementById("room-name").innerText = res.metadata.name;
|
||||||
|
document.getElementById("room-description").innerText =
|
||||||
|
res.metadata.description;
|
||||||
|
document.getElementById("room-description").style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
fetchRoomConfig,
|
||||||
|
setCurrentRoomConfig,
|
||||||
|
getCurrentRoomConfig,
|
||||||
|
joinRoom,
|
||||||
|
listRooms,
|
||||||
|
// createRoom,
|
||||||
|
// updateRoom,
|
||||||
|
// deleteRoom
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,39 @@
|
||||||
// FIXME: This file should handle the students API
|
import { authedAPIRequest } from "../utils/auth";
|
||||||
// Functions may include:
|
import { displayStudentProfile } from "./utils";
|
||||||
// - getStudent (get a student from the API by its uid or login)
|
|
||||||
// - getUserUidFromToken (get the user's uid from the token in local storage)
|
//get a student from the API by its uid or login
|
||||||
// - updateStudent (update the student's profile through the API)
|
async function getStudent(login) {
|
||||||
|
if (!login) {
|
||||||
|
console.error("Cannot fetch an undefined student");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await authedAPIRequest("/students/" + login, {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response || !response.ok) {
|
||||||
|
console.error(
|
||||||
|
"Could not retrieve student: " + response && response.statusText
|
||||||
|
? response.statusText
|
||||||
|
: " no status text",
|
||||||
|
);
|
||||||
|
console.debug(await response.text());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await response.json();
|
||||||
|
|
||||||
|
console.debug(`Retrieved student ${login}:`);
|
||||||
|
console.debug(res);
|
||||||
|
|
||||||
|
// Update HTML
|
||||||
|
displayStudentProfile(res.avatarURL, res.login, res.guild, res.quote);
|
||||||
|
}
|
||||||
|
|
||||||
|
//// get the user's uid from the token in local storage
|
||||||
|
async function getUserUidFromToken() {}
|
||||||
|
// update the student's profile through the API
|
||||||
|
async function updateStudent() {}
|
||||||
|
|
||||||
|
export { getStudent, getUserUidFromToken, updateStudent };
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
// FIXME: This file should handle the students DOM manipulation
|
// display the student's profile in the DOM
|
||||||
// Link buttons to their respective functions
|
function displayStudentProfile(picture, login, guild, quote) {
|
||||||
// Functions may include:
|
document.getElementById("tooltip-info-avatar").src = picture;
|
||||||
// - displayStudentProfile (display the student's profile in the DOM)
|
document.getElementById("tooltip-info-login").innerText = login;
|
||||||
// - showModal (add a form modal to the DOM)
|
document.getElementById("tooltip-info-guild").innerText = guild;
|
||||||
|
document.getElementById("tooltip-info-quote").innerText = quote;
|
||||||
|
}
|
||||||
|
// add a form modal to the DOM
|
||||||
|
function showModal() {}
|
||||||
|
|
||||||
|
export { displayStudentProfile, showModal };
|
||||||
|
|
|
||||||
|
|
@ -1,123 +1,178 @@
|
||||||
import * as redirect from "./redirect";
|
import * as redirect from "./redirect";
|
||||||
|
|
||||||
async function sendRequest(endpoint, body) {
|
async function sendRequest(endpoint, body) {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
for (var key in body) {
|
||||||
|
formData.append(key, body[key]);
|
||||||
|
}
|
||||||
|
|
||||||
const request = {
|
const request = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
// headers: {
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
// "Content-Type": "application/x-www-form-urlencoded",
|
||||||
},
|
// },
|
||||||
|
|
||||||
body: new URLSearchParams(body)
|
body: formData,
|
||||||
};
|
};
|
||||||
console.debug(request)
|
|
||||||
|
|
||||||
let response;
|
let response;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
response = await fetch(endpoint, request)
|
response = await fetch(endpoint, request);
|
||||||
if (!response.ok) {
|
if (!response || !response.ok) {
|
||||||
throw new Error(response.statusText)
|
if (response && response.statusText) {
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
} else {
|
||||||
|
throw new Error("No status text");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err);
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await response.json()
|
return await response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} code the authorization code received from the OIDC
|
* @param {string} code the authorization code received from the OIDC
|
||||||
* provider
|
* provider
|
||||||
* @returns {boolean} true if the token was fetched, false otherwise
|
* @returns {Promise<boolean>} true if the token was fetched, false otherwise
|
||||||
*/
|
*/
|
||||||
async function getToken(code) {
|
async function getToken(code) {
|
||||||
|
|
||||||
const auth_url = import.meta.env.VITE_URL;
|
const auth_url = import.meta.env.VITE_URL;
|
||||||
const endpoint = `${auth_url}/auth-api/token`;
|
const endpoint = `${auth_url}/auth-api/token`;
|
||||||
const formData = {
|
const formData = {
|
||||||
"grant_type": "authorization_code",
|
grant_type: "authorization_code",
|
||||||
"code": code,
|
code: code,
|
||||||
"redirect_uri": `${auth_url}/complete/epita/`,
|
redirect_uri: `${auth_url}/complete/epita/`,
|
||||||
"client_id": import.meta.env.VITE_CLIENT_ID
|
client_id: import.meta.env.VITE_CLIENT_ID,
|
||||||
}
|
};
|
||||||
const response = await sendRequest(endpoint, formData)
|
const response = await sendRequest(endpoint, formData);
|
||||||
|
|
||||||
if (response === null) {
|
if (response === null) {
|
||||||
console.error("Failed to retrieve OIDC token")
|
console.error("Failed to retrieve OIDC token");
|
||||||
alert("Failed to retrieve OIDC token")
|
alert("Failed to retrieve OIDC token");
|
||||||
|
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
redirect.redirectToLoginPage()
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem("token", response.id_token);
|
|
||||||
localStorage.setItem("refresh_token", response.refresh_token);
|
|
||||||
console.debug("Saved token and refresh tokens")
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} refreshToken the refresh token (optional)
|
|
||||||
* @returns {boolean} whether the token has been refreshed or not
|
|
||||||
*/
|
|
||||||
async function refreshToken(refreshToken) {
|
|
||||||
|
|
||||||
refreshToken= refreshToken || localStorage.getItem("refresh_token");
|
|
||||||
if (!refreshToken) {
|
|
||||||
console.error("Unable to retrieve refresh token")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const auth_url = import.meta.env.VITE_URL;
|
|
||||||
const endpoint = `${auth_url}/auth-api/token`;
|
|
||||||
const formData = {
|
|
||||||
"client_id": import.meta.env.VITE_CLIENT_ID,
|
|
||||||
// client_secret: "",
|
|
||||||
"grant_type": "authorization_code",
|
|
||||||
refresh_token: refreshToken,
|
|
||||||
scope: "epita profile picture"
|
|
||||||
}
|
|
||||||
const response = await sendRequest(endpoint, formData)
|
|
||||||
if (response === null) {
|
|
||||||
console.error("Failed to retrieve OIDC token")
|
|
||||||
alert("Failed to retrieve OIDC token")
|
|
||||||
|
|
||||||
localStorage.clear();
|
|
||||||
redirect.redirectToLoginPage()
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem("token", response.id_token);
|
|
||||||
localStorage.setItem("refresh_token", response.refresh_token);
|
|
||||||
console.debug("Saved token and refresh tokens")
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {boolean} true if the user is authenticated, false otherwise
|
|
||||||
*/
|
|
||||||
async function authenticate() {
|
|
||||||
const token = localStorage.getItem("token");
|
|
||||||
if (token !== null)
|
|
||||||
return true;
|
|
||||||
const refresh_token = localStorage.getItem("refresh_token");
|
|
||||||
if (refresh_token !== null)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
redirect.redirectToLoginPage();
|
redirect.redirectToLoginPage();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME
|
localStorage.setItem("token", response.id_token);
|
||||||
// missing functions:
|
localStorage.setItem("refresh_token", response.refresh_token);
|
||||||
// - authedAPIRequest (makes an authenticated request to the API)
|
console.debug("Saved token and refresh tokens");
|
||||||
|
|
||||||
export {
|
return true;
|
||||||
getToken,
|
|
||||||
refreshToken,
|
|
||||||
authenticate
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} refreshToken the refresh token (optional)
|
||||||
|
* @returns {Promise<boolean>} whether the token has been refreshed or not
|
||||||
|
*/
|
||||||
|
async function refreshToken(refreshToken) {
|
||||||
|
refreshToken = refreshToken || localStorage.getItem("refresh_token");
|
||||||
|
if (!refreshToken) {
|
||||||
|
console.error("Unable to retrieve refresh token");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auth_url = import.meta.env.VITE_URL;
|
||||||
|
const endpoint = `${auth_url}/auth-api/token`;
|
||||||
|
const formData = {
|
||||||
|
client_id: import.meta.env.VITE_CLIENT_ID,
|
||||||
|
// client_secret: "",
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
refresh_token: refreshToken,
|
||||||
|
scope: "epita profile picture",
|
||||||
|
};
|
||||||
|
const response = await sendRequest(endpoint, formData);
|
||||||
|
|
||||||
|
if (response === null) {
|
||||||
|
console.error("Failed to retrieve OIDC token");
|
||||||
|
alert("Failed to retrieve OIDC token");
|
||||||
|
|
||||||
|
localStorage.clear();
|
||||||
|
redirect.redirectToLoginPage();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem("token", response.id_token);
|
||||||
|
localStorage.setItem("refresh_token", response.refresh_token);
|
||||||
|
console.debug("Saved token and refresh tokens");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Promise<boolean>} true if the user is authenticated, false otherwise
|
||||||
|
*/
|
||||||
|
async function authenticate() {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
|
if (token !== null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const refresh_token = localStorage.getItem("refresh_token");
|
||||||
|
|
||||||
|
if (refresh_token !== null) {
|
||||||
|
return await refreshToken(refresh_token);
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.clear();
|
||||||
|
redirect.redirectToLoginPage();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} endpoint
|
||||||
|
* @param {object} options this object should at least contain the method.
|
||||||
|
* @returns {Promise<Response>} the response or null
|
||||||
|
* We want a {Promise<Response>} so we can read the headers as well as the
|
||||||
|
* body, rather than just the body
|
||||||
|
*/
|
||||||
|
async function authedAPIRequest(endpoint, options) {
|
||||||
|
if (!(await authenticate())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (!options.method) {
|
||||||
|
// console.error("Invalid parameter: options (missing method)");
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Deep copy req
|
||||||
|
const request = JSON.parse(JSON.stringify(options));
|
||||||
|
|
||||||
|
if (!request.headers) {
|
||||||
|
request.headers = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pcq ce PUTAIN de js est pas foutu de faire ce qu'on lui demande
|
||||||
|
request.headers.Authorization = "Bearer " + localStorage.getItem("token");
|
||||||
|
|
||||||
|
const full_endpoint = import.meta.env.VITE_URL + "/api" + endpoint;
|
||||||
|
|
||||||
|
const response = await fetch(full_endpoint, request);
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
const response_err = await response.text();
|
||||||
|
|
||||||
|
if (response_err.includes("Token expired")) {
|
||||||
|
if (await refreshToken(null)) {
|
||||||
|
return await authedAPIRequest(endpoint, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.clear();
|
||||||
|
alert("Redirecting to logging page");
|
||||||
|
redirect.redirectToLoginPage();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getToken, refreshToken, authenticate, authedAPIRequest };
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,13 @@ function createLink() {
|
||||||
client_id: import.meta.env.VITE_CLIENT_ID,
|
client_id: import.meta.env.VITE_CLIENT_ID,
|
||||||
response_type: "code",
|
response_type: "code",
|
||||||
redirect_uri: import.meta.env.VITE_URL + "/complete/epita/",
|
redirect_uri: import.meta.env.VITE_URL + "/complete/epita/",
|
||||||
scope: "epita profile picture"
|
scope: "epita profile picture",
|
||||||
});
|
});
|
||||||
|
|
||||||
const base_url = import.meta.env.VITE_AUTH_URL
|
const base_url = import.meta.env.VITE_AUTH_URL;
|
||||||
const res = `${base_url}/authorize?${params}`
|
const res = `${base_url}/authorize?${params}`;
|
||||||
return res;
|
|
||||||
|
|
||||||
|
return new URL(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -20,10 +20,8 @@ function createLink() {
|
||||||
*/
|
*/
|
||||||
function redirectToLoginPage() {
|
function redirectToLoginPage() {
|
||||||
const redirectUrl = createLink();
|
const redirectUrl = createLink();
|
||||||
window.location.href = redirectUrl;
|
|
||||||
|
window.location.href = redirectUrl.href;
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export { createLink, redirectToLoginPage };
|
||||||
createLink,
|
|
||||||
redirectToLoginPage
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
import { io } from "socket.io-client";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
// FIXME: This file should handle the sockets and the subscriptions
|
// FIXME: This file should handle the sockets and the subscriptions
|
||||||
// Exports must include
|
// Exports must include
|
||||||
// - initSocket (initialize the connection to the socket server)
|
// - initSocket (initialize the connection to the socket server)
|
||||||
|
|
@ -7,3 +10,74 @@
|
||||||
// - subscribe (subscribe to a room's stream or chat)
|
// - subscribe (subscribe to a room's stream or chat)
|
||||||
// - unsubscribe (unsubscribe from a room's stream or chat)
|
// - unsubscribe (unsubscribe from a room's stream or chat)
|
||||||
// - sendMessage (send a message to a room's chat)
|
// - sendMessage (send a message to a room's chat)
|
||||||
|
|
||||||
|
let socket = null;
|
||||||
|
const uuid = uuidv4();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the socket when authenticated
|
||||||
|
* returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function initSocket() {
|
||||||
|
if (socket !== null) {
|
||||||
|
console.warn("Blocked attempt to re-init socket connection");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug("Initializing socket connection");
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
|
const host = import.meta.env.VITE_HOST;
|
||||||
|
const port = import.meta.env.VITE_PORT;
|
||||||
|
const wsUri = "ws://" + host + (port ? ":" + port : "");
|
||||||
|
|
||||||
|
socket = io(wsUri, {
|
||||||
|
reconnectionDelayMax: 10000,
|
||||||
|
extraHeaders: {
|
||||||
|
Authorization: "Bearer " + token,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.debug("Socket active: " + socket.active);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function subscribe(room, channel) {
|
||||||
|
if (!room) {
|
||||||
|
room = "epi-place";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!channel) {
|
||||||
|
channel = "message";
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = "rooms.canvas.getStream";
|
||||||
|
|
||||||
|
if (channel.includes("chat") || channel.includes("message")) {
|
||||||
|
path = "rooms.getChat";
|
||||||
|
}
|
||||||
|
|
||||||
|
const msg = {
|
||||||
|
id: uuid,
|
||||||
|
method: "subscription",
|
||||||
|
params: {
|
||||||
|
path: path,
|
||||||
|
input: {
|
||||||
|
json: {
|
||||||
|
roomSlug: room,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.send(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// async function unsubscribe() {
|
||||||
|
|
||||||
|
// }
|
||||||
|
// async function sendMessage() {
|
||||||
|
|
||||||
|
// }
|
||||||
|
|
||||||
|
export { socket, initSocket, subscribe };
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue