Blog example
This commit is contained in:
@@ -0,0 +1,163 @@
|
|||||||
|
## Multi-Tenant Blog
|
||||||
|
|
||||||
|
In our blog app there will be anonymous users and authors. Each
|
||||||
|
author can create and edit their own posts, and read (but not edit)
|
||||||
|
the posts of other authors. Anonymous users cannot edit anything
|
||||||
|
but can sign up for author accounts. Authors can also post comments
|
||||||
|
on articles.
|
||||||
|
|
||||||
|
This example builds off the previous one. We had previously created
|
||||||
|
a signup and login system on top of JWT. We'll use this auth system
|
||||||
|
for the blog. **Run the SQL in the previous example** first, before
|
||||||
|
continuing with this example.
|
||||||
|
|
||||||
|
For your convenience, the complete sql for the blog demo is
|
||||||
|
[here](https://github.com/begriffs/postgrest/blob/master/schema-templates/blog.sql).
|
||||||
|
You can try it out in this [vagrant
|
||||||
|
image](https://github.com/ruslantalpa/blogdemo) as well.
|
||||||
|
|
||||||
|
### Adding Blog-Specific Tables
|
||||||
|
|
||||||
|
Storing the posts and comments is this simple. The comments do not
|
||||||
|
form a tree, they are linear under a post.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
create table if not exists
|
||||||
|
posts (
|
||||||
|
id bigserial primary key,
|
||||||
|
title text not null,
|
||||||
|
body text not null,
|
||||||
|
author text not null references basic_auth.users (email)
|
||||||
|
on delete restrict on update cascade
|
||||||
|
default basic_auth.current_email(),
|
||||||
|
created_at timestamptz not null default current_date
|
||||||
|
);
|
||||||
|
|
||||||
|
create table if not exists
|
||||||
|
comments (
|
||||||
|
id bigserial primary key,
|
||||||
|
body text not null,
|
||||||
|
author text not null references basic_auth.users (email)
|
||||||
|
on delete restrict on update cascade
|
||||||
|
default basic_auth.current_email(),
|
||||||
|
post bigint not null references posts (id)
|
||||||
|
on delete cascade on update cascade,
|
||||||
|
created_at timestamptz not null default current_date
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Permissions
|
||||||
|
|
||||||
|
Basic table-level permissions. We'll add an the `authenticator`
|
||||||
|
role which can't do anything itself other than switch into other
|
||||||
|
roles as directed by JWT.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
create role anon;
|
||||||
|
create role author;
|
||||||
|
create role authenticator noinherit;
|
||||||
|
grant anon, author to authenticator;
|
||||||
|
|
||||||
|
grant usage on schema public, basic_auth to anon, author;
|
||||||
|
|
||||||
|
-- anon can create new logins and can read comments/posts
|
||||||
|
grant insert on table basic_auth.users, basic_auth.tokens to anon;
|
||||||
|
grant select on table pg_authid, basic_auth.users, posts, comments to anon;
|
||||||
|
grant execute on function
|
||||||
|
login(text,text),
|
||||||
|
request_password_reset(text),
|
||||||
|
reset_password(text,uuid,text),
|
||||||
|
signup(text, text)
|
||||||
|
to anon;
|
||||||
|
|
||||||
|
-- authors can edit comments/posts
|
||||||
|
grant select, insert, update, delete
|
||||||
|
on basic_auth.tokens, basic_auth.users to anon, author;
|
||||||
|
grant select, insert, update, delete
|
||||||
|
on table users, posts, comments to author;
|
||||||
|
grant usage, select on sequence posts_id_seq, comments_id_seq to author;
|
||||||
|
```
|
||||||
|
|
||||||
|
To ensure that authors cannot edit each others' posts and comments
|
||||||
|
we'll use [row-level
|
||||||
|
security](http://www.postgresql.org/docs/9.5/static/ddl-rowsecurity.html).
|
||||||
|
Note that it requires PostgreSQL 9.5 or later.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
|
||||||
|
drop policy if exists authors_eigenedit on posts;
|
||||||
|
create policy authors_eigenedit on posts
|
||||||
|
using (true)
|
||||||
|
with check (
|
||||||
|
author = basic_auth.current_email()
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE comments ENABLE ROW LEVEL SECURITY;
|
||||||
|
drop policy if exists authors_eigenedit on comments;
|
||||||
|
create policy authors_eigenedit on comments
|
||||||
|
using (true)
|
||||||
|
with check (
|
||||||
|
author = basic_auth.current_email()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally we need to modify the `users` view from the previous example.
|
||||||
|
This is because all authors share a single db role. We could have
|
||||||
|
chosen to assign a new role for every author (all inheriting from
|
||||||
|
`author`) but we choose to tell them apart by their email addresses.
|
||||||
|
The addition below prevents authors from seeing each others' info
|
||||||
|
in the `users` view.
|
||||||
|
|
||||||
|
|
||||||
|
```diff
|
||||||
|
create or replace view users as
|
||||||
|
select actual.role as role,
|
||||||
|
'***'::text as pass,
|
||||||
|
actual.email as email,
|
||||||
|
actual.verified as verified
|
||||||
|
from basic_auth.users as actual,
|
||||||
|
(select rolname
|
||||||
|
from pg_authid
|
||||||
|
where pg_has_role(current_user, oid, 'member')
|
||||||
|
) as member_of
|
||||||
|
where actual.role = member_of.rolname
|
||||||
|
+ and (
|
||||||
|
+ actual.role <> 'author'
|
||||||
|
+ or email = basic_auth.current_email()
|
||||||
|
+ );
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example client queries
|
||||||
|
|
||||||
|
* Top ten most recent posts
|
||||||
|
|
||||||
|
```HTTP
|
||||||
|
GET /posts?order=created_at.desc
|
||||||
|
Range: 0-9
|
||||||
|
```
|
||||||
|
|
||||||
|
* Single post (randomly chose id=1) with its comments
|
||||||
|
|
||||||
|
```HTTP
|
||||||
|
GET /posts?id=eq.1&select=*,comments{*}
|
||||||
|
```
|
||||||
|
|
||||||
|
* Add a new post
|
||||||
|
|
||||||
|
```HTTP
|
||||||
|
POST /posts
|
||||||
|
Authorization: Bearer [JWT TOKEN]
|
||||||
|
|
||||||
|
{
|
||||||
|
"title": "My first post",
|
||||||
|
"body": "Meh, forgot what I wanted to say."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Conclusion
|
||||||
|
|
||||||
|
Voilà, a blog API. Most of the code ended up being for defining
|
||||||
|
security. Once you have set up an authentication system, the code
|
||||||
|
to do application specific things like blog posts and comments is
|
||||||
|
short. All the front-end routes and verbs are created automatically
|
||||||
|
for you.
|
||||||
@@ -23,3 +23,4 @@ pages:
|
|||||||
- Examples:
|
- Examples:
|
||||||
- Getting Started: examples/start.md
|
- Getting Started: examples/start.md
|
||||||
- User Management: examples/users.md
|
- User Management: examples/users.md
|
||||||
|
- Multi-Tenant Blog: examples/blog.md
|
||||||
|
|||||||
@@ -325,7 +325,8 @@ posts (
|
|||||||
title text not null,
|
title text not null,
|
||||||
body text not null,
|
body text not null,
|
||||||
author text not null references basic_auth.users (email)
|
author text not null references basic_auth.users (email)
|
||||||
on delete restrict on update cascade,
|
on delete restrict on update cascade
|
||||||
|
default basic_auth.current_email(),
|
||||||
created_at timestamptz not null default current_date
|
created_at timestamptz not null default current_date
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -334,7 +335,8 @@ comments (
|
|||||||
id bigserial primary key,
|
id bigserial primary key,
|
||||||
body text not null,
|
body text not null,
|
||||||
author text not null references basic_auth.users (email)
|
author text not null references basic_auth.users (email)
|
||||||
on delete restrict on update cascade,
|
on delete restrict on update cascade
|
||||||
|
default basic_auth.current_email(),
|
||||||
post bigint not null references posts (id)
|
post bigint not null references posts (id)
|
||||||
on delete cascade on update cascade,
|
on delete cascade on update cascade,
|
||||||
created_at timestamptz not null default current_date
|
created_at timestamptz not null default current_date
|
||||||
|
|||||||
Reference in New Issue
Block a user