Skip to content
This repository was archived by the owner on Mar 2, 2021. It is now read-only.
Merged
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
6 changes: 6 additions & 0 deletions .firebaserc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"projects": {
"staging": "radio4000-staging",
"production": "firebase-radio4000"
}
}
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,8 @@ To deploy to staging, run `yarn deploy`.

To deploy to production, first `yarn deploy` and then `now alias` (you need access to the Internet4000 team on now.sh)

## How to deploy Firebase rules

To deploy the rules in `database.rules.json`, run `firebase deploy --only database`. It will ask you whether to deploy to `staging` or `production`. See `.firebaserc` for more.

Peace
240 changes: 240 additions & 0 deletions database.rules.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
{
"rules": {
// disallow both read and write by default
// nested rules can not change rules above
".read": false,
".write": false,

"users": {
// read, nobody can (the global user list)
// write, only create new user if not logged in
".read": false,
".write": "(auth !== null) && !data.exists()",

// Currently commented out to allow deletes.
// Make sure the the authenticated user id exists after a write
// ".validate": "newData.hasChild(auth.uid)",

"$userID": {
// read: only auth user can read themselves
// write: only authed user can write a single new user, his uid, that's all
// write: only user-owner can edit himself
// write: can not delete (newData value can't be null)
// note: first the "user" is authenticated against Firebase,
// then we independently create a user model in our Firebase DB
// the authentication and our USER concept are two different things
".read": "$userID === auth.uid",

// You can write yourself only. But not delete.
//".write": "($userID === auth.uid) && (newData.val() != null)",
".write": "$userID === auth.uid",
".validate": "newData.hasChildren(['created'])",

"created": {
// ensure that you can not change an existing value and that is it not in the future
".validate": "data.exists() && data.val() === newData.val() || newData.isNumber() && newData.val() <= now && newData.val() > now - 1000"
},
"settings": {
".validate": "!newData.exists() || newData.isString() && root.child('userSettings').child(newData.val()).child('user').val() === $userID"
},
"channels": {
// trick to limit to one radio
".validate": "!data.exists()",
"$channelID": {
// we would like to prevent a user to had a channel he does no own
// but we don't store on the channel@model the user who owns it
// (because, historically, firebase stores the userId as provider:provider-id,
// so it is extremely easy to find the user on social networks)
// so instead we just prevent a user to add to himself a radio that already
// has tracks in -> so no radio hijack
".validate": "root.child('channels').child($channelID).exists() && !root.child('users').child(auth.uid).child('channels').child($channelID).exists() && !root.child('channels').child($channelID).child('tracks').exists()"
}
}/*,
"$other": {
".validate": false
}*/
}
},

"userSettings": {
".read": false,
".write": false,

"$userSettingsID": {
// read: only auth user can read its settings
// write: only user-owner can edit his settings
// write: only logged-in-user without a user settings can create a new one to himself
".read": "auth != null && (data.child('user').val() === auth.uid)",
".write": "auth != null && ((data.child('user').val() === auth.uid) || (root.child('users').child(auth.uid).hasChild('settings') === false))",
"user": {
".validate": "newData.val() === auth.uid"
},
"isRemoteActive": {
".validate": "newData.isBoolean()"
},
"$other": {
".validate": false
}
}
},

"channels": {
".read": true,
".write": false,
".indexOn": ["slug", "isFeatured"],

"$channelID": {
".read": true,

// write: only the user owner can write a channel
// write: only a user with no channel can write a new one to himself
".write": "auth != null && (root.child('users').child(auth.uid).child('channels').child($channelID).exists() || (!data.exists() && !root.child('users').child(auth.uid).child('channels').exists()))",
".validate": "newData.hasChildren(['slug', 'title', 'created']) || !newData.exists()",

"body": {
".validate": "newData.isString() && newData.val().length < 300"
},
"channelPublic": {
".validate": "newData.isString() && root.child('channelPublics').child(newData.val()).child('channel').val() == $channelID"
},
"created": {
// Ensure type::number and that you can not update it
".validate": "data.exists() && data.val() === newData.val() || newData.isNumber() && newData.val() <= now && newData.val() > now - 1000"
},
"updated": {
// Ensure type::number and that you can not update it
".validate": "data.exists() && data.val() === newData.val() || newData.isNumber() && newData.val() <= now && newData.val() > now - 1000"
},
"isFeatured": {
".validate": "newData.isBoolean() && (data.exists() && newData.val() === data.val() || newData.val() === false)"
},
"link": {
".validate": "newData.isString() && newData.val().length > 2 && newData.val().length < 100"
},
// todo: create a separate table to maintain unique slugs otherwise slug can be hijacked
"slug": {
".validate": "newData.isString() && newData.val().length > 2 && newData.val().length < 100"
},
"title": {
".validate": "newData.isString() && newData.val().length > 2 && newData.val().length < 32"
},
"tracks": {
"$track": {
".validate": "root.child('tracks').hasChild($track) && root.child('tracks').child($track).child('channel').val() === $channelID"
}
},
"favoriteChannels": {
"$favoriteChannel": {
".validate": "root.child('channels').child($favoriteChannel).exists() && !root.child('users').child(auth.uid).child('channels').child($favoriteChannel).exists()"
}
},
"images": {
"$image": {
".validate": "root.child('images').hasChild($image) && root.child('images').child($image).child('channel').val() === $channelID"
}
},
"$other": {
".validate": false
}
}
},

"channelPublics": {
// read: no one can read the full list of the channelPublics
// write: no one can write the list
".read": false,
".write": false,
".indexOn": ["followers"],
"$channelPublic": {
".read": true,
// write: user is logged in && has one channel
".write": "auth !== null && root.child('users').child(auth.uid).child('channels').exists()",
".validate": "newData.hasChild('channel')",

// there is no existing data
// && user has the channel newData in is channels
// && this channel has no publicChannel
"channel": {
".validate": "data.exists() && data.val() === newData.val() || !data.exists() && root.child('users').child(auth.uid).child('channels').child(newData.val()).exists() && !root.child('channels').child(newData.val()).child('channelPublic').exists()"
},
"followers": {
// validate: user can only add a radio he owns
// validate: owner can't add himself
"$follower": {
".validate": "root.child('users').child(auth.uid).child('channels').hasChild($follower) && root.child('channels').child($follower).child('channelPublic').val() !== $channelPublic"
}
},
"$other": {
".validate": false
}
}
},

"images": {
".read": true,
".write": false,
"$imageID": {
// ".write": "auth !== null && root.child('users').child(auth.uid).child('channels').child(newData.child('channel').val()).exists()",
".write": "auth !== null && newData.exists() && root.child('users/' + auth.uid + '/channels').child(newData.child('channel').val()).exists() || auth !== null && !newData.exists() && root.child('users/' + auth.uid + '/channels').child(data.child('channel').val()).exists()",
".validate": "newData.hasChildren(['channel', 'src'])",
"channel": {
".validate": "root.child('users').child(auth.uid).child('channels').child(newData.val()).exists()"
},
"src": {
".validate": "newData.isString()"
},
"created": {
".validate": "data.exists() && data.val() === newData.val() || newData.isNumber() && newData.val() <= now && newData.val() > now - 1000"
},
"$other": {
".validate": false
}
}
},

"tracks": {
".read": true,
".write": false,
".indexOn": ["channel", "title"],

"$trackID": {
// ".write": "auth !== null",
// ".write": "auth !== null && (!newData.exists && data.child('channel').val())",
// ".write": "auth !== null && root.child('users/' + auth.uid + '/channels').child('-Ko1h4nyODlOuwfIyQRh').exists()",
// ".write": "auth !== null && root.child('users/' + auth.uid + '/channels').child(newData.child('channel').val()).exists()",
".write": "auth !== null && newData.exists() && root.child('users/' + auth.uid + '/channels').child(newData.child('channel').val()).exists() || auth !== null && !newData.exists() && root.child('users/' + auth.uid + '/channels').child(data.child('channel').val()).exists()",
// ".write": "auth !== null && (root.child('users').child(auth.uid).child('channels').child(data.child('channel').val()).exists() || root.child('users').child(auth.uid).child('channels').child(newData.child('channel').val()).exists())",
".validate": "newData.hasChildren(['channel', 'url', 'ytid', 'title', 'created'])",

"created": {
// ".validate": "data.exists() && data.val() === newData.val() || newData.isNumber() && newData.val() <= now && newData.val() > now - 1000"
".validate": "data.exists() && data.val() === newData.val() || newData.isNumber() && newData.val() <= now"
},
"url": {
".validate": "newData.isString() && newData.val().length > 3"
},
"title": {
".validate": "newData.isString() && newData.val().length > 0"
},
"body": {
".validate": "newData.isString()"
},
"ytid": {
".validate": "newData.isString()"
},
"channel": {
// can only be updated if the value matches the authenticated users channel id
".validate": "newData.isString() && root.child('users').child(auth.uid).child('channels').child(newData.val()).exists()"
},
"$other": {
".validate": false
}
}
},

"$others": {
".validate": false
}
}
}

5 changes: 5 additions & 0 deletions firebase.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"database": {
"rules": "database.rules.json"
}
}