Skip to content

Commit 3ee46ae

Browse files
committed
First commit for beta. Added encryption, changed schema a bit
1 parent 26a9251 commit 3ee46ae

File tree

9 files changed

+260
-38
lines changed

9 files changed

+260
-38
lines changed

.gitignore

+3-1
Original file line numberDiff line numberDiff line change
@@ -238,4 +238,6 @@ temp/
238238
*.sln
239239
*.sw?
240240

241-
/config
241+
/config
242+
test.sh
243+
test.py

app/models.py

+133-21
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,94 @@
1-
from app import db
1+
from app import app, db
22
from app.model_types import GUID
33
from sqlalchemy.sql import func
4+
from sqlalchemy.ext.hybrid import hybrid_property, hybrid_method
45
from sqlalchemy import event
6+
from Crypto.Cipher import AES
7+
import binascii
58
import uuid
69
import frontmatter
710
import re
811

912

13+
key = app.config['DB_ENCRYPTION_KEY']
14+
15+
16+
def aes_encrypt(data):
17+
cipher = AES.new(key)
18+
data = data + (" " * (16 - (len(data) % 16)))
19+
return binascii.hexlify(cipher.encrypt(data))
20+
21+
def aes_decrypt(data):
22+
try:
23+
cipher = AES.new(key)
24+
return cipher.decrypt(binascii.unhexlify(data)).rstrip()
25+
except:
26+
# If data is not encrypted, just return it
27+
return data
28+
29+
1030
class User(db.Model):
1131
uuid = db.Column(GUID, primary_key=True, index=True, unique=True, default=lambda: uuid.uuid4())
1232
username = db.Column(db.String(64), unique=True, nullable=False)
1333
password_hash = db.Column(db.String(128), nullable=False)
14-
notes = db.relationship('Note', lazy='dynamic')
34+
notes = db.relationship('Note', lazy='dynamic', cascade='all, delete, delete-orphan')
35+
tags = db.relationship('Tag', lazy='dynamic', cascade='all, delete, delete-orphan')
36+
projects = db.relationship('Project', lazy='dynamic', cascade='all, delete, delete-orphan')
37+
tasks = db.relationship('Task', lazy='dynamic', cascade='all, delete, delete-orphan')
1538

1639
def __repr__(self):
1740
return '<User {}>'.format(self.uuid)
1841

1942

43+
class Tag(db.Model):
44+
uuid = db.Column(GUID, primary_key=True, index=True, unique=True, default=lambda: uuid.uuid4())
45+
user_id = db.Column(GUID, db.ForeignKey('user.uuid'), nullable=False)
46+
note_id = db.Column(GUID, db.ForeignKey('note.uuid'), nullable=False)
47+
name = db.Column(db.String)
48+
49+
def __repr__(self):
50+
return '<Tag {}>'.format(self.uuid)
51+
52+
53+
class Project(db.Model):
54+
uuid = db.Column(GUID, primary_key=True, index=True, unique=True, default=lambda: uuid.uuid4())
55+
user_id = db.Column(GUID, db.ForeignKey('user.uuid'), nullable=False)
56+
note_id = db.Column(GUID, db.ForeignKey('note.uuid'), nullable=False)
57+
name = db.Column(db.String)
58+
59+
def __repr__(self):
60+
return '<Project {}>'.format(self.uuid)
61+
62+
63+
class Task(db.Model):
64+
uuid = db.Column(GUID, primary_key=True, index=True, unique=True, default=lambda: uuid.uuid4())
65+
user_id = db.Column(GUID, db.ForeignKey('user.uuid'), nullable=False)
66+
note_id = db.Column(GUID, db.ForeignKey('note.uuid'), nullable=False)
67+
name = db.Column(db.String)
68+
swimlane = db.Column(db.String)
69+
70+
def __repr__(self):
71+
return '<Task {}>'.format(self.uuid)
72+
73+
2074
class Note(db.Model):
2175
uuid = db.Column(GUID, primary_key=True, index=True, unique=True, default=lambda: uuid.uuid4())
22-
tags = db.Column(db.String)
23-
projects = db.Column(db.String)
2476
user_id = db.Column(GUID, db.ForeignKey('user.uuid'), nullable=False)
2577
data = db.Column(db.String)
2678
title = db.Column(db.String(128), nullable=False, unique=True)
2779
date = db.Column(db.DateTime(timezone=True), server_default=func.now())
2880
is_date = db.Column(db.Boolean, default=False)
81+
tags = db.relationship('Tag', lazy='dynamic', cascade='all, delete, delete-orphan')
82+
projects = db.relationship('Project', lazy='dynamic', cascade='all, delete, delete-orphan')
83+
tasks = db.relationship('Task', lazy='dynamic', cascade='all, delete, delete-orphan')
84+
85+
@hybrid_property
86+
def text(self):
87+
return aes_decrypt(self.data)
88+
89+
@text.setter
90+
def text(self, value):
91+
self.data = aes_encrypt(value)
2992

3093
def __repr__(self):
3194
return '<Note {}>'.format(self.uuid)
@@ -53,32 +116,81 @@ def serialize_full(self):
53116
}
54117

55118

119+
# Update title automatically
56120
def before_change_note(mapper, connection, target):
57-
tags = ''
58-
projects = ''
59121
title = None
60122

61-
data = frontmatter.loads(target.data)
62-
63-
if isinstance(data.get('tags'), list):
64-
tags = ','.join(set([x.replace(',', '\,') for x in data.get('tags')]))
65-
elif isinstance(data.get('tags'), str):
66-
tags = ','.join(set(map(str.strip, data['tags'].split(','))))
67-
68-
if isinstance(data.get('projects'), list):
69-
projects = ','.join(set([x.replace(',', '\,') for x in data.get('projects')]))
70-
elif isinstance(data.get('projects'), str):
71-
projects = ','.join(set(map(str.strip, data['projects'].split(','))))
123+
data = frontmatter.loads(target.text)
72124

73125
if isinstance(data.get('title'), str) and len(data.get('title')) > 0:
74126
title = data.get('title')
75-
76-
target.tags = tags
77-
target.projects = projects
78127

79128
if title and not target.is_date:
80129
target.title = title
81130

82131

132+
# Handle changes to tasks, projects, and tags
133+
def after_change_note(mapper, connection, target):
134+
tags = []
135+
projects = []
136+
# tasks = []
137+
138+
data = frontmatter.loads(target.text)
139+
140+
if isinstance(data.get('tags'), list):
141+
tags = list(set([x.replace(',', '\,') for x in data.get('tags')]))
142+
elif isinstance(data.get('tags'), str):
143+
tags = list(set(map(str.strip, data['tags'].split(','))))
144+
145+
if isinstance(data.get('projects'), list):
146+
projects = list(set([x.replace(',', '\,') for x in data.get('projects')]))
147+
elif isinstance(data.get('projects'), str):
148+
projects = list(set(map(str.strip, data['projects'].split(','))))
149+
150+
# Parse out tasks here #
151+
152+
existing_tags = Tag.query.filter_by(note_id=target.uuid).all()
153+
existing_projects = Project.query.filter_by(note_id=target.uuid).all()
154+
# existing_tasks = Task.query.filter_by(note_id=target.uuid).all()
155+
156+
for tag in existing_tags:
157+
if tag.name not in tags:
158+
connection.execute(
159+
'DELETE FROM tag WHERE uuid = (?)',
160+
'{}'.format(tag.uuid).replace('-', '')
161+
)
162+
else:
163+
tags.remove(tag.name)
164+
165+
for tag in tags:
166+
connection.execute(
167+
'INSERT INTO tag (uuid, user_id, note_id, name) VALUES (?, ?, ?, ?)',
168+
'{}'.format(uuid.uuid4()).replace('-', ''),
169+
'{}'.format(target.user_id).replace('-', ''),
170+
'{}'.format(target.uuid).replace('-', ''),
171+
tag
172+
)
173+
174+
for project in existing_projects:
175+
if project.name not in projects:
176+
connection.execute(
177+
'DELETE FROM project WHERE uuid = (?)',
178+
'{}'.format(project.uuid).replace('-', '')
179+
)
180+
else:
181+
projects.remove(project.name)
182+
183+
for project in projects:
184+
connection.execute(
185+
'INSERT INTO project (uuid, user_id, note_id, name) VALUES (?, ?, ?, ?)',
186+
'{}'.format(uuid.uuid4()).replace('-', ''),
187+
'{}'.format(target.user_id).replace('-', ''),
188+
'{}'.format(target.uuid).replace('-', ''),
189+
project
190+
)
191+
192+
83193
event.listen(Note, 'before_insert', before_change_note)
84-
event.listen(Note, 'before_update', before_change_note)
194+
event.listen(Note, 'before_update', before_change_note)
195+
event.listen(Note, 'after_insert', after_change_note)
196+
event.listen(Note, 'after_update', after_change_note)

config.py

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
class Config(object):
77
JWT_SECRET_KEY = os.environ.get('API_SECRET_KEY')
8+
DB_ENCRYPTION_KEY = os.environ.get('DB_ENCRYPTION_KEY')
89
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URI') or 'sqlite:///' + os.path.join(basedir + '/config', 'app.db')
910
SQLALCHEMY_TRACK_MODIFICATIONS = False
1011
JWT_ACCESS_TOKEN_EXPIRES = datetime.timedelta(days=7)

migrations/env.py

+1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ def process_revision_directives(context, revision, directives):
8282
context.configure(
8383
connection=connection,
8484
target_metadata=target_metadata,
85+
# render_as_batch=True,
8586
process_revision_directives=process_revision_directives,
8687
user_module_prefix="app.model_types.",
8788
**current_app.extensions['migrate'].configure_args
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""newtables
2+
3+
Revision ID: 21156316dbbe
4+
Revises: 9ca5901af374
5+
Create Date: 2020-01-28 20:45:00.940634
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
import app.model_types
11+
12+
13+
# revision identifiers, used by Alembic.
14+
revision = '21156316dbbe'
15+
down_revision = '9ca5901af374'
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade():
21+
# ### commands auto generated by Alembic - please adjust! ###
22+
op.create_table('project',
23+
sa.Column('uuid', app.model_types.GUID(), nullable=False),
24+
sa.Column('user_id', app.model_types.GUID(), nullable=False),
25+
sa.Column('note_id', app.model_types.GUID(), nullable=False),
26+
sa.Column('name', sa.String(), nullable=True),
27+
sa.ForeignKeyConstraint(['note_id'], ['note.uuid'], ),
28+
sa.ForeignKeyConstraint(['user_id'], ['user.uuid'], ),
29+
sa.PrimaryKeyConstraint('uuid')
30+
)
31+
op.create_index(op.f('ix_project_uuid'), 'project', ['uuid'], unique=True)
32+
op.create_table('tag',
33+
sa.Column('uuid', app.model_types.GUID(), nullable=False),
34+
sa.Column('user_id', app.model_types.GUID(), nullable=False),
35+
sa.Column('note_id', app.model_types.GUID(), nullable=False),
36+
sa.Column('name', sa.String(), nullable=True),
37+
sa.ForeignKeyConstraint(['note_id'], ['note.uuid'], ),
38+
sa.ForeignKeyConstraint(['user_id'], ['user.uuid'], ),
39+
sa.PrimaryKeyConstraint('uuid')
40+
)
41+
op.create_index(op.f('ix_tag_uuid'), 'tag', ['uuid'], unique=True)
42+
op.create_table('task',
43+
sa.Column('uuid', app.model_types.GUID(), nullable=False),
44+
sa.Column('user_id', app.model_types.GUID(), nullable=False),
45+
sa.Column('note_id', app.model_types.GUID(), nullable=False),
46+
sa.Column('name', sa.String(), nullable=True),
47+
sa.Column('swimlane', sa.String(), nullable=True),
48+
sa.ForeignKeyConstraint(['note_id'], ['note.uuid'], ),
49+
sa.ForeignKeyConstraint(['user_id'], ['user.uuid'], ),
50+
sa.PrimaryKeyConstraint('uuid')
51+
)
52+
op.create_index(op.f('ix_task_uuid'), 'task', ['uuid'], unique=True)
53+
# ### end Alembic commands ###
54+
55+
56+
def downgrade():
57+
# ### commands auto generated by Alembic - please adjust! ###
58+
op.drop_index(op.f('ix_task_uuid'), table_name='task')
59+
op.drop_table('task')
60+
op.drop_index(op.f('ix_tag_uuid'), table_name='tag')
61+
op.drop_table('tag')
62+
op.drop_index(op.f('ix_project_uuid'), table_name='project')
63+
op.drop_table('project')
64+
# ### end Alembic commands ###
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""cleanup
2+
3+
Revision ID: 9ca5901af374
4+
Revises: a477f34dbaa4
5+
Create Date: 2020-01-28 20:44:00.184324
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
import app.model_types
11+
12+
13+
# revision identifiers, used by Alembic.
14+
revision = '9ca5901af374'
15+
down_revision = 'a477f34dbaa4'
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade():
21+
# ### commands auto generated by Alembic - please adjust! ###
22+
with op.batch_alter_table('note', schema=None) as batch_op:
23+
batch_op.drop_column('projects')
24+
batch_op.drop_column('tags')
25+
26+
# ### end Alembic commands ###
27+
28+
29+
def downgrade():
30+
# ### commands auto generated by Alembic - please adjust! ###
31+
with op.batch_alter_table('note', schema=None) as batch_op:
32+
batch_op.add_column(sa.Column('tags', sa.VARCHAR(), nullable=True))
33+
batch_op.add_column(sa.Column('projects', sa.VARCHAR(), nullable=True))
34+
35+
# ### end Alembic commands ###

requirements.txt

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ flask_sqlalchemy
33
flask-migrate
44
flask-jwt-extended
55
flask-argon2
6-
python-frontmatter
6+
python-frontmatter
7+
pycrypto

run.sh

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
#!/bin/sh
22

3+
if test -f "./config/.env"; then
4+
. ./config/.env
5+
fi
6+
37
./verify_env.py
48

59
if test -f "./config/.env"; then

0 commit comments

Comments
 (0)