1
0
Fork 0
mirror of https://github.com/Luzifer/cloudkeys-go.git synced 2024-11-08 14:10:05 +00:00

Initial port from PHP to Go

This commit is contained in:
Knut Ahlers 2015-07-29 09:01:23 +02:00
commit 9e7a92abe7
28 changed files with 6337 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
ca-certificates.pem
cloudkeys
gin-bin
data
cloudkeys-go

7
.gobuilder.yml Normal file
View file

@ -0,0 +1,7 @@
---
build_matrix:
general:
ldflags:
- "-X main.version $(git describe --tags)"
artifacts:
templates: templates

10
Dockerfile Normal file
View file

@ -0,0 +1,10 @@
FROM scratch
VOLUME /data
EXPOSE 3000
ENTRYPOINT ["/cloudkeys"]
CMD ["--storage=local:////data", "--password-salt=changeme", "--username-salt=changeme"]
ADD ./ca-certificates.pem /etc/ssl/ca-bundle.pem
ADD ./cloudkeys /cloudkeys
ADD ./templates /templates

27
Makefile Normal file
View file

@ -0,0 +1,27 @@
#VERSION = $(shell git describe --tags)
VERSION = dev
BOOTSTRAP_VERISON = 3.3.5
default: build
build: bundle_assets
go build .
pre-commit: bundle_assets
container: ca-certificates.pem bundle_assets
docker run -v $(CURDIR):/src -e LDFLAGS='-X main.version $(VERSION)' centurylink/golang-builder:latest
docker build .
gen_css:
lessc --verbose -O2 -x less/*.less assets/style.css
gen_js:
coffee --compile -o assets coffee/*.coffee
bundle_assets: gen_css gen_js
go-bindata assets templates
ca-certificates.pem:
curl -s https://pki.google.com/roots.pem | grep -v "^#" | grep -v "^$$" > $@
shasum $@

112
ajax.go Normal file
View file

@ -0,0 +1,112 @@
package main
import (
"crypto/sha1"
"encoding/json"
"fmt"
"net/http"
"github.com/flosch/pongo2"
"github.com/gorilla/sessions"
)
type ajaxResponse struct {
Error bool `json:"error"`
Version string `json:"version"`
Data string `json:"data"`
Type string `json:"type"`
}
func (a ajaxResponse) Bytes() []byte {
out, _ := json.Marshal(a)
return out
}
func ajaxGetHandler(res http.ResponseWriter, r *http.Request, session *sessions.Session, ctx *pongo2.Context) (*string, error) {
res.Header().Set("Content-Type", "application/json")
user, err := checkLogin(r, session)
if err != nil {
return nil, err // TODO: Handle in-app?
}
if user == nil || !storage.IsPresent(user.UserFile) {
res.Write(ajaxResponse{Error: true}.Bytes())
return nil, nil
}
userFileRaw, err := storage.Read(user.UserFile)
if err != nil {
return nil, err // TODO: Handle in-app?
}
userFile, err := readDataObject(userFileRaw)
if err != nil {
return nil, err // TODO: Handle in-app?
}
res.Write(ajaxResponse{Version: userFile.MetaData.Version, Data: userFile.Data}.Bytes())
return nil, nil
}
func ajaxPostHandler(res http.ResponseWriter, r *http.Request, session *sessions.Session, ctx *pongo2.Context) (*string, error) {
res.Header().Set("Content-Type", "application/json")
user, err := checkLogin(r, session)
if err != nil {
return nil, err // TODO: Handle in-app?
}
if user == nil {
res.Write(ajaxResponse{Error: true, Type: "login"}.Bytes())
return nil, nil
}
if !storage.IsPresent(user.UserFile) {
res.Write(ajaxResponse{Error: true, Type: "register"}.Bytes())
return nil, nil
}
userFileRaw, err := storage.Read(user.UserFile)
if err != nil {
return nil, err // TODO: Handle in-app?
}
userFile, err := readDataObject(userFileRaw)
if err != nil {
return nil, err // TODO: Handle in-app?
}
var (
version = r.FormValue("version")
checksum = r.FormValue("checksum")
data = r.FormValue("data")
)
if userFile.MetaData.Version != version {
res.Write(ajaxResponse{Error: true, Type: "version"}.Bytes())
return nil, nil
}
if checksum != fmt.Sprintf("%x", sha1.Sum([]byte(data))) {
res.Write(ajaxResponse{Error: true, Type: "checksum"}.Bytes())
return nil, nil
}
if err := storage.Backup(user.UserFile); err != nil {
return nil, err // TODO: Handle in-app?
}
userFile.MetaData.Version = checksum
userFile.Data = data
d, err := userFile.GetData()
if err != nil {
return nil, err // TODO: Handle in-app?
}
if err := storage.Write(user.UserFile, d); err != nil {
return nil, err // TODO: Handle in-app?
}
res.Write(ajaxResponse{Version: userFile.MetaData.Version, Data: userFile.Data}.Bytes())
return nil, nil
}

BIN
assets/ajax-loader.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

BIN
assets/clippy.swf Normal file

Binary file not shown.

365
assets/script.js Normal file
View file

@ -0,0 +1,365 @@
// Generated by CoffeeScript 1.9.3
(function() {
var CloudKeys;
CloudKeys = (function() {
function CloudKeys() {
this.entities = [];
this.version = "";
this.password = '';
$('#pw').focus().keyup((function(_this) {
return function(evt) {
var that = this;
if (evt.keyCode === 13) {
_this.password = $(that).val();
$('#loader').removeClass('hide');
_this.fetchData();
$('#newEntityLink').click(function() {
return _this.showForm();
});
$('#passwordRequest').addClass('hide');
$('#search').keyup(function() {
var that = this;
_this.limitItems(_this.getItems($(that).val()));
});
$('#search').focus();
return $(window).keyup(function(evt) {
if (evt.altKey === true && evt.keyCode === 66) {
if (typeof window.copyToClipboard === "function") {
copyToClipboard($('#items li.active .username').val());
} else {
$('#items li.active .username').focus().select();
}
}
if (evt.altKey === true && evt.keyCode === 79) {
if (typeof window.copyToClipboard === "function") {
copyToClipboard($('#items li.active .password').data('toggle'));
} else {
$('#items li.active .passwordtoggle em').click();
$('#items li.active .password').focus().select();
}
}
if (evt.altKey === true && evt.keyCode === 80) {
if (typeof window.copyToClipboard === "function") {
copyToClipboard($('#items li.active .password').data('toggle'));
} else {
$('#items li.active .password').focus().select();
}
}
if (evt.altKey === true && evt.keyCode === 85) {
if (typeof window.copyToClipboard === "function") {
return copyToClipboard($('#items li.active .url').val());
} else {
return $('#items li.active .url').focus().select();
}
}
});
}
};
})(this));
}
CloudKeys.prototype["import"] = function(xml) {
var e, entity, entry, group, j, l, len, len1, parsedXML, ref, ref1, tag;
parsedXML = $.parseXML(xml);
ref = $(parsedXML).find('group');
for (j = 0, len = ref.length; j < len; j++) {
group = ref[j];
tag = $(group).find('>title').text();
ref1 = $(group).find('entry');
for (l = 0, len1 = ref1.length; l < len1; l++) {
entry = ref1[l];
e = $(entry);
entity = {};
entity['title'] = e.find('title').text();
entity['username'] = e.find('username').text();
entity['password'] = e.find('password').text();
entity['url'] = e.find('url').text();
entity['comment'] = e.find('comment').text();
entity['tags'] = tag;
this.entities.push(entity);
}
}
return this.updateData((function(_this) {
return function() {
$('#import').val('');
return $('#importLink').click();
};
})(this));
};
CloudKeys.prototype.updateData = function(callback) {
var encrypted, hash;
encrypted = this.encrypt(JSON.stringify(this.entities));
hash = CryptoJS.SHA1(encrypted).toString();
return $.post('ajax', {
'version': this.version,
'checksum': hash,
'data': encrypted
}, (function(_this) {
return function(result) {
if (result.error === true) {
return alert("An error occured, please reload and try it again");
} else {
if (typeof callback !== "undefined") {
callback();
}
return _this.updateInformation(result);
}
};
})(this), "json");
};
CloudKeys.prototype.fetchData = function() {
return $.get('ajax', (function(_this) {
return function(data) {
return _this.updateInformation(data);
};
})(this), "json");
};
CloudKeys.prototype.updateInformation = function(data) {
var e;
this.version = data.version;
if (data.data === "") {
this.entities = [];
} else {
try {
this.entities = $.parseJSON(this.decrypt(data.data));
} catch (_error) {
e = _error;
window.location.reload();
}
}
this.entities.sort(this.sortItems);
this.showItems(this.getItems(''));
return this.limitItems(this.getItems($('#search').val()));
};
CloudKeys.prototype.encrypt = function(value) {
return CryptoJS.AES.encrypt(value, this.password).toString();
};
CloudKeys.prototype.decrypt = function(value) {
return CryptoJS.AES.decrypt(value, this.password).toString(CryptoJS.enc.Utf8);
};
CloudKeys.prototype.getClippyCode = function(value) {
var code;
code = '<span class="clippy"><object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" width="14" height="14" class="clippy">';
code += '<param name="movie" value="../../assets/clippy.swf"/><param name="allowScriptAccess" value="always" /><param name="quality" value="high" />';
code += "<param name=\"scale\" value=\"noscale\" /><param name=\"FlashVars\" value=\"text=" + (encodeURIComponent(value)) + "\"><param name=\"bgcolor\" value=\"#e5e3e9\">";
code += "<embed src=\"../../assets/clippy.swf\" width=\"14\" height=\"14\" name=\"clippy\" quality=\"high\" allowScriptAccess=\"always\" type=\"application/x-shockwave-flash\" pluginspage=\"http://www.macromedia.com/go/getflashplayer\" FlashVars=\"text=" + (encodeURIComponent(value)) + "\" bgcolor=\"#e5e3e9\" /></object></span>";
return code;
};
CloudKeys.prototype.limitItems = function(items) {
var current;
$('#resultdescription span').text(items.length);
current = 0;
$('#items>li').each((function(_this) {
return function(k, v) {
var item;
item = $(v);
item.removeClass('odd');
if ($.inArray(item.data('num'), items) === -1) {
item.addClass('hide');
} else {
if (item.hasClass('hide')) {
item.removeClass('hide');
}
if (current % 2 === 0) {
item.addClass('odd');
}
current = current + 1;
}
};
})(this));
};
CloudKeys.prototype.showItems = function(items) {
var additionalClass, c, char, counter, i, item, itemContainer, j, len, lines_match, password, ref, ul;
$('#items li').remove();
itemContainer = $('#items');
$('#resultdescription span').text(items.length);
for (i = j = 0, len = items.length; j < len; i = ++j) {
item = items[i];
additionalClass = "";
if (i % 2 === 0) {
additionalClass = "odd";
}
item = this.entities[item];
c = $("<li data-num=\"" + item.num + "\" class=\"" + additionalClass + "\">" + item.title + " <span>" + item.username + "</span></li>");
ul = $("<ul></ul>");
password = "";
ref = item.password;
for (char in ref) {
i = ref[char];
password += "*";
}
ul.append("<li><label>Username:</label><input type=\"text\" class=\"username\" value=\"" + item.username + "\">" + (this.getClippyCode(item.username)) + "<br></li>");
ul.append("<li class=\"passwordtoggle\"><label>Password:</label><input type=\"text\" class=\"password\" value=\"" + password + "\" data-toggle=\"" + item.password + "\"><em> (toggle visibility)</em></span>" + (this.getClippyCode(item.password)) + "<br></li>");
ul.append("<li><label>URL:</label><input type=\"text\" class=\"url\" value=\"" + item.url + "\">" + (this.getClippyCode(item.url)) + "<br></li>");
lines_match = item.comment.match(/\n/g);
if (lines_match !== null) {
counter = lines_match.length;
}
if (counter < 2) {
counter = 2;
}
ul.append("<li><label>Comment:</label><textarea class=\"comment\" rows=\"" + (counter + 2) + "\">" + item.comment + "</textarea>" + (this.getClippyCode(item.comment)) + "<br></li>");
ul.append("<li><label>Tags:</label><input type=\"text\" class=\"tags\" value=\"" + item.tags + "\">" + (this.getClippyCode(item.tags)) + "<br></li>");
ul.append("<li class=\"last\"><button class=\"btn btn-primary\">Edit</button><br></li>");
ul.find('.btn-primary').click((function(_this) {
return function() {
var t = this;
var num;
num = $(t).parent().parent().parent().data('num');
if (typeof num !== "undefined" && typeof num !== null) {
return _this.showForm(num);
}
};
})(this));
ul.find('.passwordtoggle em').click((function(_this) {
return function() {
var t = this;
var elem, original;
elem = $(t).parent().find('.password');
original = elem.data('toggle');
elem.data('toggle', elem.val());
return elem.val(original);
};
})(this));
c.append(ul);
c.click((function(_this) {
return function() {
var that = this;
var elem;
elem = $(that);
if (elem.hasClass('active') === false) {
$('#items li.active').removeClass('active').find('ul').slideUp();
elem.addClass('active');
return elem.find('ul').slideDown();
}
};
})(this));
c.find('input').focus().select();
itemContainer.append(c);
}
$('.hide').removeClass('hide');
$('#loader').addClass('hide');
$('#passwordRequest').addClass('hide');
$('#search').focus();
};
CloudKeys.prototype.getItems = function(search) {
var i, item, j, len, ref, result;
result = [];
search = search.toLowerCase();
ref = this.entities;
for (i = j = 0, len = ref.length; j < len; i = ++j) {
item = ref[i];
if (item.title.toLowerCase().indexOf(search) !== -1 || item.username.toLowerCase().indexOf(search) !== -1 || item.tags.toLowerCase().indexOf(search) !== -1) {
item.num = i;
result.push(i);
}
}
return result;
};
CloudKeys.prototype.sortItems = function(a, b) {
var aTitle, bTitle;
aTitle = a.title.toLowerCase();
bTitle = b.title.toLowerCase();
return ((aTitle < bTitle) ? -1 : ((aTitle > bTitle) ? 1 : 0));
};
CloudKeys.prototype.showForm = function(num) {
var elem, fields, j, len;
$('#editDialog input').val('');
$('#editDialog textarea').val('');
$('#editDialog .hide').removeClass('hide');
fields = ['title', 'username', 'password', 'url', 'comment', 'tags'];
if (typeof num !== "undefined" && typeof this.entities[num] !== "undefined") {
$('#editDialog input[name="num"]').val(num);
for (j = 0, len = fields.length; j < len; j++) {
elem = fields[j];
$("#editDialog #" + elem).val(this.entities[num][elem]);
}
$("#editDialog input#repeat_password").val(this.entities[num]['password']);
} else {
$('#editDialog button.btn-danger').addClass('hide');
}
$('#editDialog').modal({});
$('#editDialog .btn-danger').unbind('click').click((function(_this) {
return function() {
var confirmation;
confirmation = confirm('Are you sure?');
if (confirmation === true) {
num = $('#editDialog input[name="num"]').val();
if (typeof num !== "undefined" && typeof num !== null && num !== "") {
_this.entities.splice(num, 1);
return _this.updateData(function() {
return $('#formClose').click();
});
}
}
};
})(this));
return $('#editDialog .btn-primary').unbind('click').click((function(_this) {
return function() {
var entity, field, l, len1;
if (_this.validateForm()) {
num = $('#editDialog input[name="num"]').val();
entity = {};
for (l = 0, len1 = fields.length; l < len1; l++) {
field = fields[l];
entity[field] = $("#" + field).val();
}
if (typeof num !== "undefined" && num !== "") {
_this.entities[num] = entity;
} else {
_this.entities.push(entity);
}
_this.updateData(function() {
return $('#formClose').click();
});
}
};
})(this));
};
CloudKeys.prototype.validateForm = function() {
var success;
$('#editDialog .has-error').removeClass('has-error');
success = true;
if ($('#title').val() === "") {
$('#title').parent().addClass('has-error');
success = false;
}
if ($('#password').val() !== "" && $('#repeat_password').val() !== $('#password').val()) {
$('#password, #repeat_password').parent().addClass('has-error');
success = false;
}
return success;
};
return CloudKeys;
})();
window.CloudKeys = new CloudKeys();
$('#importLink').click((function(_this) {
return function() {
return $('#importContainer').toggle(500);
};
})(this));
$('#importContainer button').click((function(_this) {
return function() {
return window.CloudKeys["import"]($('#import').val());
};
})(this));
}).call(this);

54
assets/signin.css Normal file
View file

@ -0,0 +1,54 @@
body {
padding-top: 40px;
padding-bottom: 40px;
background-color: #eee;
}
.form-signin {
max-width: 330px;
padding: 15px;
margin: 0 auto;
}
.form-signin .form-signin-heading,
.form-signin .checkbox {
margin-bottom: 10px;
}
.form-signin .checkbox {
font-weight: normal;
}
.form-signin .form-control {
position: relative;
font-size: 16px;
height: auto;
padding: 10px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.form-signin .form-control:focus {
z-index: 2;
}
.form-signin input[type="text"] {
margin-bottom: -1px;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.container.login .form-signin input[type="password"] {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.container.register .form-signin input[name="password"] {
border-top-left-radius: 0;
border-top-right-radius: 0;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
margin-bottom: -1px;
}
.container.register .form-signin input[name="password_repeat"] {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}

1
assets/style.css Normal file
View file

@ -0,0 +1 @@
body{padding-top:70px}input{outline:0}.navbar a{cursor:pointer}.navbar a.active_account{font-weight:bold}#importContainer{display:none}#importContainer #importer{border:1px solid #111;border-radius:5px;background-image:linear-gradient(to bottom, #666 0, #171717 100%);margin:20px 0;padding:20px 20px}#importContainer #importer textarea{border:0;border-radius:5px;display:block;font-size:11px;height:350px;margin:0 auto;padding:0;width:100%}#loader img{display:block;margin:0 auto}#searchbox{border:1px solid #111;border-radius:5px;background-image:linear-gradient(to bottom, #666 0, #171717 100%);margin:20px 0;padding:10px}#searchbox input{border:0;border-radius:5px;display:block;font-size:20px;height:40px;margin:0 auto;padding:0 20px;width:100%}#content #resultdescription{margin:15px 0}#content #resultdescription span{font-weight:bold}#content ul#items{list-style:none;padding:0}#content ul#items li.active{background-color:#e5e3e9}#content ul#items li{cursor:pointer;padding:5px 20px}#content ul#items li span{font-size:70%;font-style:italic;padding:0 0 0 20px}#content ul#items li ul{display:none;list-style:none;padding-left:5px}#content ul#items li ul li.last{display:none}@media (min-width:768px){#content ul#items li ul li.last{border-bottom:0;display:block}}#content ul#items li ul li{border-bottom:1px dotted #808080}#content ul#items li ul li br{clear:both}#content ul#items li ul li label{display:block;float:left;width:100px}#content ul#items li ul li input,#content ul#items li ul li textarea{background:transparent;border:0;float:left;font-size:11px;min-height:20px;outline:0;padding-left:0;width:300px}#content ul#items li ul li input.password{width:200px}#content ul#items li ul li em{float:left;font-size:70%;width:100px}#content ul#items li ul li span.clippy{display:none}@media (min-width:992px){#content ul#items li ul li span.clippy{display:block;float:left;padding-left:0;width:20px}}#content ul#items li li.odd{background-color:transparent}#content ul#items li:hover{background-color:#e5e3e9}#content ul#items li.odd{background-color:#f6f6f6}

4475
bindata.go Normal file

File diff suppressed because it is too large Load diff

261
coffee/script.coffee Normal file
View file

@ -0,0 +1,261 @@
class CloudKeys
constructor: () ->
@entities = []
@version = ""
@password = '' #todo replace with user password
$('#pw').focus().keyup (evt) =>
`var that = this`
if evt.keyCode is 13
@password = $(that).val()
$('#loader').removeClass('hide')
@fetchData()
$('#newEntityLink').click =>
@showForm()
$('#passwordRequest').addClass('hide')
$('#search').keyup =>
`var that = this`
@limitItems(@getItems($(that).val()))
return
$('#search').focus()
$(window).keyup (evt) =>
if evt.altKey is true and evt.keyCode is 66
if typeof window.copyToClipboard is "function"
copyToClipboard($('#items li.active .username').val())
else
$('#items li.active .username').focus().select()
if evt.altKey is true and evt.keyCode is 79 # workaround to copy password very fast
if typeof window.copyToClipboard is "function"
copyToClipboard($('#items li.active .password').data('toggle'))
else
$('#items li.active .passwordtoggle em').click()
$('#items li.active .password').focus().select()
if evt.altKey is true and evt.keyCode is 80
if typeof window.copyToClipboard is "function"
copyToClipboard($('#items li.active .password').data('toggle'))
else
$('#items li.active .password').focus().select()
if evt.altKey is true and evt.keyCode is 85
if typeof window.copyToClipboard is "function"
copyToClipboard($('#items li.active .url').val())
else
$('#items li.active .url').focus().select()
import: (xml) ->
parsedXML = $.parseXML(xml)
for group in $(parsedXML).find('group')
tag = $(group).find('>title').text()
for entry in $(group).find('entry')
e = $(entry)
entity = {}
entity['title'] = e.find('title').text()
entity['username'] = e.find('username').text()
entity['password'] = e.find('password').text()
entity['url'] = e.find('url').text()
entity['comment'] = e.find('comment').text()
entity['tags'] = tag
@entities.push(entity)
@updateData =>
$('#import').val('')
$('#importLink').click()
updateData: (callback) ->
encrypted = @encrypt(JSON.stringify(@entities))
hash = CryptoJS.SHA1(encrypted).toString()
$.post 'ajax', {'version': @version, 'checksum': hash, 'data': encrypted}, (result) =>
if result.error is true
alert "An error occured, please reload and try it again"
else
if typeof callback isnt "undefined"
callback()
@updateInformation(result)
, "json"
fetchData: () ->
$.get 'ajax', (data) =>
@updateInformation(data)
, "json"
updateInformation: (data) ->
@version = data.version
if data.data == ""
@entities = []
else
try
@entities = $.parseJSON(@decrypt(data.data))
catch e
window.location.reload()
@entities.sort(@sortItems)
@showItems(@getItems(''))
@limitItems(@getItems($('#search').val()))
encrypt: (value) ->
return CryptoJS.AES.encrypt(value, @password).toString()
decrypt: (value) ->
return CryptoJS.AES.decrypt(value, @password).toString(CryptoJS.enc.Utf8)
getClippyCode: (value) ->
code = '<span class="clippy"><object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" width="14" height="14" class="clippy">'
code += '<param name="movie" value="../../assets/clippy.swf"/><param name="allowScriptAccess" value="always" /><param name="quality" value="high" />'
code += "<param name=\"scale\" value=\"noscale\" /><param name=\"FlashVars\" value=\"text=#{encodeURIComponent(value)}\"><param name=\"bgcolor\" value=\"#e5e3e9\">"
code += "<embed src=\"../../assets/clippy.swf\" width=\"14\" height=\"14\" name=\"clippy\" quality=\"high\" allowScriptAccess=\"always\" type=\"application/x-shockwave-flash\" pluginspage=\"http://www.macromedia.com/go/getflashplayer\" FlashVars=\"text=#{encodeURIComponent(value)}\" bgcolor=\"#e5e3e9\" /></object></span>"
return code
limitItems: (items) ->
$('#resultdescription span').text(items.length)
current = 0
$('#items>li').each (k, v) =>
item = $(v)
item.removeClass('odd')
if $.inArray(item.data('num'), items) is -1
item.addClass('hide')
else
if item.hasClass('hide')
item.removeClass('hide')
if current % 2 is 0
item.addClass('odd')
current = current + 1
return
return
showItems: (items) ->
$('#items li').remove()
itemContainer = $('#items')
$('#resultdescription span').text(items.length)
for item, i in items
additionalClass = ""
if i % 2 is 0
additionalClass = "odd"
item = @entities[item]
c = $("<li data-num=\"#{ item.num }\" class=\"#{ additionalClass }\">#{ item.title } <span>#{ item.username }</span></li>")
ul = $("<ul></ul>")
password = ""
for char, i of item.password
password += "*"
ul.append("<li><label>Username:</label><input type=\"text\" class=\"username\" value=\"#{ item.username }\">#{ @getClippyCode(item.username) }<br></li>")
ul.append("<li class=\"passwordtoggle\"><label>Password:</label><input type=\"text\" class=\"password\" value=\"#{ password }\" data-toggle=\"#{ item.password }\"><em> (toggle visibility)</em></span>#{ @getClippyCode(item.password) }<br></li>")
ul.append("<li><label>URL:</label><input type=\"text\" class=\"url\" value=\"#{ item.url }\">#{ @getClippyCode(item.url) }<br></li>")
lines_match = item.comment.match(/\n/g)
if lines_match isnt null
counter = lines_match.length
if counter < 2
counter = 2
ul.append("<li><label>Comment:</label><textarea class=\"comment\" rows=\"#{ counter + 2 }\">#{ item.comment }</textarea>#{ @getClippyCode(item.comment) }<br></li>")
ul.append("<li><label>Tags:</label><input type=\"text\" class=\"tags\" value=\"#{ item.tags }\">#{ @getClippyCode(item.tags) }<br></li>")
ul.append("<li class=\"last\"><button class=\"btn btn-primary\">Edit</button><br></li>")
ul.find('.btn-primary').click () =>
`var t = this`
num = $(t).parent().parent().parent().data('num')
if typeof num isnt "undefined" and typeof num isnt null
@showForm(num)
ul.find('.passwordtoggle em').click () =>
`var t = this`
elem = $(t).parent().find('.password')
original = elem.data('toggle')
elem.data('toggle', elem.val())
elem.val(original)
c.append(ul)
c.click =>
`var that = this`
elem = $(that)
if elem.hasClass('active') is false
$('#items li.active').removeClass('active').find('ul').slideUp()
elem.addClass('active')
elem.find('ul').slideDown()
c.find('input').focus().select()
itemContainer.append(c)
$('.hide').removeClass('hide')
$('#loader').addClass('hide')
$('#passwordRequest').addClass('hide')
$('#search').focus()
return
getItems: (search) ->
result = []
search = search.toLowerCase()
for item, i in @entities
if item.title.toLowerCase().indexOf(search) != -1 or item.username.toLowerCase().indexOf(search) != -1 or item.tags.toLowerCase().indexOf(search) != -1
item.num = i
result.push(i)
return result
sortItems: (a, b) ->
aTitle = a.title.toLowerCase()
bTitle = b.title.toLowerCase()
`((aTitle < bTitle) ? -1 : ((aTitle > bTitle) ? 1 : 0))`
showForm: (num) ->
$('#editDialog input').val('')
$('#editDialog textarea').val('')
$('#editDialog .hide').removeClass('hide')
fields = ['title', 'username', 'password', 'url', 'comment', 'tags']
if typeof num isnt "undefined" and typeof @entities[num] isnt "undefined"
$('#editDialog input[name="num"]').val(num)
for elem in fields
$("#editDialog ##{elem}").val(@entities[num][elem])
$("#editDialog input#repeat_password").val(@entities[num]['password'])
else
$('#editDialog button.btn-danger').addClass('hide')
$('#editDialog').modal({})
$('#editDialog .btn-danger').unbind('click').click =>
confirmation = confirm('Are you sure?')
if confirmation is true
num = $('#editDialog input[name="num"]').val()
if typeof num isnt "undefined" and typeof num isnt null and num != ""
@entities.splice(num, 1)
@updateData =>
$('#formClose').click()
$('#editDialog .btn-primary').unbind('click').click =>
if @validateForm()
num = $('#editDialog input[name="num"]').val()
entity = {}
for field in fields
entity[field] = $("##{field}").val()
if typeof num != "undefined" and num != ""
@entities[num] = entity
else
@entities.push(entity)
@updateData =>
$('#formClose').click()
return
validateForm: () ->
$('#editDialog .has-error').removeClass('has-error')
success = true
if $('#title').val() == ""
$('#title').parent().addClass('has-error')
success = false
if $('#password').val() isnt "" && $('#repeat_password').val() isnt $('#password').val()
$('#password, #repeat_password').parent().addClass('has-error')
success = false
return success
window.CloudKeys = new CloudKeys()
$('#importLink').click =>
$('#importContainer').toggle(500)
$('#importContainer button').click =>
window.CloudKeys.import($('#import').val())
#$('#import').val('')

28
config.go Normal file
View file

@ -0,0 +1,28 @@
package main
import (
"net/url"
"github.com/Luzifer/rconfig"
)
type config struct {
// General Config
PasswordSalt string `env:"passwordSalt" flag:"password-salt" description:"A random unique salt for encrypting the passwords"`
UsernameSalt string `env:"usernameSalt" flag:"username-salt" description:"A random unique salt for encrypting the usernames"`
Storage string `env:"storage" flag:"storage" default:"local:///./data" description:"Configuration for storage adapter (see README.md)"`
Listen string `flag:"listen" default:":3000" description:"IP and port to listen on"`
CookieSigningKey string `flag:"cookie-authkey" description:"Key used to authenticate the session"`
CookieEncryptKey string `flag:"cookie-encryptkey" description:"Key used to encrypt the session"`
}
func (c config) ParsedStorage() (*url.URL, error) {
return url.Parse(c.Storage)
}
func loadConfig() *config {
cfg := &config{}
rconfig.Parse(cfg)
return cfg
}

36
dataObject.go Normal file
View file

@ -0,0 +1,36 @@
package main
import (
"bytes"
"encoding/gob"
"encoding/json"
"io"
)
type authorizedAccounts []authorizedAccount
type authorizedAccount struct {
Name string
UserFile string
}
func init() {
gob.Register(authorizedAccounts{})
}
type dataObject struct {
MetaData struct {
Version string `json:"version"`
Password string `json:"password"`
} `json:"metadata"`
Data string `json:"data"`
}
func readDataObject(in io.Reader) (*dataObject, error) {
t := &dataObject{}
return t, json.NewDecoder(in).Decode(t)
}
func (d *dataObject) GetData() (io.Reader, error) {
buf := bytes.NewBuffer([]byte{})
return buf, json.NewEncoder(buf).Encode(d)
}

66
httpHelper.go Normal file
View file

@ -0,0 +1,66 @@
package main
import (
"fmt"
"net/http"
"github.com/flosch/pongo2"
"github.com/gorilla/sessions"
)
type httpHelperFunc func(res http.ResponseWriter, r *http.Request, session *sessions.Session, ctx *pongo2.Context) (*string, error)
func httpHelper(f httpHelperFunc) http.HandlerFunc {
return func(res http.ResponseWriter, r *http.Request) {
sess, _ := cookieStore.Get(r, "cloudkeys-go")
ctx := pongo2.Context{}
if errFlash := sess.Flashes("error"); len(errFlash) > 0 {
ctx["error"] = errFlash[0].(string)
}
template, err := f(res, r, sess, &ctx)
if err != nil {
http.Error(res, err.Error(), http.StatusInternalServerError)
fmt.Printf("ERR: %s\n", err)
return
}
if template != nil {
// Postponed until https://github.com/flosch/pongo2/issues/68
//
// tplsrc, err := Asset("templates/" + *template)
// if err != nil {
// fmt.Printf("ERR: Could not find template '%s'\n", *template)
// http.Error(res, "An error ocurred.", http.StatusInternalServerError)
// return
// }
ts := pongo2.NewSet("frontend")
ts.SetBaseDirectory("templates")
tpl, err := ts.FromFile(*template)
if err != nil {
fmt.Printf("ERR: Could not parse template '%s': %s\n", *template, err)
http.Error(res, "An error ocurred.", http.StatusInternalServerError)
return
}
out, err := tpl.Execute(ctx)
if err != nil {
fmt.Printf("ERR: Unable to execute template '%s': %s\n", *template, err)
http.Error(res, "An error ocurred.", http.StatusInternalServerError)
return
}
res.Write([]byte(out))
}
}
}
func simpleTemplateOutput(template string) httpHelperFunc {
return func(res http.ResponseWriter, r *http.Request, session *sessions.Session, ctx *pongo2.Context) (*string, error) {
return &template, nil
}
}
func String(s string) *string {
return &s
}

159
less/style.less Normal file
View file

@ -0,0 +1,159 @@
body {
padding-top: 70px;
}
input {
outline: 0;
}
.navbar a {
cursor: pointer;
&.active_account {
font-weight: bold;
}
}
#importContainer {
display: none;
#importer {
border: 1px solid #111111;
border-radius: 5px;
background-image: linear-gradient(to bottom, #666666 0%, #171717 100%);
margin: 20px 0;
padding: 20px 20px;
textarea {
border: 0;
border-radius: 5px;
display: block;
font-size: 11px;
height: 350px;
margin: 0px auto;
padding: 0;
width: 100%;
}
}
}
#loader {
img {
display: block;
margin: 0px auto;
}
}
#searchbox {
border: 1px solid #111111;
border-radius: 5px;
background-image: linear-gradient(to bottom, #666666 0%, #171717 100%);
margin: 20px 0;
padding: 10px;
input {
border: 0;
border-radius: 5px;
display: block;
font-size: 20px;
height: 40px;
margin: 0px auto;
padding: 0 20px;
width: 100%;
}
}
#content {
#resultdescription {
margin: 15px 0;
span {
font-weight: bold;
}
}
ul#items {
list-style: none;
padding: 0px;
li.active {
background-color: #e5e3e9;
}
li {
cursor: pointer;
padding: 5px 20px;
span {
font-size: 70%;
font-style: italic;
padding: 0 0 0 20px;
}
ul {
display: none;
list-style: none;
padding-left: 5px;
li.last {
display: none;
@media (min-width:768px) {
border-bottom: 0;
display: block;
}
}
li {
border-bottom: 1px dotted gray;
br { clear: both; }
label {
display: block;
float: left;
width: 100px;
}
input, textarea {
background: transparent;
border: 0;
float: left;
font-size: 11px;
min-height: 20px;
outline: 0;
padding-left: 0;
width: 300px;
}
input.password {
width: 200px;
}
em {
float: left;
font-size: 70%;
width: 100px;
}
span.clippy {
display: none;
}
@media (min-width:992px) {
span.clippy {
display: block;
float: left;
padding-left: 0;
width: 20px;
}
}
}
}
li.odd {
background-color: transparent;
}
}
li:hover {
background-color: #e5e3e9;
}
li.odd {
background-color: #f6f6f6;
}
}
}

90
login.go Normal file
View file

@ -0,0 +1,90 @@
package main
import (
"crypto/sha1"
"fmt"
"net/http"
"strconv"
"github.com/flosch/pongo2"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
)
func loginHandler(res http.ResponseWriter, r *http.Request, session *sessions.Session, ctx *pongo2.Context) (*string, error) {
var (
username = r.FormValue("username")
password = fmt.Sprintf("%x", sha1.Sum([]byte(cfg.PasswordSalt+r.FormValue("password"))))
)
if !storage.IsPresent(createUserFilename(username)) {
(*ctx)["error"] = true
return String("login.html"), nil
}
userFileRaw, err := storage.Read(createUserFilename(username))
if err != nil {
return nil, err // TODO: Handle in-app?
}
userFile, err := readDataObject(userFileRaw)
if err != nil {
return nil, err // TODO: Handle in-app?
}
if userFile.MetaData.Password != password {
(*ctx)["error"] = true
return String("login.html"), nil
}
auth, ok := session.Values["authorizedAccounts"].(authorizedAccounts)
if !ok {
auth = authorizedAccounts{}
}
for i, v := range auth {
if v.Name == username {
http.Redirect(res, r, fmt.Sprintf("u/%d/overview", i), http.StatusFound)
return nil, nil
}
}
auth = append(auth, authorizedAccount{
Name: username,
UserFile: createUserFilename(username),
})
session.Values["authorizedAccounts"] = auth
if err := session.Save(r, res); err != nil {
return nil, err
}
http.Redirect(res, r, fmt.Sprintf("u/%d/overview", len(auth)-1), http.StatusFound)
return nil, nil
}
func logoutHandler(res http.ResponseWriter, r *http.Request, session *sessions.Session, ctx *pongo2.Context) (*string, error) {
session.Values["authorizedAccounts"] = authorizedAccounts{}
session.Save(r, res)
http.Redirect(res, r, "overview", http.StatusFound)
return nil, nil
}
func checkLogin(r *http.Request, session *sessions.Session) (*authorizedAccount, error) {
vars := mux.Vars(r)
idx, err := strconv.ParseInt(vars["userIndex"], 10, 64)
if err != nil {
return nil, err // TODO: Handle in-app?
}
auth, ok := session.Values["authorizedAccounts"].(authorizedAccounts)
if !ok {
auth = authorizedAccounts{}
}
if len(auth)-1 < int(idx) {
return nil, nil
}
return &auth[idx], nil
}

102
main.go Normal file
View file

@ -0,0 +1,102 @@
package main // import "github.com/Luzifer/cloudkeys-go"
import (
"crypto/sha1"
"fmt"
"mime"
"net/http"
"os"
"path/filepath"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
"github.com/satori/go.uuid"
)
var (
storage storageAdapter
cookieStore *sessions.CookieStore
cfg = loadConfig()
version = "dev"
)
func init() {
if _, err := cfg.ParsedStorage(); err != nil {
fmt.Printf("ERR: Please provide a valid storage URI\n")
os.Exit(1)
}
if cfg.CookieSigningKey == "" {
cfg.CookieSigningKey = uuid.NewV4().String()[:32]
fmt.Printf("WRN: cookie-authkey was set randomly, this will break your sessions!\n")
}
if cfg.CookieEncryptKey == "" {
cfg.CookieEncryptKey = uuid.NewV4().String()[:32]
fmt.Printf("WRN: cookie-encryptkey was set randomly, this will break your sessions!\n")
}
cookieStore = sessions.NewCookieStore(
[]byte(cfg.CookieSigningKey),
[]byte(cfg.CookieEncryptKey),
)
}
func main() {
s, err := getStorageAdapter(cfg)
if err != nil {
fmt.Printf("ERR: Could not instanciate storage: %s\n", err)
os.Exit(1)
}
storage = s
r := mux.NewRouter()
r.PathPrefix("/assets/").HandlerFunc(serveAssets)
r.HandleFunc("/register", httpHelper(simpleTemplateOutput("register.html"))).
Methods("GET")
r.HandleFunc("/register", httpHelper(registerHandler)).
Methods("POST")
r.HandleFunc("/login", httpHelper(simpleTemplateOutput("login.html"))).
Methods("GET")
r.HandleFunc("/login", httpHelper(loginHandler)).
Methods("POST")
r.HandleFunc("/logout", httpHelper(logoutHandler)).
Methods("GET")
r.HandleFunc("/u/{userIndex:[0-9]+}/overview", httpHelper(overviewHandler)).
Methods("GET")
r.HandleFunc("/u/{userIndex:[0-9]+}/ajax", httpHelper(ajaxGetHandler)).
Methods("GET")
r.HandleFunc("/u/{userIndex:[0-9]+}/ajax", httpHelper(ajaxPostHandler)).
Methods("POST")
/* --- SUPPORT FOR DEPRECATED METHODS --- */
r.HandleFunc("/", func(res http.ResponseWriter, r *http.Request) {
http.Redirect(res, r, "u/0/overview", http.StatusFound)
}).Methods("GET")
r.HandleFunc("/overview", func(res http.ResponseWriter, r *http.Request) {
http.Redirect(res, r, "u/0/overview", http.StatusFound)
}).Methods("GET")
http.ListenAndServe(cfg.Listen, r)
}
func serveAssets(res http.ResponseWriter, r *http.Request) {
data, err := Asset(r.RequestURI[1:])
if err != nil {
http.Error(res, "Not found", http.StatusNotFound)
return
}
res.Header().Set("Content-Type", mime.TypeByExtension(filepath.Ext(r.RequestURI)))
res.Write(data)
}
func createUserFilename(username string) string {
return fmt.Sprintf("%x", sha1.Sum([]byte(cfg.UsernameSalt+username)))
}

34
overview.go Normal file
View file

@ -0,0 +1,34 @@
package main
import (
"net/http"
"github.com/flosch/pongo2"
"github.com/gorilla/sessions"
)
func overviewHandler(res http.ResponseWriter, r *http.Request, session *sessions.Session, ctx *pongo2.Context) (*string, error) {
user, err := checkLogin(r, session)
if err != nil {
return nil, err // TODO: Handle in-app?
}
if user == nil || !storage.IsPresent(user.UserFile) {
http.Redirect(res, r, "../../login", http.StatusFound)
return nil, nil
}
frontendAccounts := []string{}
idx := -1
for i, v := range session.Values["authorizedAccounts"].(authorizedAccounts) {
frontendAccounts = append(frontendAccounts, v.Name)
if v.Name == user.Name {
idx = i
}
}
(*ctx)["authorized_accounts"] = frontendAccounts
(*ctx)["current_user_index"] = idx
return String("overview.html"), nil
}

42
register.go Normal file
View file

@ -0,0 +1,42 @@
package main
import (
"crypto/sha1"
"fmt"
"net/http"
"github.com/flosch/pongo2"
"github.com/gorilla/sessions"
)
func registerHandler(res http.ResponseWriter, r *http.Request, session *sessions.Session, ctx *pongo2.Context) (*string, error) {
var (
username = r.FormValue("username")
password = r.FormValue("password")
passwordCheck = r.FormValue("password_repeat")
hashedPassword = fmt.Sprintf("%x", sha1.Sum([]byte(cfg.PasswordSalt+password)))
)
if username == "" || password == "" || password != passwordCheck {
return String("register.html"), nil
}
if storage.IsPresent(createUserFilename(username)) {
(*ctx)["exists"] = true
return String("register.html"), nil
}
d := dataObject{}
d.MetaData.Password = hashedPassword
data, err := d.GetData()
if err != nil {
return nil, err // TODO: Handle in-app?
}
if err := storage.Write(createUserFilename(username), data); err == nil {
(*ctx)["created"] = true
return String("register.html"), nil
}
return nil, err // TODO: Handle in-app?
}

42
storage.go Normal file
View file

@ -0,0 +1,42 @@
package main
import (
"fmt"
"io"
"net/url"
)
var (
storageAdapters = map[string]storageAdapterInitializer{}
)
type storageAdapter interface {
Write(identifier string, data io.Reader) error
Read(identifier string) (io.Reader, error)
IsPresent(identifier string) bool
Backup(identifier string) error
}
type storageAdapterInitializer func(*url.URL) (storageAdapter, error)
func getStorageAdapter(cfg *config) (storageAdapter, error) {
storageURI, _ := cfg.ParsedStorage()
if sa, ok := storageAdapters[storageURI.Scheme]; ok {
s, err := sa(storageURI)
if err != nil {
return nil, err
}
return s, nil
}
return nil, fmt.Errorf("Did not find storage adapter for '%s'", storageURI.Scheme)
}
func registerStorage(scheme string, f storageAdapterInitializer) error {
if _, ok := storageAdapters[scheme]; ok {
return fmt.Errorf("Cannot register '%s', is already registered", scheme)
}
storageAdapters[scheme] = f
return nil
}

79
storageLocal.go Normal file
View file

@ -0,0 +1,79 @@
package main
import (
"fmt"
"io"
"net/url"
"os"
"path"
"strconv"
"time"
)
func init() {
registerStorage("local", NewLocalStorage)
}
// LocalStorage implements a storage option for local file storage
type LocalStorage struct {
path string
}
// NewLocalStorage checks config, creates the path and initializes a LocalStorage
func NewLocalStorage(u *url.URL) (storageAdapter, error) {
p := u.Path[1:]
if len(p) == 0 {
return nil, fmt.Errorf("Path not present.")
}
if err := os.MkdirAll(path.Join(p, "backup"), 0755); err != nil {
return nil, fmt.Errorf("Unable to create path '%s'", p)
}
return &LocalStorage{
path: p,
}, nil
}
// Write store the data of a dataObject into the storage
func (l *LocalStorage) Write(identifier string, data io.Reader) error {
f, err := os.Create(path.Join(l.path, identifier))
if err != nil {
return err
}
_, err = io.Copy(f, data)
return err
}
// Read reads the data of a dataObject from the storage
func (l *LocalStorage) Read(identifier string) (io.Reader, error) {
return os.Open(path.Join(l.path, identifier))
}
// IsPresent checks for the presence of an userfile identifier
func (l *LocalStorage) IsPresent(identifier string) bool {
_, err := os.Stat(path.Join(l.path, identifier))
return err == nil
}
// Backup creates a backup of the old data
func (l *LocalStorage) Backup(identifier string) error {
ts := strconv.FormatInt(time.Now().Unix(), 10)
o, err := os.Open(path.Join(l.path, identifier))
if err != nil {
return err
}
n, err := os.Create(path.Join(l.path, "backup", fmt.Sprintf("%s.%s", identifier, ts)))
if err != nil {
return err
}
defer o.Close()
defer n.Close()
_, err = io.Copy(n, o)
return err
}

87
storageS3.go Normal file
View file

@ -0,0 +1,87 @@
package main
import (
"bytes"
"fmt"
"io"
"net/url"
"path"
"strconv"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/s3"
)
func init() {
registerStorage("s3", NewS3Storage)
}
// S3Storage implements a storage option for Amazon S3
type S3Storage struct {
bucket string
path string
conn *s3.S3
}
// NewS3Storage checks config, creates the path and initializes a S3Storage
func NewS3Storage(u *url.URL) (storageAdapter, error) {
return &S3Storage{
bucket: u.Host,
path: u.Path,
conn: s3.New(&aws.Config{}),
}, nil
}
// Write store the data of a dataObject into the storage
func (s *S3Storage) Write(identifier string, data io.Reader) error {
buf := bytes.NewBuffer([]byte{})
io.Copy(buf, data)
_, err := s.conn.PutObject(&s3.PutObjectInput{
Bucket: aws.String(s.bucket),
Body: bytes.NewReader(buf.Bytes()),
Key: aws.String(path.Join(s.path, identifier)),
})
return err
}
// Read reads the data of a dataObject from the storage
func (s *S3Storage) Read(identifier string) (io.Reader, error) {
out, err := s.conn.GetObject(&s3.GetObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(path.Join(s.path, identifier)),
})
if err != nil {
return nil, err
}
return out.Body, nil
}
// IsPresent checks for the presence of an userfile identifier
func (s *S3Storage) IsPresent(identifier string) bool {
out, err := s.conn.HeadObject(&s3.HeadObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(path.Join(s.path, identifier)),
})
if err != nil {
return false
}
return *out.ContentLength > 0
}
// Backup creates a backup of the old data
func (s *S3Storage) Backup(identifier string) error {
ts := strconv.FormatInt(time.Now().Unix(), 10)
_, err := s.conn.CopyObject(&s3.CopyObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(path.Join(s.path, "backup", fmt.Sprintf("%s.%s", identifier, ts))),
CopySource: aws.String(path.Join(s.bucket, s.path, identifier)),
})
return err
}

28
templates/404.html Normal file

File diff suppressed because one or more lines are too long

21
templates/login.html Normal file
View file

@ -0,0 +1,21 @@
{% extends "outer.html" %}
{% block title %}Login{% endblock %}
{% block pagetype %}login{% endblock %}
{% block content %}
{% if error %}
<div class="row">
<div class="col-md-4 col-md-push-4 alert alert-danger fade in">
<h4>Login Failed</h4>
<p>For security reasons we don't have more information for you. Sorry.</p>
</div>
</div>
{% endif %}
<div class="row">
<form class="form-signin" method="post">
<h2 class="form-signin-heading">Please sign in</h2>
<input type="text" name="username" class="form-control" placeholder="Username">
<input type="password" name="password" class="form-control" placeholder="Password">
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
</form>
</div>
{% endblock %}

32
templates/outer.html Normal file
View file

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>cloudkeys - {% block title%}{% endblock %}</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet" media="screen">
<link href="assets/signin.css" rel="stylesheet">
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="overview">cloud<strong>keys</strong></a>
</div>
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav hide">
<li class="active"><a href="#">Home</a></li>
</ul>
<ul class="nav navbar-nav pull-right">
<li><a href="register">Register</a></li>
</ul>
</div>
</div>
</div>
<div class="container {% block pagetype %}{% endblock %}">{% block content %}{% endblock %}</div>
<script src="//code.jquery.com/jquery-2.1.4.min.js"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
</body>
</html>

141
templates/overview.html Normal file
View file

@ -0,0 +1,141 @@
<!DOCTYPE html>
<html>
<head>
<title>cloudkeys</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1">
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet" media="screen">
<link href="../../assets/style.css" rel="stylesheet" media="screen">
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbarcollapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="../../u/{{ current_user_index }}/overview">cloud<strong>keys</strong></a>
</div>
<div class="collapse navbar-collapse" id="navbarcollapse">
<ul class="nav navbar-nav hide">
<li class="active"><a href="../../u/{{ current_user_index }}/overview">Home</a></li>
<li><a id="newEntityLink">New</a></li>
<li><a id="importLink">Import</a></li>
</ul>
<ul class="nav navbar-nav pull-right">
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Switch Account <b class="caret"></b></a>
<ul class="dropdown-menu">
{% for user in authorized_accounts %}
{% if forloop.Counter0 == current_user_index %}
<li><a href="../../u/{{ forloop.Counter0 }}/overview" class="active_account">{{ user }}</a></li>
{% else %}
<li><a href="../../u/{{ forloop.Counter0 }}/overview">{{ user }}</a></li>
{% endif %}
{% endfor %}
<li class="divider"></li>
<li><a href="../../login">Add Account</a></li>
</ul>
</li>
<li><a href="../../logout">Logout</a></li>
</ul>
</div>
</div>
</div>
<div id="loader" class="container hide">
<div class="row">
<img src="../../assets/ajax-loader.gif" />
</div>
</div>
<div id="passwordRequest" class="container">
<div class="row">
<div id="searchbox" class="col-md-8 col-md-push-2">
<input type="password" id="pw" placeholder="Password">
</div>
</div>
<div class="row">
<div class="col-md-8 col-md-push-2">
<small><strong>Hint:</strong> I you are here for the first time, this password will be set as your encryption password for your storage. We don't send it to our server, but only your encrypted storage. We're never able to read your password storage.<br>After that you can import and create passwords.</small>
</div>
</div>
</div>
<div class="container hide">
<div class="row" id="importContainer">
<div id="importer" class="col-md-8 col-md-push-2">
<textarea id="import"></textarea>
</div>
<div class="col-md-8 col-md-push-2">
<button class="btn btn-primary">Import</button>
</div>
</div>
<div class="row">
<div id="searchbox" class="col-md-8 col-md-push-2">
<input type="text" id="search" placeholder="Search">
</div>
</div>
<div class="row">
<div id="content" class="col-md-12" role="main">
<div id="resultdescription" class="col-md-12">Search results: <span>0</span> items</div>
<ul id="items">
</ul>
</div>
</div>
</div>
<div id="editDialog" class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Edit</h4>
</div>
<div class="modal-body">
<form role="form">
<input type="hidden" name="num" value="">
<div class="form-group">
<label class="control-label" for="title">Title</label>
<input type="text" class="form-control" id="title" placeholder="Enter Title">
</div>
<div class="form-group">
<label class="control-label" for="username">Username</label>
<input type="text" class="form-control" id="username" placeholder="Enter Username">
</div>
<div class="form-group">
<label class="control-label" for="password">Password</label>
<input type="password" class="form-control" id="password" placeholder="Enter Password">
</div>
<div class="form-group">
<label class="control-label" for="repeat_password">Repeat Password</label>
<input type="password" class="form-control" id="repeat_password" placeholder="Repeat Password">
</div>
<div class="form-group">
<label class="control-label" for="url">URL</label>
<input type="url" class="form-control" id="url" placeholder="Enter URL">
</div>
<div class="form-group">
<label class="control-label" for="Comment">Comment</label>
<textarea type="text" class="form-control" id="comment" placeholder="Enter Comment"></textarea>
</div>
<div class="form-group">
<label class="control-label" for="tags">Tags</label>
<input type="text" class="form-control" id="tags" placeholder="Enter Tags">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger">Delete</button>
<button id="formClose" type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary">Save changes</button>
</div>
</div>
</div>
</div>
<script src="//crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/aes.js"></script>
<script src="//crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/sha1.js"></script>
<script src="//code.jquery.com/jquery-2.1.4.min.js"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
<script src="../../assets/script.js"></script>
</body>
</html>

33
templates/register.html Normal file
View file

@ -0,0 +1,33 @@
{% extends "outer.html" %}
{% block title %}Registration{% endblock %}
{% block pagetype %}register{% endblock %}
{% block content %}
{% if created %}
<div class="row">
<div class="col-md-4 col-md-push-4 alert alert-success fade in">
<h4>Woohoo!</h4>
<p>Your account has been created. Go to the <a href="login">Login page</a> to login.</p>
</div>
</div>
{% endif %}
{% if exists %}
<div class="row">
<div class="col-md-4 col-md-push-4 alert alert-danger fade in">
<h4>Sorry!</h4>
<p>Your username already exists</p>
</div>
</div>
{% endif %}
<form class="form-signin" method="post">
<h2 class="form-signin-heading">Please register</h2>
<input type="text" name="username" class="form-control" placeholder="Username">
<input type="password" name="password" class="form-control" placeholder="Password">
<input type="password" name="password_repeat" class="form-control" placeholder="Repeat Password">
<button class="btn btn-lg btn-primary btn-block" type="submit">Register</button>
</form>
<div class="row">
<div class="col-md-4 col-md-push-4">
This login is to protect your password storage. This username and password are only for login reasons, but your passwords will not be encrypted with <strong>this</strong> password. You can set your encryption password after your first login.
</div>
</div>
{% endblock %}