diff --git a/.gitignore b/.gitignore index ec5a1d0..51297d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,14 @@ *.log *.pyc *.swp docs/_build/ kadmin/local_settings.py kadmin/static/.webassets-cache/ -kadmin/static/fonts/ -kadmin/static/*.css -kadmin/static/*.js +kadmin/static/assets/css/ +kadmin/static/assets/fonts/ +kadmin/static/assets/js/ kadmin/translations/*/LC_MESSAGES/*.mo tests/coverage/ tests/settings.py tmp/ venv/ diff --git a/kadmin/themes/default/__init__.py b/kadmin/themes/default/__init__.py index 3666655..9213d0c 100644 --- a/kadmin/themes/default/__init__.py +++ b/kadmin/themes/default/__init__.py @@ -1,22 +1,22 @@ from flask.ext.assets import Bundle def register(assets, bundles): bundles["default_js"] = Bundle( "js/lib/jquery.js", "js/lib/bootstrap.js", "js/lib/jquery-ui.js", "js/kadmin.js", filters='jsmin', - output="kadmin.js" + output="assets/js/kadmin.js" ) bundles["default_css"] = Bundle( "css/lib/bootstrap.css", "css/lib/jquery-ui.css", "css/lib/styles.css", "css/kadmin.css", filters='cssmin', - output="kadmin.css" + output="assets/css/kadmin.css" ) return bundles diff --git a/kadmin/themes/kolabnow/__init__.py b/kadmin/themes/kolabnow/__init__.py index f87388f..7c27d5b 100644 --- a/kadmin/themes/kolabnow/__init__.py +++ b/kadmin/themes/kolabnow/__init__.py @@ -1,43 +1,43 @@ import glob import os import shutil from flask.ext.assets import Bundle def noop(_in, out, **kw): out.write(_in.read()) def register(assets, bundles): bundles["kolabnow_js"] = Bundle( "../themes/kolabnow/static/js/kolabnow.js", filters='jsmin', - output="kolabnow.js" + output="assets/js/kolabnow.js" ) bundles["kolabnow_css"] = Bundle( "../themes/kolabnow/static/css/kolabnow.css", filters='cssmin', - output="kolabnow.css" + output="assets/css/kolabnow.css" ) fonts_path = os.path.abspath( os.path.join( os.path.dirname(__file__), '..', '..', '..', 'kadmin', 'static', 'fonts' ) ) if not os.path.isdir(fonts_path): os.mkdir(fonts_path) for font in glob.glob("kadmin/themes/kolabnow/static/fonts/*.*"): - shutil.copy(font, 'kadmin/static/fonts/') + shutil.copy(font, 'kadmin/static/assets/fonts/') #assets.add(Bundle(font, filters=(noop,), output="fonts/%s" % (os.path.basename(font)))) return bundles diff --git a/kadmin/themes/kolabnow/static/css/kolabnow.css b/kadmin/themes/kolabnow/static/css/kolabnow.css index e2cf6aa..6397ffa 100644 --- a/kadmin/themes/kolabnow/static/css/kolabnow.css +++ b/kadmin/themes/kolabnow/static/css/kolabnow.css @@ -1,1516 +1,1516 @@ /* @import url("http://fast.fonts.net/t/1.css?apiType=css&projectid=63e7316c-72be-48b9-bcc7-850e03a21d13"); */ @font-face { font-family: "Brandon Grotesque"; font-weight: normal; font-style: normal; - src: url("fonts/BrandonGrotW01-Regular.eot?#iefix"); - src: url("fonts/BrandonGrotW01-Regular.eot?#iefix") format("eot"), - url("fonts/BrandonGrotW01-Regular.woff") format("woff"), - url("fonts/BrandonGrotW01-Regular.ttf") format("truetype"), - url("fonts/BrandonGrotW01-Regular.svg#80f420d4-9e57-4016-b805-01b95b2e08f3") format("svg"); + src: url("../fonts/BrandonGrotW01-Regular.eot?#iefix"); + src: url("../fonts/BrandonGrotW01-Regular.eot?#iefix") format("eot"), + url("../fonts/BrandonGrotW01-Regular.woff") format("woff"), + url("../fonts/BrandonGrotW01-Regular.ttf") format("truetype"), + url("../fonts/BrandonGrotW01-Regular.svg#80f420d4-9e57-4016-b805-01b95b2e08f3") format("svg"); } @font-face { font-family: "Brandon Grotesque"; font-weight: 700; font-style: normal; - src: url("fonts/BrandonGrotW01-Medium.eot?#iefix"); - src: url("fonts/BrandonGrotW01-Medium.eot?#iefix") format("eot"), - url("fonts/BrandonGrotW01-Medium.woff") format("woff"), - url("fonts/BrandonGrotW01-Medium.ttf") format("truetype"), - url("fonts/BrandonGrotW01-Medium.svg#37c88f3d-9532-4547-9e11-7cca7f66048c") format("svg"); + src: url("../fonts/BrandonGrotW01-Medium.eot?#iefix"); + src: url("../fonts/BrandonGrotW01-Medium.eot?#iefix") format("eot"), + url("../fonts/BrandonGrotW01-Medium.woff") format("woff"), + url("../fonts/BrandonGrotW01-Medium.ttf") format("truetype"), + url("../fonts/BrandonGrotW01-Medium.svg#37c88f3d-9532-4547-9e11-7cca7f66048c") format("svg"); } @font-face { font-family: "Brandon Grotesque"; font-weight: normal; font-style: italic; - src: url("fonts/BrandonGrotW01-Italic.eot?#iefix"); - src: url("fonts/BrandonGrotW01-Italic.eot?#iefix") format("eot"), - url("fonts/BrandonGrotW01-Italic.woff") format("woff"), - url("fonts/BrandonGrotW01-Italic.ttf") format("truetype"), - url("fonts/BrandonGrotW01-Italic.svg#9da820e7-d5a8-4857-ab6f-fe8d9fd5608a") format("svg"); + src: url("../fonts/BrandonGrotW01-Italic.eot?#iefix"); + src: url("../fonts/BrandonGrotW01-Italic.eot?#iefix") format("eot"), + url("../fonts/BrandonGrotW01-Italic.woff") format("woff"), + url("../fonts/BrandonGrotW01-Italic.ttf") format("truetype"), + url("../fonts/BrandonGrotW01-Italic.svg#9da820e7-d5a8-4857-ab6f-fe8d9fd5608a") format("svg"); } @font-face { font-family: "Brandon Grotesque Light"; font-weight: normal; font-style: normal; - src: url("fonts/BrandonGrotW01-Light.eot?#iefix"); - src: url("fonts/BrandonGrotW01-Light.eot?#iefix") format("eot"), - url("fonts/BrandonGrotW01-Light.woff") format("woff"), - url("fonts/BrandonGrotW01-Light.ttf") format("truetype"), - url("fonts/BrandonGrotW01-Light.svg#47f089a6-c8ce-46fa-b98f-03b8c0619d8a") format("svg"); + src: url("../fonts/BrandonGrotW01-Light.eot?#iefix"); + src: url("../fonts/BrandonGrotW01-Light.eot?#iefix") format("eot"), + url("../fonts/BrandonGrotW01-Light.woff") format("woff"), + url("../fonts/BrandonGrotW01-Light.ttf") format("truetype"), + url("../fonts/BrandonGrotW01-Light.svg#47f089a6-c8ce-46fa-b98f-03b8c0619d8a") format("svg"); } @font-face { font-family: "Brandon Grotesque Medium"; font-weight: normal; font-style: normal; - src: url("fonts/BrandonGrotW01-Medium.eot?#iefix"); - src: url("fonts/BrandonGrotW01-Medium.eot?#iefix") format("eot"), - url("fonts/BrandonGrotW01-Medium.woff") format("woff"), - url("fonts/BrandonGrotW01-Medium.ttf") format("truetype"), - url("fonts/BrandonGrotW01-Medium.svg#37c88f3d-9532-4547-9e11-7cca7f66048c") format("svg"); + src: url("../fonts/BrandonGrotW01-Medium.eot?#iefix"); + src: url("../fonts/BrandonGrotW01-Medium.eot?#iefix") format("eot"), + url("../fonts/BrandonGrotW01-Medium.woff") format("woff"), + url("../fonts/BrandonGrotW01-Medium.ttf") format("truetype"), + url("../fonts/BrandonGrotW01-Medium.svg#37c88f3d-9532-4547-9e11-7cca7f66048c") format("svg"); } @font-face { font-family: "Kolab Symbols"; font-weight: normal; font-style: normal; - src: url("fonts/kolabsystems.eot"); - src: url("fonts/kolabsystems.eot?#iefix") format("eot"), - url("fonts/kolabsystems.woff") format("woff"), - url("fonts/kolabsystems.ttf") format("truetype"), - url("fonts/kolabsystems.svg#kolabsystems") format("svg"); + src: url("../fonts/kolabsystems.eot"); + src: url("../fonts/kolabsystems.eot?#iefix") format("eot"), + url("../fonts/kolabsystems.woff") format("woff"), + url("../fonts/kolabsystems.ttf") format("truetype"), + url("../fonts/kolabsystems.svg#kolabsystems") format("svg"); } body { font-family: "Brandon Grotesque", "Trebuchet MS", sans-serif; font-size: 18px; line-height: 20px; color: #4d4d4d; background-color: #fff; margin: 0; padding-top: 180px; } body.page-secondary { background-color: #e7e7e7; } div, span, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, a, abbr, acronym, address, del, dfn, img, q, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, dialog, figure, footer, header, hgroup, nav, section { font: inherit; vertical-align: baseline; line-height: inherit; } a, a:hover { color: #eba735; } a:focus { color: #f15a40; } h1, .h1, h2 .h2 { font-family: "Brandon Grotesque Light", "Trebuchet MS", sans-serif; font-weight: normal; } h1.pagetitle { clear: both; width: 100%; margin-left: 0; margin-right: 0; margin-bottom: 0.85em; text-align: center; font-weight: normal; font-size: 33px; line-height: 1em; letter-spacing: 5px; text-transform: uppercase; color: #383838; } #content-secondary-wrapper h1.pagetitle { color: #4d4d4d; font-size: 30px; } h2, .h2, h2.block-title, .modal-header h2 { font-size: 24px; text-transform: uppercase; color: #383838; } .modal-header h3 { font-size: 22px; text-transform: uppercase; color: #383838; } h2.signup-title, h3.signup-title, .page-dashboard h3 { font-family: "Brandon Grotesque Medium", "Trebuchet MS", sans-serif; font-size: 24px; text-align: center; color: #4d4d4d; margin-bottom: 0.85em; text-transform: none; } h3, .h3 { color: #4d4d4d; font-size: 22px; margin-bottom: 15px; } .page-dashboard h3 { text-align: left; margin-top: 30px; margin-bottom: 15px; } hr { clear: both; border-top-color: #eba735; } .form-horizontal hr { margin: 30px 0; } pre { border-color: #f5f5f5; border-radius: 3px; } .hint, .formlabel { color: #7a7a7a; font-family: "Brandon Grotesque Light", "Trebuchet MS", sans-serif; } p.hint { margin: 1em 0 1em 0.5em; line-height: 24px; } .footnote { font-size: 16px; } .center-block { text-align: center; } .btn { text-transform: uppercase; font-family: "Brandon Grotesque Medium", "Trebuchet MS", sans-serif; font-size: 16px; letter-spacing: 1px; border-radius: 3px; } .btn-sm { font-size: 12px; border-radius: 2px; } .btn-default { color: #383838; background: #e2e2e2; border-color: #e2e2e2; } #content-secondary-wrapper .btn-default { background: #fff; border-color: #fff; } #content-secondary-wrapper .btn-back { color: inherit; background: inherit; } .btn-primary { color: #fff; background-color: #f3852f; border-color: #f3852f; } .btn-active, .btn-default:hover, .btn-default:focus, .btn-primary:hover, .btn-primary:focus, #content-secondary-wrapper .btn-default:hover, #content-secondary-wrapper .btn-default:focus { color: #fff; background-color: #3a3a3a; border-color: #3a3a3a; } .tooltip-icon { color: #eba735; } .form-vertical .form-group:after { clear: both; } .form-control, .input-group-addon { border-radius: 0 !important; } .form-control-inline { display: inline-block; width: auto; } .form-control, .form-group .controls input, .form-group .controls select, .form-group .controls textarea { font-size: inherit; border-color: #bec0c2; border-radius: 0 !important; -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0, .075), rgba(50,50,50, .15) 0px 0px 6px; -mox-box-shadow: inset 0 1px 1px rgba(0,0,0, .075), rgba(50,50,50, .15) 0px 0px 6px; box-shadow: inset 0 1px 1px rgba(0,0,0, .075), rgba(50,50,50, .15) 0px 0px 6px; } select.form-control { padding: 3px 12px; } .row, .form-group, .form-horizontal .form-group { margin-left: 0; margin-right: 0; } .form-group .formlabel { font-size: 16px; margin-left: 1em; } .form-group .checkbox-decorator + .formlabel { margin-left: 0; } .form-vertical .form-group .control { position: relative; } .form-vertical .form-group .control .tooltip-icon { right: -24px; z-index: 10; } .form-vertical .col-sm-6 .form-control-feedback, .form-vertical .col-sm-12 .form-control-feedback { right: 15px; } .form-group-narrow .control-label { padding-top: 4px; margin-bottom: 0; text-align: right; } .form-group-narrow .form-control-static { padding-top: 0px; padding-bottom: 2px; } form .help-block { font-family: "Brandon Grotesque Light", "Trebuchet MS", sans-serif; } form .formerror { font-size: 16px; } .form-vertical .checkbox .help-block { margin-left: 36px; margin-top: 10px; } .form-boxed, .invoice-head { border-radius: 3px; background: #e7e7e7; border-color: #e7e7e7; } .invoice-head > .form-group:last-child { margin-bottom: 0; } .recordview .line { padding: 8px 0; } label.inline, .recordview .line label { color: #4d4d4d; font-family: "Brandon Grotesque Light", "Trebuchet MS", sans-serif; min-width: 12em; } .table > thead > tr > th, .invoice-head .form-group .property { font-family: "Brandon Grotesque Light", "Trebuchet MS", sans-serif; font-size: 16px; text-transform: uppercase; } .invoice-head .form-group .property { min-width: 12em; } /** JQuery UI overrides **/ .ui-widget { font-family: "Brandon Grotesque", "Trebuchet MS", sans-serif; font-size: 0.9em; } .ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { border-color: #eba735; background: #eba735; } .ui-slider-horizontal .ui-slider-handle { border-color: #eba735; background: #eba735; border-radius: 0.6em; } /** Site layout **/ .container { max-width: 1160px; margin-left: auto; margin-right: auto; } @media (min-width: 992px) { .container { width: 920px; } .page-pricing .container, .page-signup .container, .page-admin .container { width: 980px; } } @media (max-width: 768px) { .container { padding-left: 20px; padding-right: 20px; } } .block .block-content { position: relative; } #header-wrapper { position: fixed; top: 0; left: 0; width: 100%; z-index: 998; } #top { height: 0px; overflow: hidden; background: #fff; } #header { position: relative; background: transparent none; height: 100px; overflow: visible; } header .stripe { width: 50%; height: 6px; background: url("https://kolabnow.com/cockpit/skins/kolabnow/images/top_gradient.png") center top repeat-x; position: relative; } header .stripe:before { position: absolute; left: -1000px; width: 1000px; top: 0; height: 6px; content: ""; background: url("https://kolabnow.com/cockpit/skins/kolabnow/images/top_gradient_left.png") 0 0 repeat; } header .stripe:after { position: absolute; right: -1000px; width: 1000px; top: 0; height: 6px; content: ""; background: url("https://kolabnow.com/cockpit/skins/kolabnow/images/top_gradient_right.png") 0 0 repeat; } #header .container { position: relative; background: transparent url("https://kolabnow.com/cockpit/skins/kolabnow/images/gradient.png") center top repeat-y; height: 94px; width: auto; padding: 0; } #header .container:before { position: absolute; left: -1000px; width: 1000px; height: 94px; top: 0; content: ""; background: url("https://kolabnow.com/cockpit/skins/kolabnow/images/gradient_left.png") 0 0 repeat; } #header .container:after { position: absolute; right: -1000px; width: 1000px; height: 94px; top: 0; content: ""; background: url("https://kolabnow.com/cockpit/skins/kolabnow/images/gradient_right.png") 0 0 repeat; visibility: visible; } #header div.logo { float: left; width: 15.51724%; margin-right: 1.37931%; } #header div.logo a { display: block; padding: 19px 0px 0px 10px; } #header-button-block { float: left; width: 23.96552%; margin-top: 30px; margin-right: 1.37931%; white-space: nowrap; } #header-button-block .block-content p { margin: 0; } #header-button-block .btn { background: #fff; padding: 10px 10px; margin-right: 4%; min-width: 44%; font-size: 20px; line-height: 26px; font-family: "Brandon Grotesque", "Trebuchet MS", sans-serif; } #header-button-block .btn.login { color: #eba735; } #header-button-block .block-content a.login:hover { background-color: #3a3a3a; color: #fff; } #header-button-block .block-content a.sign-up { margin-right: 0; background: #f15a40; color: #fff; } #header-button-block .block-content a.sign-up:hover { background-color: #3a3a3a; color: #f15a40; } nav#primary-menu { float: left; height: 43px; width: 53.53448%; margin-right: 1.37931%; } nav#primary-menu .block-content .menu { list-style: outside none none; margin: 0; padding: 0; float: right; border: none; } nav#primary-menu .block-content .menu li { float: left; display: inline; position: relative; list-style: outside none none; margin: 0 25px 0 0; padding: 0; background: transparent none; } nav#primary-menu .block-content .menu li.last { margin-right: 0; } nav#primary-menu .block-content .menu a.username { margin-left: 45px; color: #000; } nav#primary-menu .block-content .menu li a { display: block; position: relative; font-size: 12px; line-height: 60px; padding-top: 26px; text-transform: uppercase; letter-spacing: 2px; color: #4d4d4d; font-family: "Brandon Grotesque Medium", "Trebuchet MS", sans-serif; z-index: 2; } nav#primary-menu .block-content .menu li a:hover { text-decoration: none; color: #000; } nav#primary-menu .block-content .menu li.active a, nav#primary-menu .block-content .menu li a.active-trail, nav#primary-menu .block-content .menu li a.active { color: #fff; } @media (max-width: 1160px) { nav#primary-menu .block-content .menu li { margin: 7px 13px 0 0; } } nav#secondary-menu .container { text-align: center; height: 50px; } nav#secondary-menu .navbar-nav { float: none; display: inline-block; margin: 0 auto; } nav#secondary-menu .navbar-nav li, #main-wrapper .navbar-default .navbar-nav li { float: left; display: block; font-family: "Brandon Grotesque Light", "Trebuchet MS", sans-serif; font-weight: normal; letter-spacing: 1px; text-transform: uppercase; } .navbar-kolabnow { background: rgba(255,255,255,0.92); border-top: 0; border-bottom: 1px solid #ededed; margin: 0; } .navbar-kolabnow .navbar-nav > li > a { color: #4d4d4d; border-right: 1px solid #ededed; } .navbar-kolabnow .navbar-nav > li:first-child > a { border-left: 1px solid #ededed; } .navbar-kolabnow .navbar-nav > li.active > a, #main-wrapper .navbar-default .navbar-nav > li.active > a { color: #eba735; background: #3a3a3a; } #main-wrapper .navbar { border: 0; background: transparent; text-align: center; margin-bottom: 10px; } #main-wrapper .navbar-default .navbar-collapse { float: left; padding: 0; border: 1px solid #c5c5c5; background: #fff; -webkit-box-shadow: 0 0 10px 0 rgba(0,0,0,0.15); box-shadow: 0 0 10px 0 rgba(0,0,0,0.15); } #main-wrapper .navbar-default .navbar-nav > li > a { color: #4d4d4d; border-right: 1px solid #ededed; } #main-wrapper .navbar-default .navbar-nav > li:last-child > a { border-right: 0; } #main-wrapper .navbar .container { display: inline-block; width: auto; margin: 0; padding: 0; } #main-wrapper .navbar-default .navbar-brand { font-family: "Brandon Grotesque Light", "Trebuchet MS", sans-serif; color: #7a7a7a; text-transform: uppercase; } .page-admin .nav-stacked { border: 1px solid #c5c5c5; background: #fff; -webkit-box-shadow: 0 0 10px 0 rgba(0,0,0,0.15); box-shadow: 0 0 10px 0 rgba(0,0,0,0.15); margin-top: 20px; } .page-admin .nav-stacked > li { margin: 0; } .page-admin .nav-stacked > li > a { border-bottom: 1px solid #ededed; border-radius: 0; } .page-admin .nav-stacked > li:last-child > a { border-bottom: 0; } .page-admin .nav-stacked > li.active > a, .page-admin .nav-stacked > li.active > a:hover, .page-admin .nav-stacked > li.active > a:focus { color: #eba735; background: #3a3a3a; } #block-lang-dropdown-language { display: none; position: absolute; top: 6px; right: 1.37931%; width: 11.2931%; } #block-lang-dropdown-language h2 { float: left; text-transform: uppercase; letter-spacing: 2px; margin: 0; margin-right: 4px; color: #4d4d4d; font-family: "Brandon Grotesque Medium", "Trebuchet MS", sans-serif; font-size: 12px; } #block-lang-dropdown-language .block-content { float: left; width: 40px; } #block-lang-dropdown-language .block-content .sbHolder { width: 40px; height: auto; } #block-lang-dropdown-language .block-content .sbHolder .sbToggle { width: 20px; height: 11px; float: right; background: url("https://kolabnow.com/cockpit/skins/kolabnow/images/lang-switcher-open.png") 0 0 no-repeat; margin-top: 4px; margin-left: 4px; display: none; } #block-lang-dropdown-language .block-content .sbHolder .sbSelector { float: left; color: #fff; font-size: 12px; text-transform: uppercase; text-decoration: none; } #block-lang-dropdown-language .block-content .sbHolder .sbOptions { list-style-type: none; font-size: 12px; line-height: 12px; margin: 5px 0 0; padding: 0; } #block-lang-dropdown-language .block-content .sbHolder .sbOptions li { list-style-type: none; margin: 0; padding: 0; padding: 5px 5px; background: #e7e7e7; } * .alert { font-size: 20px; border: 0; border-radius: 0; text-align: center; } #messagesbox .alert { margin: 0 8%; } #main-wrapper { padding-bottom: 2em; min-height: 20em; } #content-secondary-wrapper { background: #e7e7e7 none; overflow: hidden; padding: 45px 0; } #header-wrapper + #content-secondary-wrapper { padding-top: 0; } #bottom { float: left; width: 100%; } #bottom .bottom-block { padding: 20px 0 40px 0; clear: both; width: 100%; float: left; margin-left: 0; margin-right: 0; } footer { float: left; width: 100%; margin-left: 0; margin-right: 0; padding: 50px 0px 50px; background: #383838 none; color: #acacac; } #footer .container { width: auto; } #copyright-block, #footer-menu { width: 61.98276%; float: left; margin-right: 1.37931%; padding-left: 4.22414%; } /* #footer .container #copyright-block { float: left; width: 57.75862%; margin-right: 1.37931%; margin-left: 4.22414%; padding-left: 0; } */ #footer .container #social-block { float: right; width: 28.18966%; margin-right: 0; } #footer .container #madeby-block { clear: both; width: 95.77586%; margin-left: 4.22414%; margin-right: 0; margin-top: 20px; } #footer .block-content p, #footer .block-content div { font-size: 12px; line-height: 1.3em; color: #acacac; margin-bottom: 0; } #footer .block-content a { color: #acacac; margin: 0; padding: 0; } #footer .block-content a:hover { color: #fff; text-decoration: none; } #footer-menu .block-content > div, #copyright-block .block-content > div { float: left; font-size: 12px; line-height: 18px; } #footer-menu .block-content > ul { float: left; margin: 0; padding: 0; } #footer-menu .block-content > ul li { float: left; margin: 0 0 0 5px; padding: 0 0 0 5px; list-style-image: none; list-style-type: none; border-left: 1px solid #acacac; } #footer-menu .block-content > ul li a { display: block; font-size: 12px; line-height: 18px; color: #acacac; } /** Reduced header size when authenticated */ body.authenticated { padding-top: 150px; } body.authenticated #header { height: 70px; } body.authenticated #header .container, body.authenticated #header .container:before, body.authenticated #header .container:after { height: 66px; } body.authenticated header .stripe, body.authenticated header .stripe:before, body.authenticated header .stripe:after { height: 4px; } body.authenticated #header div.logo a { padding-top: 5px; } body.authenticated nav#primary-menu .block-content .menu li a { padding-top: 4px; } body.authenticated #header-button-block { margin-top: 8px; } /** Decorated checkboxes and radio buttons **/ input[type="radio"].decorated, input[type="checkbox"].decorated { visibility: hidden; } input[type="checkbox"] + .checkbox-decorator { content: ''; display: inline-block; position: relative; top: -2px; left: -20px; width: 26px !important; height: 26px !important; min-width: 26px !important; max-width: 26px !important; padding: 0; margin-right: -12px; vertical-align: middle; box-sizing: border-box; -moz-box-sizing: border-box; border: 1px solid #bec0c2; border-radius: 0; background: #fff; overflow: hidden; -webkit-box-shadow: rgba(50,50,50,0.2) 0px 0px 6px; -moz-box-shadow: rgba(50,50,50,0.2) 0px 0px 6px; box-shadow: rgba(50,50,50,0.2) 0px 0px 6px; } input[type="radio"] + .radio-decorator { content: ''; display: inline-block; position: relative; top: -2px; left: -20px; width: 26px !important; height: 26px !important; min-width: 26px !important; max-width: 26px !important; padding: 0; margin-right: -12px; vertical-align: middle; box-sizing: border-box; -moz-box-sizing: border-box; border: 1px solid #bec0c2; border-radius: 50%; background: #fff; overflow: hidden; -webkit-box-shadow: rgba(50,50,50,0.2) 0px 0px 6px; -moz-box-shadow: rgba(50,50,50,0.2) 0px 0px 6px; box-shadow: rgba(50,50,50,0.2) 0px 0px 6px; } input[type="radio"][disabled] + .radio-decorator, input[type="checkbox"][disabled] + .checkbox-decorator { border-color: #c7c8ca; background: #e7e7e7; } input[type="checkbox"] + .checkbox-decorator:after { content: ''; position: absolute; top: 4px; left: 5px; width: 15px; height: 10px; border: 3px solid #bec0c2; border-top: none; border-right: none; background: transparent; -webkit-transform: rotate(-45deg); -moz-transform: rotate(-45deg); -o-transform: rotate(-45deg); -ms-transform: rotate(-45deg); transform: rotate(-45deg); -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; filter: alpha(opacity=0); opacity: 0; } input[type="radio"] + .radio-decorator:after { content: ''; position: absolute; top: 6px; left: 6px; width: 12px; height: 12px; border: 0; border-radius: 50%; background: #bec0c2; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; filter: alpha(opacity=0); opacity: 0; } input[type="radio"] + .radio-decorator:hover::after, input[type="checkbox"] + .checkbox-decorator:hover::after { -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=30)"; filter: alpha(opacity=30); opacity: 0.5; } input[type="radio"][disabled] + .radio-decorator:hover::after, input[type="checkbox"][disabled] + .checkbox-decorator:hover::after { opacity: 0; } input[type="radio"]:checked + .radio-decorator:after, input[type="checkbox"]:checked + .checkbox-decorator:after { -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)"; filter: alpha(opacity=100); opacity: 1; } input[type="checkbox"]:checked + .checkbox-decorator:after { border-color: #eba735; } input[type="radio"]:checked + .radio-decorator:after { background: #eba735; } /** Login form **/ form#login-form { width: 56%; padding: 0 30px 30px 30px; margin: 0 auto; margin-bottom: 70px; background: #fff none; overflow: hidden; } #login-form .form-actions { margin-top: 30px; text-align: center; } #login-form .btn-primary { width: 200px; padding: 14px; } p.login-hint { width: 52%; margin: 3em auto; text-align: center; } /** Signup form */ p.description, ul.description { font-family: "Brandon Grotesque Light", "Trebuchet MS", sans-serif; margin-bottom: 2em; } *.description strong { font-family: "Brandon Grotesque Medium", "Trebuchet MS", sans-serif; font-weight: bold; } ul.description { padding-left: 20px; } #signupform .form-actions { margin-top: 20px; } #signupform div.radio { margin-bottom: 25px; } #signupform .paymentplans h3, #signupform .subscriptionplans h3 { margin-bottom: 20px; } #signupform .subscriptionplans .hint { display: block; font-size: 16px; margin-left: 16px; margin-top: 8px; } #signupform .foreign.currency { margin-left: 1em; font-size: 16px; color: #7a7a7a; } #signupform .tosdr { white-space: nowrap; margin-left: 2em; } #signupform .checkbox .label-text { font-family: "Brandon Grotesque Light", "Trebuchet MS", sans-serif; font-size: 22px; margin-right: 0.5em; } #signupform .col-sm-12.checkbox .price, #signupform .col-sm-12.checkbox .foreign.currency { font-size: 20px; } #quotaslider { background-color: #d2d2d2; border-color: #d2d2d2; border-radius: 8px; } .form-vertical #quotaslider { margin-left: 8px; margin-bottom: 18px; } .singup-progress { display: block; list-style: none; height: 100px; text-align: center; padding: 0; margin-bottom: 36px; } .singup-progress li { list-style: none; display: inline-block; position: relative; font-family: "Brandon Grotesque Light", "Trebuchet MS", sans-serif; font-size: 16px; text-align: center; text-transform: uppercase; padding: 64px 20px 0 20px; min-width: 200px; } .singup-progress li:before { content: "1"; position: absolute; top: 0px; left: 50%; width: 42px; height: 42px; line-height: 40px; margin-left: -21px; border: 2px solid #eba735; border-radius: 50%; background: #eba735; color: #fff; text-align: center; font-size: 20px; z-index: 5; } .singup-progress li:after { content: ""; position: absolute; top: 19px; left: 50%; width: 100%; height: 5px; z-index: 2; background-image: url('https://kolabnow.com/cockpit/skins/kolabnow/images/dotted-line.png'); background-position: bottom; background-repeat: repeat-x; } .singup-progress li.three:after { display: none; } .singup-progress li.two:before { content: "2"; } .singup-progress li.three:before { content: "3"; } .singup-progress li.active { color: #000; font-family: "Brandon Grotesque", "Trebuchet MS", sans-serif; } .singup-progress li.active:before { background-color: #fff; color: #eba735; } #paymentform p.hint { text-align: center; } h3.payments-title { text-align: center; font-size: 18px; font-weight: normal; margin-top: 0; } .payment-logos { display: block; float: left; list-style: none; height: 42px; padding: 0; margin: 10px 10px 20px 22%; } .payment-logos li { display: block; float: left; padding: 0; margin: 0 30px 0 0; width: 42px; height: 42px; text-indent: -5000px; font-size: 0; overflow: hidden; background: url('https://kolabnow.com/cockpit/skins/kolabnow/images/payment_logos.png') 0 50% no-repeat; } .payment-logos li.paypal { width: 112px; } .payment-logos li.mastercard { width: 60px; background-position: -116px 50%; } .payment-logos li.visa { width: 100px; background-position: -176px 50%; } .payment-logos li.amex { background-position: -287px 50%; } .payment-logos li.bitcoin { background-position: -335px 50%; } .ssl-logo { display: block; width: 92px; height: 92px; margin-top: -18px; text-indent: -5000px; font-size: 0; overflow: hidden; background: url('https://kolabnow.com/cockpit/skins/kolabnow/images/payment_logos.png') top right no-repeat; } .bank-account-details td { padding: 4px; } .bank-account-details td.title { font-family: "Brandon Grotesque Light", "Trebuchet MS", sans-serif; padding-right: 1em; white-space: nowrap; } /** Kolab Now static page content **/ #hero-wrapper { margin-top: -180px; z-index: 1; } #hero-image-block { position: relative; height: 700px; } #hero-wrapper .shadow { display: block; position: absolute; height: 170px; width: 100%; left: 0; bottom: 130px; background: transparent url("https://kolabnow.com/cockpit/skins/kolabnow/images/hero_shadow.png") center 0 no-repeat; z-index: 102; } #hero-wrapper .container { width: auto; max-width: 1515px; margin-left: auto; margin-right: auto; height: 700px; } #hero-wrapper .hero-content { position: relative; max-width: 1160.0px; margin-left: auto; margin-right: auto; height: 700px; } #hero-wrapper .hero-content .hero-content-inner { position: relative; top: 190px; left: 0; width: 36.63793%; padding-left: 4.22414%; } #hero-wrapper .hero-content .hero-content-inner h1 { text-transform: none; font-size: 52px; line-height: 1em; color: #f7b73e; margin-bottom: 0; } #hero-wrapper .hero-content .hero-content-inner p { margin: 1.5em 0; font-size: 20px; line-height: 1.3em; } #hero-wrapper .kolab-hero-image-pricing { background-image: url('https://kolabnow.com/cockpit/skins/kolabnow/images/hero_signup.jpg'); background-position: top center; background-repeat: no-repeat; } .page-pricing #main-wrapper, .page-signup #main-wrapper { margin-top: -80px; } .pricing-block { text-align: center; margin-left: 20px; margin-right: 20px; } .pricing-block h2 { position: relative; padding-left: 140px; height: 90px; font-size: 28px; line-height: 1.3em; text-align: left; } .pricing-block h2.block-title:before { content: "K"; font-family: "Kolab Symbols"; position: absolute; top: 12px; left: 2px; font-size: 115px; color: #eba735; text-align: center; text-transform: none; } .pricing-block.type-domainaccount h2.block-title:before { content: "J"; } .pricing-block .description { margin: 25px 10px; line-height: 1.3em; text-align: left; } .pricing-block p.description + ul.description { margin: -10px 10px; } .pricing-block .description li { text-align: left; margin-bottom: 0.3em; } .pricing-block .pricing-price { font-size: 28px; text-transform: uppercase; margin-bottom: 6px; } .pricing-block .pricing-subline { text-transform: uppercase; margin-bottom: 25px; } .pricing-block .btn-primary { width: 220px; padding: 14px; } .page-signup .form-actions .btn { padding: 14px 24px; } .maintenance { border: 1px solid #f6d093; background-color: #fdeedb; text-align: center; color: #c96d16; } .maintenance h1.pagetitle { color: #c96d16; } /** IE hack classes **/ .lt-ie9-block, .lt-ie9-inline-block, .lt-ie9-inline { display: none; } html.lt-ie9 .lt-ie9-block { display: block !important; } html.lt-ie9 .lt-ie9-inline-block { display: inline-block; } html.lt-ie9 .lt-ie9-inline { display: inline; } html.lt-ie9 .lt-ie9-none { display: none !important; } html.lt-ie9 .container { width: 920px; max-width: 1160px; margin-left: auto; margin-right: auto; padding-left: 0; padding-right: 0; } html.lt-ie9 .page-pricing .container, html.lt-ie9 .page-signup .container, html.lt-ie9 .page-admin .container { width: 980px; } diff --git a/kadmin/web/webapp.py b/kadmin/web/webapp.py index 2f6d979..232e317 100644 --- a/kadmin/web/webapp.py +++ b/kadmin/web/webapp.py @@ -1,737 +1,764 @@ # -*- coding: utf8 -*- """ The main web application for the KADMIN. This currently holds all the routes and modules and functionality, and should probably be split up in blueprints and views and the like. """ import datetime import os import time import uuid from flask import Flask from flask import abort from flask import flash from flask import g from flask import jsonify from flask import redirect from flask import request +from flask import _request_ctx_stack +from flask import send_from_directory from flask import session from flask import url_for from flask.ext.assets import Bundle from flask.ext.assets import Environment from flask.ext.babel import Babel from flask.ext.babel import get_locale as get_babel_locale from flask.ext.babel import gettext from flask.ext.cache import Cache from flask.ext.themes import render_theme_template as _render_theme_template from flask.ext.themes import get_themes_list from flask.ext.themes import setup_themes from functools import wraps from jinja2.exceptions import * import sqlalchemy_utils app = Flask('kadmin') app.config.from_object('kadmin.default_settings') if os.environ.has_key('KADMIN_SETTINGS'): if os.path.isfile(os.environ['KADMIN_SETTINGS']): app.config.from_envvar('KADMIN_SETTINGS') cache_timeout = app.config.get("CACHE_TIMEOUT", None) # Insert a post-filter app.jinja_env.filters['gettext'] = gettext sqlalchemy_utils.i18n.get_locale = get_babel_locale from kadmin.db import db from kadmin.db.model import * if app.config.get('FACEBOOK_APP_ID', False): from kadmin.web.oauth.facebook import facebook if app.config.get('GOOGLE_CLIENT_ID', False): from kadmin.web.oauth.google import google if app.config.get('TWITTER_API_KEY', False): from kadmin.web.oauth.twitter import twitter from flask.ext.oauthlib.client import OAuthException babel = Babel(app) cache = Cache( app, config = app.config ) setup_themes(app) assets = Environment(app) bundles = {} for theme in [ 'default', 'kolabnow' ]: theme_mod = __import__('kadmin.themes.' + theme, fromlist=['register']) bundles = theme_mod.register(assets, bundles) assets.register(bundles) @babel.localeselector def get_locale(): locale = getattr(g, 'locale', None) if not locale == None: return locale try: translations = get_translations() except ValueError, errmsg: try: cache.delete_memoized(get_translations) except ValueError, errmsg: cache.clear() finally: translations = get_translations() result = request.accept_languages.best_match(translations) if result == None: result = 'en' g.locale = result return g.locale @babel.timezoneselector def get_timezone(): timezone = getattr(g, 'timezone', None) if not timezone == None: return timezone g.timezone = "UTC" return g.timezone @cache.memoize() def get_translations(): translations = [x.language for x in babel.list_translations()] return translations @app.before_request def before_request(): g.before_request = time.time() g.user = None g.locale = None g.timezone = None if not 'uuid' in session: session['uuid'] = uuid.uuid4().hex if 'account_id' in session: try: user = db.session.query(Account).get(session['account_id']) except Exception: db.session.rollback() user = db.session.query(Account).get(session['account_id']) if user == None: session.clear() else: g.user = user.id g.locale = user.locale g.timezone = user.timezone # For some reason, when starting with sessions and the g object, the # locale selector function is lost ...? babel = app.extensions['babel'] if babel.locale_selector_func == None: babel.locale_selector_func = get_locale @app.after_request def after_request(response): g.after_request = time.time() diff = g.after_request - g.before_request try: if (response.response): response.response[0] = response.response[0].replace('__EXECUTION_TIME__', str(diff)) except: pass try: db.session.commit() except Exception: db.session.rollback() finally: db.session.close() return response #@app.teardown_request #def teardown_request(exception=None): #db = getattr(g, 'db', None) #if not db == None: #db.session.commit() #db.session.close() @cache.memoize(timeout=cache_timeout) def render_theme_template(theme, template, locale, country, currency, user, _flash, **kwargs): """ A cacheable, theme-based, language specific proxy function to :py:func:`flaskext.themes.render_theme_template`. :param theme: The name of the theme to use. :param template: The template to render. :param locale: The language to use. :param kwargs: The context to pass on to the template. """ return _render_theme_template(theme, template, lang=locale, country=country, currency=currency, **kwargs) def render_template(template, country=None, currency=None, **kwargs): """ A theme-based template renderer. This function calls :py:func:`kadmin.web.webapp.render_theme_template` with the language the user prefers. :param template: The template to render. :param country: Positional argument to facilitate caching. Inserted if not already set. :param currency: Positional argument to facilitate caching. Inserted if not already set. """ from kadmin.intl import country_by_ipaddr from kadmin.intl import currency_by_ipaddr if country == None: country = country_by_ipaddr(request.remote_addr) if currency == None: currency = currency_by_ipaddr(request.remote_addr) theme = session.get('theme', app.config['DEFAULT_THEME']) user = session.get('uuid', None) # flashes is a list of tuples (category, message) # We need to pass this along to asure the templates are cached correctly flashes = session.get('_flashes', []) _flash = "" for c,m in flashes: _flash += "%r/%r" % (c,m) return render_theme_template(theme, template, g.locale, country, currency, user, _flash, **kwargs) @app.errorhandler(403) def error_403(error): """ Handler for an unauthorized error. """ return render_template('errors/403.html'), 403 @app.errorhandler(404) def error_404(error): """ Handler for a page not found error. """ return render_template('errors/404.html'), 404 @app.errorhandler(500) def error_500(error): """ Handler for a generic Internal Server Error. """ return render_template('errors/500.html'), 500 @app.errorhandler(TemplateNotFound) def error_templatenotfound(error): """ Handler for a syntax error in a template. """ if app.config.get('ENVIRONMENT', 'development') == 'production': return render_template('errors/500.html'), 500 else: raise error @app.errorhandler(TemplateSyntaxError) def error_templatesyntaxerror(error): """ Handler for a syntax error in a template. """ if app.config.get('ENVIRONMENT', 'development') == 'production': return render_template('errors/500.html'), 500 else: raise error def login_required(f): """ Decorator function for route controllers that require a login. """ @wraps(f) def decorated_function(*args, **kwargs): if g.user == None: return redirect(url_for('login', next=request.uri)) return f(*args, **kwargs) return decorated_function @app.route('/') def index(): if len(request.args) > 0: return abort(500) from kadmin.intl import country_by_ipaddr from kadmin.intl import currency_by_ipaddr from kadmin.intl import exchange_rate currency = currency_by_ipaddr(request.remote_addr) country = country_by_ipaddr(request.remote_addr) exchange_rate = exchange_rate(currency) products = db.session.query(Product).options(db.joinedload(Product.translations[get_babel_locale()])).filter_by(signup_enabled=True).all() return render_template( 'index.html', products=products, currency=currency, country=country, exchange_rate=exchange_rate ) @app.route('/about/') def about(topic=None): """ Placeholder for "about" pages. """ return "TODO", 200 @app.route('/login') def login(): """ Overview page for means to login. """ if request.method == 'GET': return render_template('login.html') elif request.method == 'POST': return 'TODO', 200 @app.route('/logout') #@app.route('//logout') def logout(csrf_token=None): """ Logout URL. .. IMPORTANT:: MUST be CSRF protected still. """ session.clear() flash("We hate to see you leave, but we love to watch you go. Yummy, momma want.") return redirect(url_for('index')) if csrf_token == None: return abort(403) elif len(csrf_token) != 12: return abort(503) else: # TODO: Validate CSRF token return 'TODO', 200 @login_required @app.route('/profile') def profile(): """ Placeholder portal to profile settings. """ return "TODO", 200 @app.route('/kb') @app.route('/kb/') def kb(article_title=None): """ Knowledge base controller. Takes localized titles as the argument for the article to reproduce such that, for example: * `/kb/an-article `_ * `/kb/ein-artikel `_ * `/kb/een-artikel `_ end up with pretty much the same article -- aside from localized contents. If no ``article_title`` is specified, returns a list of articles. Also handles "logged in" requirements -- currently staticly requiring authentication for all articles, without authorization requirements on a per-article / per-category basis. """ if article_title == None: articles = db.session.query(KBArticle).options(db.joinedload(KBArticle.translations[get_babel_locale()])).all() return render_template( 'kb.html', articles=articles ) articles = db.session.query(KBArticle).options(db.joinedload(KBArticle.translations[get_babel_locale()])).filter_by(href=article_title).all() article = [a for a in articles if a.href == article_title] if article == None or len(article) == 0: return abort(404) return render_template( 'kb.html', article=article[0] ) @app.route('/oauth/facebook/login') def oauth_facebook_login(): """ Authentication against Facebook starts here. .. IMPORTANT:: The callback URL **MUST** be randomized (say, through a session UUID or CSRF token, but not either of those two), or the fact a visitor is allowed to authenticate using the third party establishes an exploit path -- the third party holds all the data necessary to authenticate on the user's behalf and fully controls the infrastructure against which the user must hypothetically authenticate. """ oauth_uuid = uuid.uuid4().hex db.session.add( Session( uuid = session.get('uuid'), oauth_uuid = oauth_uuid, name = 'facebook', expires = datetime.datetime.utcnow() + datetime.timedelta(seconds=30) ) ) db.session.commit() callback = url_for( 'oauth_facebook_authenticate', uuid = oauth_uuid, _external = True, next = request.args.get('next') or request.referrer or url_for('index') ) return facebook.authorize(callback=callback) @app.route('/oauth/facebook/authenticate/') def oauth_facebook_authenticate(uuid=None): """ This is the callback URL to which the user is redirected after successfully authenticating against Facebook. """ if uuid == None or len(uuid) is not 32: return abort(401) valid_uuids = db.session.query(Session).filter_by( name='facebook', uuid=session.get('uuid'), oauth_uuid=uuid ).filter( Session.expires > datetime.datetime.utcnow() ).all() if len(valid_uuids) is not 1: for valid_uuid in valid_uuids: db.session.delete(valid_uuid) db.session.commit() return abort(401) db.session.delete(valid_uuids[0]) db.session.commit() resp = facebook.authorized_response() if resp is None: return 'Access denied: reason=%s error=%s' % ( request.args['error_reason'], request.args['error_description'] ) if isinstance(resp, OAuthException): return 'Access denied: %s' % resp.message else: session['facebook_oauth_token'] = (resp['access_token'], '') me = facebook.get('/me') account = db.session.query(Account).filter_by(remote_id=me.data['id'], type_name='facebook').first() if account == None: account = Account(name=me.data['name'], remote_id=me.data['id'], type_name='facebook', locale=get_locale(), timezone=get_timezone()) db.session.add(account) db.session.commit() account = db.session.query(Account).filter_by(name=me.data['name'], remote_id=me.data['id'], type_name='facebook').first() flash("Successfully logged in with Facebook. Welcome, %s" % me.data['name']) else: account.name = me.data['name'] flash("Successfully logged in with Facebook. Welcome back %s" % me.data['name']) account.lastlogin = datetime.datetime.utcnow() db.session.commit() session['account_id'] = account.id return redirect(url_for('index')) @app.route('/oauth/google/login') def oauth_google_login(): """ Authentication against Google starts here. .. IMPORTANT:: The callback URL **MUST** be randomized (say, through a session UUID or CSRF token, but not either of those two), or the fact a visitor is allowed to authenticate using the third party establishes an exploit path -- the third party holds all the data necessary to authenticate on the user's behalf and fully controls the infrastructure against which the user must hypothetically authenticate. """ oauth_uuid = uuid.uuid4().hex db.session.add( Session( uuid = session.get('uuid'), oauth_uuid = oauth_uuid, name = 'google', expires = datetime.datetime.utcnow() + datetime.timedelta(seconds=30) ) ) db.session.commit() callback = url_for( 'oauth_google_authenticate', uuid = oauth_uuid, _external = True, next = request.args.get('next') or request.referrer or url_for('index') ) return google.authorize(callback=callback) @app.route('/oauth/google/authenticate/') def oauth_google_authenticate(uuid=None): """ This is the callback URL to which the user is redirected after successfully authenticating against Google. """ uuid = request.args.get('uuid', None) if uuid == None or len(uuid) is not 32: return abort(401) valid_uuids = db.session.query(Session).filter_by( name='google', uuid=session.get('uuid'), oauth_uuid=uuid ).filter( Session.expires > datetime.datetime.utcnow() ).all() if len(valid_uuids) is not 1: for valid_uuid in valid_uuids: db.session.delete(valid_uuid) db.session.commit() return abort(401) db.session.delete(valid_uuids[0]) db.session.commit() resp = google.authorized_response() if resp is None: return 'Access denied: reason=%s error=%s' % ( request.args['error_reason'], request.args['error_description'] ) else: session['google_oauth_token'] = (resp['access_token'],) me = google.get('userinfo') account = db.session.query(Account).filter_by(remote_id=me.data['id'], type_name='google').first() if account == None: account = Account(name=me.data['name'], remote_id=me.data['id'], type_name='google', locale=get_locale(), timezone=get_timezone()) db.session.add(account) db.session.commit() account = db.session.query(Account).filter_by(name=me.data['name'], remote_id=me.data['id'], type_name='google').first() flash("Successfully logged in with Google. Welcome, %s" % me.data['name']) else: account.name = me.data['name'] flash("Successfully logged in with Google. Welcome back %s" % me.data['name']) account.lastlogin = datetime.datetime.utcnow() db.session.commit() session['account_id'] = account.id return redirect(url_for('index')) @app.route('/oauth/twitter/login') def oauth_twitter_login(): """ Authentication against Twitter starts here. .. IMPORTANT:: The callback URL **MUST** be randomized (say, through a session UUID or CSRF token, but not either of those two), or the fact a visitor is allowed to authenticate using the third party establishes an exploit path -- the third party holds all the data necessary to authenticate on the user's behalf and fully controls the infrastructure against which the user must hypothetically authenticate. """ oauth_uuid = uuid.uuid4().hex db.session.add( Session( uuid = session.get('uuid'), oauth_uuid = oauth_uuid, name = 'twitter', expires = datetime.datetime.utcnow() + datetime.timedelta(seconds=30) ) ) db.session.commit() app.logger.info("Issued %s for %s" % (oauth_uuid, session.get('uuid'))) callback = url_for( 'oauth_twitter_authenticate', uuid = oauth_uuid, _external = True, next = request.args.get('next') or request.referrer or url_for('index') ) return twitter.authorize(callback=callback) @app.route('/oauth/twitter/logout') def oauth_twitter_logout(): session.pop('twitter_oauth_token', None) return redirect(url_for('index')) @app.route('/oauth/twitter/authenticate/') def oauth_twitter_authenticate(uuid=None): """ This is the callback URL to which the user is redirected after successfully authenticating against Facebook. """ if uuid == None or len(uuid) is not 32: app.logger.error("uuid is %r (%d)" % (uuid, len(uuid))) return abort(401) valid_uuids = db.session.query(Session).filter_by( name='twitter', uuid=session.get('uuid'), oauth_uuid=uuid ).filter( Session.expires > datetime.datetime.utcnow() ).all() if len(valid_uuids) is not 1: app.logger.error("seemingly valid UUIDs found: %d" % (len(valid_uuids))) for valid_uuid in valid_uuids: db.session.delete(valid_uuid) db.session.commit() return abort(401) db.session.delete(valid_uuids[0]) db.session.commit() # Example response: # # { # 'oauth_token_secret': u'9rDh18TKUz2tEMQCOQh5so79ZupZUEgbBQABYzsybDPQS', # 'user_id': u'21991789', # 'x_auth_expires': u'0', # 'oauth_token': u'21991789-lqH1oIwQApqzzJMc2B9nw1tWTYvtUPbqjdsZqv3TF', # 'screen_name': u'kanarip' # } resp = twitter.authorized_response() if resp is None: flash('You denied the request to sign in.') return redirect(url_for('login')) if isinstance(resp, OAuthException): return abort(503) else: session['twitter_oauth_token'] = resp account = db.session.query(Account).filter_by(remote_id=resp['user_id'], type_name='twitter').first() if account == None: account = Account(name=resp['screen_name'], remote_id=resp['user_id'], type_name='twitter', locale=get_locale(), timezone=get_timezone()) db.session.add(account) db.session.commit() account = db.session.query(Account).filter_by(name=resp['screen_name'], remote_id=resp['user_id'], type_name='twitter').first() flash("Successfully logged in with Twitter. Welcome, @%s" % resp['screen_name']) else: account.name = resp['screen_name'] flash("Successfully logged in with Twitter. Welcome back @%s" % resp['screen_name']) account.lastlogin = datetime.datetime.utcnow() db.session.commit() session['account_id'] = account.id return redirect(url_for('index')) @app.route('/register') def register(): return "TODO", 200 @app.route('/signup/') def signup(product=None): if product == None: return abort(404) product = db.session.query(Product).options(db.joinedload(Product.translations[get_locale()])).filter_by(signup_enabled=True,key=product).first() if product == None: return abort(404) return render_template('signup/%s.html' % (product.key), product=product) +@app.route('/themes//static/') +def theme_static(themeid, filename): + """ + The use of assets and themes causes asset debugging + to request the wrong URI from the themes hander. + + Firstly, assets stubbornly use a theme URL prefix of + ``/themes``. Registering this in + :py:func:`flask.ext.themes.setup_themes` using the + ``theme_url_prefix`` does not help. + + What we are overriding and/or assisting here is + :py:func:`flask.ext.themes.static`. + Note also that using a route for + ``/themes//`` leads to a duplicate + ``/static/`` because the theme.static_path is set correctly. + """ + try: + ctx = _request_ctx_stack.top + theme = ctx.app.theme_manager.themes[themeid] + except KeyError: + print "Aborting, theme not found" + abort(404) + + return send_from_directory(theme.static_path, filename)