Compare commits
2 Commits
7b82086ff0
...
36c9b5015f
Author | SHA1 | Date | |
---|---|---|---|
36c9b5015f | |||
87dedb8e02 |
@ -2,8 +2,7 @@ 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
|
RUN pip3 install uwsgi email-validator random-word
|
||||||
RUN pip3 install email-validator
|
|
||||||
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
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import StringField, PasswordField, BooleanField, SubmitField
|
from wtforms import StringField, PasswordField, BooleanField, SubmitField
|
||||||
from wtforms.validators import DataRequired, ValidationError, Email, EqualTo
|
from wtforms.validators import DataRequired, ValidationError, Email, EqualTo
|
||||||
from database.models import User
|
from database.models import User, Device
|
||||||
import email_validator
|
import email_validator
|
||||||
|
|
||||||
class LoginForm(FlaskForm):
|
class LoginForm(FlaskForm):
|
||||||
@ -31,3 +31,8 @@ class RegistrationForm(FlaskForm):
|
|||||||
class DeviceForm(FlaskForm):
|
class DeviceForm(FlaskForm):
|
||||||
deviceName=StringField('New Device ID', validators=[DataRequired()])
|
deviceName=StringField('New Device ID', validators=[DataRequired()])
|
||||||
submit = SubmitField('Add New Device')
|
submit = SubmitField('Add New Device')
|
||||||
|
|
||||||
|
def validate_deviceName (self, deviceName):
|
||||||
|
device = Device.query.filter_by(deviceName=deviceName.data).first()
|
||||||
|
if device is None:
|
||||||
|
raise ValidationError('Device not Found')
|
||||||
|
@ -68,7 +68,7 @@ def login():
|
|||||||
# for the OAuth 2.0 client, which you configured in the API Console. If this
|
# for the OAuth 2.0 client, which you configured in the API Console. If this
|
||||||
# value doesn't match an authorized URI, you will get a 'redirect_uri_mismatch'
|
# value doesn't match an authorized URI, you will get a 'redirect_uri_mismatch'
|
||||||
# error.
|
# error.
|
||||||
flow.redirect_uri = request.base_url + "/callback"
|
flow.redirect_uri = "https://longitudecalendar.com/login/google/callback"
|
||||||
authorization_url, state = flow.authorization_url(
|
authorization_url, state = flow.authorization_url(
|
||||||
# Enable offline access so that you can refresh an access token without
|
# Enable offline access so that you can refresh an access token without
|
||||||
# re-prompting the user for permission. Recommended for web server apps.
|
# re-prompting the user for permission. Recommended for web server apps.
|
||||||
@ -88,8 +88,8 @@ def verifyResponse():
|
|||||||
|
|
||||||
flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
|
flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
|
||||||
GC.CLIENT_SECRETS_FILE, scopes=GC.SCOPES, state=state)
|
GC.CLIENT_SECRETS_FILE, scopes=GC.SCOPES, state=state)
|
||||||
flow.redirect_uri = flask.url_for('callback', _external=True)
|
flow.redirect_uri = "https://longitudecalendar.com/login/google/callback"
|
||||||
|
|
||||||
# Use the authorization server's response to fetch the OAuth 2.0 tokens.
|
# Use the authorization server's response to fetch the OAuth 2.0 tokens.
|
||||||
authorization_response = flask.request.url
|
authorization_response = flask.request.url
|
||||||
flow.fetch_token(authorization_response=authorization_response)
|
flow.fetch_token(authorization_response=authorization_response)
|
||||||
@ -122,7 +122,7 @@ class Calendar:
|
|||||||
self.toggle=toggle
|
self.toggle=toggle
|
||||||
self.calendarId = calendarId
|
self.calendarId = calendarId
|
||||||
|
|
||||||
# TODO move this to databas
|
# TODO move this to database
|
||||||
def calendarsFromDb():
|
def calendarsFromDb():
|
||||||
pyCalendars = []
|
pyCalendars = []
|
||||||
for calendar in current_user.calendars:
|
for calendar in current_user.calendars:
|
||||||
@ -147,18 +147,14 @@ def updateCalendars():
|
|||||||
# print(a, flush=True)
|
# print(a, flush=True)
|
||||||
# print(current_user.getGoogleCredentials(), flush=True)
|
# print(current_user.getGoogleCredentials(), flush=True)
|
||||||
if current_user.google_token == None:
|
if current_user.google_token == None:
|
||||||
print("notok", flush=True)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
client_token = GC.build_credentials(current_user.google_token.token,
|
client_token = GC.build_credentials(current_user.google_token.token,
|
||||||
current_user.google_token.refresh_token)
|
current_user.google_token.refresh_token)
|
||||||
credentials = google.oauth2.credentials.Credentials(**client_token)
|
credentials = google.oauth2.credentials.Credentials(**client_token)
|
||||||
calendars = caltojson.getCalendarList(credentials)
|
calendars = caltojson.getCalendarList(credentials)
|
||||||
print(calendars, flush=True)
|
|
||||||
for calendar in calendars:
|
for calendar in calendars:
|
||||||
print(calendar, flush=True)
|
|
||||||
if not current_user.hasCalendar(calendar.calendarId):
|
if not current_user.hasCalendar(calendar.calendarId):
|
||||||
print("adding", flush=True)
|
|
||||||
c = dbCalendar(calendar_id=calendar.calendarId,
|
c = dbCalendar(calendar_id=calendar.calendarId,
|
||||||
name = calendar.summary,
|
name = calendar.summary,
|
||||||
toggle = "False",
|
toggle = "False",
|
||||||
@ -166,14 +162,12 @@ def updateCalendars():
|
|||||||
db.session.add(c)
|
db.session.add(c)
|
||||||
current_user.calendars.append(c)
|
current_user.calendars.append(c)
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
print("updated Calendars")
|
|
||||||
# Save credentials back to session in case access token was refreshed.
|
# Save credentials back to session in case access token was refreshed.
|
||||||
# ACTION ITEM: In a production app, you likely want to save these
|
# ACTION ITEM: In a production app, you likely want to save these
|
||||||
# credentials in a persistent database instead.
|
# credentials in a persistent database instead.
|
||||||
# TODO add save updated token to database here
|
# TODO add save updated token to database here
|
||||||
flask.session['credentials'] = credentials_to_dict(credentials)
|
current_user.google_token.token = credentials.token
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
def credentials_to_dict(credentials):
|
def credentials_to_dict(credentials):
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
# Python standard libraries
|
# Python standard libraries
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sqlite3
|
|
||||||
|
|
||||||
# Third-party libraries
|
# Third-party libraries
|
||||||
import flask
|
import flask
|
||||||
@ -14,6 +13,7 @@ from flask_login import (
|
|||||||
login_user,
|
login_user,
|
||||||
logout_user,
|
logout_user,
|
||||||
)
|
)
|
||||||
|
from random_word import RandomWords
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
import server.googleHandler as google
|
import server.googleHandler as google
|
||||||
@ -29,6 +29,10 @@ os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
|
|||||||
def account():
|
def account():
|
||||||
return flask.redirect('account')
|
return flask.redirect('account')
|
||||||
|
|
||||||
|
@app.route("/privacy")
|
||||||
|
def privacy():
|
||||||
|
return flask.render_template('privacy.html')
|
||||||
|
|
||||||
@app.route("/account")
|
@app.route("/account")
|
||||||
def index():
|
def index():
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
@ -61,17 +65,12 @@ def devices():
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# if this is part of the device form
|
# if this is part of the device form
|
||||||
# TODO add this device to the user - do not create new device
|
|
||||||
form = DeviceForm()
|
form = DeviceForm()
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
print(form.deviceName.data, flush=True)
|
device = db.session.query(Device).filter(Device.deviceName==form.deviceName.data).first()
|
||||||
device = Device()
|
|
||||||
device.deviceName = form.deviceName.data
|
|
||||||
device.connection = False
|
|
||||||
db.session.add(device)
|
|
||||||
current_user.devices.append(device)
|
current_user.devices.append(device)
|
||||||
# TODO add device to database here
|
db.session.commit()
|
||||||
db.session.commit()
|
|
||||||
return flask.render_template('devices.html',
|
return flask.render_template('devices.html',
|
||||||
devices=current_user.devices,
|
devices=current_user.devices,
|
||||||
form=form)
|
form=form)
|
||||||
@ -133,7 +132,7 @@ def deleteAccount():
|
|||||||
|
|
||||||
@app.route("/login/google")
|
@app.route("/login/google")
|
||||||
def googlelogin():
|
def googlelogin():
|
||||||
if current_user.is_authenticated and current_user.google_token.refresh_token != None:
|
if current_user.is_authenticated and current_user.google_token != None:
|
||||||
return redirect(url_for('account'))
|
return redirect(url_for('account'))
|
||||||
|
|
||||||
authorization_url = google.login()
|
authorization_url = google.login()
|
||||||
@ -187,17 +186,22 @@ def credentials_to_dict(credentials):
|
|||||||
'scopes': credentials.scopes}
|
'scopes': credentials.scopes}
|
||||||
|
|
||||||
|
|
||||||
@app.route("/userinfo/<path:device>/calendarevents.json")
|
@app.route("/device/<path:device>/calendarevents.json")
|
||||||
def downloader(device):
|
def downloader(device):
|
||||||
path = "/home/calendarwatch/userinfo/" + device + "/"
|
path = "/home/calendarwatch/device/" + device + "/"
|
||||||
# return flask.send_from_directory(path, "calendarevents.json")
|
request_device = db.session.query(Device).filter(Device.deviceName==device).first()
|
||||||
request_user = db.session.query(User).filter(User.userid==device).first()
|
if request_device == None:
|
||||||
print(device, flush=True)
|
return jsonify(kind="not found")
|
||||||
if request_user == None:
|
if request_device.user_id == None:
|
||||||
return jsonify(kind="unregistered")
|
return jsonify(kind="unregistered")
|
||||||
|
|
||||||
|
request_device.connection=True
|
||||||
|
db.session.commit()
|
||||||
|
request_user = db.session.query(User).filter(User.id==request_device.user_id).first()
|
||||||
|
|
||||||
routine = Routine()
|
routine = Routine()
|
||||||
|
# TODO add test if googke token exists
|
||||||
|
# if request_user.google_token != Null:
|
||||||
client_token = google.GC.build_credentials(request_user.google_token.token,
|
client_token = google.GC.build_credentials(request_user.google_token.token,
|
||||||
request_user.google_token.refresh_token)
|
request_user.google_token.refresh_token)
|
||||||
calendarjson = routine.updateCalendar(request_user, client_token)
|
calendarjson = routine.updateCalendar(request_user, client_token)
|
||||||
@ -206,10 +210,28 @@ def downloader(device):
|
|||||||
@app.route("/devicefingerprint.json")
|
@app.route("/devicefingerprint.json")
|
||||||
def generateDeviceFingerprint():
|
def generateDeviceFingerprint():
|
||||||
# Create Three Random Words
|
# Create Three Random Words
|
||||||
# check not in Device Database
|
r = RandomWords()
|
||||||
# Save as new Device
|
while True:
|
||||||
# Send to User
|
fingerprint = ""
|
||||||
return jsonify(deviceName="Carrot-Enamel-Storm")
|
length = 3
|
||||||
|
randos = r.get_random_words(limit=length, hasDictionaryDef="true",
|
||||||
|
includePartOfSpeech="noun", minDictionaryCount=1,
|
||||||
|
maxDictionaryCount=10, minLength=5, maxLength=10)
|
||||||
|
for i in range(len(randos)):
|
||||||
|
fingerprint += randos[i]
|
||||||
|
if i < length-1:
|
||||||
|
fingerprint += "-"
|
||||||
|
|
||||||
|
# check not in Device Database
|
||||||
|
if not db.session.query(Device).filter(Device.deviceName==fingerprint).first():
|
||||||
|
# Save as new Device
|
||||||
|
device = Device(deviceName=fingerprint, connection=False)
|
||||||
|
db.session.add(device)
|
||||||
|
db.session.commit()
|
||||||
|
break;
|
||||||
|
|
||||||
|
# Send to Device
|
||||||
|
return jsonify(deviceName=fingerprint)
|
||||||
|
|
||||||
# POST
|
# POST
|
||||||
|
|
||||||
@ -221,7 +243,6 @@ def user():
|
|||||||
color = request.json.get('color', None)
|
color = request.json.get('color', None)
|
||||||
toggle = request.json.get('toggle', None)
|
toggle = request.json.get('toggle', None)
|
||||||
|
|
||||||
print(request.json, flush=True)
|
|
||||||
if color != None:
|
if color != None:
|
||||||
current_user.updateCalendar(calId, color=color)
|
current_user.updateCalendar(calId, color=color)
|
||||||
if toggle != None:
|
if toggle != None:
|
||||||
|
@ -1,7 +1,31 @@
|
|||||||
|
|
||||||
body *
|
html,
|
||||||
|
body
|
||||||
{
|
{
|
||||||
font-family: "Trebuchet MS", Helvetica, sans-serif;
|
font-family: "Trebuchet MS", Helvetica, sans-serif;
|
||||||
|
margin: 0.5rem;
|
||||||
|
padding: 0;
|
||||||
|
height: calc(100% - 1rem)
|
||||||
|
}
|
||||||
|
|
||||||
|
#container {
|
||||||
|
min-height:100%;
|
||||||
|
position:relative;
|
||||||
|
}
|
||||||
|
#main {
|
||||||
|
padding-bottom: 3rem;
|
||||||
|
padding-top: 3rem;
|
||||||
|
padding: 30px 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
#footer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.banner {
|
.banner {
|
||||||
@ -15,7 +39,6 @@ body *
|
|||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
margin: none;
|
|
||||||
color: blue;
|
color: blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,13 +56,37 @@ body *
|
|||||||
width: 200px;
|
width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* bot navigation */
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
background-color: #d8d8d8;
|
||||||
|
display: flex;
|
||||||
|
justify-content:center;
|
||||||
|
align-items:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer p {
|
||||||
|
margin: 0px;
|
||||||
|
text-decoration: none;
|
||||||
|
display: flex;
|
||||||
|
float: left;
|
||||||
|
color: #424242;
|
||||||
|
padding: 1rem;
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
.footer a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #085a87;
|
||||||
|
}
|
||||||
|
|
||||||
/* top navigation */
|
/* top navigation */
|
||||||
.topnav {
|
.navigation {
|
||||||
background-color: #333;
|
background-color: orange;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topnav a {
|
.navigation a {
|
||||||
float: left;
|
float: left;
|
||||||
display: flex;
|
display: flex;
|
||||||
color: #f2f2f2;
|
color: #f2f2f2;
|
||||||
@ -49,40 +96,40 @@ body *
|
|||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topnav a:hover {
|
.navigation a:hover {
|
||||||
background-color: #ddd;
|
background-color: #ddd;
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Add an active class to highlight the current page */
|
/* Add an active class to highlight the current page */
|
||||||
.topnav a.active {
|
.navigation a.active {
|
||||||
background-color: #4CAF50;
|
background-color: #4CAF50;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide the link that should open and close the topnav on small screens */
|
/* Hide the link that should open and close the navigation on small screens */
|
||||||
.topnav .icon {
|
.navigation .icon {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* When the screen is less than 600 pixels wide, hide all links, except for the first one ("Home"). Show the link that contains should open and close the topnav (.icon) */
|
/* When the screen is less than 600 pixels wide, hide all links, except for the first one ("Home"). Show the link that contains should open and close the navigation (.icon) */
|
||||||
@media screen and (max-width: 600px) {
|
@media screen and (max-width: 600px) {
|
||||||
.topnav a:not(:first-child) {display: none;}
|
.navigation a:not(:first-child) {display: none;}
|
||||||
.topnav a.icon {
|
.navigation a.icon {
|
||||||
float: right;
|
float: right;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The "responsive" class is added to the topnav with JavaScript when the user clicks on the icon. This class makes the topnav look good on small screens (display the links vertically instead of horizontally) */
|
/* The "responsive" class is added to the navigation with JavaScript when the user clicks on the icon. This class makes the navigation look good on small screens (display the links vertically instead of horizontally) */
|
||||||
@media screen and (max-width: 600px) {
|
@media screen and (max-width: 600px) {
|
||||||
.topnav.responsive {position: relative;}
|
.navigation.responsive {position: relative;}
|
||||||
.topnav.responsive a.icon {
|
.navigation.responsive a.icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
.topnav.responsive a {
|
.navigation.responsive a {
|
||||||
float: none;
|
float: none;
|
||||||
display: block;
|
display: block;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
/* Toggle between adding and removing the "responsive" class to topnav when the user clicks on the icon */
|
/* Toggle between adding and removing the "responsive" class to navigation when the user clicks on the icon */
|
||||||
function menuBars() {
|
function menuBars() {
|
||||||
var x = document.getElementById("myTopnav");
|
var x = document.getElementById("navigation");
|
||||||
if (x.className === "topnav") {
|
if (x.className === "navigation") {
|
||||||
x.className += " responsive";
|
x.className += " responsive";
|
||||||
} else {
|
} else {
|
||||||
x.className = "topnav";
|
x.className = "navigation";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,8 +11,12 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
<div id=container>
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
|
<div id=main>
|
||||||
|
</div>
|
||||||
|
{% include "footer.html" %}
|
||||||
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -37,6 +37,9 @@
|
|||||||
<div style="width: 7rem; margin: 1rem">{{ form.deviceName.label }}</div>
|
<div style="width: 7rem; margin: 1rem">{{ form.deviceName.label }}</div>
|
||||||
<div style="width: 12rem; margin: 1rem">{{ form.deviceName(size=24) }}</div>
|
<div style="width: 12rem; margin: 1rem">{{ form.deviceName(size=24) }}</div>
|
||||||
<div style="with: 7rem; margin: 1rem">{{ form.submit() }}</div>
|
<div style="with: 7rem; margin: 1rem">{{ form.submit() }}</div>
|
||||||
|
{% for error in form.deviceName.errors %}
|
||||||
|
<span style="color: red;">[{{ error }}]</span>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
11
server/template/footer.html
Normal file
11
server/template/footer.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
<div id="footer">
|
||||||
|
<footer>
|
||||||
|
|
||||||
|
<div class = "footer">
|
||||||
|
<p>made by Raphael Maenle </p>
|
||||||
|
<p><a href="mailto:raphael@maenle.net">raphael@maenle.net</a></p>
|
||||||
|
<p><a href="/privacy">privacy policy</a></p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
52
server/template/privacy.html
Normal file
52
server/template/privacy.html
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="banner">
|
||||||
|
<h1 class="title">Privacy</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style="margin-left:10rem">Summary</h3>
|
||||||
|
<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.
|
||||||
|
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>
|
||||||
|
|
||||||
|
|
||||||
|
<h3 style="margin-left:10rem">What Information is saved?</h3>
|
||||||
|
<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 Databas is
|
||||||
|
<ul>
|
||||||
|
<li>Username and hashed password or alternatively</li>
|
||||||
|
<li>Google Username and Id with Google Login Token</li>
|
||||||
|
<li>Profile Picture</li>
|
||||||
|
<li>Email</li>
|
||||||
|
<li>Google Calendar read Token connected with this service</li>
|
||||||
|
<li>Names and Ids of calendars as well as color preferences</li>
|
||||||
|
<li>Device Fingerprints</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
All this information is erased as soon as the user deletes his account. Further, this information can be exported for the user to view if he so requests via email.
|
||||||
|
</div>
|
||||||
|
<h3 style="margin-left:10rem">How do you handle calendar information?</h3>
|
||||||
|
<div style="margin-left:10rem; margin-right:10rem;">
|
||||||
|
As previously stated, Longitude does not save calendar event information. Instead, any user or device request dynamically pulls only the neccessary information and
|
||||||
|
generates the response. The information is then immediately discarded.
|
||||||
|
</div>
|
||||||
|
<h3 style="margin-left:10rem">Are there any Cookies, and what does your javascript do?</h3>
|
||||||
|
<div style="margin-left:10rem; margin-right:10rem;">
|
||||||
|
longitudecalendar.com saves a session cookie on your device while you are on the website.
|
||||||
|
Javascript is used to send data to the server and is necessary for the color picker.
|
||||||
|
</div>
|
||||||
|
<h3 style="margin-left:10rem">Will there be Changes to these Policies?</h3>
|
||||||
|
<div style="margin-left:10rem; margin-right:10rem;">
|
||||||
|
This Privacy Policy statement may be upated at any time, if any material changes are made, the users of this service
|
||||||
|
will be notified in advance through the email provided with the creation of their user account. If a user continues to
|
||||||
|
use the service after changes in the privacy policy are in effect, he or she thereby agrees to the policy revisions.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style="margin-left:10rem">What do I do if I have further questions?</h3>
|
||||||
|
<div style="margin-left:10rem; margin-right:10rem;">
|
||||||
|
If you have any further questions about this policy, please do not hesitate to contact the developer of this service.
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -20,30 +20,29 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
<div id="container">
|
||||||
|
<!-- Side navigation -->
|
||||||
|
<div class="navigation" id="navigation">
|
||||||
|
<a href="/view">View</a>
|
||||||
|
<a href="/calendar">Calendar</a>
|
||||||
|
<a href="/account">Account</a>
|
||||||
|
<a href="/devices">Devices</a>
|
||||||
|
<a href="javascript:void(0);" class="icon" onclick="menuBars()">
|
||||||
|
<i class="fa fa-bars"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Page content -->
|
||||||
|
<div id="main">
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
// content here
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
<!-- Side navigation -->
|
</div>
|
||||||
<div class="topnav" id="myTopnav">
|
{% include "footer.html" %}
|
||||||
<a href="/view">View</a>
|
|
||||||
<a href="/calendar">Calendar</a>
|
|
||||||
<a href="/account">Account</a>
|
|
||||||
<a href="/devices">Devices</a>
|
|
||||||
<a href="javascript:void(0);" class="icon" onclick="menuBars()">
|
|
||||||
<i class="fa fa-bars"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Page content -->
|
|
||||||
<div class="main">
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
// content here
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
Loading…
Reference in New Issue
Block a user