Skip to content

Commit

Permalink
@colyseus/auth integration. allow to specify 'client.auth.token'
Browse files Browse the repository at this point in the history
  • Loading branch information
endel committed Dec 27, 2023
1 parent 0ec969b commit 7c964f9
Show file tree
Hide file tree
Showing 8 changed files with 1,347 additions and 1,267 deletions.
2,349 changes: 1,102 additions & 1,247 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
{
"name": "@colyseus/playground",
"version": "0.15.7",
"version": "0.15.8",
"dependencies": {
"@colyseus/core": "^0.15.0"
"@colyseus/core": "^0.15.0",
"@colyseus/auth": "^0.15.0"
},
"peerDependencies": {
"express": ">=4.16.0"
Expand Down
17 changes: 16 additions & 1 deletion src-backend/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import path from "path";
import express from "express";
import { auth, JWT } from "@colyseus/auth";
import { matchMaker, RoomListingData } from '@colyseus/core';
import { allRoomNames } from "./colyseus.ext";

export const playground = express.Router();

export type AuthConfig = {
oauth: string[],
register: boolean,
anonymous: boolean,
};

// serve static frontend
playground.use("/", express.static(path.resolve(__dirname, "..", "build")));

Expand All @@ -23,7 +30,15 @@ playground.get("/rooms", async (req, res) => {

res.json({
rooms: allRoomNames,

roomsByType,
roomsById
roomsById,

auth: {
// list of OAuth providers
oauth: Object.keys(auth.oauth.providers),
register: typeof(auth.settings.onRegisterWithEmailAndPassword) === "function",
anonymous: typeof(JWT.settings.secret) === "string",
} as AuthConfig
});
});
173 changes: 173 additions & 0 deletions src/components/AuthOptions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import React, { useEffect, useState } from "react";
import { client } from "../utils/Types";
import type { AuthConfig } from "../../src-backend/index";

export function AuthOptions({
authToken,
onAuthTokenChange,
authConfig,
}: {
authToken: string,
onAuthTokenChange: (e: string, autoClose?: boolean) => void,
authConfig: AuthConfig,
}) {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");

const [emailAndPasswordError, setEmailAndPasswordError] = useState("");
const [emailAndPasswordLoading, setEmailAndPasswordLoading] = useState(false);

const [anonymousLoading, setAnonymousLoading] = useState(false);
const [anonymousError, setAnonymousError] = useState("");

const [oAuthLoading, setOAuthLoading] = useState(false);
const [oAuthError, setOAuthError] = useState("");

const handleAuthTokenChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onAuthTokenChange(e.target.value, false);
};

const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value);
};

const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value);
};

const signInWithEmailAndPassword = async function(e: React.MouseEvent<HTMLButtonElement> | React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setEmailAndPasswordError("");

try {
setEmailAndPasswordLoading(true);
await client.auth.signInWithEmailAndPassword(email, password);

} catch (e: any) {
console.error(e);
setEmailAndPasswordError(e.message);

} finally {
setEmailAndPasswordLoading(false);
}
};

const signInAnonymously = async function(e: React.MouseEvent<HTMLButtonElement>) {
setAnonymousError("");

try {
setAnonymousLoading(true);
await client.auth.signInAnonymously();

} catch (e: any) {
console.error(e);
setAnonymousError(e.message);

} finally {
setAnonymousLoading(false);
}
};

const signInWithProvider = function(provider) {
return async (e: React.MouseEvent<HTMLButtonElement>) => {
setOAuthError("");

try {
setOAuthLoading(true);
await client.auth.signInWithProvider(provider);

} catch (e: any) {
console.error(e);
setOAuthError(e.message);

} finally {
setOAuthLoading(false);
}
}
}

const onLogoutClick = async function(e: React.MouseEvent<HTMLButtonElement>) {
client.auth.signOut();
}

useEffect(() => {
// propagate auth token changes to parent component
const onAuthChange = client.auth.onChange((auth) => {
onAuthTokenChange(auth.token || "");
});

return () => onAuthChange();
}, []);

return (
<div className="border-b pb-4">
<div className="flex">
<input
type="text"
name="token"
value={authToken}
onChange={handleAuthTokenChange}
className={"w-full mt-2 p-2 border-r-0 overflow-hidden rounded-l text-ellipsis border border-gray-300"}
/>
<button disabled={(authToken === "")} className="bg-red-500 disabled:cursor-not-allowed disabled:opacity-50 enabled:hover:bg-red-700 text-white font-bold mt-2 py-1 px-3 rounded-r transition" onClick={onLogoutClick}>
Clear
</button>
</div>
{/* <hr className="my-4" /> */}
<div className="flex flex-col mt-2 bg-gray-100 rounded p-4">

{(authConfig.register)
? <div>
<h2 className="font-semibold text-sm">Email / Password</h2>
<form className="flex mt-2 gap-2 w-full" onSubmit={signInWithEmailAndPassword}>
<input onChange={handleEmailChange} type="text" name="email" placeholder="Email" className="flex-grow p-2 overflow-hidden rounded text-ellipsis border border-gray-300" />
<input onChange={handlePasswordChange} type="password" name="password" placeholder="Password" className="flex-grow p-2 overflow-hidden rounded text-ellipsis border border-gray-300" />
<button className="bg-blue-500 disabled:cursor-not-allowed disabled:opacity-50 enabled:hover:bg-blue-700 text-white font-bold py-2 px-4 rounded transition" onClick={signInWithEmailAndPassword} disabled={emailAndPasswordLoading}>Sign-in</button>
</form>
{(emailAndPasswordError) && <div className="mt-2 bg-red-100 rounded p-2 text-red-900 text-xs">{emailAndPasswordError}</div>}
</div>
: null}

{(authConfig.oauth.length > 0)
? <>
<hr className="my-4" />
<div>
<h2 className="font-semibold text-sm">OAuth 2.0 Provider</h2>
<div className="mt-2 gap-2">
{authConfig.oauth.map((provider) => (
<button
key={provider}
onClick={signInWithProvider(provider)}
className="bg-blue-500 disabled:cursor-not-allowed disabled:opacity-50 enabled:hover:bg-blue-700 text-white font-bold py-2 px-4 rounded transition"
disabled={oAuthLoading}
>
{provider.charAt(0).toUpperCase() + provider.slice(1)}
</button>
))}
</div>
</div>
</>
: null}

{(authConfig.anonymous)
? <>
<hr className="my-4" />

<div>
<h2 className="font-semibold text-sm">Anonymous</h2>
<button
onClick={signInAnonymously}
className="mt-2 bg-blue-500 disabled:cursor-not-allowed disabled:opacity-50 enabled:hover:bg-blue-700 text-white font-bold py-2 px-4 rounded transition"
disabled={anonymousLoading}
>
Sign-in Anonymously
</button>
</div>
</>
: null}


</div>
</div>
);
}
3 changes: 2 additions & 1 deletion src/components/InspectConnection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@ export function InspectConnection({
</p>
: <div className="flex items-center">
<div className="flex mt-2">
<select placeholder="Message type" className="border p-2 rounded" value={messageType} onChange={handleMessageTypeChange}>
<select className="border p-2 rounded" value={messageType} onChange={handleMessageTypeChange}>
<option disabled={true} value="">Message type</option>
{(messageTypes).map((type) => (
<option key={type} value={type}>{type}</option>
))}
Expand Down
51 changes: 39 additions & 12 deletions src/components/JoinRoomForm.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
import { Client, Room, RoomAvailable } from "colyseus.js";
import { useEffect, useState } from "react";
import { global, client, endpoint, roomsBySessionId, messageTypesByRoom, Connection, matchmakeMethods, getRoomColorClass } from "../utils/Types";
import { useState } from "react";
import { global, client, roomsBySessionId, messageTypesByRoom, Connection, matchmakeMethods, getRoomColorClass } from "../utils/Types";
import { DEVMODE_RESTART, RAW_EVENTS_KEY, onRoomConnected } from "../utils/ColyseusSDKExt";
import { LimitedArray } from "../utils/LimitedArray";
import { JSONEditor } from "../elements/JSONEditor";
import * as JSONEditorModule from "jsoneditor";
import { RoomWithId } from "../elements/RoomWithId";
import { AuthOptions } from "./AuthOptions";
import type { AuthConfig } from "../../src-backend/index";

export function JoinRoomForm ({
roomNames,
roomsById,
roomsByType,
authConfig,
onConnectionSuccessful,
onDisconnection,
} : {
roomNames: string[]
roomsById: { [key: string]: RoomAvailable & { locked: boolean } },
roomsByType: {[key: string]: number},
authConfig: AuthConfig,
onConnectionSuccessful: (connection: Connection) => void
onDisconnection: (sessionId: string) => void
}) {
Expand All @@ -28,13 +32,8 @@ export function JoinRoomForm ({
const [error, setError] = useState("");
const [isButtonEnabled, setButtonEnabled] = useState(true);

// // auto-fetch room stats at every 5 seconds
// useEffect(() => {
// const fetchInterval = setInterval(fetchRoomStats, 5000);
// return () => clearInterval(fetchInterval);
// }, []);

const onChangeOptions = (json: any) => setOptionsJSON(json);
const [isAuthBlockOpen, setAuthBlockOpen] = useState(false);
const [authToken, setAuthToken] = useState(client.auth.token || "");

const handleSelectedRoomChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (selectedMethod === "joinById") {
Expand All @@ -59,11 +58,27 @@ export function JoinRoomForm ({
}
}

const handleOptionsChange = (e: React.ChangeEvent<HTMLTextAreaElement>) =>
setOptionsJSON(e.target.value);
const onChangeOptions = (json: any) =>
setOptionsJSON(json);

const onAuthTokenChange = (newToken: string, autoClose: boolean = true) => {
if (authToken !== newToken) {
client.auth.token = newToken;
setAuthToken(client.auth.token);

if (autoClose) {
setAuthBlockOpen(false);
}
}
};

const toggleAuthBlock = function(e: React.MouseEvent) {
e.preventDefault();
setAuthBlockOpen(!isAuthBlockOpen);
}

const onJoinClick = async () => {
const method = selectedMethod;
const method = selectedMethod as "joinById" | "reconnect" | "joinOrCreate" | "join";
const roomName = (method === "joinById") ? selectedRoomId : selectedRoomName;

setError(""); // clear previous error
Expand Down Expand Up @@ -250,6 +265,18 @@ export function JoinRoomForm ({
className={"mt-2 h-24 overflow-hidden rounded border " + (isButtonEnabled ? "border-gray-300" : "border-red-300")}
/>

<p className="mt-4 cursor-pointer truncate overflow-hidden text-ellipsis" onClick={toggleAuthBlock}>
<span className={`caret inline-block transition-all ${(isAuthBlockOpen) ? "rotate-90" : "rotate-0"}`}></span> <strong>Auth Token </strong><small className="text-xs text-slate-600">{authToken && `(${authToken.length} chars) "${authToken}"` || "(none)"}</small>
</p>

{(isAuthBlockOpen)
? <AuthOptions
authToken={authToken}
onAuthTokenChange={onAuthTokenChange}
authConfig={authConfig}
/>
: null }

<div className="flex mt-4">
<button
className="bg-purple-500 disabled:cursor-not-allowed disabled:opacity-50 enabled:hover:bg-purple-700 text-white font-bold py-2 px-4 rounded transition"
Expand Down
7 changes: 6 additions & 1 deletion src/sections/Playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { ConnectionList } from "../components/ConnectionList";
import { JoinRoomForm } from "../components/JoinRoomForm";
import { StateView } from "../components/StateView";

import { AuthConfig } from "../../src-backend";

enum ServerState {
CONNECTING = "connecting",
CONNECTED = "connected",
Expand All @@ -22,6 +24,7 @@ export function Playground() {
const [roomNames, setRoomNames] = useState([]);
const [roomsById, setRoomsById] = useState({} as { [key: string]: RoomAvailable & { locked: boolean } });
const [roomsByType, setRoomsByType] = useState({} as {[key: string]: number});
const [authConfig, setAuthConfig] = useState({} as AuthConfig);

const onConnectionSuccessful = (connection: Connection) => {
if (global.connections.indexOf(connection) !== -1) {
Expand Down Expand Up @@ -70,10 +73,11 @@ export function Playground() {
setRoomNames(stats.rooms);
setRoomsByType(stats.roomsByType);
setRoomsById(stats.roomsById);
setAuthConfig(stats.auth);
}).
catch((e) => {
setServerState(ServerState.OFFLINE);
console.error(e)
console.error(e);
});
}

Expand Down Expand Up @@ -102,6 +106,7 @@ export function Playground() {
roomsById={roomsById}
onConnectionSuccessful={onConnectionSuccessful}
onDisconnection={onDisconnection}
authConfig={authConfig}
/>}
</div>

Expand Down
9 changes: 6 additions & 3 deletions src/utils/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ export type Connection = {
events: LimitedArray;
};

export const baseEndpoint = `${window.location.protocol}//${window.location.host}`;
export const endpoint = `${baseEndpoint}${window.location.pathname.replace(/\/+$/, '')}`;
// export const baseEndpoint = `${window.location.protocol}//${window.location.host}`;
// export const endpoint = `${baseEndpoint}${window.location.pathname.replace(/\/+$/, '')}`;

export const client = new Client(baseEndpoint);
// export const client = new Client(baseEndpoint);

export const endpoint = "http://localhost:2567/playground";
export const client = new Client("http://localhost:2567");

export const global = { connections: [] as Connection[], };

Expand Down

0 comments on commit 7c964f9

Please sign in to comment.