Skip to content

Commit

Permalink
Merge pull request #133 from colyseus/auth-api
Browse files Browse the repository at this point in the history
Auth API
  • Loading branch information
endel authored Dec 27, 2023
2 parents 8cc961a + 06986f2 commit 1f2208d
Show file tree
Hide file tree
Showing 7 changed files with 251 additions and 169 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "colyseus.js",
"version": "0.15.14",
"version": "0.15.15-preview.22",
"description": "Colyseus Multiplayer SDK for JavaScript/TypeScript",
"author": "Endel Dreyer",
"license": "MIT",
Expand Down
287 changes: 132 additions & 155 deletions src/Auth.ts
Original file line number Diff line number Diff line change
@@ -1,206 +1,183 @@
import * as http from "httpie";
import { getItem, setItem, removeItem } from "./Storage";
import { HTTP } from "./HTTP";
import { getItem, removeItem, setItem } from "./Storage";
import { createNanoEvents } from './core/nanoevents';

const TOKEN_STORAGE = "colyseus-auth-token";

export enum Platform {
ios = "ios",
android = "android",
export interface AuthSettings {
path: string;
key: string;
}

export interface Device {
id: string,
platform: Platform
export interface PopupSettings {
prefix: string;
width: number;
height: number;
}

export interface IStatus {
status: boolean;
export interface AuthData {
user: any;
token: string;
}

export interface IUser {
_id: string;
username: string;
displayName: string;
avatarUrl: string;

isAnonymous: boolean;
email: string;

lang: string;
location: string;
timezone: string;
metadata: any;
export class Auth {
settings: AuthSettings = {
path: "/auth",
key: "colyseus-auth-token",
};

devices: Device[];
#_initialized = false;
#_initializationPromise: Promise<void>;
#_signInWindow = undefined;
#_events = createNanoEvents();

facebookId: string;
twitterId: string;
googleId: string;
gameCenterId: string;
steamId: string;

friendIds: string[];
blockedUserIds: string[];
constructor(protected http: HTTP) {
getItem(this.settings.key, (token) => this.token = token);
}

createdAt: Date;
updatedAt: Date;
}
public set token(token: string) {
this.http.authToken = token;
}

export class Auth implements IUser {
_id: string = undefined;
username: string = undefined;
displayName: string = undefined;
avatarUrl: string = undefined;
public get token(): string {
return this.http.authToken;
}

isAnonymous: boolean = undefined;
email: string = undefined;
public onChange(callback: (response: AuthData) => void) {
const unbindChange = this.#_events.on("change", callback);
if (!this.#_initialized) {
this.#_initializationPromise = new Promise<void>((resolve, reject) => {
this.getUserData().then((userData) => {
this.emitChange({ ...userData, token: this.token });

lang: string = undefined;
location: string = undefined;
timezone: string = undefined;
metadata: any = undefined;
}).catch((e) => {
// user is not logged in, or service is down
this.emitChange({ user: null, token: undefined });

devices: Device[] = undefined;
}).finally(() => {
resolve();
});
});
}
this.#_initialized = true;
return unbindChange;
}

facebookId: string = undefined;
twitterId: string = undefined;
googleId: string = undefined;
gameCenterId: string = undefined;
steamId: string = undefined;
public async getUserData() {
if (this.token) {
return (await this.http.get(`${this.settings.path}/userdata`)).data;
} else {
throw new Error("missing auth.token");
}
}

friendIds: string[] = undefined;
blockedUserIds: string[] = undefined;
public async registerWithEmailAndPassword(email: string, password: string, options?: any) {
const data = (await this.http.post(`${this.settings.path}/register`, {
body: { email, password, options, },
})).data;

createdAt: Date = undefined;
updatedAt: Date = undefined;
this.emitChange(data);

// auth token
token: string = undefined;
return data;
}

protected endpoint: string;
protected keepOnlineInterval: any;
public async signInWithEmailAndPassword(email: string, password: string) {
const data = (await this.http.post(`${this.settings.path}/login`, {
body: { email, password, },
})).data;

constructor(endpoint: string) {
this.endpoint = endpoint.replace("ws", "http");
getItem(TOKEN_STORAGE, (token) => this.token = token);
}
this.emitChange(data);

get hasToken() {
return !!this.token;
return data;
}

async login (options: {
accessToken?: string,
deviceId?: string,
platform?: string,
email?: string,
password?: string,
} = {}) {
const queryParams: any = Object.assign({}, options);

if (this.hasToken) {
queryParams.token = this.token;
}
public async signInAnonymously(options?: any) {
const data = (await this.http.post(`${this.settings.path}/anonymous`, {
body: { options, }
})).data;

const data = await this.request('post', '/auth', queryParams);
this.emitChange(data);

// set & cache token
this.token = data.token;
setItem(TOKEN_STORAGE, this.token);
return data;
}

for (let attr in data) {
if (this.hasOwnProperty(attr)) { this[attr] = data[attr]; }
}
public async sendPasswordResetEmail(email: string) {
const data = (await this.http.post(`${this.settings.path}/forgot-password`, {
body: { email, }
})).data;

this.registerPingService();
this.emitChange(data);

return this;
return data;
}

async save() {
await this.request('put', '/auth', {}, {
username: this.username,
displayName: this.displayName,
avatarUrl: this.avatarUrl,
lang: this.lang,
location: this.location,
timezone: this.timezone,
});

return this;
}
public async signInWithProvider(providerName: string, settings: Partial<PopupSettings> = {}) {
return new Promise((resolve, reject) => {
const w = settings.width || 480;
const h = settings.height || 768;

async getFriends() {
return (await this.request('get', '/friends/all')) as IUser[];
}
// forward existing token for upgrading
const upgradingToken = this.token ? `?token=${this.token}` : "";

async getOnlineFriends() {
return (await this.request('get', '/friends/online')) as IUser[];
}
// Capitalize first letter of providerName
const title = `Login with ${(providerName[0].toUpperCase() + providerName.substring(1))}`;
const url = this.http['client']['getHttpEndpoint'](`${(settings.prefix || `${this.settings.path}/provider`)}/${providerName}${upgradingToken}`);

async getFriendRequests() {
return (await this.request('get', '/friends/requests')) as IUser[];
}
const left = (screen.width / 2) - (w / 2);
const top = (screen.height / 2) - (h / 2);

async sendFriendRequest(friendId: string) {
return (await this.request('post', '/friends/requests', { userId: friendId })) as IStatus;
}
this.#_signInWindow = window.open(url, title, 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left);

async acceptFriendRequest(friendId: string) {
return (await this.request('put', '/friends/requests', { userId: friendId })) as IStatus;
}
const onMessage = (event: MessageEvent) => {
// TODO: it is a good idea to check if event.origin can be trusted!
// if (event.origin.indexOf(window.location.hostname) === -1) { return; }

async declineFriendRequest(friendId: string) {
return (await this.request('del', '/friends/requests', { userId: friendId })) as IStatus;
}
// require 'user' and 'token' inside received data.
if (event.data.user === undefined && event.data.token === undefined) { return; }

async blockUser(friendId: string) {
return (await this.request('post', '/friends/block', { userId: friendId })) as IStatus;
}
clearInterval(rejectionChecker);
this.#_signInWindow.close();
this.#_signInWindow = undefined;

async unblockUser(friendId: string) {
return (await this.request('put', '/friends/block', { userId: friendId })) as IStatus;
}
window.removeEventListener("message", onMessage);

async request(
method: 'get' | 'post' | 'put' | 'del',
segments: string,
query: {[key: string]: number | string} = {},
body?: any,
headers: {[key: string]: string} = {}
) {
headers['Accept'] = 'application/json';
if (this.hasToken) { headers['Authorization'] = 'Bearer ' + this.token; }

const queryParams: string[] = [];
for (const name in query) {
queryParams.push(`${name}=${query[name]}`);
}
if (event.data.error !== undefined) {
reject(event.data.error);

const queryString = (queryParams.length > 0)
? `?${queryParams.join("&")}`
: '';
} else {
resolve(event.data);
this.emitChange(event.data);
}
}

const opts: Partial<http.Options> = { headers };
if (body) { opts.body = body; }
const rejectionChecker = setInterval(() => {
if (!this.#_signInWindow || this.#_signInWindow.closed) {
this.#_signInWindow = undefined;
reject("cancelled");
window.removeEventListener("message", onMessage);
}
}, 200);

return (await http[method](`${this.endpoint}${segments}${queryString}`, opts)).data;
window.addEventListener("message", onMessage);
});
}

logout() {
this.token = undefined;
removeItem(TOKEN_STORAGE);
this.unregisterPingService();
public async signOut() {
this.emitChange({ user: null, token: null });
}

registerPingService(timeout: number = 15000) {
this.unregisterPingService();
private emitChange(authData: Partial<AuthData>) {
if (authData.token !== undefined) {
this.token = authData.token;

this.keepOnlineInterval = setInterval(() => this.request('get', '/auth'), timeout);
}
if (authData.token === null) {
removeItem(this.settings.key);

} else {
// store key in localStorage
setItem(this.settings.key, authData.token);
}
}

unregisterPingService() {
clearInterval(this.keepOnlineInterval);
this.#_events.emit("change", authData);
}

}
Loading

0 comments on commit 1f2208d

Please sign in to comment.