forked from bdoms/trestle
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmodel.py
More file actions
169 lines (128 loc) · 5.26 KB
/
model.py
File metadata and controls
169 lines (128 loc) · 5.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
import base64
import os
from datetime import datetime
from hashlib import sha512
from peewee import (PostgresqlDatabase, BooleanField, CharField, DateTimeField,
ForeignKeyField, Model, DoesNotExist) # TextField
from tornado.ioloop import IOLoop
from config import constants
peewee_db = PostgresqlDatabase(constants.DB_NAME, user=constants.DB_USER, password=constants.DB_PASS,
host=constants.DB_HOST, port=constants.DB_PORT, sslmode=constants.DB_SSLMODE, autoconnect=False)
# NOTE: database functions are synchronous, so we have to run them in another thread to make them asynchronous
# while this can create additional overhead you SHOULD use these functions if you have a long running query
# if not, it can lock up the server
def threaded_db(func, *args):
result = None
with peewee_db.connection_context():
result = func(*args)
return result
# example use: async_db(list, Account.select()) or async_db(Account.select().count)
async def async_db(func, *args):
result = await IOLoop.current().run_in_executor(None, threaded_db, func, *args)
# NOTE: peewee is supposed to store connections per thread
# and while you get an error about it not being open every time, it also looks like
# closing the connection in the child can also close it in the main thread
# so we check and re-open if needed - this assumes the connection is closed elsewhere (end of request)
if peewee_db.is_closed():
peewee_db.connect()
return result
class BaseModel(Model):
class Meta:
database = peewee_db
# used for backups on all model types
created_dt = DateTimeField(default=datetime.utcnow)
modified_dt = DateTimeField(default=datetime.utcnow)
@property
def slug(self):
return str(self.id)
@classmethod
def getBySlug(cls, slug):
# this is preferable to `get_by_id` because we can return None rather than an error
return cls.select().where(cls.id == slug).first()
def refresh(self):
try:
entity = type(self).get(self._pk_expr())
except DoesNotExist:
entity = None
return entity
def save(self, *args, **kwargs):
self.modified_dt = datetime.utcnow()
return super().save(*args, **kwargs)
class Account(BaseModel):
email = CharField()
password_salt = CharField()
hashed_password = CharField()
hashed_token = CharField(null=True)
token_dt = DateTimeField(null=True)
# pic_url = CharField(null=True)
is_admin = BooleanField(default=False)
is_dev = BooleanField(default=False) # set this directly via the console
class Meta:
table_name = 'account'
@classmethod
def getByAuth(cls, slug):
auth = Auth.getBySlug(slug)
return auth and auth.account or None
@classmethod
def getByEmail(cls, email):
return cls.select().where(cls.email == email).first()
@classmethod
def hashPassword(cls, password, salt):
return sha512(password.encode('utf8') + salt).hexdigest()
@classmethod
def changePassword(cls, password):
salt = base64.b64encode(os.urandom(64))
hashed_password = cls.hashPassword(password, salt)
return salt, hashed_password
def getAuth(self, user_agent):
return self.auths.where(Auth.user_agent == user_agent).first()
def hashToken(self, token):
token_salt = self.slug + '-' + self.token_dt.isoformat()
return Account.hashPassword(token, token_salt.encode())
def resetPassword(self):
# python b64 always ends in '==' so we remove them because this is for use in a URL
token = base64.urlsafe_b64encode(os.urandom(16)).decode().replace('=', '')
self.token_dt = datetime.utcnow()
self.hashed_token = self.hashToken(token)
self.save()
return token
def toDict(self):
return {'email': self.email}
class Auth(BaseModel):
user_agent = CharField()
os = CharField(null=True)
browser = CharField(null=True)
device = CharField(null=True)
ip = CharField()
account = ForeignKeyField(Account, backref='auths')
class Meta:
table_name = 'auth'
def toDict(self):
return {
'slug': self.slug,
'user_agent': self.user_agent,
'os': self.os,
'browser': self.browser,
'device': self.device,
'ip': self.ip,
'modified_dt': self.modified_dt.isoformat()
}
# CAREFUL when creating new models - avoid Postgres keywords if that's your backend
# while peewee can handle them other ORMs or manual work might not
# see https://www.postgresql.org/docs/current/sql-keywords-appendix.html
def generateToken(size):
return base64.urlsafe_b64encode(os.urandom(size)).decode().replace('=', '')
def reset():
# order matters here - have to delete in the right direction given foreign key constraints
tables = [Auth, Account]
for table in tables:
table.drop_table()
# order matters here too for establishing the foriegn keys, but it's reversed from above
tables.reverse()
for table in tables:
table.create_table()
if __name__ == '__main__':
ok = input('WARNING! This will drop all tables in the database. Continue? y/n ')
if ok.lower() in ['y', 'yes']:
peewee_db.connect()
reset()