FEATURE: users can now login via an openID client
This commit is contained in:
parent
74975ac9d8
commit
9c803a69ff
26 changed files with 866 additions and 1536 deletions
3
front/dist/resources/html/gameMenu.html
vendored
3
front/dist/resources/html/gameMenu.html
vendored
|
@ -60,6 +60,9 @@
|
|||
<section>
|
||||
<button id="enableNotification">Enable notifications</button>
|
||||
</section>
|
||||
<section hidden>
|
||||
<button id="oidcLogin">Oauth Login</button>
|
||||
</section>
|
||||
<section>
|
||||
<button id="sparkButton">Create map</button>
|
||||
</section>
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
"@types/mini-css-extract-plugin": "^1.4.3",
|
||||
"@types/node": "^15.3.0",
|
||||
"@types/quill": "^1.3.7",
|
||||
"@types/uuidv4": "^5.0.0",
|
||||
"@types/webpack-dev-server": "^3.11.4",
|
||||
"@typescript-eslint/eslint-plugin": "^4.23.0",
|
||||
"@typescript-eslint/parser": "^4.23.0",
|
||||
|
@ -53,7 +54,8 @@
|
|||
"rxjs": "^6.6.3",
|
||||
"simple-peer": "^9.11.0",
|
||||
"socket.io-client": "^2.3.0",
|
||||
"standardized-audio-context": "^25.2.4"
|
||||
"standardized-audio-context": "^25.2.4",
|
||||
"uuidv4": "^6.2.10"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "run-p templater serve svelte-check-watch",
|
||||
|
|
|
@ -14,6 +14,7 @@ class ConnectionManager {
|
|||
private connexionType?: GameConnexionTypes;
|
||||
private reconnectingTimeout: NodeJS.Timeout | null = null;
|
||||
private _unloading: boolean = false;
|
||||
private authToken: string | null = null;
|
||||
|
||||
private serviceWorker?: _ServiceWorker;
|
||||
|
||||
|
@ -27,21 +28,57 @@ class ConnectionManager {
|
|||
if (this.reconnectingTimeout) clearTimeout(this.reconnectingTimeout);
|
||||
});
|
||||
}
|
||||
|
||||
public loadOpenIDScreen() {
|
||||
localUserStore.setAuthToken(null);
|
||||
const state = localUserStore.generateState();
|
||||
const nonce = localUserStore.generateNonce();
|
||||
window.location.assign(`http://${PUSHER_URL}/login-screen?state=${state}&nonce=${nonce}`);
|
||||
}
|
||||
|
||||
public logout() {
|
||||
localUserStore.setAuthToken(null);
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to login to the node server and return the starting map url to be loaded
|
||||
*/
|
||||
public async initGameConnexion(): Promise<Room> {
|
||||
const connexionType = urlManager.getGameConnexionType();
|
||||
this.connexionType = connexionType;
|
||||
|
||||
let room: Room | null = null;
|
||||
if (connexionType === GameConnexionTypes.register) {
|
||||
if (connexionType === GameConnexionTypes.jwt) {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const code = urlParams.get("code");
|
||||
const state = urlParams.get("state");
|
||||
if (!state || !localUserStore.verifyState(state)) {
|
||||
throw "Could not validate state!";
|
||||
}
|
||||
if (!code) {
|
||||
throw "No Auth code provided";
|
||||
}
|
||||
const nonce = localUserStore.getNonce();
|
||||
const { authToken } = await Axios.get(`${PUSHER_URL}/login-callback`, { params: { code, nonce } }).then(
|
||||
(res) => res.data
|
||||
);
|
||||
localUserStore.setAuthToken(authToken);
|
||||
this.authToken = authToken;
|
||||
room = await Room.createRoom(
|
||||
new URL(localUserStore.getLastRoomUrl())
|
||||
);
|
||||
urlManager.pushRoomIdToUrl(room);
|
||||
|
||||
} else if (connexionType === GameConnexionTypes.register) {
|
||||
//@deprecated
|
||||
const organizationMemberToken = urlManager.getOrganizationToken();
|
||||
const data = await Axios.post(`${PUSHER_URL}/register`, { organizationMemberToken }).then(
|
||||
(res) => res.data
|
||||
);
|
||||
this.localUser = new LocalUser(data.userUuid, data.authToken, data.textures);
|
||||
this.localUser = new LocalUser(data.userUuid, data.textures);
|
||||
this.authToken = data.authToken;
|
||||
localUserStore.saveUser(this.localUser);
|
||||
localUserStore.setAuthToken(this.authToken);
|
||||
|
||||
const roomUrl = data.roomUrl;
|
||||
|
||||
|
@ -61,24 +98,12 @@ class ConnectionManager {
|
|||
connexionType === GameConnexionTypes.anonymous ||
|
||||
connexionType === GameConnexionTypes.empty
|
||||
) {
|
||||
let localUser = localUserStore.getLocalUser();
|
||||
if (localUser && localUser.jwtToken && localUser.uuid && localUser.textures) {
|
||||
this.localUser = localUser;
|
||||
try {
|
||||
await this.verifyToken(localUser.jwtToken);
|
||||
} catch (e) {
|
||||
// If the token is invalid, let's generate an anonymous one.
|
||||
console.error("JWT token invalid. Did it expire? Login anonymously instead.");
|
||||
await this.anonymousLogin();
|
||||
}
|
||||
} else {
|
||||
this.authToken = localUserStore.getAuthToken();
|
||||
//todo: add here some kind of warning if authToken has expired.
|
||||
if (!this.authToken) {
|
||||
await this.anonymousLogin();
|
||||
}
|
||||
|
||||
localUser = localUserStore.getLocalUser();
|
||||
if (!localUser) {
|
||||
throw "Error to store local user data";
|
||||
}
|
||||
this.localUser = localUserStore.getLocalUser() as LocalUser; //if authToken exist in localStorage then localUser cannot be null
|
||||
|
||||
let roomPath: string;
|
||||
if (connexionType === GameConnexionTypes.empty) {
|
||||
|
@ -97,19 +122,18 @@ class ConnectionManager {
|
|||
room = await Room.createRoom(new URL(roomPath));
|
||||
if (room.textures != undefined && room.textures.length > 0) {
|
||||
//check if texture was changed
|
||||
if (localUser.textures.length === 0) {
|
||||
localUser.textures = room.textures;
|
||||
if (this.localUser.textures.length === 0) {
|
||||
this.localUser.textures = room.textures;
|
||||
} else {
|
||||
room.textures.forEach((newTexture) => {
|
||||
const alreadyExistTexture = localUser?.textures.find((c) => newTexture.id === c.id);
|
||||
if (localUser?.textures.findIndex((c) => newTexture.id === c.id) !== -1) {
|
||||
const alreadyExistTexture = this.localUser.textures.find((c) => newTexture.id === c.id);
|
||||
if (this.localUser.textures.findIndex((c) => newTexture.id === c.id) !== -1) {
|
||||
return;
|
||||
}
|
||||
localUser?.textures.push(newTexture);
|
||||
this.localUser.textures.push(newTexture);
|
||||
});
|
||||
}
|
||||
this.localUser = localUser;
|
||||
localUserStore.saveUser(localUser);
|
||||
localUserStore.saveUser(this.localUser);
|
||||
}
|
||||
}
|
||||
if (room == undefined) {
|
||||
|
@ -120,21 +144,19 @@ class ConnectionManager {
|
|||
return Promise.resolve(room);
|
||||
}
|
||||
|
||||
private async verifyToken(token: string): Promise<void> {
|
||||
await Axios.get(`${PUSHER_URL}/verify`, { params: { token } });
|
||||
}
|
||||
|
||||
public async anonymousLogin(isBenchmark: boolean = false): Promise<void> {
|
||||
const data = await Axios.post(`${PUSHER_URL}/anonymLogin`).then((res) => res.data);
|
||||
this.localUser = new LocalUser(data.userUuid, data.authToken, []);
|
||||
this.localUser = new LocalUser(data.userUuid, []);
|
||||
this.authToken = data.authToken;
|
||||
if (!isBenchmark) {
|
||||
// In benchmark, we don't have a local storage.
|
||||
localUserStore.saveUser(this.localUser);
|
||||
localUserStore.setAuthToken(this.authToken);
|
||||
}
|
||||
}
|
||||
|
||||
public initBenchmark(): void {
|
||||
this.localUser = new LocalUser("", "test", []);
|
||||
this.localUser = new LocalUser("", []);
|
||||
}
|
||||
|
||||
public connectToRoomSocket(
|
||||
|
@ -147,7 +169,7 @@ class ConnectionManager {
|
|||
): Promise<OnConnectInterface> {
|
||||
return new Promise<OnConnectInterface>((resolve, reject) => {
|
||||
const connection = new RoomConnection(
|
||||
this.localUser.jwtToken,
|
||||
this.authToken,
|
||||
roomUrl,
|
||||
name,
|
||||
characterLayers,
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import {MAX_USERNAME_LENGTH} from "../Enum/EnvironmentVariable";
|
||||
import { MAX_USERNAME_LENGTH } from "../Enum/EnvironmentVariable";
|
||||
|
||||
export interface CharacterTexture {
|
||||
id: number,
|
||||
level: number,
|
||||
url: string,
|
||||
rights: string
|
||||
id: number;
|
||||
level: number;
|
||||
url: string;
|
||||
rights: string;
|
||||
}
|
||||
|
||||
export const maxUserNameLength: number = MAX_USERNAME_LENGTH;
|
||||
|
@ -24,6 +24,5 @@ export function areCharacterLayersValid(value: string[] | null): boolean {
|
|||
}
|
||||
|
||||
export class LocalUser {
|
||||
constructor(public readonly uuid:string, public readonly jwtToken: string, public textures: CharacterTexture[]) {
|
||||
}
|
||||
constructor(public readonly uuid: string, public textures: CharacterTexture[]) {}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { areCharacterLayersValid, isUserNameValid, LocalUser } from "./LocalUser";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
const playerNameKey = "playerName";
|
||||
const selectedPlayerKey = "selectedPlayer";
|
||||
|
@ -12,6 +13,9 @@ const audioPlayerMuteKey = "audioMute";
|
|||
const helpCameraSettingsShown = "helpCameraSettingsShown";
|
||||
const fullscreenKey = "fullscreen";
|
||||
const lastRoomUrl = "lastRoomUrl";
|
||||
const authToken = "authToken";
|
||||
const state = "state";
|
||||
const nonce = "nonce";
|
||||
|
||||
class LocalUserStore {
|
||||
saveUser(localUser: LocalUser) {
|
||||
|
@ -116,6 +120,36 @@ class LocalUserStore {
|
|||
getLastRoomUrl(): string {
|
||||
return localStorage.getItem(lastRoomUrl) ?? "";
|
||||
}
|
||||
|
||||
setAuthToken(value: string | null) {
|
||||
value ? localStorage.setItem(authToken, value) : localStorage.removeItem(authToken);
|
||||
}
|
||||
getAuthToken(): string | null {
|
||||
return localStorage.getItem(authToken);
|
||||
}
|
||||
|
||||
generateState(): string {
|
||||
const newState = uuidv4();
|
||||
localStorage.setItem(state, newState);
|
||||
return newState;
|
||||
}
|
||||
|
||||
verifyState(value: string): boolean {
|
||||
const oldValue = localStorage.getItem(state);
|
||||
localStorage.removeItem(state);
|
||||
return oldValue === value;
|
||||
}
|
||||
generateNonce(): string {
|
||||
const newNonce = uuidv4();
|
||||
localStorage.setItem(nonce, newNonce);
|
||||
return newNonce;
|
||||
}
|
||||
|
||||
getNonce(): string | null {
|
||||
const oldValue = localStorage.getItem(nonce);
|
||||
localStorage.removeItem(nonce);
|
||||
return oldValue;
|
||||
}
|
||||
}
|
||||
|
||||
export const localUserStore = new LocalUserStore();
|
||||
|
|
|
@ -76,7 +76,7 @@ export class RoomConnection implements RoomConnection {
|
|||
|
||||
/**
|
||||
*
|
||||
* @param token A JWT token containing the UUID of the user
|
||||
* @param token A JWT token containing the email of the user
|
||||
* @param roomUrl The URL of the room in the form "https://example.com/_/[instance]/[map_url]" or "https://example.com/@/[org]/[event]/[map]"
|
||||
*/
|
||||
public constructor(
|
||||
|
@ -217,6 +217,9 @@ export class RoomConnection implements RoomConnection {
|
|||
} else if (message.hasWorldfullmessage()) {
|
||||
worldFullMessageStream.onMessage();
|
||||
this.closed = true;
|
||||
} else if (message.hasTokenexpiredmessage()) {
|
||||
connectionManager.loadOpenIDScreen();
|
||||
this.closed = true; //technically, this isn't needed since loadOpenIDScreen() will do window.location.assign() but I prefer to leave it for consistency
|
||||
} else if (message.hasWorldconnexionmessage()) {
|
||||
worldFullMessageStream.onMessage(message.getWorldconnexionmessage()?.getMessage());
|
||||
this.closed = true;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const DEBUG_MODE: boolean = process.env.DEBUG_MODE == "true";
|
||||
const START_ROOM_URL: string =
|
||||
process.env.START_ROOM_URL || "/_/global/maps.workadventure.localhost/Floor0/floor0.json";
|
||||
process.env.START_ROOM_URL || "/_/global/maps.workadventure.localhost/Floor1/floor1.json";
|
||||
const PUSHER_URL = process.env.PUSHER_URL || "//pusher.workadventure.localhost";
|
||||
export const ADMIN_URL = process.env.ADMIN_URL || "//workadventu.re";
|
||||
const UPLOADER_URL = process.env.UPLOADER_URL || "//uploader.workadventure.localhost";
|
||||
|
|
|
@ -344,6 +344,9 @@ export class MenuScene extends Phaser.Scene {
|
|||
case "editGameSettingsButton":
|
||||
this.openGameSettingsMenu();
|
||||
break;
|
||||
case "oidcLogin":
|
||||
connectionManager.loadOpenIDScreen();
|
||||
break;
|
||||
case "toggleFullscreen":
|
||||
this.toggleFullscreen();
|
||||
break;
|
||||
|
|
|
@ -1,45 +1,46 @@
|
|||
import type {Room} from "../Connexion/Room";
|
||||
import type { Room } from "../Connexion/Room";
|
||||
|
||||
export enum GameConnexionTypes {
|
||||
anonymous=1,
|
||||
anonymous = 1,
|
||||
organization,
|
||||
register,
|
||||
empty,
|
||||
unknown,
|
||||
jwt,
|
||||
}
|
||||
|
||||
//this class is responsible with analysing and editing the game's url
|
||||
class UrlManager {
|
||||
|
||||
//todo: use that to detect if we can find a token in localstorage
|
||||
public getGameConnexionType(): GameConnexionTypes {
|
||||
const url = window.location.pathname.toString();
|
||||
if (url.includes('_/')) {
|
||||
if (url === "/jwt") {
|
||||
return GameConnexionTypes.jwt;
|
||||
} else if (url.includes("_/")) {
|
||||
return GameConnexionTypes.anonymous;
|
||||
} else if (url.includes('@/')) {
|
||||
} else if (url.includes("@/")) {
|
||||
return GameConnexionTypes.organization;
|
||||
} else if(url.includes('register/')) {
|
||||
} else if (url.includes("register/")) {
|
||||
return GameConnexionTypes.register;
|
||||
} else if(url === '/') {
|
||||
} else if (url === "/") {
|
||||
return GameConnexionTypes.empty;
|
||||
} else {
|
||||
return GameConnexionTypes.unknown;
|
||||
}
|
||||
}
|
||||
|
||||
public getOrganizationToken(): string|null {
|
||||
public getOrganizationToken(): string | null {
|
||||
const match = /\/register\/(.+)/.exec(window.location.pathname.toString());
|
||||
return match ? match [1] : null;
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
public pushRoomIdToUrl(room:Room): void {
|
||||
public pushRoomIdToUrl(room: Room): void {
|
||||
if (window.location.pathname === room.id) return;
|
||||
const hash = window.location.hash;
|
||||
const search = room.search.toString();
|
||||
history.pushState({}, 'WorkAdventure', room.id+(search?'?'+search:'')+hash);
|
||||
history.pushState({}, "WorkAdventure", room.id + (search ? "?" + search : "") + hash);
|
||||
}
|
||||
|
||||
public getStartLayerNameFromUrl(): string|null {
|
||||
public getStartLayerNameFromUrl(): string | null {
|
||||
const hash = window.location.hash;
|
||||
return hash.length > 1 ? hash.substring(1) : null;
|
||||
}
|
||||
|
|
|
@ -291,6 +291,18 @@
|
|||
dependencies:
|
||||
source-map "^0.6.1"
|
||||
|
||||
"@types/uuid@8.3.0":
|
||||
version "8.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.0.tgz#215c231dff736d5ba92410e6d602050cce7e273f"
|
||||
integrity sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==
|
||||
|
||||
"@types/uuidv4@^5.0.0":
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/uuidv4/-/uuidv4-5.0.0.tgz#2c94e67b0c06d5adb28fb7ced1a1b5f0866ecd50"
|
||||
integrity sha512-xUrhYSJnkTq9CP79cU3svoKTLPCIbMMnu9Twf/tMpHATYSHCAAeDNeb2a/29YORhk5p4atHhCTMsIBU/tvdh6A==
|
||||
dependencies:
|
||||
uuidv4 "*"
|
||||
|
||||
"@types/webpack-dev-server@^3.11.4":
|
||||
version "3.11.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/webpack-dev-server/-/webpack-dev-server-3.11.4.tgz#90d47dd660b696d409431ab8c1e9fa3615103a07"
|
||||
|
@ -5775,11 +5787,24 @@ utils-merge@1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
||||
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
|
||||
|
||||
uuid@8.3.2:
|
||||
version "8.3.2"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||
|
||||
uuid@^3.3.2, uuid@^3.4.0:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
|
||||
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
|
||||
|
||||
uuidv4@*, uuidv4@^6.2.10:
|
||||
version "6.2.10"
|
||||
resolved "https://registry.yarnpkg.com/uuidv4/-/uuidv4-6.2.10.tgz#42fc1c12b6f85ad536c2c5c1e836079d1e15003c"
|
||||
integrity sha512-FMo1exd9l5UvoUPHRR6NrtJ/OJRePh0ca7IhPwBuMNuYRqjtuh8lE3WDxAUvZ4Yss5FbCOsPFjyWJf9lVTEmnw==
|
||||
dependencies:
|
||||
"@types/uuid" "8.3.0"
|
||||
uuid "8.3.2"
|
||||
|
||||
v8-compile-cache@^2.0.3, v8-compile-cache@^2.2.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue