Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Store users information in a separate firestore collection #19

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export class Slye extends Component<{}, {}> {
<div className="drop user login button raised">
<div
className="center">
Hello, { values.Auth.currentUser.displayName }
Hello, { values.Auth.currentUser.firstname }!
</div>
<ul>
<li>
Expand Down
10 changes: 9 additions & 1 deletion context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import React from "react";
import { db } from "./fs";
import * as types from "./types";
import { goto } from "./util";

const { Provider, Consumer } = React.createContext<types.Context>(undefined);
export { Consumer };
Expand Down Expand Up @@ -47,7 +48,7 @@ export class SlyeProvider extends React.Component<{}, State> {
create() {
// TODO(qti3e) async create(): Promise<id>;
db.create().then(id => {
location.hash = "#/editor/" + id;
goto("/editor/" + id);
});
}
}
Expand All @@ -56,6 +57,9 @@ export class SlyeProvider extends React.Component<{}, State> {

componentWillMount() {
db.onAuthStateChanged(user => {
// TODO(qti3e) Check if this is the first time user
// logged in to the system, and if so show the welcome
// page.
this.setState({
values: {
...this.state.values,
Expand All @@ -65,6 +69,10 @@ export class SlyeProvider extends React.Component<{}, State> {
currentUser: user
}
}
}, () => {
if (user && user.firstLogin) {
goto("/welcome");
}
});
});
}
Expand Down
21 changes: 17 additions & 4 deletions firestore.rules
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,12 @@ service cloud.firestore {

function dontChangeOwner() {
return request.auth.uid != null
&& request.auth.uid == resource.data.owner.uid
&& request.auth.uid == resource.data.owner;
}

function validate() {
return request.auth.uid != null
&& request.resource.data.owner.uid == request.auth.uid
&& request.resource.data.owner.photoURL is string
&& request.resource.data.owner.displayName is string
&& request.resource.data.owner == request.auth.uid
&& request.resource.data.updated == request.time;
}

Expand All @@ -23,6 +21,21 @@ service cloud.firestore {
allow create: if validate() && createdNow();
allow update: if validate() && dontChangeOwner();
}

// Users collection

function validateUser() {
return request.auth.uid != null
&& request.resource.uid == request.auth.id
&& request.resource.data.owner.photoURL is string
&& request.resource.data.owner.displayName is string;
}

match /users/{user} {
allow read: if true;
allow create: if validateUser() && createdNow();
allow update: if validateUser() && request.auth.uid == resource.data.uid;
}
}
}

123 changes: 87 additions & 36 deletions fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface DB {
login(): void;
logout(): void;
onAuthStateChanged(cb: (u: types.User) => void);
queryUser(uid: string): Promise<types.User>;
}

export const db: DB = Object.create(null);
Expand All @@ -50,7 +51,9 @@ class FirestoreDB implements DB {
private auth: firebase.auth.Auth;
private db: firebase.firestore.Firestore;
private collectionRef: firebase.firestore.CollectionReference;
private usersCollectionRef: firebase.firestore.CollectionReference;
private storageRef: firebase.storage.Reference;
private currentUser: types.User;

constructor(config) {
firebase.initializeApp(config);
Expand All @@ -60,58 +63,60 @@ class FirestoreDB implements DB {
timestampsInSnapshots: true
});
this.collectionRef = this.db.collection("presentations");
this.usersCollectionRef = this.db.collection("users");
this.storageRef = firebase.storage().ref();
}

async queryLatest() {
const query = this.collectionRef.orderBy("created", "desc").limit(20);
private async exeQuery(
query: firebase.firestore.Query
): Promise<types.PresentationInfo[]> {
const snapshots = await query.get();
const out = [];
snapshots.forEach(snap => {
const out: types.PresentationInfo[] = [];
const promises = [];
snapshots.forEach(async snap => {
const id = snap.id;
const data = snap.data() as types.Presentation;
out.push({ id, data, thumbnail: null });
// TODO(qti3e) It's possible to optimize this part.
const ownerInfo = this.queryUser(data.ownerId);
promises.push(ownerInfo);
out.push({ id, data, ownerInfo: await ownerInfo });
});
await Promise.all(promises);
return out;
}

async queryProfile(uid) {
queryLatest(): Promise<types.PresentationInfo[]> {
const query = this.collectionRef.orderBy("created", "desc").limit(20);
return this.exeQuery(query);
}

queryProfile(uid: string): Promise<types.PresentationInfo[]> {
const query = this.collectionRef.where("owner.uid", "==", uid);
const snapshots = await query.get();
const out = [];
snapshots.forEach(snap => {
const id = snap.id;
const data = snap.data() as types.Presentation;
out.push({ id, data, thumbnail: null });
});
return out;
return this.exeQuery(query);
}

async getPresentation(id) {
async getPresentation(id: string): Promise<types.Presentation> {
const docRef = this.collectionRef.doc(id);
const snap = await docRef.get();
if (snap.exists) {
return snap.data() as types.Presentation;
} else {
throw Error(`Presentation does not exist ${id}`);
const error = new Error(`Presentation does not exist ${id}`);
error.name = types.ErrorCodes.PresentationNotFound;
throw error;
}
}

getThumbnailLink(p) {
const path = thumbnailPath(p.data.owner.uid, p.id);
getThumbnailLink(p: types.PresentationInfo): Promise<string> {
const path = thumbnailPath(p.ownerInfo.uid, p.id);
const thumbRef = this.storageRef.child(path);
return thumbRef.getDownloadURL();
}

async create() {
const u = this.auth.currentUser;
async create(): Promise<string> {
const stepId = util.randomString();
const presentation = {
owner: {
displayName: u.displayName,
photoURL: u.photoURL,
uid: u.uid
},
owner: this.currentUser.uid,
steps: {
[stepId]: util.emptyStep()
},
Expand All @@ -123,15 +128,15 @@ class FirestoreDB implements DB {
return doc.id;
}

async uploadThumbnail(p, blob) {
const userId = p.data.owner.uid;
async uploadThumbnail(p: types.PresentationInfo, blob): Promise<void> {
const userId = p.ownerInfo.uid;
const path = thumbnailPath(userId, p.id);
const ref = this.storageRef.child(path);
await ref.put(blob);
}

async update(id: string, p: types.Presentation) {
if (!ownsDoc(this.auth.currentUser, p)) {
async update(id: string, p: types.Presentation): Promise<void> {
if (!ownsDoc(this.currentUser, p)) {
throw new Error("Not owned by this user.");
}
const docRef = this.collectionRef.doc(id);
Expand All @@ -143,25 +148,71 @@ class FirestoreDB implements DB {
await docRef.update(newProps);
}

login() {
async queryUser(uid: string): Promise<types.User> {
const docRef = this.usersCollectionRef.doc(uid);
const snap = await docRef.get();
if (snap.exists) {
return snap.data() as types.User;
} else {
const error = new Error(`User does not exist ${uid}.`);
error.name = types.ErrorCodes.UserNotFound;
throw error;
}
}

login(): void {
const provider = new firebase.auth.GoogleAuthProvider();
return firebase.auth().signInWithPopup(provider);
firebase.auth().signInWithPopup(provider);
}

logout(): void {
firebase.auth().signOut();
}

logout() {
return firebase.auth().signOut();
private async handleSignUp(): Promise<void> {
const data = this.auth.currentUser;

const user: types.User = {
uid: data.uid,
firstname: data.displayName,
lastname: "",
photoURL: data.photoURL,
};

this.currentUser = user;

try {
await this.usersCollectionRef.add(user);
} catch (e) {
// TODO(qti3e) Alert user, or try again?
}
}

onAuthStateChanged(cb) {
firebase.auth().onAuthStateChanged(cb);
onAuthStateChanged(cb: (u: types.User) => void): void {
firebase.auth().onAuthStateChanged(async (user: firebase.UserInfo) => {
if (!user) return cb(undefined);
try {
this.currentUser = await this.queryUser(user.uid);
cb(this.currentUser);
} catch (e) {
if (e.name === types.ErrorCodes.UserNotFound && this.auth.currentUser) {
this.handleSignUp();
this.currentUser.firstLogin = true;
return cb(this.currentUser);
}
// TODO(qti3e) Handle this!
console.error(e);
}
});
}
}

// Some util functions
// TODO(qti3e) Move to util.ts
export function thumbnailPath(userId, presentationId) {
return `/data/${userId}/${presentationId}/thumb.png`;
}

export function ownsDoc(u: types.User, p: types.Presentation) {
return u.uid === p.owner.uid;
return u.uid === p.ownerId;
}
6 changes: 3 additions & 3 deletions index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ const Preview = ({ info }: { info: types.PresentationInfo }) => (
<Img src={ db.getThumbnailLink(info) } />
</a>
<div className="owner-box">
<img src={ info.data.owner.photoURL } />
<img src={ info.ownerInfo.photoURL } />
<p>
<span className="by">By </span>
<a href={ "#/profile/" + info.data.owner.uid }>
{ info.data.owner.displayName }
<a href={ "#/profile/" + info.ownerInfo.uid }>
{ info.ownerInfo.firstname + " " + info.ownerInfo.lastname }
</a>
</p>
</div>
Expand Down
6 changes: 3 additions & 3 deletions profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ interface ProfileState {
}

export class Profile extends Component<{}, ProfileState> {
state = {
state: ProfileState = {
isLoading: true,
presentations: null,
user: null
Expand All @@ -46,7 +46,7 @@ export class Profile extends Component<{}, ProfileState> {
const presentations = await db.queryProfile(this.uid);
let user;
if (presentations.length > 0) {
user = presentations[0].data.owner;
user = presentations[0].data.ownerId;
} else {
// TODO Save users info in some document.
// user = await db.getUser(uid);
Expand Down Expand Up @@ -75,7 +75,7 @@ export class Profile extends Component<{}, ProfileState> {
<div id="profile-page">
<div className="user">
<img src={ user.photoURL } />
<h3>{ user.displayName }</h3>
<h3>{ user.firstname }</h3>
</div>
<div className="list">
{ presentations.map(p => <Preview
Expand Down
4 changes: 3 additions & 1 deletion router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Editor } from "./editor";
import { Index } from "./index";
import { Profile } from "./profile";
import { View } from "./view";
import { Welcome } from "./welcome";

export class Router extends Component<{}, {}> {
render() {
Expand All @@ -25,7 +26,8 @@ export class Router extends Component<{}, {}> {
<Route path="/" component={ Index } exact />
<Route path="/view/:id" component={ View } exact />
<Route path="/editor/:id" component={ Editor } exact />
<Route path="/profile/:uid" component={ Profile } exact />
<Route path="/profile/:uid" component={ Profile } exact />
<Route path="/welcome" component={ Welcome } exact />
</Switch>
</HashRouter>
);
Expand Down
21 changes: 18 additions & 3 deletions types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export interface Step {
}

export interface Presentation {
owner: User;
ownerId: string;
steps: {
[key: string]: Step
};
Expand All @@ -36,9 +36,18 @@ export type Axis = "x" | "y" | "z";
export type StepVec3Props = "position" | "orientation";

export interface User {
displayName: string;
photoURL: string;
uid: string;
// TODO(qti3e) Find a way to support usernames,
// firestore does not support uniqu keys atm.
// But we can accomplish this using:
//
// 1) Firebase cloud functions.
// 2) Store like: /usernames/{username}
// username: string;
firstname: string;
lastname: string;
photoURL: string;
firstLogin?: boolean;
}

export interface SlyeRenderer {
Expand All @@ -54,6 +63,7 @@ export interface SlyeRenderer {
export interface PresentationInfo {
id: string;
data: Presentation;
ownerInfo: User;
}

export interface Actions {
Expand Down Expand Up @@ -86,3 +96,8 @@ export interface Context {
actions: Actions;
values: Values;
}

export enum ErrorCodes {
UserNotFound = "UserNotFound",
PresentationNotFound = "PresentationNotFound"
}
Loading