Skip to content

Commit 70f66d1

Browse files
committed
Google auth requirement added. DB and REST API
updated slightly to handle sync better. Signed-off-by: Jonas Kalderstam <[email protected]>
1 parent 8e1146e commit 70f66d1

File tree

5 files changed

+262
-45
lines changed

5 files changed

+262
-45
lines changed

server-app/README.md

Lines changed: 154 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,39 @@
11
## Basics first - Hello world
2-
This is the first commit: c8c736c87020a
2+
__SHA1:__ c8c736c87020a
33

44
run this and visit [localhost:5500](http://localhost:5500)
55

6-
```python
6+
```bash
77
python app.py
88
```
99

1010
## REST
11+
__SHA1:__ d97dc973476324
12+
1113
I'm going to start by creating a REST skeleton of what I
1214
want to do.
1315

14-
__SHA1:__ 76b25c4e3715
1516
```python
1617
import bottle
17-
from bottle import route, run
18+
from bottle import run, get, post, delete
1819

19-
@route('/')
20-
@route('/links', method='GET')
20+
@get('/')
21+
@get('/links')
2122
def list_links():
2223
'''Return a complete list of all links'''
2324
return 'List of links'
2425

25-
@route('/links/<id>', method='GET')
26+
@get('/links/<id>')
2627
def get_link(id):
2728
'''Returns a specific link'''
2829
return 'Link {}'.format(id)
2930

30-
@route('/links/<id>', method='DELETE')
31+
@delete('/links/<id>')
3132
def delete_link(id):
3233
'''Deletes a specific link from the list'''
3334
return 'Link {} deleted'.format(id)
3435

35-
@route('/links', method='POST')
36+
@post('/links')
3637
def add_link():
3738
'''Adds a link to the list'''
3839
return 'Link added'
@@ -42,36 +43,37 @@ if __name__ == '__main__':
4243
run(port=5500)
4344
```
4445

45-
### JSON
46+
## JSON
47+
__SHA1__: 08c71200b96fc7
48+
4649
Adding all the methods was really easy. But the REST methods should
4750
return JSON, not strings. So let's tweak it so it returns
4851
dummy JSON data instead.
4952

50-
__SHA1__: 957166a1e4
5153
```python
5254
import bottle
53-
from bottle import route, run
55+
from bottle import run, get, post, delete
5456

55-
@route('/')
56-
@route('/links', method='GET')
57+
@get('/')
58+
@get('/links')
5759
def list_links():
5860
'''Return a complete list of all links'''
5961
return dict(links=[])
6062

61-
@route('/links/<id>', method='GET')
63+
@get('/links/<id>')
6264
def get_link(id):
6365
'''Returns a specific link'''
6466
return dict(link={"sha":"1111111",
6567
"url":"http://www.google.com",
6668
"timestamp":"2013-09-19 08:22:19.000"})
6769

68-
@route('/links/<id>', method='DELETE')
70+
@delete('/links/<id>')
6971
def delete_link(id):
7072
'''Deletes a specific link from the list.
7173
On success, returns an empty response'''
7274
return {}
7375

74-
@route('/links', method='POST')
76+
@post('/links')
7577
def add_link():
7678
'''Adds a link to the list.
7779
On success, returns the entry created.'''
@@ -83,3 +85,138 @@ if __name__ == '__main__':
8385
bottle.debug(True)
8486
run(port=5500)
8587
```
88+
89+
## Adding a database
90+
__SHA1:__ 599d6fda70fbeaa
91+
92+
Getting the skeleton up was really fast and now it's already
93+
time to implement some real data. The data will be stored
94+
in an sqlite database. The database is really simple and created
95+
in _dbsetup.py:_
96+
97+
```python
98+
import sqlite3 as sql
99+
import sys
100+
101+
_CREATE_TABLE = \
102+
"""CREATE TABLE IF NOT EXISTS links
103+
(_id INTEGER PRIMARY KEY,
104+
sha TEXT NOT NULL,
105+
url TEXT NOT NULL,
106+
timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
107+
108+
UNIQUE(url) ON CONFLICT REPLACE,
109+
UNIQUE(sha) ON CONFLICT REPLACE)
110+
"""
111+
112+
def init_db(filename='test.db'):
113+
con = sql.connect(filename)
114+
con.row_factory = sql.Row
115+
with con:
116+
cur = con.cursor()
117+
cur.execute(_CREATE_TABLE)
118+
119+
if __name__ == '__main__':
120+
if len(sys.argv) > 1:
121+
init_db(sys.argv[1])
122+
else:
123+
init_db()
124+
```
125+
126+
To make use of it in our app, we import the *bottle_sqlite* plugin
127+
and add some logic to our existing methods:
128+
129+
```python
130+
from hashlib import sha1
131+
from bottle import run, get, post, delete, install, HTTPError, request
132+
from bottle_sqlite import SQLitePlugin
133+
134+
from dbsetup import init_db
135+
136+
137+
DBNAME='test.db'
138+
139+
init_db(DBNAME)
140+
install(SQLitePlugin(dbfile=DBNAME))
141+
142+
def to_dict(row):
143+
return dict(sha=row['sha'],
144+
url=row['url'],
145+
timestamp=row['timestamp'])
146+
147+
@get('/')
148+
@get('/links')
149+
def list_links(db):
150+
'''Return a complete list of all links'''
151+
links = []
152+
for row in db.execute('SELECT * from links'):
153+
links.append(to_dict(row))
154+
return dict(links=links)
155+
156+
@get('/links/<sha>')
157+
def get_link(db, sha):
158+
'''Returns a specific link'''
159+
row = db.execute('SELECT * from links WHERE sha IS ?', [sha]).fetchone()
160+
if row:
161+
return dict(link=to_dict(row))
162+
163+
return HTTPError(404, "No such item")
164+
165+
@delete('/links/<sha>')
166+
def delete_link(db, sha):
167+
'''Deletes a specific link from the list.
168+
On success, returns an empty response'''
169+
db.execute('DELETE FROM links WHERE sha IS ?', [sha])
170+
if db.total_changes > 0:
171+
return {}
172+
173+
return HTTPError(404, "No such item")
174+
175+
@post('/links')
176+
def add_link(db):
177+
'''Adds a link to the list.
178+
On success, returns the entry created.'''
179+
# Only accept json data
180+
if request.content_type != 'application/json':
181+
return HTTPError(415, "Only json is accepted")
182+
# Check required fields
183+
if 'url' not in request.json:
184+
return HTTPError(400, "Must specify a url")
185+
186+
# Sha is optional, generate if not present
187+
if 'sha' not in request.json:
188+
request.json['sha'] = sha1(request.json['url']).hexdigest()
189+
190+
args = [request.json['url'],
191+
request.json['sha']]
192+
if 'timestamp' in request.json:
193+
args.append(request.json['timestamp'])
194+
stmt = 'INSERT INTO links (url, sha, timestamp) VALUES(?, ?, ?)'
195+
else:
196+
stmt = 'INSERT INTO links (url, sha) VALUES(?, ?)'
197+
198+
db.execute(stmt, args)
199+
200+
return get_link(db, request.json['sha'])
201+
202+
203+
if __name__ == '__main__':
204+
# Restart server automatically when this file changes
205+
run(host='0.0.0.0', port=5500, reloader=True, debug=True)
206+
```
207+
208+
Wow. That was fairly straightforward. The one thing that is
209+
missing is a requirement to login.
210+
211+
## Adding Google authentication and users
212+
To make sure we don't mix user's data, we'll add a column in
213+
the database that will hold the username, e.g. their e-mail.
214+
215+
But we also need people to login with Google, and the server
216+
to verify that, so that people can't just use any e-mail
217+
they'd like.
218+
219+
### Creating a project with Google
220+
Create projects in api console...
221+
222+
todo

server-app/app.py

Lines changed: 53 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,104 @@
11
from hashlib import sha1
2+
from dateutil import parser as dateparser
23
from bottle import run, get, post, delete, install, HTTPError, request
34
from bottle_sqlite import SQLitePlugin
45

56
from dbsetup import init_db
67

8+
from google_auth import gauth
79

810
DBNAME='test.db'
911

1012
init_db(DBNAME)
1113
install(SQLitePlugin(dbfile=DBNAME))
1214

15+
install(gauth)
16+
1317
def to_dict(row):
1418
return dict(sha=row['sha'],
1519
url=row['url'],
16-
timestamp=row['timestamp'])
20+
timestamp=row['timestamp'],
21+
deleted=row['deleted'])
22+
1723

1824
@get('/')
1925
@get('/links')
20-
def list_links(db):
26+
def list_links(db, userid):
2127
'''Return a complete list of all links'''
28+
args = [userid]
29+
30+
deleted_part = ' AND deleted IS 0'
31+
if ('showDeleted' in request.query and
32+
'true' == request.query['showDeleted']):
33+
deleted_part = ''
34+
35+
timestamp_part = ''
36+
if 'timestampMin' in request.query:
37+
timestamp_part = ' AND timestamp > ?'
38+
args.append(request.query['timestampMin'])
39+
40+
latest_time = None
2241
links = []
23-
for row in db.execute('SELECT * from links'):
42+
stmt = 'SELECT * from links WHERE userid IS ?'
43+
stmt += deleted_part + timestamp_part
44+
for row in db.execute(stmt,
45+
args):
2446
links.append(to_dict(row))
25-
return dict(links=links)
47+
# Keep track of the latest timestamp here
48+
if latest_time is None:
49+
latest_time = row['timestamp']
50+
else:
51+
delta = dateparser.parse(row['timestamp']) - dateparser.parse(latest_time)
52+
if delta.total_seconds() > 0:
53+
latest_time = row['timestamp']
54+
55+
return dict(latestTimestamp=latest_time,
56+
links=links)
2657

2758
@get('/links/<sha>')
28-
def get_link(db, sha):
59+
def get_link(db, sha, userid):
2960
'''Returns a specific link'''
30-
row = db.execute('SELECT * from links WHERE sha IS ?', [sha]).fetchone()
61+
row = db.execute('SELECT * from links WHERE sha IS ? AND userid IS ?',
62+
[sha, userid]).fetchone()
3163
if row:
32-
return dict(link=to_dict(row))
64+
return to_dict(row)
3365

3466
return HTTPError(404, "No such item")
3567

3668
@delete('/links/<sha>')
37-
def delete_link(db, sha):
69+
def delete_link(db, sha, userid):
3870
'''Deletes a specific link from the list.
3971
On success, returns an empty response'''
40-
db.execute('DELETE FROM links WHERE sha IS ?', [sha])
41-
if db.total_changes > 0:
42-
return {}
72+
db.execute('UPDATE links SET deleted = 1, timestamp = CURRENT_TIMESTAMP \
73+
WHERE sha IS ? AND userid is ?', [sha, userid])
4374

44-
return HTTPError(404, "No such item")
75+
#if db.total_changes > 0:
76+
return {}
77+
#return HTTPError(404, "No such item")
4578

4679
@post('/links')
47-
def add_link(db):
80+
def add_link(db, userid):
4881
'''Adds a link to the list.
4982
On success, returns the entry created.'''
50-
# Only accept json data
51-
if request.content_type != 'application/json':
83+
if 'application/json' not in request.content_type:
5284
return HTTPError(415, "Only json is accepted")
5385
# Check required fields
54-
if 'url' not in request.json:
86+
if ('url' not in request.json or request.json['url'] is None
87+
or len(request.json['url']) < 1):
5588
return HTTPError(400, "Must specify a url")
5689

5790
# Sha is optional, generate if not present
5891
if 'sha' not in request.json:
5992
request.json['sha'] = sha1(request.json['url']).hexdigest()
6093

61-
args = [request.json['url'],
94+
args = [userid,
95+
request.json['url'],
6296
request.json['sha']]
63-
if 'timestamp' in request.json:
64-
args.append(request.json['timestamp'])
65-
stmt = 'INSERT INTO links (url, sha, timestamp) VALUES(?, ?, ?)'
66-
else:
67-
stmt = 'INSERT INTO links (url, sha) VALUES(?, ?)'
97+
stmt = 'INSERT INTO links (userid, url, sha) VALUES(?, ?, ?)'
6898

6999
db.execute(stmt, args)
70100

71-
return get_link(db, request.json['sha'])
101+
return get_link(db, request.json['sha'], userid)
72102

73103

74104
if __name__ == '__main__':

server-app/curl_tests.sh

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
#!/bin/sh
22

3-
echo "Getting all"
4-
curl -X GET http://localhost:5500/links
5-
63
echo "\n\nDeleting one"
74
curl -X DELETE http://localhost:5500/links/55
85

@@ -18,3 +15,11 @@ curl -X POST -d '{"username":"xyz","password":"xyz"}' http://localhost:5500/link
1815

1916
echo "\n\nAdding no url"
2017
curl -X POST -H "Content-Type: application/json" -d '{"username":"xyz","password":"xyz"}' http://localhost:5500/links
18+
19+
20+
echo "Getting all"
21+
curl -X GET -H "Content-Type: application/json" -d '{"access_token":"zyzxcvz"}' http://localhost:5500/links
22+
23+
24+
#echo "Auth test"
25+
#curl -H "Authorization: Bearer 1/fFBGRNJru1FQd44AzqT3Zg" https://www.googleapis.com/oauth2/v1/userinfo

server-app/dbsetup.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
_CREATE_TABLE = \
55
"""CREATE TABLE IF NOT EXISTS links
66
(_id INTEGER PRIMARY KEY,
7+
userid TEXT NOT NULL,
78
sha TEXT NOT NULL,
89
url TEXT NOT NULL,
10+
deleted INTEGER NOT NULL DEFAULT 0,
911
timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
1012
11-
UNIQUE(url) ON CONFLICT REPLACE,
12-
UNIQUE(sha) ON CONFLICT REPLACE)
13+
UNIQUE(userid, url) ON CONFLICT REPLACE,
14+
UNIQUE(userid, sha) ON CONFLICT REPLACE)
1315
"""
1416

1517
def init_db(filename='test.db'):

0 commit comments

Comments
 (0)