1
0
mirror of http://git.whoc.org.uk/git/password-manager.git synced 2024-11-14 18:59:03 +01:00

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:
Giulio Cesare Solaroli 2015-06-27 18:33:01 +02:00
commit 75d5724c8a
29 changed files with 1260 additions and 168 deletions

View File

@ -0,0 +1,5 @@
{
"request.path": "../pm",
"dump.path": "/dump",
"should.pay.toll": "false"
}

View 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)

View File

@ -0,0 +1,9 @@
from clipperz import app, db
def main():
db.create_all()
app.run(debug=True)
if __name__ == "__main__":
main()

View 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

View 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})

View 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

View 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)

View 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)

View 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

View 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))

View 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))

View 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))

View File

@ -0,0 +1,4 @@
This is a database migration repository.
More information at
http://code.google.com/p/sqlalchemy-migrate/

View File

@ -0,0 +1,5 @@
#!/usr/bin/env python
from migrate.versioning.shell import main
if __name__ == '__main__':
main()

View 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

View 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
View 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

View 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'],
}
)

View File

@ -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 */

View File

@ -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")
])
])
]);
},

View File

@ -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});

View File

@ -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 {

View 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()

View File

@ -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(