Compare commits

..

No commits in common. "master" and "dev" have entirely different histories.
master ... dev

15 changed files with 69 additions and 220 deletions

@ -1 +1 @@
Subproject commit f939127a0c9099f9c22c39bfaaed33b70b006fa2 Subproject commit 45cd71cc4bcddf23f46b1eddcffd8c7fda2b9d41

View File

@ -1,14 +0,0 @@
from server import db
import time
from database.models import Device
def cleanDevices():
allDevs = db.session.query(Device)
devices = allDevs.filter(Device.lastConnection <= int(round(time.time())) - 60*60*24*30).all()
devices += allDevs.filter(Device.lastConnection == None)
for device in devices:
print(device.deviceName)
db.session.delete(device)
db.session.commit()

View File

@ -1,30 +0,0 @@
"""empty message
Revision ID: 9882522aafa9
Revises: e5ef5e4a807b
Create Date: 2020-07-25 09:34:07.987380
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '9882522aafa9'
down_revision = 'e5ef5e4a807b'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('calendar', sa.Column('calendar_type', sa.String(length=32), nullable=True))
op.create_index(op.f('ix_calendar_calendar_type'), 'calendar', ['calendar_type'], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_calendar_calendar_type'), table_name='calendar')
op.drop_column('calendar', 'calendar_type')
# ### end Alembic commands ###

View File

@ -61,13 +61,11 @@ class Device(db.Model):
user_id = db.Column(db.Integer, db.ForeignKey('user.id')) user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
deviceName = db.Column(db.String(64), unique=True) deviceName = db.Column(db.String(64), unique=True)
connection = db.Column(db.Boolean) connection = db.Column(db.Boolean)
lastConnection = db.Column(db.BigInteger)
class Calendar(db.Model): class Calendar(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True) id = db.Column(db.Integer, primary_key=True, autoincrement=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True, nullable=False) user_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True, nullable=False)
calendar_id = db.Column(db.String(256), primary_key=True) calendar_id = db.Column(db.String(256), primary_key=True)
calendar_type = db.Column(db.String(32), index=True)
name = db.Column(db.String(256), index=True) name = db.Column(db.String(256), index=True)
toggle = db.Column(db.String(8)) toggle = db.Column(db.String(8))
color = db.Column(db.String(16)) color = db.Column(db.String(16))

View File

@ -2,10 +2,9 @@ FROM python:3.8-slim-buster
RUN apt-get update && apt-get upgrade RUN apt-get update && apt-get upgrade
RUN pip3 install flask Flask-SQLAlchemy flask_migrate flask_login flask_wtf python-dotenv RUN pip3 install flask Flask-SQLAlchemy flask_migrate flask_login flask_wtf python-dotenv
RUN apt-get install gcc libpcre3 libpcre3-dev libmariadbclient-dev -y RUN apt-get install gcc libpcre3 libpcre3-dev libmariadbclient-dev -y
RUN pip3 install uwsgi email-validator RandomWords ics RUN pip3 install uwsgi email-validator RandomWords
RUN pip3 install google google-oauth google-auth-oauthlib google-api-python-client mysqlclient RUN pip3 install google google-oauth google-auth-oauthlib google-api-python-client mysqlclient
COPY docker-entrypoint.sh /usr/local/bin/ COPY docker-entrypoint.sh /usr/local/bin/
EXPOSE 8084 EXPOSE 8084
EXPOSE 3001 EXPOSE 3001
ENTRYPOINT ["docker-entrypoint.sh"] ENTRYPOINT ["docker-entrypoint.sh"]
# CMD tail -f /dev/null

View File

@ -1,17 +1,5 @@
#!/bin/sh #!/bin/sh
cd /home/calendarwatch cd /home/calendarwatch
uwsgi --ini wsgi.ini
# use flasks own uwsgi server for debugging:
# export FLASK_APP=/home/calendarwatch/server.py
# python3 server.py
# the --lazy flag forks() a new instance of the server
# instead of forking from the parent and copying the same mysql
# connection. If you don't do that, then multiple forks will use
# the same connection at the same time, causing the server to throw
# a 'connection has gone away' error.
# more here: https://serverfault.com/questions/407612/error-2006-mysql-server-has-gone-away
uwsgi --ini wsgi.ini --lazy
echo "server has been started" echo "server has been started"

View File

@ -46,13 +46,3 @@ class DeviceForm(FlaskForm):
device = Device.query.filter_by(deviceName=deviceName.data).first() device = Device.query.filter_by(deviceName=deviceName.data).first()
if device is None: if device is None:
raise ValidationError('Device not Found') raise ValidationError('Device not Found')
class CalendarForm(FlaskForm):
iCalURL = StringField('New ical URL', validators=[DataRequired()])
calName = StringField('Calendar Name', validators=[DataRequired()])
submit = SubmitField('Add URL')
def validate_iCalURL (self, iCalURL):
return None

View File

@ -121,23 +121,19 @@ def deleteAccount(user):
def fetchCalendarEvents(user, calendars, startDate, endDate): def fetchCalendarEvents(user, calendars, startDate, endDate):
service = None
if user.google_token is not None:
client_token = GC.build_credentials(user.google_token.token,
user.google_token.refresh_token)
credentials = google.oauth2.credentials.Credentials(**client_token)
service = build(GC.API_SERVICE_NAME, GC.API_VERSION, credentials=credentials) client_token = GC.build_credentials(user.google_token.token,
user.google_token.refresh_token)
credentials = google.oauth2.credentials.Credentials(**client_token)
service = build(GC.API_SERVICE_NAME, GC.API_VERSION, credentials=credentials)
all_events = [] all_events = []
for calendar in calendars: for calendar in calendars:
if (calendar.toggle == "True" and if calendar.toggle == "True":
calendar.calendar_type == "Google" and
service != None):
event_result = service.events().list(calendarId=calendar.calendar_id, event_result = service.events().list(calendarId=calendar.calendar_id,
timeMin=startDate.isoformat(), timeMin=startDate,
timeMax=endDate.isoformat(), timeMax=endDate,
maxResults=10, maxResults=10,
singleEvents=True, singleEvents=True,
orderBy='startTime').execute() orderBy='startTime').execute()
@ -160,10 +156,7 @@ def fetchCalendarEvents(user, calendars, startDate, endDate):
all_events.append(newEvent) all_events.append(newEvent)
if service != None: colors = service.colors().get().execute()
colors = service.colors().get().execute()
else:
colors = None
return all_events, colors return all_events, colors
@ -185,7 +178,6 @@ def fetchCalendars():
calendar_list = service.calendarList().list(pageToken=page_token).execute() calendar_list = service.calendarList().list(pageToken=page_token).execute()
for calendar in calendar_list['items']: for calendar in calendar_list['items']:
calendars.append(Calendar(name=calendar['summary'], calendars.append(Calendar(name=calendar['summary'],
calType="Google",
calendarId=calendar['id'], calendarId=calendar['id'],
color=calendar['colorId'])) color=calendar['colorId']))
page_token = calendar_list.get('nextPageToken') page_token = calendar_list.get('nextPageToken')

View File

@ -1,7 +1,6 @@
# Python standard libraries # Python standard libraries
import json import json
import os import os
import time
# Third-party libraries # Third-party libraries
import flask import flask
@ -17,11 +16,10 @@ from flask_login import (
from random_words import RandomWords from random_words import RandomWords
import requests import requests
import backend.icalHandler as ical
import server.googleHandler as google import server.googleHandler as google
from server import login_manager, app, db from server import login_manager, app, db
from server.forms import LoginForm, RegistrationForm, DeviceForm, CalendarForm from server.forms import LoginForm, RegistrationForm, DeviceForm
import backend import backend
from database.models import User, Calendar, Device, GoogleToken from database.models import User, Calendar, Device, GoogleToken
@ -95,58 +93,11 @@ def devices():
form=form) form=form)
@app.route("/calendar", methods=['GET', 'POST', 'DELETE']) @app.route("/calendar")
@login_required
def calendar(): def calendar():
if not current_user.is_authenticated:
return flask.render_template('login.html')
calendars = backend.calendarsFromDb(current_user) calendars = backend.calendarsFromDb(current_user)
return flask.render_template('calendar.html', calendars=calendars)
form = CalendarForm()
if request.method == 'POST':
if request.form.get("submit") == "Remove":
calendar = db.session.query(Calendar).filter(Calendar.calendar_id==request.form.get("calendar")).first()
db.session.delete(calendar)
db.session.commit()
return flask.redirect(url_for('calendar'))
elif form.validate_on_submit():
ical.icalToCalendarDb(form.iCalURL.data, form.calName.data, current_user)
return flask.redirect(url_for('calendar'))
# otherwise it is a javascript POST
else:
try:
calId = request.json.get('calendar_id')
color = request.json.get('color', None)
toggle = request.json.get('toggle', None)
except:
return flask.render_template('calendar.html', calendars=calendars, form=form)
if color != None:
current_user.updateCalendar(calId, color=color)
if toggle != None:
current_user.updateCalendar(calId, toggle=toggle)
# toggle specific calendar of user
try:
calId = request.json.get('calendar_id')
color = request.json.get('color', None)
toggle = request.json.get('toggle', None)
except:
return flask.render_template('calendar.html', calendars=calendars, form=form)
if color != None:
current_user.updateCalendar(calId, color=color)
if toggle != None:
current_user.updateCalendar(calId, toggle=toggle)
# toggle specific calendar of user
return flask.render_template('calendar.html', calendars=calendars, form=form)
# POST
@app.route('/login/email', methods=['GET', 'POST']) @app.route('/login/email', methods=['GET', 'POST'])
def emaillogin(): def emaillogin():
@ -253,7 +204,6 @@ def downloader(device):
if request_device.user_id == None: if request_device.user_id == None:
return jsonify(kind="unregistered") return jsonify(kind="unregistered")
request_device.lastConnection=int(round(time.time()))
request_device.connection=True request_device.connection=True
db.session.commit() db.session.commit()
request_user = db.session.query(User).filter(User.id==request_device.user_id).first() request_user = db.session.query(User).filter(User.id==request_device.user_id).first()
@ -263,7 +213,6 @@ def downloader(device):
# TODO only pass along google calendars form user # TODO only pass along google calendars form user
startDate, endDate = backend.getTimeStamps() startDate, endDate = backend.getTimeStamps()
events, colors = google.fetchCalendarEvents(request_user, request_user.calendars, startDate, endDate) events, colors = google.fetchCalendarEvents(request_user, request_user.calendars, startDate, endDate)
events.extend(ical.fetchCalendarEvents(request_user.calendars, startDate, endDate))
calendarjson = backend.generateJsonFromCalendarEntries(events, colors) calendarjson = backend.generateJsonFromCalendarEntries(events, colors)
return jsonify(calendarjson) return jsonify(calendarjson)
@ -284,7 +233,6 @@ def generateDeviceFingerprint():
if not db.session.query(Device).filter(Device.deviceName==fingerprint).first(): if not db.session.query(Device).filter(Device.deviceName==fingerprint).first():
# Save as new Device # Save as new Device
device = Device(deviceName=fingerprint, connection=False) device = Device(deviceName=fingerprint, connection=False)
device.lastConnection = int(round(time.time()))
db.session.add(device) db.session.add(device)
db.session.commit() db.session.commit()
break; break;
@ -292,3 +240,27 @@ def generateDeviceFingerprint():
# Send to Device # Send to Device
return jsonify(deviceName=fingerprint) return jsonify(deviceName=fingerprint)
# POST
@app.route('/calendar', methods = ['POST', 'DELETE'])
@login_required
def user():
if request.method == 'POST':
calId = request.json.get('calendar_id')
color = request.json.get('color', None)
toggle = request.json.get('toggle', None)
if color != None:
current_user.updateCalendar(calId, color=color)
if toggle != None:
current_user.updateCalendar(calId, toggle=toggle)
# toggle specific calendar of user
elif request.method == 'DELETE':
# do nothing
return 'NONE'
else:
# POST Error 405
print("405")
return 'OK'

View File

@ -69,7 +69,7 @@ body
} }
.grayblock .padded { .grayblock .padded {
padding: 0rem; padding: 3rem;
} }
.horizontal { .horizontal {
@ -91,13 +91,11 @@ body
} }
.vertical .content .image { .vertical .content .image {
max-width: 90%;
width: 25rem; width: 25rem;
} }
.vertical .content .text { .vertical .content .text {
max-width: 90%; margin-left: 2rem;
margin: auto;
width: 26rem; width: 26rem;
} }
@ -108,6 +106,8 @@ body
.horizontal .image { .horizontal .image {
margin: 1rem; margin: 1rem;
margin-left: 4rem;
margin-right: 4rem;
height: 20rem; height: 20rem;
border-radius: 1rem; border-radius: 1rem;
width: auto; width: auto;
@ -156,7 +156,7 @@ body
background-color: #eaeaea; background-color: #eaeaea;
overflow: hidden; overflow: hidden;
margin: auto; margin: auto;
display: block; display: flex;
} }
.navigation_rightside { .navigation_rightside {
@ -165,7 +165,7 @@ body
.navigation a { .navigation a {
float: left; float: left;
display: block; display: flex;
color: #333; color: #333;
text-align: center; text-align: center;
padding: 14px 16px; padding: 14px 16px;
@ -211,7 +211,6 @@ body
display: block; display: block;
text-align: left; text-align: left;
} }
} }
/* Style page content */ /* Style page content */
@ -290,27 +289,25 @@ body
} }
.container { .container {
display: flex; display: inline-flex;
flex-wrap: wrap;
justify-content: center; justify-content: center;
align-items:center; align-items:center;
/* flex-direction: row; */ flex-direction: row;
/*padding: 0px 2rem 0px 2rem;*/ padding: 0px 2rem 0px 2rem;
} }
.container .button { .container .button {
padding: 1rem 1.5rem 1rem 1.5rem; padding: 1rem 1.5rem 1rem 1.5rem;
font-size: 2rem; font-size: 2rem;
margin: 4rem 1rem 1rem 1rem; margin: 4rem;
color: black; color: black;
text-decoration: none; text-decoration: none;
} }
.container .preview { .container .preview {
width: 20rem; width: 20rem;
height: auto; height: 20rem;
margin: 1rem 3rem 4rem 3rem; margin: 1rem 3rem 4rem 3rem;
max-width: 100%;
} }
.container .button.logout { .container .button.logout {
@ -331,9 +328,8 @@ body
} }
.sub.container { .sub.container {
width: 40%; width: 20rem;
justify-content: left; justify-content: left;
display: flex;
} }
.profile { .profile {
@ -353,7 +349,6 @@ body
.profile .name { .profile .name {
font-size: 3rem; font-size: 3rem;
color: #333; color: #333;
text-align: center;
} }
.grey { .grey {
@ -371,10 +366,3 @@ body
border-radius: 5px; border-radius: 5px;
margin-right: 1rem; margin-right: 1rem;
} }
@media (max-width:800px) {
.sub.container {
justify-content: center;
}
}

View File

@ -2,32 +2,18 @@
{% block body%} {% block body%}
<div class="container"> <div class="container">
<div style="width: 4rem;margin:1rem;"></div> <div style="width: 15rem; margin: 1rem">Calendar</div>
<div style="width: 10rem; margin: 1rem; font-weight: bold">Calendar</div> <div style="width: 10rem; margin: 1rem; padding-right: 5rem">Show on device</div>
<div style="display: inline-flex"> <div style="width: 2rem; margin: 1rem">Color</div>
<div style="width: 5rem; margin: 1rem; padding-right: 1rem;font-weight: bold">Show on device</div>
<div style="width: 2rem; margin: 1rem;font-weight: bold">Color</div>
</div>
</div> </div>
{% for item in calendars %} {% for item in calendars %}
<div class="container" style="margin-top: 1.5rem"> <div class="container">
<!--action button-->
{% if "ical" == item.calType %}
<div style="width: 4rem; margin: 1rem;">
<form action="" method="post">
<input type="hidden" name="calendar" value={{ item.calendarId }}>
<input type="submit" name="submit" value="Remove">
</form>
</div>
{% else %}
<div style="width: 4rem; margin: 1rem;"></div>
{% endif %}
<!--Name--> <!--Name-->
<div style="width: 10rem; margin-left: 1rem; margin-right: 1rem; margin-top: 0.5rem">{{ item.name }}</div> <div style="width: 15rem; margin: 1rem;">{{ item.name }}</div>
<div style="display: inline-flex">
<!--Toggle--> <!--Toggle-->
<div style="width: 5rem; margin-left: 1rem; margin-right: 1rem; margin-top: 0.5rem; padding-right: 1rem"> <div style="width: 10rem; margin: 1rem; padding-right: 5rem">
<!-- Rounded switch --> <!-- Rounded switch -->
<label class="switch"> <label class="switch">
<input class="toggle" id={{item.calendarId}} type="checkbox" toggled={{item.toggle}} onclick="toggleReaction(this)"> <input class="toggle" id={{item.calendarId}} type="checkbox" toggled={{item.toggle}} onclick="toggleReaction(this)">
@ -36,38 +22,22 @@
</div> </div>
<!--Color Selector--> <!--Color Selector-->
<div style="width: 2rem; margin-left: 1rem; margin-right: 1rem; margin-top: 0.5rem"> <div style="width: 2rem; margin: 1rem;">
<div class="colorPickSelector" id={{item.calendarId}} defaultColor={{item.color}}></div> <div class="colorPickSelector" id={{item.calendarId}} defaultColor={{item.color}}></div>
<!--svg height="20" width="20"> <!--svg height="20" width="20">
<circle cx="10" cy="10" r="10" stroke="black" stroke-width="0" fill={{ item.color }} /> <circle cx="10" cy="10" r="10" stroke="black" stroke-width="0" fill={{ item.color }} />
</svg--> </svg-->
</div> </div>
</div>
</div> </div>
{% endfor %} {% endfor %}
<div id=calendars class="container"> <div id=calendars class="container">
<a class="button" href="login/google">Google Calendar</a>
<a class="button" href="#" >Nextcloud Calendar</a>
</div> </div>
<form action="" method="post"> <div class="container">
<div class="container grey" style="margin-top: 3rem;">
<div>{{ form.hidden_tag() }}</div>
<div style="display: flex">
<div style="margin: 1rem">{{ form.calName.label }}</div>
<div style="margin: 1rem">{{ form.calName(size=24) }}</div>
</div>
<div style="display: flex">
<div style="margin: 1rem">{{ form.iCalURL.label }}</div>
<div style="margin: 1rem">{{ form.iCalURL(size=24) }}</div>
</div>
<div style="with: 8rem; margin: 1rem">{{ form.submit() }}</div>
{% for error in form.iCalURL.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</div>
</form>
<!--div class="container">
<a class="button addcalendar" href="/login/google" style="width: auto; margin: 4rem">Add Calendar</a> <a class="button addcalendar" href="/login/google" style="width: auto; margin: 4rem">Add Calendar</a>
</div--> </div>
<script type="text/javascript"> <script type="text/javascript">

View File

@ -1,7 +1,7 @@
<div id="footer"> <div id="footer">
<div class="footer container"> <div class="footer">
<p>made by Raphael Maenle </p> <p>made by Raphael Maenle </p>
<p><a href="mailto:raphael@maenle.net">raphael@maenle.net</a></p> <p><a href="mailto:raphael@maenle.net">raphael@maenle.net</a></p>
<p><a href="/privacy">privacy policy</a></p> <p><a href="/privacy">privacy policy</a></p>

View File

@ -7,7 +7,7 @@
</div> </div>
<h3 style="margin-left:10rem">Summary</h3> <h3 style="margin-left:10rem">Summary</h3>
<div style="margin-left:10rem; margin-right:10rem;">This Privacy Statement describes how Longitude handles your data and how the developer makes sure, that the user's information remains as secure as possible. <div style="margin-left:10rem; margin-right:10rem;">This Privacy Statement descibes how Longitude handles your data and how the developer makes sure, that the users information remains as secure as possible.
This application does not share any user information with third parties and takes care to only save the minimum amount of information about the user. This application does not share any user information with third parties and takes care to only save the minimum amount of information about the user.
The following chapters cover all essential points of interest about which information is saved and when it is removed from the server. The following chapters cover all essential points of interest about which information is saved and when it is removed from the server.
If you have any further questions or suggestions, please email us at <a href="mailto:raphael@maenle.net">raphael@maenle.net</a>.</div> If you have any further questions or suggestions, please email us at <a href="mailto:raphael@maenle.net">raphael@maenle.net</a>.</div>
@ -15,7 +15,7 @@ If you have any further questions or suggestions, please email us at <a href="ma
<h3 style="margin-left:10rem">What Information is saved?</h3> <h3 style="margin-left:10rem">What Information is saved?</h3>
<div style="margin-left:10rem; margin-right:10rem;"> <div style="margin-left:10rem; margin-right:10rem;">
Longitude Calendar saves as little information about their users as possible. The application handles sensitive information only when directly prompted by the user or a device associated with the user. The service only provides this information to the user or a device associated with the user. The data saved in the Longidute Database is Longitude Calendar saves as little information about their users as possible. The application handles sensitive information only when directly prompted by the user or a device associated with the user. The service only provides this information to the user or a device associated with the user. The data saved in the Longidute Databas is
<ul> <ul>
<li>Username and hashed password or alternatively</li> <li>Username and hashed password or alternatively</li>
<li>Google Username and Id with Google Login Token</li> <li>Google Username and Id with Google Login Token</li>

View File

@ -39,8 +39,6 @@
// content here // content here
{% endblock %} {% endblock %}
</div>
<div id="main">
</div> </div>
{% include "footer.html" %} {% include "footer.html" %}
</div> </div>

View File

@ -12,15 +12,13 @@
<div class="grayblock horizontal"> <div class="grayblock horizontal">
<div class="content padded"> <div class="content padded">
<div style='margin: 1rem'> <div style='margin: 1rem'>
<!--Connect your <img src='/static/res/googlelogo.png' style='height: 3.2rem; vertical-align:middle; padding-Bottom: 0.1rem'/> Calendar...--> Connect your <img src='/static/res/googlelogo.png' style='height: 2.2rem; vertical-align:middle; padding-Bottom: 0.1rem'/> Calendar...
Connect your Calendar..
</div> </div>
<img class="image" src='/static/res/calendar.svg'/> <img class="image" src='/static/res/calendar.svg'/>
</div> </div>
<div class="content padded"> <div class="content padded">
<div style='margin: 1rem'> <div style='margin: 1rem'>
<!--...with your <img src='/static/res/tizenlogo.png' style='height: 2rem; vertical-align:middle; padding-Bottom:0.3rem;'/> Watchface--> ...with your <img src='/static/res/tizenlogo.png' style='height: 2rem; vertical-align:middle; padding-Bottom:0.3rem;'/> Watchface
..with your Tizen Watchface
</div> </div>
<img class="image" src='/static/res/watchface.svg'/> <img class="image" src='/static/res/watchface.svg'/>
</div> </div>