From 9c85015ddfc6572af7105bac41ae2c3e7f1b6530 Mon Sep 17 00:00:00 2001 From: Parsa Ghadimi Date: Tue, 4 Sep 2018 11:27:45 +0430 Subject: [PATCH 1/6] Change types --- firestore.rules | 21 +++++++++++++++++---- types.ts | 4 ++-- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/firestore.rules b/firestore.rules index ee527d5..75d82f0 100644 --- a/firestore.rules +++ b/firestore.rules @@ -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; } @@ -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; + } } } diff --git a/types.ts b/types.ts index 75ac122..933b2ef 100644 --- a/types.ts +++ b/types.ts @@ -22,7 +22,7 @@ export interface Step { } export interface Presentation { - owner: User; + owner: string; steps: { [key: string]: Step }; @@ -36,9 +36,9 @@ export type Axis = "x" | "y" | "z"; export type StepVec3Props = "position" | "orientation"; export interface User { + uid: string; displayName: string; photoURL: string; - uid: string; } export interface SlyeRenderer { From 7c17c491fa2dbfba04dc2ef7891df2945ffea8ed Mon Sep 17 00:00:00 2001 From: Parsa Ghadimi Date: Tue, 4 Sep 2018 12:50:48 +0430 Subject: [PATCH 2/6] Implement handleSignUp() --- context.tsx | 10 +++++++- fs.ts | 70 ++++++++++++++++++++++++++++++++++++++++++++--------- router.tsx | 4 ++- types.ts | 10 +++++++- util.tsx | 4 +++ welcome.tsx | 25 +++++++++++++++++++ 6 files changed, 109 insertions(+), 14 deletions(-) create mode 100644 welcome.tsx diff --git a/context.tsx b/context.tsx index 08518d8..53cc492 100644 --- a/context.tsx +++ b/context.tsx @@ -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(undefined); export { Consumer }; @@ -47,7 +48,7 @@ export class SlyeProvider extends React.Component<{}, State> { create() { // TODO(qti3e) async create(): Promise; db.create().then(id => { - location.hash = "#/editor/" + id; + goto("/editor/" + id); }); } } @@ -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, @@ -65,6 +69,10 @@ export class SlyeProvider extends React.Component<{}, State> { currentUser: user } } + }, () => { + if (user && user.firstLogin) { + goto("/welcome"); + } }); }); } diff --git a/fs.ts b/fs.ts index 4e3a904..1c798fe 100644 --- a/fs.ts +++ b/fs.ts @@ -32,6 +32,7 @@ export interface DB { login(): void; logout(): void; onAuthStateChanged(cb: (u: types.User) => void); + queryUser(uid: string): Promise; } export const db: DB = Object.create(null); @@ -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); @@ -60,6 +63,7 @@ class FirestoreDB implements DB { timestampsInSnapshots: true }); this.collectionRef = this.db.collection("presentations"); + this.usersCollectionRef = this.db.collection("users"); this.storageRef = firebase.storage().ref(); } @@ -93,7 +97,9 @@ class FirestoreDB implements DB { 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; } } @@ -104,14 +110,9 @@ class FirestoreDB implements DB { } async create() { - const u = this.auth.currentUser; const stepId = util.randomString(); const presentation = { - owner: { - displayName: u.displayName, - photoURL: u.photoURL, - uid: u.uid - }, + owner: this.currentUser.uid, steps: { [stepId]: util.emptyStep() }, @@ -131,7 +132,7 @@ class FirestoreDB implements DB { } async update(id: string, p: types.Presentation) { - if (!ownsDoc(this.auth.currentUser, p)) { + if (!ownsDoc(this.currentUser, p)) { throw new Error("Not owned by this user."); } const docRef = this.collectionRef.doc(id); @@ -143,6 +144,18 @@ class FirestoreDB implements DB { await docRef.update(newProps); } + async queryUser(uid: string): Promise { + 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() { const provider = new firebase.auth.GoogleAuthProvider(); return firebase.auth().signInWithPopup(provider); @@ -152,16 +165,51 @@ class FirestoreDB implements DB { return firebase.auth().signOut(); } - onAuthStateChanged(cb) { - firebase.auth().onAuthStateChanged(cb); + private async handleSignUp() { + const data = this.auth.currentUser; + + const user: types.User = { + uid: data.uid, + firstname: data.displayName, + lastname: "", + username: data.uid, + photoURL: data.photoURL, + }; + + this.currentUser = user; + + try { + await this.usersCollectionRef.add(user); + } catch (e) { + // TODO(qti3e) Alert user, or try again? + } + } + + onAuthStateChanged(cb: (u: types.User) => 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.owner; } diff --git a/router.tsx b/router.tsx index 1629c7f..e219e4e 100644 --- a/router.tsx +++ b/router.tsx @@ -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() { @@ -25,7 +26,8 @@ export class Router extends Component<{}, {}> { - + + ); diff --git a/types.ts b/types.ts index 933b2ef..032360a 100644 --- a/types.ts +++ b/types.ts @@ -37,8 +37,11 @@ export type StepVec3Props = "position" | "orientation"; export interface User { uid: string; - displayName: string; + username: string; + firstname: string; + lastname: string; photoURL: string; + firstLogin?: boolean; } export interface SlyeRenderer { @@ -86,3 +89,8 @@ export interface Context { actions: Actions; values: Values; } + +export enum ErrorCodes { + UserNotFound = "UserNotFound", + PresentationNotFound = "PresentationNotFound" +} diff --git a/util.tsx b/util.tsx index 542a7db..aad2238 100644 --- a/util.tsx +++ b/util.tsx @@ -75,3 +75,7 @@ export function delay(t: number): Promise { setTimeout(r, t); }); } + +export function goto(url: string): void { + location.hash = "#" + url; +} diff --git a/welcome.tsx b/welcome.tsx new file mode 100644 index 0000000..3f2aec0 --- /dev/null +++ b/welcome.tsx @@ -0,0 +1,25 @@ +/** + * _____ __ + * / ___// /_ _____ + * \__ \/ / / / / _ \ + * ___/ / / /_/ / __/ + * /____/_/\__, /\___/ + * /____/ + * Copyright 2018 Parsa Ghadimi. All Rights Reserved. + * Licence: MIT License + */ + +import React, { Component } from "react"; + +// After signing-up, show this page to user, so they can +// choose their username and give us other informations. + +export interface WelcomeProps { } + +export interface WelcomeState { } + +export class Welcome extends Component { + render() { + return
Test
; + } +} From 56563c8b2ecda4f0f1597d77c4b3f79df2758dbb Mon Sep 17 00:00:00 2001 From: Parsa Ghadimi Date: Tue, 4 Sep 2018 13:11:09 +0430 Subject: [PATCH 3/6] Use new data type in preview component --- app.tsx | 2 +- fs.ts | 34 +++++++++++++++++++--------------- index.tsx | 6 +++--- types.ts | 1 + 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/app.tsx b/app.tsx index 284faf0..410ac12 100644 --- a/app.tsx +++ b/app.tsx @@ -31,7 +31,7 @@ export class Slye extends Component<{}, {}> {
- Hello, { values.Auth.currentUser.displayName } + Hello, { values.Auth.currentUser.firstname }!
  • diff --git a/fs.ts b/fs.ts index 1c798fe..c13cba2 100644 --- a/fs.ts +++ b/fs.ts @@ -67,31 +67,35 @@ class FirestoreDB implements DB { this.storageRef = firebase.storage().ref(); } - async queryLatest() { - const query = this.collectionRef.orderBy("created", "desc").limit(20); + private async exeQuery( + query: firebase.firestore.Query + ): Promise { 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.owner); + promises.push(ownerInfo); + out.push({ id, data, ownerInfo: await ownerInfo }); }); + await Promise.all(promises); return out; } - async queryProfile(uid) { + queryLatest(): Promise { + const query = this.collectionRef.orderBy("created", "desc").limit(20); + return this.exeQuery(query); + } + + queryProfile(uid): Promise { 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): Promise { const docRef = this.collectionRef.doc(id); const snap = await docRef.get(); if (snap.exists) { diff --git a/index.tsx b/index.tsx index fef7c64..a166a13 100644 --- a/index.tsx +++ b/index.tsx @@ -26,11 +26,11 @@ const Preview = ({ info }: { info: types.PresentationInfo }) => ( diff --git a/types.ts b/types.ts index 032360a..7ae5534 100644 --- a/types.ts +++ b/types.ts @@ -57,6 +57,7 @@ export interface SlyeRenderer { export interface PresentationInfo { id: string; data: Presentation; + ownerInfo: User; } export interface Actions { From 102cd361c23b638a5462bcb04302d4fccc26b8af Mon Sep 17 00:00:00 2001 From: Parsa Ghadimi Date: Tue, 4 Sep 2018 13:14:27 +0430 Subject: [PATCH 4/6] Update view component --- view.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/view.tsx b/view.tsx index 84a24e4..8475a0a 100644 --- a/view.tsx +++ b/view.tsx @@ -18,19 +18,22 @@ import * as types from "./types"; export interface ViewState { loading: boolean; presentation: types.Presentation; + owner: types.User; } export class View extends Component<{}, ViewState> { - state = { + state: ViewState = { loading: true, - presentation: null + presentation: null, + owner: null }; playerRef: Player; async componentWillMount() { const id = (this.props as any).match.params.id; const presentation = await db.getPresentation(id); - this.setState({ loading: false, presentation }); + const owner = await db.queryUser(presentation.owner); + this.setState({ loading: false, owner, presentation }); } handleFullScreen = () => { @@ -45,7 +48,7 @@ export class View extends Component<{}, ViewState> { return
    ; } - const { owner } = this.state.presentation; + const { owner } = this.state; return (
    @@ -61,9 +64,9 @@ export class View extends Component<{}, ViewState> {
    From 941f16c5d947bd919bd4607ddfcd55ee2a600f36 Mon Sep 17 00:00:00 2001 From: Parsa Ghadimi Date: Tue, 4 Sep 2018 13:19:17 +0430 Subject: [PATCH 5/6] Fixes --- fs.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/fs.ts b/fs.ts index c13cba2..68888fc 100644 --- a/fs.ts +++ b/fs.ts @@ -90,12 +90,12 @@ class FirestoreDB implements DB { return this.exeQuery(query); } - queryProfile(uid): Promise { + queryProfile(uid: string): Promise { const query = this.collectionRef.where("owner.uid", "==", uid); return this.exeQuery(query); } - async getPresentation(id): Promise { + async getPresentation(id: string): Promise { const docRef = this.collectionRef.doc(id); const snap = await docRef.get(); if (snap.exists) { @@ -107,13 +107,13 @@ class FirestoreDB implements DB { } } - getThumbnailLink(p) { - const path = thumbnailPath(p.data.owner.uid, p.id); + getThumbnailLink(p: types.PresentationInfo): Promise { + const path = thumbnailPath(p.ownerInfo.uid, p.id); const thumbRef = this.storageRef.child(path); return thumbRef.getDownloadURL(); } - async create() { + async create(): Promise { const stepId = util.randomString(); const presentation = { owner: this.currentUser.uid, @@ -128,14 +128,14 @@ class FirestoreDB implements DB { return doc.id; } - async uploadThumbnail(p, blob) { - const userId = p.data.owner.uid; + async uploadThumbnail(p: types.PresentationInfo, blob): Promise { + 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) { + async update(id: string, p: types.Presentation): Promise { if (!ownsDoc(this.currentUser, p)) { throw new Error("Not owned by this user."); } @@ -160,16 +160,16 @@ class FirestoreDB implements DB { } } - login() { + login(): void { const provider = new firebase.auth.GoogleAuthProvider(); - return firebase.auth().signInWithPopup(provider); + firebase.auth().signInWithPopup(provider); } - logout() { - return firebase.auth().signOut(); + logout(): void { + firebase.auth().signOut(); } - private async handleSignUp() { + private async handleSignUp(): Promise { const data = this.auth.currentUser; const user: types.User = { @@ -189,7 +189,7 @@ class FirestoreDB implements DB { } } - onAuthStateChanged(cb: (u: types.User) => void) { + onAuthStateChanged(cb: (u: types.User) => void): void { firebase.auth().onAuthStateChanged(async (user: firebase.UserInfo) => { if (!user) return cb(undefined); try { From 73f7a8d6573d7e8b1724b70d431217963f220bc1 Mon Sep 17 00:00:00 2001 From: Parsa Ghadimi Date: Tue, 4 Sep 2018 17:54:02 +0430 Subject: [PATCH 6/6] Drop usernames and s/owner/ownerId/ --- fs.ts | 5 ++--- profile.tsx | 6 +++--- types.ts | 10 ++++++++-- view.tsx | 2 +- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/fs.ts b/fs.ts index 68888fc..6f016e8 100644 --- a/fs.ts +++ b/fs.ts @@ -77,7 +77,7 @@ class FirestoreDB implements DB { const id = snap.id; const data = snap.data() as types.Presentation; // TODO(qti3e) It's possible to optimize this part. - const ownerInfo = this.queryUser(data.owner); + const ownerInfo = this.queryUser(data.ownerId); promises.push(ownerInfo); out.push({ id, data, ownerInfo: await ownerInfo }); }); @@ -176,7 +176,6 @@ class FirestoreDB implements DB { uid: data.uid, firstname: data.displayName, lastname: "", - username: data.uid, photoURL: data.photoURL, }; @@ -215,5 +214,5 @@ export function thumbnailPath(userId, presentationId) { } export function ownsDoc(u: types.User, p: types.Presentation) { - return u.uid === p.owner; + return u.uid === p.ownerId; } diff --git a/profile.tsx b/profile.tsx index 5cfde46..4071252 100644 --- a/profile.tsx +++ b/profile.tsx @@ -34,7 +34,7 @@ interface ProfileState { } export class Profile extends Component<{}, ProfileState> { - state = { + state: ProfileState = { isLoading: true, presentations: null, user: null @@ -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); @@ -75,7 +75,7 @@ export class Profile extends Component<{}, ProfileState> {
    -

    { user.displayName }

    +

    { user.firstname }

    { presentations.map(p => { async componentWillMount() { const id = (this.props as any).match.params.id; const presentation = await db.getPresentation(id); - const owner = await db.queryUser(presentation.owner); + const owner = await db.queryUser(presentation.ownerId); this.setState({ loading: false, owner, presentation }); }