Merge remote-tracking branch 'clipperz/export' into import
Conflicts: frontend/delta/css/clipperz.css frontend/delta/js/Clipperz/PM/DataModel/Record.js frontend/delta/js/Clipperz/PM/UI/Components/ExtraFeatures/DeleteAccount.js frontend/delta/js/Clipperz/PM/UI/Components/Panels/ExtraFeaturesPanel.js frontend/delta/js/Clipperz/PM/UI/MainController.js frontend/delta/properties/delta.properties.json frontend/delta/scss/style/settingsPanel.scss
This commit is contained in:
commit
75d5724c8a
5
backend/flask/properties/flask.properties.json
Normal file
5
backend/flask/properties/flask.properties.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"request.path": "../pm",
|
||||
"dump.path": "/dump",
|
||||
"should.pay.toll": "false"
|
||||
}
|
9
backend/flask/src/README.md
Normal file
9
backend/flask/src/README.md
Normal file
@ -0,0 +1,9 @@
|
||||
clipperz
|
||||
========
|
||||
A flask based backend for the Clipperz (https://clipperz.is) Password Manager. This backend is for development and personal use only. As such it does not implement any bot protection mechanisms such as tolls.
|
||||
|
||||
Running
|
||||
-------
|
||||
Once you have built the backend using the clipperz build process you can use run.sh to create an environment for testing against. The database will be created in the target directory which means it will be over-ridden every time you build. To change this you can specify a `DATABASE_URL` environment variable that points to another location.
|
||||
|
||||
Once it is running, you can open the clipperz by going to `http://localhost:5000/<frontend version>` for instance (http://localhost:5000/delta)
|
9
backend/flask/src/clipperz.py
Normal file
9
backend/flask/src/clipperz.py
Normal file
@ -0,0 +1,9 @@
|
||||
from clipperz import app, db
|
||||
|
||||
|
||||
def main():
|
||||
db.create_all()
|
||||
app.run(debug=True)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
38
backend/flask/src/clipperz/__init__.py
Normal file
38
backend/flask/src/clipperz/__init__.py
Normal file
@ -0,0 +1,38 @@
|
||||
import os
|
||||
|
||||
from flask import Flask
|
||||
from flask.ext.login import LoginManager
|
||||
from flask.ext.sqlalchemy import SQLAlchemy
|
||||
from simplekv.db.sql import SQLAlchemyStore
|
||||
from flask.ext.kvsession import KVSessionExtension
|
||||
from config import *
|
||||
|
||||
APP_ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||
app = Flask(__name__, static_url_path='')
|
||||
lm = LoginManager()
|
||||
lm.init_app(app)
|
||||
app.config.from_object(DevelopmentConfig)
|
||||
db = SQLAlchemy(app)
|
||||
store = SQLAlchemyStore(db.engine, db.metadata, 'sessions')
|
||||
kvsession = KVSessionExtension(store, app)
|
||||
|
||||
if not app.debug and os.environ.get('HEROKU') is None:
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
file_handler = RotatingFileHandler('tmp/microblog.log', 'a',
|
||||
1 * 1024 * 1024, 10)
|
||||
file_handler.setLevel(logging.INFO)
|
||||
file_handler.setFormatter(logging.Formatter(
|
||||
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'))
|
||||
app.logger.addHandler(file_handler)
|
||||
app.logger.setLevel(logging.INFO)
|
||||
app.logger.info('microblog startup')
|
||||
|
||||
if os.environ.get('HEROKU') is not None:
|
||||
import logging
|
||||
stream_handler = logging.StreamHandler()
|
||||
app.logger.addHandler(stream_handler)
|
||||
app.logger.setLevel(logging.INFO)
|
||||
app.logger.info('microblog startup')
|
||||
|
||||
from clipperz import views, models, api
|
622
backend/flask/src/clipperz/api.py
Normal file
622
backend/flask/src/clipperz/api.py
Normal file
@ -0,0 +1,622 @@
|
||||
import json
|
||||
import random
|
||||
import hashlib
|
||||
|
||||
from flask import jsonify, session, g
|
||||
from datetime import datetime
|
||||
from flask.ext.login import logout_user, current_user, login_user, \
|
||||
login_required
|
||||
from sqlalchemy.orm.exc import NoResultFound
|
||||
from clipperz import app, db
|
||||
from .exceptions import InvalidUsage
|
||||
from .models import User, Record, RecordVersion, OneTimePassword
|
||||
|
||||
|
||||
#==============================================================================
|
||||
# Helpers
|
||||
#==============================================================================
|
||||
def randomSeed():
|
||||
return hex(random.getrandbits(32*8))[2:-1]
|
||||
|
||||
|
||||
def clipperzHash(aString):
|
||||
firstRound = hashlib.sha256()
|
||||
firstRound.update(aString)
|
||||
result = hashlib.sha256()
|
||||
result.update(firstRound.digest())
|
||||
|
||||
return result.hexdigest()
|
||||
#==============================================================================
|
||||
# Method handlers
|
||||
#==============================================================================
|
||||
|
||||
|
||||
class HandlerMixin:
|
||||
def handle_request(self, request):
|
||||
parameters = json.loads(request.form['parameters'])
|
||||
app.logger.debug('raw parameters: %s', parameters)
|
||||
parameters = parameters['parameters']
|
||||
if 'message' in parameters:
|
||||
message = parameters['message']
|
||||
app.logger.debug('message: %s', message)
|
||||
app.logger.debug('parameters: %s', parameters)
|
||||
try:
|
||||
handler = getattr(self, message)
|
||||
except AttributeError:
|
||||
raise InvalidUsage(
|
||||
'This message handler is not yet implemented for this method',
|
||||
status_code=501)
|
||||
return handler(parameters, request)
|
||||
|
||||
|
||||
class registration(HandlerMixin):
|
||||
def completeRegistration(self, parameters, request):
|
||||
credentials = parameters['credentials']
|
||||
data = parameters['user']
|
||||
user = User()
|
||||
user.updateCredentials(credentials)
|
||||
user.update(data)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return jsonify(lock=user.lock,
|
||||
result='done')
|
||||
|
||||
|
||||
class handshake(HandlerMixin):
|
||||
srp_n = '115b8b692e0e045692cf280b436735c77a5a9e8a9e7ed56c965f87db5b2a2ece3'
|
||||
srp_g = 2
|
||||
srp_n = long(srp_n, 16)
|
||||
|
||||
def connect(self, parameters, request):
|
||||
result = {}
|
||||
session['C'] = parameters['parameters']['C']
|
||||
session['A'] = parameters['parameters']['A']
|
||||
app.logger.debug('username: %s', session['C'])
|
||||
|
||||
user = User().query.filter_by(username=session['C']).one()
|
||||
|
||||
if user is not None and session['A'] != 0:
|
||||
session['s'] = user.srp_s
|
||||
session['v'] = user.srp_v
|
||||
if 'otpid' in session:
|
||||
try:
|
||||
otpId = session['otpId']
|
||||
|
||||
one_time_password = OneTimePassword().filter_by(
|
||||
id=otpId
|
||||
).one()
|
||||
|
||||
if one_time_password.user.username != user.username:
|
||||
one_time_password.reset('DISABLED')
|
||||
raise Exception(("user mismatch between the current "
|
||||
"session and 'one time password' "
|
||||
"user"))
|
||||
elif one_time_password.status != 'requested':
|
||||
one_time_password.reset('DISABLED')
|
||||
raise Exception(("Trying to use an 'one time password'"
|
||||
" in the wrong state"))
|
||||
|
||||
one_time_password.reset("USED")
|
||||
|
||||
result['oneTimePassword'] = one_time_password.reference
|
||||
db.session.add(one_time_password)
|
||||
db.session.commit()
|
||||
|
||||
except Exception, detail:
|
||||
app.logger.error("connect.optid: " + str(detail))
|
||||
|
||||
else:
|
||||
# invalid user
|
||||
invalid = ('112233445566778899aabbccddeeff00112233445566778899'
|
||||
'aabbccddeeff00')
|
||||
session['s'] = invalid
|
||||
session['v'] = invalid
|
||||
|
||||
session['b'] = randomSeed()
|
||||
k = '0x64398bff522814e306a97cb9bfc4364b7eed16a8c17c5208a40a2bad2933c8e'
|
||||
k = long(k, 16)
|
||||
app.logger.debug('k: %s (%s)', k, hex(k))
|
||||
session['B'] = hex(k * long("0x%s" % session['v'], 16) +
|
||||
pow(self.srp_g,
|
||||
long("0x%s" % session['b'], 16),
|
||||
self.srp_n)
|
||||
)[2:-1]
|
||||
result['s'] = session['s']
|
||||
result['B'] = session['B']
|
||||
app.logger.debug('Session: %s', session)
|
||||
return jsonify({'result': result})
|
||||
|
||||
def credentialCheck(self, parameters, request):
|
||||
country = 'US'
|
||||
# hard-coded for development
|
||||
result = {
|
||||
'accountInfo': {
|
||||
'features': [
|
||||
'UPDATE_CREDENTIALS',
|
||||
'EDIT_CARD',
|
||||
'CARD_DETAILS',
|
||||
'ADD_CARD',
|
||||
'DELETE_CARD',
|
||||
'OFFLINE_COPY',
|
||||
'LIST_CARDS'
|
||||
],
|
||||
'paramentVerificationPending': False,
|
||||
'currentSubscriptionType': 'EARLY_ADOPTER',
|
||||
'isExpiring': False,
|
||||
'latestActiveLevel': 'EARLY_ADOPTER',
|
||||
'payments': [],
|
||||
'featureSet': 'FULL',
|
||||
'latestActiveThreshold': -1.0,
|
||||
'referenceDate': datetime.now(),
|
||||
'isExpired': False,
|
||||
'expirationDate': datetime(4001, 1, 1)
|
||||
},
|
||||
}
|
||||
|
||||
A = long("0x%s" % session['A'], 16)
|
||||
B = long("0x%s" % session['B'], 16)
|
||||
b = long("0x%s" % session['b'], 16)
|
||||
v = long("0x%s" % session['v'], 16)
|
||||
u = long("0x%s" % clipperzHash(str(A) + str(B)), 16)
|
||||
s = long("0x%s" % session['s'], 16)
|
||||
C = session['C']
|
||||
n = self.srp_n
|
||||
|
||||
S = pow((A * pow(v, u, n)), b, n)
|
||||
K = clipperzHash(str(S))
|
||||
M1 = '{0}{1}{2}{3}{4}{5}{6}'.format(
|
||||
'5976268709782868014401975621485889074340014836557888656093758064',
|
||||
'39877501869636875571920406529',
|
||||
clipperzHash(str(C)),
|
||||
str(s),
|
||||
str(A),
|
||||
str(B),
|
||||
str(K)
|
||||
)
|
||||
M1 = clipperzHash(M1)
|
||||
if M1 == parameters['parameters']['M1']:
|
||||
session['K'] = K
|
||||
M2 = clipperzHash(str(A) + M1 + K)
|
||||
|
||||
result['M2'] = M2
|
||||
result['connectionId'] = ''
|
||||
result['loginInfo'] = {}
|
||||
result['loginInfo']['current'] = {
|
||||
'date': datetime.now(),
|
||||
'ip': request.remote_addr,
|
||||
'browser': request.user_agent.browser,
|
||||
'operatingSystem': request.user_agent.platform,
|
||||
'disconnectionType': 'STILL_CONNECTED',
|
||||
'country': country
|
||||
},
|
||||
result['loginInfo']['latest'] = {}
|
||||
result['offlineCopyNeeded'] = False
|
||||
user = User().query.filter_by(username=session['C']).one()
|
||||
result['lock'] = user.lock
|
||||
login_user(user)
|
||||
session['User'] = user
|
||||
else:
|
||||
result['error'] = '?'
|
||||
|
||||
result['s'] = session['s']
|
||||
result['B'] = session['B']
|
||||
return jsonify({'result': result})
|
||||
|
||||
def oneTimePassword(self, parameters, request):
|
||||
#"parameters": {
|
||||
#"message": "oneTimePassword",
|
||||
#"version": "0.2",
|
||||
#"parameters": {
|
||||
# "oneTimePasswordKey": "03bd882...396082c",
|
||||
# "oneTimePasswordKeyChecksum": "f73f629...041031d"
|
||||
#}
|
||||
#}
|
||||
result = {}
|
||||
|
||||
try:
|
||||
key = parameters['parameters']['oneTimePasswordKey']
|
||||
checksum = parameters['parameters']['oneTimePasswordKeyChecksum']
|
||||
otp = OneTimePassword().query.filter_by(key_value=key).one()
|
||||
if otp.status == 'ACTIVE':
|
||||
if otp.key_checksum == checksum:
|
||||
session['userId'] = otp.user.id
|
||||
session['otpId'] = otp.id
|
||||
result['data'] = otp.data
|
||||
result['version'] = otp.version
|
||||
otp.data = ''
|
||||
otp.status = 'REQUESTED'
|
||||
else:
|
||||
otp.data = ''
|
||||
otp.status = 'DISABLED'
|
||||
db.session.add(otp)
|
||||
db.session.commit()
|
||||
except NoResultFound, details:
|
||||
app.logger.debug('OTP No Results Found: ', details)
|
||||
|
||||
return jsonify({'result': result})
|
||||
|
||||
|
||||
class message(HandlerMixin):
|
||||
@login_required
|
||||
def getUserDetails(self, parameters, request):
|
||||
app.logger.debug(parameters)
|
||||
if 'srpSharedSecret' not in parameters:
|
||||
raise InvalidUsage(
|
||||
'Mal-formed message format.',
|
||||
status_code=400)
|
||||
srpSharedSecret = parameters['srpSharedSecret']
|
||||
if srpSharedSecret != session['K']:
|
||||
raise InvalidUsage(
|
||||
'Your session is invalid, please re-authenticate',
|
||||
status_code=401)
|
||||
# Online results
|
||||
# {"result":
|
||||
# {
|
||||
# "header": "{\"records\":{\"index\":{\"383036...eeefbe48\":\"0\"},\"data\":\"zrhb3/791SDdb48v3vXfPzeDrv0Jhs4rAaOKHx+jDF6pwm/qi9DGSR0JwrprOgwv3bjYJgU2xHA8cuA0bPvABHSHK6fnGwvhSlyYjskY2Cy/WbRJhcA4kw+VUsOjZPRxtM8bSJnSxViAXsghTcya6+5M3MdMJHE=\"},\"directLogins\":{\"index\":{},\"data\":\"s7KYzHwKISmjYufv9h0mpTiM\"},\"preferences\":{\"data\":\"mf8fWjpOQjlV18ukEO9FN3LP\"},\"oneTimePasswords\":{\"data\":\"8tV1yRHv30lsl3FadG9YnTOo\"},\"version\":\"0.1\"}",
|
||||
# "lock": "3D4B4501-D7A9-6E4F-A487-9428C0B6E79D",
|
||||
# "version": "0.4",
|
||||
# "recordsStats": {
|
||||
# "383036...eeefbe48":{
|
||||
# "updateDate": "Sun, 12 April 2015 17:11:01 UTC",
|
||||
# "accessDate": "Sun, 12 April 2015 17:11:01 UTC"
|
||||
# }
|
||||
# },
|
||||
# "offlineCopyNeeded":true,
|
||||
# "statistics":"ByYItDeZMdZ+e/pafp14bGrR"
|
||||
# }
|
||||
# }
|
||||
# Dev results
|
||||
#{"result":
|
||||
# {"header": "{\"records\":{\"index\":{\"843a95d8...5f734b\":\"1\"},\"data\":\"fKgc5Jt9JH/CibCIpcRmwyLuLIvufWchNJga7GoFcWT9K8LR+ai0BvzWBUxcPccivE9zPv2Swe5E8wPEIc+Lv0U73NobJEct7WqBcCdLxszBE1SokxPEZDUVdWVQtAiwgOS219inCFmI5CaB\"},\"directLogins\":{\"index\":{},\"data\":\"rnMQBB81ezh6JKNGXkDCyY+q\"},\"preferences\":{\"data\":\"9jzR9Goo5PGpXbAdmsXHuQGp\"},\"oneTimePasswords\":{\"data\":\"iXEUuQGskZhMyHEwU+3tRGQM\"},\"version\":\"0.1\"}",
|
||||
# "recordStats": {
|
||||
# "843a95d8...5f734b": {
|
||||
# "updateDate": "Sun, 12 Apr 2015 13:08:44 GMT"
|
||||
# }
|
||||
# },
|
||||
# "statistics": "",
|
||||
# "version": "0.4"}}
|
||||
result = {}
|
||||
user = User().query.filter_by(username=session['C']).one()
|
||||
|
||||
records_stats = {}
|
||||
for record in user.records:
|
||||
records_stats[record.reference] = {
|
||||
'updateDate': record.update_date,
|
||||
'accessDate': record.access_date
|
||||
}
|
||||
|
||||
result['recordsStats'] = records_stats
|
||||
result['header'] = user.header
|
||||
result['statistics'] = user.statistics
|
||||
result['version'] = user.version
|
||||
result['offlineCopyNeeded'] = not user.offline_saved
|
||||
result['lock'] = user.lock
|
||||
return jsonify({'result': result})
|
||||
|
||||
@login_required
|
||||
def saveChanges(self, parameters, request):
|
||||
result = {}
|
||||
parameters = parameters['parameters']
|
||||
if ('user' not in parameters
|
||||
or 'records' not in parameters):
|
||||
app.logger.debug('saveChanges parameters: %s', parameters)
|
||||
raise InvalidUsage(
|
||||
'Mal-formed message format.',
|
||||
status_code=400)
|
||||
|
||||
user_data = parameters['user']
|
||||
record_data = parameters['records']
|
||||
|
||||
user = User().query.filter_by(username=session['C']).one()
|
||||
app.logger.debug('user_data: %s', user_data)
|
||||
user.update(user_data)
|
||||
db.session.add(user)
|
||||
|
||||
if 'updated' in record_data:
|
||||
for entry in record_data['updated']:
|
||||
reference = entry['record']['reference']
|
||||
try:
|
||||
record = Record().query.filter_by(reference=reference).one()
|
||||
except NoResultFound:
|
||||
record = Record(user=user)
|
||||
record_version = RecordVersion(record=record)
|
||||
record_version.update(entry)
|
||||
db.session.add(record)
|
||||
db.session.add(record_version)
|
||||
|
||||
if 'deleted' in record_data:
|
||||
for reference in record_data['deleted']:
|
||||
try:
|
||||
record = Record().query.filter_by(reference=reference).one()
|
||||
db.session.delete(record)
|
||||
except NoResultFound:
|
||||
pass
|
||||
|
||||
db.session.commit()
|
||||
result['lock'] = user.lock
|
||||
result['result'] = 'done'
|
||||
|
||||
return jsonify({'result': result})
|
||||
|
||||
@login_required
|
||||
def getRecordDetail(self, parameters, request):
|
||||
#{
|
||||
# "parameters": {
|
||||
# "srpSharedSecret": "bf79ad3cf0c1...63462a9fb560",
|
||||
# "message": "getRecordDetail",
|
||||
# "parameters": {
|
||||
# "reference": "e3a5856...20e080fc97f13c14c"
|
||||
# }
|
||||
# }
|
||||
#}
|
||||
app.logger.debug(parameters)
|
||||
if 'srpSharedSecret' not in parameters:
|
||||
raise InvalidUsage(
|
||||
'Mal-formed message format.',
|
||||
status_code=400)
|
||||
srpSharedSecret = parameters['srpSharedSecret']
|
||||
if (srpSharedSecret != session['K'] and session['User'] != g.user):
|
||||
raise InvalidUsage(
|
||||
'Your session is incorrect, please re-authenticate',
|
||||
status_code=401)
|
||||
|
||||
reference = parameters['parameters']['reference']
|
||||
result = {}
|
||||
|
||||
record = Record().query.filter_by(reference=reference).one()
|
||||
app.logger.debug(record.current_record_version)
|
||||
record_versions = {}
|
||||
oldest_encryption_version = None
|
||||
versions = RecordVersion().query.filter_by(record=record).all()
|
||||
for record_version in versions:
|
||||
version_entry = {}
|
||||
version_entry['reference'] = record_version.reference
|
||||
version_entry['data'] = record_version.data
|
||||
version_entry['header'] = record_version.header
|
||||
version_entry['version'] = record_version.api_version
|
||||
version_entry['creationDate'] = record_version.creation_date
|
||||
version_entry['updateDate'] = record_version.update_date
|
||||
version_entry['accessDate'] = record_version.access_date
|
||||
try:
|
||||
previous_version = RecordVersion().query.filter_by(
|
||||
id=record_version.previous_version_id).one()
|
||||
reference = previous_version.reference
|
||||
key = record_version.previous_version_key
|
||||
version_entry['previousVersion'] = reference
|
||||
version_entry['previousVersionKey'] = key
|
||||
except NoResultFound:
|
||||
pass
|
||||
if (not oldest_encryption_version
|
||||
or oldest_encryption_version > record_version.api_version):
|
||||
oldest_encryption_version = record_version.api_version
|
||||
record_versions[record_version.reference] = version_entry
|
||||
|
||||
result['reference'] = record.reference
|
||||
result['data'] = record.data
|
||||
result['version'] = record.api_version
|
||||
result['creationDate'] = str(record.creation_date)
|
||||
result['updateDate'] = str(record.update_date)
|
||||
result['accessDate'] = str(record.access_date)
|
||||
result['oldestUsedEncryptedVersion'] = oldest_encryption_version
|
||||
result['versions'] = record_versions
|
||||
result['currentVersion'] = record.current_record_version.reference
|
||||
return jsonify({'result': result})
|
||||
|
||||
@login_required
|
||||
def getOneTimePasswordsDetails(self, parameters, request):
|
||||
#{
|
||||
# "parameters": {
|
||||
# "srpSharedSecret": "bf79ad3cf0c1...63462a9fb560",
|
||||
# "message": "getOneTimePasswordsDetails",
|
||||
# "parameters": {}
|
||||
# }
|
||||
#}
|
||||
if 'srpSharedSecret' not in parameters:
|
||||
raise InvalidUsage(
|
||||
'Mal-formed message format.',
|
||||
status_code=400)
|
||||
srpSharedSecret = parameters['srpSharedSecret']
|
||||
if (srpSharedSecret != session['K'] and session['User'] != g.user):
|
||||
raise InvalidUsage(
|
||||
'Your session is incorrect, please re-authenticate',
|
||||
status_code=401)
|
||||
|
||||
result = {}
|
||||
|
||||
otps = OneTimePassword().query.filter_by(user=current_user).all()
|
||||
for otp in otps:
|
||||
#{"e8541...af0c6b951":{"status":"ACTIVE"}}
|
||||
result[otp.reference] = {'status': otp.status}
|
||||
|
||||
return jsonify({'result': result})
|
||||
|
||||
@login_required
|
||||
def getLoginHistory(self, parameters, request):
|
||||
#{
|
||||
# "parameters": {
|
||||
# "srpSharedSecret": "bf79ad3cf0c1...63462a9fb560",
|
||||
# "message": "getOneTimePasswordsDetails",
|
||||
# "parameters": {}
|
||||
# }
|
||||
#}
|
||||
if 'srpSharedSecret' not in parameters:
|
||||
raise InvalidUsage(
|
||||
'Mal-formed message format.',
|
||||
status_code=400)
|
||||
srpSharedSecret = parameters['srpSharedSecret']
|
||||
if (srpSharedSecret != session['K'] and session['User'] != g.user):
|
||||
raise InvalidUsage(
|
||||
'Your session is incorrect, please re-authenticate',
|
||||
status_code=401)
|
||||
|
||||
result = {}
|
||||
|
||||
user = User().query.filter_by(username=session['C']).one()
|
||||
|
||||
if (user != g.user):
|
||||
raise InvalidUsage(
|
||||
'Your session is incorrect, please re-authenticate',
|
||||
status_code=401)
|
||||
|
||||
return jsonify({'result': result})
|
||||
|
||||
@login_required
|
||||
def addNewOneTimePassword(self, parameters, request):
|
||||
# "parameters": {
|
||||
# "message": "addNewOneTimePassword",
|
||||
# "srpSharedSecret": "1e8e037a8...85680f931d45dfc20472cf9d1",
|
||||
# "parameters": {
|
||||
# "user": {
|
||||
# "header": <header>
|
||||
# "statistics": "WxHa6VSMmZunOjLCwAVQrkYI",
|
||||
# "version": "0.4",
|
||||
# "lock": "new lock"
|
||||
# },
|
||||
# "oneTimePassword": {
|
||||
# "reference": "ffaec6f...7b123d39b8965e7e5",
|
||||
# "key": "496dc431db...faec137698b16c",
|
||||
# "keyChecksum": "f927c1...eb970552360a311dda",
|
||||
# "data": "GcfCFsoSc5RT...MF8nstFXXHYSXF+Vyj4w=",
|
||||
# "version": "0.4"
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
#}
|
||||
if 'srpSharedSecret' not in parameters:
|
||||
raise InvalidUsage(
|
||||
'Mal-formed message format.',
|
||||
status_code=400)
|
||||
srpSharedSecret = parameters['srpSharedSecret']
|
||||
if (srpSharedSecret != session['K'] and session['User'] != g.user):
|
||||
raise InvalidUsage(
|
||||
'Your session is incorrect, please re-authenticate',
|
||||
status_code=401)
|
||||
|
||||
result = {}
|
||||
parameters = parameters['parameters']
|
||||
|
||||
user = User().query.filter_by(username=session['C']).one()
|
||||
|
||||
if (user != g.user):
|
||||
raise InvalidUsage(
|
||||
'Your session is incorrect, please re-authenticate',
|
||||
status_code=401)
|
||||
|
||||
user_data = parameters['user']
|
||||
|
||||
app.logger.debug('user_data: %s', user_data)
|
||||
user.update(user_data)
|
||||
db.session.add(user)
|
||||
|
||||
one_time_password = parameters['oneTimePassword']
|
||||
otp = OneTimePassword(
|
||||
reference=one_time_password['reference'],
|
||||
key_value=one_time_password['key'],
|
||||
key_checksum=one_time_password['keyChecksum'],
|
||||
data=one_time_password['data'],
|
||||
version=one_time_password['version'],
|
||||
user=user,
|
||||
status='ACTIVE'
|
||||
)
|
||||
db.session.add(otp)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'result': result})
|
||||
|
||||
def echo(self, parameters, request):
|
||||
result = {}
|
||||
if 'srpSharedSecret' not in parameters:
|
||||
raise InvalidUsage(
|
||||
'Mal-formed message format.',
|
||||
status_code=400)
|
||||
srpSharedSecret = parameters['srpSharedSecret']
|
||||
if (srpSharedSecret != session['K'] and session['User'] != g.user):
|
||||
raise InvalidUsage(
|
||||
'Your session is incorrect, please re-authenticate',
|
||||
status_code=401)
|
||||
|
||||
user = User().query.filter_by(username=session['C']).one()
|
||||
|
||||
if (user != g.user):
|
||||
raise InvalidUsage(
|
||||
'Your session is incorrect, please re-authenticate',
|
||||
status_code=401)
|
||||
|
||||
user.offline_saved = True
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return jsonify({'result': result})
|
||||
|
||||
@login_required
|
||||
def deleteUser(self, parameters, request):
|
||||
result = {}
|
||||
if 'srpSharedSecret' not in parameters:
|
||||
raise InvalidUsage(
|
||||
'Mal-formed message format.',
|
||||
status_code=400)
|
||||
srpSharedSecret = parameters['srpSharedSecret']
|
||||
if (srpSharedSecret != session['K'] and session['User'] != g.user):
|
||||
raise InvalidUsage(
|
||||
'Your session is incorrect, please re-authenticate',
|
||||
status_code=401)
|
||||
|
||||
user = User().query.filter_by(username=session['C']).one()
|
||||
|
||||
if (user != g.user):
|
||||
raise InvalidUsage(
|
||||
'Your session is incorrect, please re-authenticate',
|
||||
status_code=401)
|
||||
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
return jsonify({'result': result})
|
||||
|
||||
@login_required
|
||||
def upgradeUserCredentials(self, parameters, request):
|
||||
#{"parameters":{"message":"upgradeUserCredentials","srpSharedSecret":"36...d6","parameters":{"credentials":{"C":"59d02038fdb47cee5b7837a697bc8ff41cc66d8844c8fce844cdf45b0b08b1e4","s":"fe40513b99fbaca9bfe51b8d6e9b3eb42b1e01ce8b0ae32461bec0294c1030ed","v":"300b92f4a3e34034d78cd5081f8db36dbf2a4c5f7a41db6954518815a3554278","version":"0.2"},"user":{"header":"{\"records\":{\"index\":{},\"data\":\"VIIDc5vFNoIflyXF8syb8fRS\"},\"directLogins\":{\"index\":{},\"data\":\"9elg3tu2UqsJ0zbUAdQkLE69\"},\"preferences\":{\"data\":\"Sbwar35Ynd/XobuAm4K66lqj\"},\"oneTimePasswords\":{\"data\":\"tAcTsWVTwALSfxXvCChHi4FD\"},\"version\":\"0.1\"}","statistics":"","version":"0.4","lock":null}}}}
|
||||
result = {}
|
||||
if 'srpSharedSecret' not in parameters:
|
||||
raise InvalidUsage(
|
||||
'Mal-formed message format.',
|
||||
status_code=400)
|
||||
srpSharedSecret = parameters['srpSharedSecret']
|
||||
if (srpSharedSecret != session['K'] and session['User'] != g.user):
|
||||
raise InvalidUsage(
|
||||
'Your session is incorrect, please re-authenticate',
|
||||
status_code=401)
|
||||
|
||||
user = User().query.filter_by(username=session['C']).one()
|
||||
|
||||
if (user != g.user):
|
||||
raise InvalidUsage(
|
||||
'Your session is incorrect, please re-authenticate',
|
||||
status_code=401)
|
||||
|
||||
parameters = parameters['parameters']
|
||||
user.updateCredentials(parameters['credentials'])
|
||||
user.update(parameters['user'])
|
||||
|
||||
if 'oneTimePasswords' in parameters:
|
||||
for otpRef in parameters['oneTimePasswords']:
|
||||
try:
|
||||
otp = OneTimePassword().query.filter_by(
|
||||
reference=otpRef).one()
|
||||
otp.data = parameters['oneTimePasswords'][otpRef]
|
||||
db.session.add(otp)
|
||||
except NoResultFound:
|
||||
pass
|
||||
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
result['lock'] = user.lock
|
||||
result['result'] = 'done'
|
||||
return jsonify({'result': result})
|
||||
|
||||
|
||||
class logout(HandlerMixin):
|
||||
def handle_request(self, request):
|
||||
result = {}
|
||||
logout_user()
|
||||
session.clear()
|
||||
result['method'] = 'logout'
|
||||
return jsonify({'result': result})
|
25
backend/flask/src/clipperz/exceptions.py
Normal file
25
backend/flask/src/clipperz/exceptions.py
Normal file
@ -0,0 +1,25 @@
|
||||
from clipperz import app
|
||||
from flask import jsonify
|
||||
|
||||
|
||||
class InvalidUsage(Exception):
|
||||
status_code = 400
|
||||
|
||||
def __init__(self, message, status_code=None, payload=None):
|
||||
Exception.__init__(self)
|
||||
self.message = message
|
||||
if status_code is not None:
|
||||
self.status_code = status_code
|
||||
self.payload = payload
|
||||
|
||||
def to_dict(self):
|
||||
rv = dict(self.payload or ())
|
||||
rv['message'] = self.message
|
||||
return rv
|
||||
|
||||
|
||||
@app.errorhandler(InvalidUsage)
|
||||
def handle_invalid_usage(error):
|
||||
response = jsonify(error.to_dict())
|
||||
response.status_code = error.status_code
|
||||
return response
|
151
backend/flask/src/clipperz/models.py
Normal file
151
backend/flask/src/clipperz/models.py
Normal file
@ -0,0 +1,151 @@
|
||||
import datetime
|
||||
|
||||
from flask.ext.login import UserMixin
|
||||
|
||||
from clipperz import app, db
|
||||
|
||||
|
||||
class User(db.Model, UserMixin):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(), unique=True, index=True)
|
||||
srp_s = db.Column(db.String(128))
|
||||
srp_v = db.Column(db.String(128))
|
||||
header = db.Column(db.Text())
|
||||
statistics = db.Column(db.Text())
|
||||
auth_version = db.Column(db.String())
|
||||
version = db.Column(db.String())
|
||||
lock = db.Column(db.String())
|
||||
records = db.relationship(
|
||||
'Record',
|
||||
backref='user',
|
||||
lazy='dynamic',
|
||||
cascade='save-update, merge, delete, delete-orphan')
|
||||
otps = db.relationship(
|
||||
'OneTimePassword',
|
||||
backref='user',
|
||||
lazy='dynamic',
|
||||
cascade='save-update, merge, delete, delete-orphan')
|
||||
offline_saved = db.Column(db.Boolean(), default=False)
|
||||
update_date = db.Column(db.DateTime(), nullable=True)
|
||||
|
||||
def updateCredentials(self, credentials):
|
||||
self.username = credentials['C']
|
||||
self.srp_s = credentials['s']
|
||||
self.srp_v = credentials['v']
|
||||
self.auth_version = credentials['version']
|
||||
|
||||
def update(self, data):
|
||||
self.header = data['header']
|
||||
self.statistics = data['statistics']
|
||||
self.version = data['version']
|
||||
if 'lock' in data:
|
||||
self.lock = data['lock']
|
||||
self.update_date = datetime.datetime.utcnow()
|
||||
self.offline_saved = False
|
||||
|
||||
def __repr__(self):
|
||||
return '<User %r>' % (self.username)
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
|
||||
class RecordVersion(db.Model):
|
||||
id = db.Column(db.Integer(), primary_key=True)
|
||||
reference = db.Column(db.String(), unique=True, index=True)
|
||||
header = db.Column(db.Text())
|
||||
data = db.Column(db.Text())
|
||||
api_version = db.Column(db.String())
|
||||
version = db.Column(db.Integer())
|
||||
previous_version_key = db.Column(db.String())
|
||||
previous_version_id = db.Column(db.Integer(),
|
||||
db.ForeignKey('record_version.id'))
|
||||
creation_date = db.Column(db.DateTime(), default=datetime.datetime.utcnow)
|
||||
update_date = db.Column(db.DateTime(), default=datetime.datetime.utcnow)
|
||||
access_date = db.Column(db.DateTime(), default=datetime.datetime.utcnow)
|
||||
|
||||
record_id = db.Column(db.Integer(),
|
||||
db.ForeignKey('record.id'),
|
||||
nullable=False)
|
||||
|
||||
record = db.relationship('Record',
|
||||
backref=db.backref('record_versions',
|
||||
order_by=id,
|
||||
cascade='all,delete'))
|
||||
|
||||
def update(self, someData):
|
||||
app.logger.debug(someData)
|
||||
recordVersionData = someData['currentRecordVersion']
|
||||
self.reference = recordVersionData['reference']
|
||||
self.data = recordVersionData['data']
|
||||
self.api_version = recordVersionData['version']
|
||||
self.version = self.record.version
|
||||
self.previous_version_key = recordVersionData['previousVersionKey']
|
||||
self.update_date = datetime.datetime.utcnow()
|
||||
|
||||
self.record.update(someData['record'], self)
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
|
||||
class Record(db.Model):
|
||||
id = db.Column(db.Integer(), primary_key=True)
|
||||
user_id = db.Column(db.ForeignKey('user.id'))
|
||||
reference = db.Column(db.String(), unique=True, index=True)
|
||||
data = db.Column(db.Text())
|
||||
api_version = db.Column(db.String())
|
||||
version = db.Column(db.Integer(), default=0)
|
||||
creation_date = db.Column(db.DateTime(), default=datetime.datetime.utcnow)
|
||||
update_date = db.Column(db.DateTime(), default=datetime.datetime.utcnow)
|
||||
access_date = db.Column(db.DateTime(), default=datetime.datetime.utcnow)
|
||||
|
||||
current_record_version = db.relationship(
|
||||
'RecordVersion',
|
||||
uselist=False,
|
||||
cascade='save-update, merge, delete, delete-orphan')
|
||||
|
||||
def update(self, data, record_version):
|
||||
self.reference = data['reference']
|
||||
self.data = data['data']
|
||||
self.api_version = data['version']
|
||||
self.update_date = datetime.datetime.now()
|
||||
self.current_record_version = record_version
|
||||
if self.version:
|
||||
self.version += 1
|
||||
else:
|
||||
self.version = 1
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
|
||||
class OneTimePassword(db.Model):
|
||||
id = db.Column(db.Integer(), primary_key=True)
|
||||
user_id = db.Column(db.ForeignKey('user.id'))
|
||||
status = db.Column(db.String())
|
||||
reference = db.Column(db.String(), unique=True)
|
||||
key_value = db.Column(db.String())
|
||||
key_checksum = db.Column(db.String())
|
||||
data = db.Column(db.Text())
|
||||
version = db.Column(db.String())
|
||||
creation_date = db.Column(db.DateTime(), default=datetime.datetime.utcnow)
|
||||
request_date = db.Column(db.DateTime())
|
||||
usage_date = db.Column(db.DateTime())
|
||||
|
||||
def update(self, someParameters, aStatus):
|
||||
self.reference = someParameters['reference']
|
||||
self.key_value = someParameters['key']
|
||||
self.key_checksum = someParameters['keyChecksum']
|
||||
self.data = someParameters['data']
|
||||
self.version = someParameters['version']
|
||||
self.status = aStatus
|
||||
|
||||
def reset(self, aStatus):
|
||||
self.data = ""
|
||||
self.status = aStatus
|
||||
return self
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
|
||||
class Session(db.Model):
|
||||
id = db.Column(db.Integer(), primary_key=True)
|
||||
sessionId = db.Column(db.String())
|
||||
access_date = db.Column(db.DateTime(), default=datetime.datetime.utcnow)
|
128
backend/flask/src/clipperz/views.py
Normal file
128
backend/flask/src/clipperz/views.py
Normal file
@ -0,0 +1,128 @@
|
||||
from flask import session, request, g, jsonify
|
||||
from clipperz import app, db, lm
|
||||
from .models import User
|
||||
from .api import *
|
||||
from .exceptions import InvalidUsage
|
||||
from flask.ext.login import login_required
|
||||
|
||||
|
||||
@lm.user_loader
|
||||
def load_user(id):
|
||||
return User.query.get(int(id))
|
||||
|
||||
|
||||
@app.before_request
|
||||
def before_request():
|
||||
g.user = current_user
|
||||
|
||||
|
||||
@app.teardown_appcontext
|
||||
def shutdown_session(exception=None):
|
||||
db.session.remove()
|
||||
|
||||
|
||||
@app.route('/beta/dump/<string:frontend_version>')
|
||||
@app.route('/gamma/dump/<string:frontend_version>')
|
||||
@app.route('/delta/dump/<string:frontend_version>')
|
||||
@login_required
|
||||
def dump(frontend_version):
|
||||
user = User().query.filter_by(username=session['C']).one()
|
||||
|
||||
if (user != g.user):
|
||||
raise InvalidUsage(
|
||||
'Your session is incorrect, please re-authenticate',
|
||||
status_code=401)
|
||||
|
||||
user.offline_saved = True
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
user_data = {}
|
||||
user_data['users'] = {
|
||||
'catchAllUser': {
|
||||
'__masterkey_test_value__': 'masterkey',
|
||||
's': ('112233445566778899aabbccddeeff00112233445566778899'
|
||||
'aabbccddeeff00'),
|
||||
'v': ('112233445566778899aabbccddeeff00112233445566778899'
|
||||
'aabbccddeeff00'),
|
||||
}
|
||||
}
|
||||
|
||||
records = {}
|
||||
for current_record in user.records:
|
||||
versions = {}
|
||||
for version in current_record.record_versions:
|
||||
versions[version.reference] = {
|
||||
'header': version.header,
|
||||
'data': version.data,
|
||||
'version': version.api_version,
|
||||
'creationDate': str(version.creation_date),
|
||||
'updateDate': str(version.update_date),
|
||||
'accessDate': str(version.access_date)
|
||||
}
|
||||
|
||||
records[current_record.reference] = {
|
||||
'data': current_record.data,
|
||||
'version': current_record.version,
|
||||
'creationDate': str(current_record.creation_date),
|
||||
'updateDate': str(current_record.update_date),
|
||||
'accessDate': str(current_record.access_date),
|
||||
'currentVersion': current_record.current_record_version,
|
||||
'versions': versions
|
||||
}
|
||||
|
||||
user_data['users'][user.username] = {
|
||||
's': user.srp_s,
|
||||
'v': user.srp_v,
|
||||
'version': user.auth_version,
|
||||
'maxNumberOfRecords': '100',
|
||||
'userDetails': user.header,
|
||||
'statistics': user.statistics,
|
||||
'userDetailsVersion': user.version,
|
||||
'records': records
|
||||
}
|
||||
|
||||
offline_data_placeholder = (
|
||||
'_clipperz_data_ = {user_data}\n'
|
||||
'Clipperz.PM.Proxy.defaultProxy = new Clipperz.PM.Proxy.Offline();'
|
||||
'\n'
|
||||
'Clipperz.Crypto.PRNG.defaultRandomGenerator()'
|
||||
'.fastEntropyAccumulationForTestingPurpose();'
|
||||
'\n').format(user_data=user_data)
|
||||
|
||||
with open(os.path.join(APP_ROOT,
|
||||
'{0}/index.html'.format(frontend_version))) as f:
|
||||
offline_dump = f.read()
|
||||
|
||||
offline_dump = offline_dump.replace('/*offline_data_placeholder*/',
|
||||
offline_data_placeholder)
|
||||
response = make_response(offline_dump)
|
||||
content_disposition = "attachment; filename='Clipperz.html'"
|
||||
response.headers['Content-Disposition'] = content_disposition
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@app.route('/beta/<path:path>')
|
||||
def beta(path):
|
||||
return send_from_directory('beta', path)
|
||||
|
||||
|
||||
@app.route('/gamma/<path:path>')
|
||||
def gamma(path):
|
||||
return send_from_directory('gamma', path)
|
||||
|
||||
|
||||
@app.route('/delta/<path:path>')
|
||||
def delta(path):
|
||||
return send_from_directory('delta', path)
|
||||
|
||||
|
||||
@app.route('/pm', methods=['GET', 'OPTIONS', 'POST'])
|
||||
def pm():
|
||||
method = request.form['method']
|
||||
if method not in globals():
|
||||
raise InvalidUsage('This method is not yet implemented',
|
||||
status_code=501)
|
||||
handler = globals()[method]()
|
||||
app.logger.debug(method)
|
||||
return handler.handle_request(request)
|
35
backend/flask/src/config.py
Normal file
35
backend/flask/src/config.py
Normal file
@ -0,0 +1,35 @@
|
||||
import datetime
|
||||
import os
|
||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
|
||||
CSRF_ENABLED = True
|
||||
|
||||
|
||||
if os.environ.get('DATABASE_URL') is None:
|
||||
SQLALCHEMY_DATABASE_URI = ('sqlite:///' + os.path.join(basedir, 'app.db') +
|
||||
'?check_same_thread=False')
|
||||
else:
|
||||
SQLALCHEMY_DATABASE_URI = os.environ['DATABASE_URL']
|
||||
SQLALCHEMY_MIGRATE_REPO = os.path.join(basedir, 'db_repository')
|
||||
SQLALCHEMY_RECRD_QUERIES = True
|
||||
|
||||
ADMINS = ['you@example.com']
|
||||
|
||||
|
||||
class Config(object):
|
||||
DEBUG = False
|
||||
TESTING = False
|
||||
SQLALCHEMY_ECHO = False
|
||||
WTF_CSRF_ENABLED = True
|
||||
SECRET_KEY = 'you-will-never-guess'
|
||||
sessionTimeout = datetime.timedelta(minutes=-2)
|
||||
|
||||
|
||||
class DevelopmentConfig(Config):
|
||||
DEBUG = True
|
||||
SQLALCHEMY_ECHO = True
|
||||
|
||||
|
||||
class TestingConfig(Config):
|
||||
TESTING = True
|
13
backend/flask/src/db_create.py
Normal file
13
backend/flask/src/db_create.py
Normal file
@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env python
|
||||
from migrate.versioning import api
|
||||
from config import SQLALCHEMY_DATABASE_URI
|
||||
from config import SQLALCHEMY_MIGRATE_REPO
|
||||
from app import db
|
||||
import os.path
|
||||
db.create_all()
|
||||
if not os.path.exists(SQLALCHEMY_MIGRATE_REPO):
|
||||
api.create(SQLALCHEMY_MIGRATE_REPO, 'database repository')
|
||||
api.version_control(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO)
|
||||
else:
|
||||
api.version_control(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO,
|
||||
api.version(SQLALCHEMY_MIGRATE_REPO))
|
8
backend/flask/src/db_downgrade.py
Normal file
8
backend/flask/src/db_downgrade.py
Normal file
@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env python
|
||||
from migrate.versioning import api
|
||||
from config import SQLALCHEMY_DATABASE_URI
|
||||
from config import SQLALCHEMY_MIGRATE_REPO
|
||||
v = api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO)
|
||||
api.downgrade(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO, v - 1)
|
||||
v = api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO)
|
||||
print('Current database version: ' + str(v))
|
20
backend/flask/src/db_migrate.py
Normal file
20
backend/flask/src/db_migrate.py
Normal file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env python
|
||||
import imp
|
||||
from migrate.versioning import api
|
||||
from app import db
|
||||
from config import SQLALCHEMY_DATABASE_URI
|
||||
from config import SQLALCHEMY_MIGRATE_REPO
|
||||
v = api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO)
|
||||
migration = SQLALCHEMY_MIGRATE_REPO + ('/versions/%03d_migration.py' % (v+1))
|
||||
tmp_module = imp.new_module('old_model')
|
||||
old_model = api.create_model(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO)
|
||||
exec(old_model, tmp_module.__dict__)
|
||||
script = api.make_update_script_for_model(SQLALCHEMY_DATABASE_URI,
|
||||
SQLALCHEMY_MIGRATE_REPO,
|
||||
tmp_module.meta,
|
||||
db.metadata)
|
||||
open(migration, "wt").write(script)
|
||||
api.upgrade(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO)
|
||||
v = api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO)
|
||||
print('New migration saved as ' + migration)
|
||||
print('Current database version: ' + str(v))
|
4
backend/flask/src/db_repository/README
Normal file
4
backend/flask/src/db_repository/README
Normal file
@ -0,0 +1,4 @@
|
||||
This is a database migration repository.
|
||||
|
||||
More information at
|
||||
http://code.google.com/p/sqlalchemy-migrate/
|
0
backend/flask/src/db_repository/__init__.py
Normal file
0
backend/flask/src/db_repository/__init__.py
Normal file
5
backend/flask/src/db_repository/manage.py
Normal file
5
backend/flask/src/db_repository/manage.py
Normal file
@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env python
|
||||
from migrate.versioning.shell import main
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
25
backend/flask/src/db_repository/migrate.cfg
Normal file
25
backend/flask/src/db_repository/migrate.cfg
Normal file
@ -0,0 +1,25 @@
|
||||
[db_settings]
|
||||
# Used to identify which repository this database is versioned under.
|
||||
# You can use the name of your project.
|
||||
repository_id=clpperz
|
||||
|
||||
# The name of the database table used to track the schema version.
|
||||
# This name shouldn't already be used by your project.
|
||||
# If this is changed once a database is under version control, you'll need to
|
||||
# change the table name in each database too.
|
||||
version_table=migrate_version
|
||||
|
||||
# When committing a change script, Migrate will attempt to generate the
|
||||
# sql for all supported databases; normally, if one of them fails - probably
|
||||
# because you don't have that database installed - it is ignored and the
|
||||
# commit continues, perhaps ending successfully.
|
||||
# Databases in this list MUST compile successfully during a commit, or the
|
||||
# entire commit will fail. List the databases your application will actually
|
||||
# be using to ensure your updates to that database work properly.
|
||||
# This must be a list; example: ['postgres','sqlite']
|
||||
required_dbs=[]
|
||||
|
||||
# When creating new change scripts, Migrate will stamp the new script with
|
||||
# a version number. By default this is latest_version + 1. You can set this
|
||||
# to 'true' to tell Migrate to use the UTC timestamp instead.
|
||||
use_timestamp_numbering=False
|
7
backend/flask/src/db_upgrade.py
Normal file
7
backend/flask/src/db_upgrade.py
Normal file
@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
from migrate.versioning import api
|
||||
from config import SQLALCHEMY_DATABASE_URI
|
||||
from config import SQLALCHEMY_MIGRATE_REPO
|
||||
api.upgrade(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO)
|
||||
v = api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO)
|
||||
print('Current database version: ' + str(v))
|
7
backend/flask/src/run.sh
Normal file
7
backend/flask/src/run.sh
Normal file
@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env sh
|
||||
# Sets up flask development environment and activates it
|
||||
|
||||
virtualenv .
|
||||
|
||||
bin/python setup.py develop
|
||||
bin/python clipperz.py
|
72
backend/flask/src/setup.py
Normal file
72
backend/flask/src/setup.py
Normal file
@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python
|
||||
from __future__ import print_function
|
||||
from setuptools import setup
|
||||
from setuptools.command.test import test as TestCommand
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
|
||||
def read(*filenames, **kwargs):
|
||||
encoding = kwargs.get('encoding', 'utf-8')
|
||||
sep = kwargs.get('sep', '\n')
|
||||
buf = []
|
||||
try:
|
||||
for filename in filenames:
|
||||
with io.open(filename, encoding=encoding) as f:
|
||||
buf.append(f.read())
|
||||
except IOError:
|
||||
pass
|
||||
return sep.join(buf)
|
||||
|
||||
long_description = read('README.txt', 'CHANGES.txt')
|
||||
|
||||
|
||||
class PyTest(TestCommand):
|
||||
def finalize_options(self):
|
||||
TestCommand.finalize_options(self)
|
||||
self.test_args = []
|
||||
self.test_suite = True
|
||||
|
||||
def run_tests(self):
|
||||
import pytest
|
||||
errcode = pytest.main(self.test_args)
|
||||
sys.exit(errcode)
|
||||
|
||||
setup(
|
||||
name='clipperz',
|
||||
version='0.1.0',
|
||||
url='http://github.com/clipperz/password-manager/',
|
||||
license='Apache Software License',
|
||||
author='Jokajak',
|
||||
tests_require=['pytest'],
|
||||
install_requires=['Flask>=0.10.1',
|
||||
'Flask-SQLAlchemy>=1.0',
|
||||
'SQLAlchemy>=0.8.2',
|
||||
'Flask-Login',
|
||||
'Flask-KVSession',
|
||||
],
|
||||
cmdclass={'test': PyTest},
|
||||
author_email='jokajak@gmail.com',
|
||||
description='Clipperz password manager server',
|
||||
long_description=long_description,
|
||||
packages=['clipperz'],
|
||||
include_package_data=True,
|
||||
platforms='any',
|
||||
test_suite='clipperz.test.test_clipperz',
|
||||
classifiers=[
|
||||
'Programming Language :: Python',
|
||||
'Development Status :: 4 - Beta',
|
||||
'Natural Language :: English',
|
||||
'Environment :: Web Environment',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Operating System :: OS Independent',
|
||||
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
|
||||
],
|
||||
extras_require={
|
||||
'testing': ['pytest'],
|
||||
}
|
||||
)
|
@ -128,6 +128,7 @@ http://jonibologna.com/flexbox-cheatsheet/
|
||||
-ms-transform: rotate(0deg) translate(0, 0);
|
||||
-o-transform: rotate(0deg) translate(0, 0);
|
||||
transform: rotate(0deg) translate(0, 0); }
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(359deg) translate(0, 0);
|
||||
-moz-transform: rotate(359deg) translate(0, 0);
|
||||
@ -141,6 +142,7 @@ http://jonibologna.com/flexbox-cheatsheet/
|
||||
-ms-transform: rotate(0deg) translate(0, 0);
|
||||
-o-transform: rotate(0deg) translate(0, 0);
|
||||
transform: rotate(0deg) translate(0, 0); }
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(359deg) translate(0, 0);
|
||||
-moz-transform: rotate(359deg) translate(0, 0);
|
||||
@ -154,6 +156,7 @@ http://jonibologna.com/flexbox-cheatsheet/
|
||||
-ms-transform: rotate(0deg) translate(0, 0);
|
||||
-o-transform: rotate(0deg) translate(0, 0);
|
||||
transform: rotate(0deg) translate(0, 0); }
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(359deg) translate(0, 0);
|
||||
-moz-transform: rotate(359deg) translate(0, 0);
|
||||
@ -167,6 +170,7 @@ http://jonibologna.com/flexbox-cheatsheet/
|
||||
-ms-transform: rotate(0deg) translate(0, 0);
|
||||
-o-transform: rotate(0deg) translate(0, 0);
|
||||
transform: rotate(0deg) translate(0, 0); }
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(359deg) translate(0, 0);
|
||||
-moz-transform: rotate(359deg) translate(0, 0);
|
||||
@ -484,61 +488,73 @@ div.overlay {
|
||||
@-webkit-keyframes overlay-spin {
|
||||
from {
|
||||
opacity: 1; }
|
||||
|
||||
to {
|
||||
opacity: 0.25; } }
|
||||
@-moz-keyframes overlay-spin {
|
||||
from {
|
||||
opacity: 1; }
|
||||
|
||||
to {
|
||||
opacity: 0.25; } }
|
||||
@-ms-keyframes overlay-spin {
|
||||
from {
|
||||
opacity: 1; }
|
||||
|
||||
to {
|
||||
opacity: 0.25; } }
|
||||
@keyframes overlay-spin {
|
||||
from {
|
||||
opacity: 1; }
|
||||
|
||||
to {
|
||||
opacity: 0.25; } }
|
||||
@-webkit-keyframes ios-overlay-show {
|
||||
0% {
|
||||
opacity: 0; }
|
||||
|
||||
100% {
|
||||
opacity: 1; } }
|
||||
@-moz-keyframes ios-overlay-show {
|
||||
0% {
|
||||
opacity: 0; }
|
||||
|
||||
100% {
|
||||
opacity: 1; } }
|
||||
@-ms-keyframes ios-overlay-show {
|
||||
0% {
|
||||
opacity: 0; }
|
||||
|
||||
100% {
|
||||
opacity: 1; } }
|
||||
@keyframes ios-overlay-show {
|
||||
0% {
|
||||
opacity: 0; }
|
||||
|
||||
100% {
|
||||
opacity: 1; } }
|
||||
@-webkit-keyframes ios-overlay-hide {
|
||||
0% {
|
||||
opacity: 1; }
|
||||
|
||||
100% {
|
||||
opacity: 0; } }
|
||||
@-moz-keyframes ios-overlay-hide {
|
||||
0% {
|
||||
opacity: 1; }
|
||||
|
||||
100% {
|
||||
opacity: 0; } }
|
||||
@-ms-keyframes ios-overlay-hide {
|
||||
0% {
|
||||
opacity: 1; }
|
||||
|
||||
100% {
|
||||
opacity: 0; } }
|
||||
@keyframes ios-overlay-hide {
|
||||
0% {
|
||||
opacity: 1; }
|
||||
|
||||
100% {
|
||||
opacity: 0; } }
|
||||
/*
|
||||
@ -850,7 +866,7 @@ html {
|
||||
-moz-flex: auto;
|
||||
-ms-flex: auto;
|
||||
flex: auto;
|
||||
overflow: auto;
|
||||
overflow: scroll;
|
||||
-webkit-overflow-scrolling: touch; }
|
||||
#extraFeaturesPanel .extraFeatureIndex footer {
|
||||
-webkit-box-flex: none;
|
||||
@ -873,7 +889,7 @@ html {
|
||||
height: 100%; }
|
||||
#extraFeaturesPanel .extraFeatureContent .extraFeature .content {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
overflow: scroll;
|
||||
-webkit-overflow-scrolling: touch; }
|
||||
|
||||
.container {
|
||||
@ -1368,7 +1384,7 @@ div.dialogBox {
|
||||
margin: 0px; }
|
||||
|
||||
#loginPage {
|
||||
overflow: auto;
|
||||
overflow: scroll;
|
||||
-webkit-overflow-scrolling: touch; }
|
||||
#loginPage div.loginForm {
|
||||
display: -webkit-box;
|
||||
@ -1516,11 +1532,11 @@ div.dialogBox {
|
||||
flex: 1;
|
||||
font-size: 8pt; }
|
||||
#loginPage div.loginForm footer .applicationVersion span {
|
||||
color: #999; }
|
||||
color: #999999; }
|
||||
#loginPage div.loginForm footer .applicationVersion span:after {
|
||||
content: ":"; }
|
||||
#loginPage div.loginForm footer .applicationVersion a {
|
||||
color: #999;
|
||||
color: #999999;
|
||||
text-decoration: none;
|
||||
padding-left: 5px;
|
||||
font-weight: bold; }
|
||||
@ -2050,13 +2066,13 @@ span.count {
|
||||
#extraFeaturesPanel .extraFeatureIndex footer {
|
||||
font-size: 8pt;
|
||||
padding: 5px 5px 5px 5px;
|
||||
border-top: 1px solid #999; }
|
||||
border-top: 1px solid #999999; }
|
||||
#extraFeaturesPanel .extraFeatureIndex footer span {
|
||||
color: #999; }
|
||||
color: #999999; }
|
||||
#extraFeaturesPanel .extraFeatureIndex footer span:after {
|
||||
content: ":"; }
|
||||
#extraFeaturesPanel .extraFeatureIndex footer a {
|
||||
color: #999;
|
||||
color: #999999;
|
||||
text-decoration: none;
|
||||
padding-left: 5px;
|
||||
font-weight: bold; }
|
||||
@ -2175,57 +2191,12 @@ span.count {
|
||||
#extraFeaturesPanel .extraFeatureContent .extraFeature .description p em {
|
||||
text-decoration: underline; }
|
||||
#extraFeaturesPanel .extraFeatureContent .extraFeature .button {
|
||||
display: inline-block;
|
||||
display: inline;
|
||||
color: white;
|
||||
background-color: #ff9900;
|
||||
font-size: 14pt;
|
||||
border: 1px solid white;
|
||||
padding: 6px 10px; }
|
||||
#extraFeaturesPanel .extraFeatureContent .extraFeature .button.disabled {
|
||||
background-color: #c0c0c0;
|
||||
cursor: default; }
|
||||
#extraFeaturesPanel .extraFeatureContent .dataImport .stepNavbar li {
|
||||
display: inline-block;
|
||||
margin-right: 1em; }
|
||||
#extraFeaturesPanel .extraFeatureContent .dataImport .stepNavbar li.disabled {
|
||||
color: gray; }
|
||||
#extraFeaturesPanel .extraFeatureContent .dataImport .stepNavbar li.active {
|
||||
text-decoration: underline; }
|
||||
#extraFeaturesPanel .extraFeatureContent .dataImport .error {
|
||||
margin: 1em 0; }
|
||||
#extraFeaturesPanel .extraFeatureContent .dataImport textarea {
|
||||
width: 100%;
|
||||
min-height: 400px;
|
||||
display: block;
|
||||
margin: 1em 0;
|
||||
border: 0; }
|
||||
#extraFeaturesPanel .extraFeatureContent .dataImport .csvTable {
|
||||
background: white;
|
||||
margin: 1em 0; }
|
||||
#extraFeaturesPanel .extraFeatureContent .dataImport .dropArea {
|
||||
margin: 1em 0;
|
||||
width: calc(100% - 6px);
|
||||
text-align: center;
|
||||
height: inherit;
|
||||
line-height: 3em;
|
||||
border: 3px dashed white;
|
||||
background: black; }
|
||||
#extraFeaturesPanel .extraFeatureContent .dataImport .button {
|
||||
margin-right: 1em; }
|
||||
#extraFeaturesPanel .extraFeatureContent .dataImport .jsonPreview {
|
||||
width: 100%;
|
||||
height: 80%;
|
||||
overflow: auto;
|
||||
margin-top: 1em; }
|
||||
#extraFeaturesPanel .extraFeatureContent .dataImport .jsonPreview h3 {
|
||||
font-weight: bold; }
|
||||
#extraFeaturesPanel .extraFeatureContent .dataImport .jsonPreview ul {
|
||||
margin-bottom: 1em;
|
||||
padding-left: 1em; }
|
||||
#extraFeaturesPanel .extraFeatureContent .dataImport .jsonPreview ul li .label {
|
||||
font-weight: bold; }
|
||||
#extraFeaturesPanel .extraFeatureContent form input.valid + .invalidMsg, #extraFeaturesPanel .extraFeatureContent form input.empty + .invalidMsg, #extraFeaturesPanel .extraFeatureContent form input:focus + .invalidMsg, #extraFeaturesPanel .extraFeatureContent form input.invalid:focus + .invalidMsg {
|
||||
visibility: hidden; }
|
||||
|
||||
.mainPage.narrow #extraFeaturesPanel .extraFeatureContent header {
|
||||
display: block;
|
||||
@ -2307,7 +2278,7 @@ div.cardList ul {
|
||||
padding-right: 0px;
|
||||
box-shadow: -4px 0px 3px -1px rgba(0, 0, 0, 0.2); }
|
||||
div.cardList ul li.archived {
|
||||
background-color: #eee;
|
||||
background-color: #eeeeee;
|
||||
color: #999; }
|
||||
div.cardList ul li .favicon {
|
||||
width: 48px;
|
||||
@ -2335,7 +2306,7 @@ div.cardList ul {
|
||||
padding-right: 8px; }
|
||||
|
||||
div.cardList.narrow {
|
||||
overflow: auto;
|
||||
overflow: scroll;
|
||||
-webkit-overflow-scrolling: touch; }
|
||||
div.cardList.narrow.loadingCard li.selected:after {
|
||||
color: white;
|
||||
@ -2393,7 +2364,7 @@ div.cardList.narrow {
|
||||
content: ""; }
|
||||
|
||||
#cardDetailPage .view.archived, .cardDetail .view.archived {
|
||||
background-color: #eee; }
|
||||
background-color: #eeeeee; }
|
||||
#cardDetailPage .view .cardDetailToolbar, .cardDetail .view .cardDetailToolbar {
|
||||
background-color: #1863a1;
|
||||
color: white; }
|
||||
@ -2587,7 +2558,7 @@ div.cardList.narrow {
|
||||
cursor: grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: -webkit-grab;
|
||||
background: repeating-linear-gradient(0deg, white, white 2px, #ddd 2px, #ddd 3px);
|
||||
background: repeating-linear-gradient(0deg, white, white 2px, #dddddd 2px, #dddddd 3px);
|
||||
width: 28px;
|
||||
height: 20px;
|
||||
margin-left: 6px;
|
||||
@ -2822,5 +2793,3 @@ This configuration is now located in the first script included in the index_temp
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/*# sourceMappingURL=clipperz.css.map */
|
||||
|
@ -77,19 +77,19 @@ Clipperz.PM.UI.Components.ExtraFeatures.DeleteAccountClass = React.createClass({
|
||||
return React.DOM.div({className:'extraFeature deleteAccount'}, [
|
||||
React.DOM.h1({}, "Delete Account"),
|
||||
React.DOM.div({'className': 'content'}, [
|
||||
React.DOM.form({'key':'form', 'className':'deleteAccountForm', 'onChange': this.handleFormChange, 'onSubmit':this.handleDeleteAccount}, [
|
||||
React.DOM.div({'key':'fields'},[
|
||||
React.DOM.label({'key':'username-label', 'htmlFor' :'name'}, "username"),
|
||||
React.DOM.input({'key':'username', 'className': this.state['username'], 'type':'text', 'name':'name', 'ref':'username', 'placeholder':"username", 'autoCapitalize':'none'}),
|
||||
React.DOM.label({'key':'passphrase-label', 'autoFocus': 'true', 'htmlFor' :'passphrase'}, "passphrase"),
|
||||
React.DOM.input({'key':'passphrase', 'className': this.state['passphrase'], 'type':'password', 'name':'passphrase', 'ref':'passphrase', 'placeholder':"passphrase"}),
|
||||
React.DOM.p({}, [
|
||||
React.DOM.input({'key':'confirm', 'className':'confirmCheckbox', 'type':'checkbox', 'name':'confirm', 'ref':'confirm'}),
|
||||
React.DOM.span({}, "I understand that all my data will be deleted and that this action is irreversible.")
|
||||
React.DOM.form({'key':'form', 'className':'deleteAccountForm', 'onChange': this.handleFormChange, 'onSubmit':this.handleDeleteAccount}, [
|
||||
React.DOM.div({'key':'fields'},[
|
||||
React.DOM.label({'key':'username-label', 'htmlFor' :'name'}, "username"),
|
||||
React.DOM.input({'key':'username', 'className':this.state['username'], 'type':'text', 'name':'name', 'ref':'username', 'placeholder':"username", 'autoCapitalize':'none'}),
|
||||
React.DOM.label({'key':'passphrase-label', 'autoFocus': 'true', 'htmlFor' :'passphrase'}, "passphrase"),
|
||||
React.DOM.input({'key':'passphrase', 'className':this.state['passphrase'], 'type':'password', 'name':'passphrase', 'ref':'passphrase', 'placeholder':"passphrase"}),
|
||||
React.DOM.p({}, [
|
||||
React.DOM.input({'key':'confirm', 'className':'confirmCheckbox', 'type':'checkbox', 'name':'confirm', 'ref':'confirm'}),
|
||||
React.DOM.span({}, "I understand that all my data will be deleted and that this action is irreversible.")
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
React.DOM.button({'key':'button', 'type':'submit', 'disabled':!this.shouldEnableDeleteAccountButton(), 'className':'button'}, "Delete my account")
|
||||
])
|
||||
React.DOM.button({'key':'button', 'type':'submit', 'disabled':!this.shouldEnableDeleteAccountButton(), 'className':'button'}, "Delete my account")
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
@ -262,7 +262,7 @@ console.log("THE BROWSER IS OFFLINE");
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
|
||||
checkPassphrase: function( passphraseIn ) {
|
||||
checkPassphrase: function (passphraseIn) {
|
||||
var deferredResult;
|
||||
|
||||
deferredResult = new Clipperz.Async.Deferred("MainController.checkPassphrase", {trace: false});
|
||||
|
@ -89,7 +89,7 @@ refer to http://www.clipperz.com.
|
||||
|
||||
&.offlineCopy {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.open {
|
||||
@ -281,7 +281,7 @@ refer to http://www.clipperz.com.
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
display: inline;
|
||||
|
||||
color: white;
|
||||
background-color: $main-color;
|
||||
@ -293,84 +293,9 @@ refer to http://www.clipperz.com.
|
||||
|
||||
&:after {
|
||||
};
|
||||
|
||||
&.disabled {
|
||||
background-color: #c0c0c0;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dataImport {
|
||||
.stepNavbar {
|
||||
li {
|
||||
display: inline-block;
|
||||
margin-right:1em;
|
||||
|
||||
&.disabled {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
&.active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width:100%;
|
||||
min-height:400px;
|
||||
display: block;
|
||||
margin: 1em 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.csvTable {
|
||||
background: white;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.dropArea {
|
||||
margin: 1em 0;
|
||||
width: calc(100% - 6px);
|
||||
text-align: center;
|
||||
height: inherit;
|
||||
line-height: 3em;
|
||||
|
||||
border: 3px dashed white;
|
||||
background: black;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-right:1em;
|
||||
}
|
||||
|
||||
.jsonPreview {
|
||||
width: 100%;
|
||||
height:80%;
|
||||
overflow: auto;
|
||||
margin-top:1em;
|
||||
|
||||
h3 {
|
||||
font-weight:bold;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-bottom:1em;
|
||||
padding-left:1em;
|
||||
|
||||
li {
|
||||
.label {
|
||||
font-weight:bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
.changePassphraseForm {
|
||||
@ -402,15 +327,11 @@ refer to http://www.clipperz.com.
|
||||
}
|
||||
*/
|
||||
|
||||
form {
|
||||
input.valid + .invalidMsg, input.empty + .invalidMsg, input:focus + .invalidMsg, input.invalid:focus + .invalidMsg {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.mainPage.narrow {
|
||||
#extraFeaturesPanel {
|
||||
.extraFeatureContent {
|
||||
|
18
scripts/builder/backends/flaskBuilder.py
Executable file
18
scripts/builder/backends/flaskBuilder.py
Executable file
@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
import os
|
||||
import shutil
|
||||
from scriptLanguageBuilder import ScriptLanguageBuilder
|
||||
|
||||
|
||||
class FlaskBuilder(ScriptLanguageBuilder):
|
||||
|
||||
def name(self):
|
||||
return "Flask builder"
|
||||
|
||||
def relativePath(self):
|
||||
return 'flask'
|
||||
|
||||
def createPackage(self):
|
||||
super(FlaskBuilder, self).createPackage()
|
@ -1,5 +1,5 @@
|
||||
from frontendBuilder import FrontendBuilder
|
||||
from scss import Scss
|
||||
#from scss import Scss
|
||||
|
||||
import os
|
||||
import shutil
|
||||
@ -38,6 +38,7 @@ class DeltaBuilder(FrontendBuilder):
|
||||
return ""
|
||||
|
||||
def preprocessCSS (self, targetFile):
|
||||
from scss import Scss
|
||||
logging.basicConfig()
|
||||
scssVariables = {}
|
||||
scssCompiler = Scss(
|
||||
|
Loading…
Reference in New Issue
Block a user