├── test └── stonecutter │ ├── browser │ ├── screenshots │ │ └── .gitkeep │ └── common.clj │ ├── integration │ └── kerodon │ │ ├── kerodon_helpers.clj │ │ └── steps.clj │ ├── run_all_tests.sh │ ├── run_browser_tests.sh │ └── test │ ├── util │ ├── image.clj │ ├── time.clj │ └── ring.clj │ ├── handler.clj │ ├── view │ ├── forgotten_password_confirmation.clj │ ├── error.clj │ ├── authorise_failure.clj │ ├── delete_app.clj │ ├── confirmation_sign_in.clj │ └── unshare_profile_card.clj │ ├── translation.clj │ ├── db │ ├── confirmation.clj │ ├── expiry.clj │ ├── invitations.clj │ └── forgotten_password.clj │ ├── config.clj │ ├── helper.clj │ ├── controller │ └── common.clj │ └── email.clj ├── .midje.clj ├── Procfile ├── test-resources ├── test-translations.yml ├── avatar.png ├── avatar-large.png ├── mail_stub.sh ├── test-client-credentials.yml ├── test-key.json ├── test.vcf └── avatar-encoding.txt ├── config ├── icon.ico ├── clients.yml ├── stonecutter_ansible.env ├── stonecutter.env ├── rsa-keypair.json └── logo.svg ├── ops ├── roles │ ├── mail │ │ ├── vars │ │ │ └── main.yml │ │ ├── tasks │ │ │ └── main.yml │ │ ├── files │ │ │ └── providers │ │ │ │ └── mailgun │ │ └── templates │ │ │ └── send_mail.sh │ ├── common │ │ └── tasks │ │ │ └── main.yml │ ├── nginx │ │ ├── files │ │ │ ├── stonecutter │ │ │ ├── stonecutter.key │ │ │ └── stonecutter.crt │ │ ├── templates │ │ │ └── stonecutter.j2 │ │ └── tasks │ │ │ └── main.yml │ ├── leiningen │ │ └── tasks │ │ │ └── main.yml │ ├── ferm │ │ ├── tasks │ │ │ └── main.yml │ │ └── files │ │ │ └── ferm.conf │ ├── mongo │ │ └── tasks │ │ │ └── main.yml │ ├── stonecutter_application_config │ │ ├── tasks │ │ │ └── main.yml │ │ └── templates │ │ │ └── stonecutter_config.j2 │ ├── docker │ │ └── tasks │ │ │ └── main.yml │ └── dev │ │ └── tasks │ │ └── main.yml ├── development.inventory ├── dob_vm_playbook.yml ├── development_playbook.yml ├── provision-production-stonecutter.sh ├── provision-staging-stonecutter.sh ├── dob_vm.inventory ├── dob.inventory-staging ├── dob.inventory ├── dob_playbook.yml ├── deploy_vagrant.sh ├── deploy_snap.sh └── Vagrantfile ├── assets ├── icons │ ├── dcent-favicon.ico │ └── stonecutter-favicon.ico ├── images │ ├── card-created.png │ ├── card-deleted.png │ ├── demo-logo@2x.png │ └── temp-avatar-300x300.png ├── jade │ ├── mixins │ │ ├── _view_helpers.jade │ │ ├── _flash_message.jade │ │ ├── _helpers.jade │ │ └── _profile.jade │ ├── _user_navigation.jade │ ├── _admin_navigation.jade │ ├── forgot-password-confirmation.jade │ ├── profile-deleted.jade │ ├── authorise-failure.jade │ ├── confirm-email-expired.jade │ ├── confirm-email-resent.jade │ ├── error-500.jade │ ├── forgot-password.jade │ ├── email-demo.jade │ ├── profile-created.jade │ ├── delete-app.jade │ ├── delete-account.jade │ ├── admin-invite-user.jade │ ├── layout │ │ └── layout.jade │ ├── unshare-profile-card.jade │ ├── change-email.jade │ ├── reset-password.jade │ ├── confirmation-sign-in.jade │ ├── user-list.jade │ ├── admin-sign-in.jade │ └── routes.jade └── stylesheets │ ├── components │ ├── _profile_card_description.scss │ ├── _authorise_buttons.scss │ ├── _settings.scss │ ├── _welcome.scss │ ├── _modal.scss │ ├── _verify_email_address.scss │ ├── _search-row.scss │ ├── _authorised_apps.scss │ ├── _admin-app-item.scss │ ├── _header.scss │ ├── _header-nav.scss │ ├── _authorise_text.scss │ ├── _flash_message.scss │ ├── _toggle.scss │ └── _user-list.scss │ ├── utilities │ ├── _forms.scss │ ├── _responsive.scss │ ├── _positioning.scss │ ├── _backgrounds.scss │ ├── _shapes.scss │ └── _typography.scss │ ├── _utilities.scss │ ├── dcent_theme.scss │ ├── core │ ├── _base.scss │ ├── _typography.scss │ └── _grid.scss │ ├── application.scss │ └── _variables.scss ├── npm-postinstall.sh ├── src └── stonecutter │ ├── util │ ├── uuid.clj │ ├── map.clj │ ├── gencred.clj │ ├── ring.clj │ ├── gen_key_pair.clj │ ├── time.clj │ └── image.clj │ ├── db │ ├── token.clj │ ├── confirmation.clj │ ├── forgotten_password.clj │ ├── invitations.clj │ ├── expiry.clj │ ├── client_seed.clj │ └── storage.clj │ ├── logging.clj │ ├── view │ ├── forgotten_password_confirmation.clj │ ├── delete_app.clj │ ├── reset_password.clj │ ├── authorise_failure.clj │ ├── unshare_profile_card.clj │ ├── error.clj │ ├── delete_account.clj │ ├── confirmation_sign_in.clj │ ├── profile_created.clj │ ├── forgotten_password.clj │ ├── invite_user.clj │ ├── change_email.clj │ └── authorise.clj │ ├── routes.clj │ ├── session.clj │ ├── controller │ ├── common.clj │ └── stylesheets.clj │ ├── lein.clj │ ├── admin.clj │ ├── helper.clj │ ├── translation.clj │ ├── middleware.clj │ └── jwt.clj ├── start_app_vm.sh ├── test-cljs └── stonecutter │ └── test │ ├── macros.clj │ └── unit │ └── translations.cljs ├── Dockerfile ├── deploy_heroku.sh ├── test-var └── var │ └── test-client-credentials-file.yml ├── src-cljs └── stonecutter │ └── js │ ├── controller │ ├── client_translations.cljs │ └── user_list.cljs │ └── dom │ ├── common.cljs │ ├── change_password.cljs │ ├── change_profile_form.cljs │ ├── upload_photo.cljs │ └── register_form.cljs ├── .gitignore ├── docs ├── oauth_specs_decisions.md ├── auth_flow_diagram.txt ├── conventions.md └── CONFIG.md ├── resources ├── log4j.dev ├── client-credentials.yml └── lang │ ├── en-client.yml │ └── fi-client.yml ├── deploy_prod.sh ├── package.json ├── LICENSE.md └── README.md /test/stonecutter/browser/screenshots/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.midje.clj: -------------------------------------------------------------------------------- 1 | ;(change-defaults :print-level :print-facts) -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: lein with-profile production trampoline run 2 | -------------------------------------------------------------------------------- /test-resources/test-translations.yml: -------------------------------------------------------------------------------- 1 | a: 2 | hello: Hello 3 | goodbye: Goodbye -------------------------------------------------------------------------------- /config/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d-cent/stonecutter/HEAD/config/icon.ico -------------------------------------------------------------------------------- /ops/roles/mail/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | email_service_dir: /var/stonecutter/email_service 3 | -------------------------------------------------------------------------------- /ops/roles/common/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: update apt 3 | command: sudo apt-get update 4 | 5 | -------------------------------------------------------------------------------- /test-resources/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d-cent/stonecutter/HEAD/test-resources/avatar.png -------------------------------------------------------------------------------- /assets/icons/dcent-favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d-cent/stonecutter/HEAD/assets/icons/dcent-favicon.ico -------------------------------------------------------------------------------- /assets/images/card-created.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d-cent/stonecutter/HEAD/assets/images/card-created.png -------------------------------------------------------------------------------- /assets/images/card-deleted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d-cent/stonecutter/HEAD/assets/images/card-deleted.png -------------------------------------------------------------------------------- /assets/images/demo-logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d-cent/stonecutter/HEAD/assets/images/demo-logo@2x.png -------------------------------------------------------------------------------- /test-resources/avatar-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d-cent/stonecutter/HEAD/test-resources/avatar-large.png -------------------------------------------------------------------------------- /ops/roles/nginx/files/stonecutter: -------------------------------------------------------------------------------- 1 | server { 2 | location / { 3 | proxy_pass http://localhost:5000; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /assets/icons/stonecutter-favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d-cent/stonecutter/HEAD/assets/icons/stonecutter-favicon.ico -------------------------------------------------------------------------------- /assets/images/temp-avatar-300x300.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d-cent/stonecutter/HEAD/assets/images/temp-avatar-300x300.png -------------------------------------------------------------------------------- /config/clients.yml: -------------------------------------------------------------------------------- 1 | - name: Dummy Party 2 | client-id: AAAA 3 | client-secret: AAAA 4 | url: "http://dummyparty.com" 5 | -------------------------------------------------------------------------------- /ops/development.inventory: -------------------------------------------------------------------------------- 1 | default ansible_ssh_host=127.0.0.1 ansible_ssh_port=2222 2 | 3 | [development] 4 | default 5 | 6 | -------------------------------------------------------------------------------- /ops/dob_vm_playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: dob_vm 3 | 4 | roles: 5 | - common 6 | - nginx 7 | - ferm 8 | - docker 9 | -------------------------------------------------------------------------------- /assets/jade/mixins/_view_helpers.jade: -------------------------------------------------------------------------------- 1 | //- initialization 2 | - var blocks = {}; 3 | 4 | mixin set(key) 5 | - blocks[key] = this.block 6 | -------------------------------------------------------------------------------- /npm-postinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | npm rebuild node-sass 4 | npm rebuild optipng-bin 5 | npm rebuild jpegtran-bin 6 | npm run gulp -- build -------------------------------------------------------------------------------- /src/stonecutter/util/uuid.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.util.uuid 2 | (:import (java.util UUID))) 3 | 4 | (defn uuid [] 5 | (str (UUID/randomUUID))) 6 | -------------------------------------------------------------------------------- /start_app_vm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | npm run gulp -- build 3 | lein cljs-build 4 | HOST=0.0.0.0 \ 5 | BASE_URL=http://192.168.50.60:5000 \ 6 | lein run 7 | -------------------------------------------------------------------------------- /ops/development_playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: development 3 | 4 | roles: 5 | - common 6 | - dev 7 | - mongo 8 | - leiningen 9 | 10 | -------------------------------------------------------------------------------- /ops/provision-production-stonecutter.sh: -------------------------------------------------------------------------------- 1 | ansible-playbook dob_playbook.yml -i dob.inventory --extra-vars="CONFIG_FILE_PATH=stonecutter-production-config.env" 2 | -------------------------------------------------------------------------------- /ops/provision-staging-stonecutter.sh: -------------------------------------------------------------------------------- 1 | ansible-playbook dob_playbook.yml -i dob.inventory-staging --extra-vars="CONFIG_FILE_PATH=stonecutter-staging-config.env" 2 | -------------------------------------------------------------------------------- /assets/stylesheets/components/_profile_card_description.scss: -------------------------------------------------------------------------------- 1 | .profile-card-description { 2 | margin-bottom: 30px; 3 | >:last-child { 4 | margin-bottom: 0; 5 | } 6 | } -------------------------------------------------------------------------------- /ops/dob_vm.inventory: -------------------------------------------------------------------------------- 1 | default ansible_ssh_host=127.0.0.1 ansible_ssh_port=2223 2 | 3 | [dob_vm] 4 | default site_address="http://192.168.50.61" cert_location="stonecutter.crt" cert_key_location="stonecutter.key" 5 | -------------------------------------------------------------------------------- /src/stonecutter/db/token.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.db.token 2 | (:require [clauth.token :as cl-token])) 3 | 4 | (defn generate-login-access-token [token-store user] 5 | (:token (cl-token/create-token token-store nil user))) 6 | -------------------------------------------------------------------------------- /ops/dob.inventory-staging: -------------------------------------------------------------------------------- 1 | default ansible_ssh_host=sso-staging.dcentproject.eu 2 | 3 | [dob] 4 | default site_address="https://sso-staging.dcentproject.eu" cert_location="secure/stonecutter.crt" cert_key_location="secure/stonecutter.key" 5 | -------------------------------------------------------------------------------- /test-cljs/stonecutter/test/macros.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.test.macros 2 | (:require [clojure.java.io :as io])) 3 | 4 | (defmacro load-template [r] 5 | "Reads and returns a template as a string." 6 | (slurp (io/resource r))) 7 | 8 | 9 | -------------------------------------------------------------------------------- /ops/dob.inventory: -------------------------------------------------------------------------------- 1 | default ansible_ssh_host=sso.dcentproject.eu 2 | 3 | [dob] 4 | default 5 | 6 | [dob:vars] 7 | site_address="https://sso.dcentproject.eu" 8 | cert_location="secure/stonecutter.crt" 9 | cert_key_location="secure/stonecutter.key" 10 | -------------------------------------------------------------------------------- /src/stonecutter/util/map.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.util.map) 2 | 3 | (defn deep-merge 4 | "Recursively merges maps. If keys are not maps, the last value wins." 5 | [& vals] 6 | (if (every? map? vals) 7 | (apply merge-with deep-merge vals) 8 | (last vals))) -------------------------------------------------------------------------------- /test-resources/mail_stub.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | email_address=$1 4 | subject=$2 5 | body=$3 6 | 7 | echo "{:email-address \""$email_address"\"" \ 8 | " :subject \""$subject"\"" \ 9 | " :body "$body"}" \ 10 | > test-tmp/test-email.txt 11 | -------------------------------------------------------------------------------- /assets/jade/_user_navigation.jade: -------------------------------------------------------------------------------- 1 | a.header-nav__link.clj--profile__link(href="./profile", data-l8n="content:profile/profile-link") !Profile 2 | a.header-nav__link.clj--sign-out__link.func--sign-out__link(href="./sign-out", data-l8n="content:profile/sign-out-link") !Sign out 3 | -------------------------------------------------------------------------------- /assets/stylesheets/utilities/_forms.scss: -------------------------------------------------------------------------------- 1 | @mixin placeholder { 2 | &::-webkit-input-placeholder { 3 | @content 4 | } 5 | &:-moz-placeholder { 6 | @content 7 | } 8 | &::-moz-placeholder { 9 | @content 10 | } 11 | &:-ms-input-placeholder { 12 | @content 13 | } 14 | } -------------------------------------------------------------------------------- /ops/dob_playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: dob 3 | sudo: true 4 | remote_user: root 5 | 6 | vars_files: 7 | - "{{ CONFIG_FILE_PATH }}" 8 | 9 | roles: 10 | - common 11 | - nginx 12 | - ferm 13 | - mail 14 | - docker 15 | - stonecutter_application_config 16 | -------------------------------------------------------------------------------- /src/stonecutter/util/gencred.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.util.gencred 2 | (:require [crypto.random :as random])) 3 | 4 | (defn -main 5 | [& args] 6 | (let [client-id (random/base32 20) 7 | client-secret (random/base32 20)] 8 | (prn :client-id client-id 9 | :client-secret client-secret))) -------------------------------------------------------------------------------- /src/stonecutter/logging.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.logging 2 | (:require [clj-logging-config.log4j :as c])) 3 | 4 | (defn init-logger! [] 5 | (c/set-loggers! 6 | ["stonecutter"] 7 | {:name "logger" 8 | :level :debug 9 | :pattern "%d{yyyy-MM-dd HH:mm:ss} %-5p %c:%L - %m%n"})) 10 | -------------------------------------------------------------------------------- /ops/roles/leiningen/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: install jdk7 3 | apt: name="openjdk-7-jdk" state=present 4 | 5 | - name: create /home/vagrant/bin 6 | command: "mkdir -p /home/vagrant/bin" 7 | 8 | - name: copy lein script 9 | copy: src="lein" dest="/home/vagrant/bin/lein" mode=0755 10 | 11 | -------------------------------------------------------------------------------- /src/stonecutter/view/forgotten_password_confirmation.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.view.forgotten-password-confirmation 2 | (:require [stonecutter.view.view-helpers :as vh])) 3 | 4 | (defn forgotten-password-confirmation [request] 5 | (vh/load-template-with-lang "public/forgot-password-confirmation.html" request)) 6 | -------------------------------------------------------------------------------- /assets/stylesheets/components/_authorise_buttons.scss: -------------------------------------------------------------------------------- 1 | .authorise-buttons { 2 | @include columns; 3 | padding-top: 20px; 4 | padding-bottom: 20px; 5 | &__primary { 6 | @include column-two-thirds($medium_device); 7 | } 8 | &__secondary { 9 | @include column-one-third($medium_device); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /assets/stylesheets/_utilities.scss: -------------------------------------------------------------------------------- 1 | @import "utilities/backgrounds"; 2 | @import "utilities/positioning"; 3 | @import "utilities/responsive"; 4 | @import "utilities/shapes"; 5 | @import "utilities/typography"; 6 | @import "utilities/forms"; 7 | 8 | 9 | @mixin calc($property, $expression) { 10 | #{$property}: calc(#{$expression}); 11 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM dcent/clojure-npm-grunt-gulp 2 | 3 | COPY . /usr/src/app 4 | RUN lein with-profile production deps && \ 5 | npm install && \ 6 | npm install gulp-imagemin && \ 7 | gulp build && \ 8 | lein uberjar 9 | 10 | WORKDIR /usr/src/app/target 11 | 12 | CMD java -Xmx231m -jar stonecutter-0.1.0-SNAPSHOT-standalone.jar 13 | -------------------------------------------------------------------------------- /test/stonecutter/integration/kerodon/kerodon_helpers.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.integration.kerodon.kerodon-helpers) 2 | 3 | (defn print-enlive [state] 4 | (prn (-> state :enlive)) 5 | state) 6 | 7 | (defn print-request [state] 8 | (prn (-> state :request)) 9 | state) 10 | 11 | (defn print-state [state] 12 | (prn state) 13 | state) 14 | -------------------------------------------------------------------------------- /test/stonecutter/run_all_tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | npm run gulp -- build 3 | lein cljs-build 4 | if [ -z "$DISPLAY" ] 5 | then 6 | start-stop-daemon --start -b -x /usr/bin/Xvfb -- :1 -screen 0 1280x1024x16 7 | DISPLAY=:1 lein do clean, midje $* 8 | start-stop-daemon --stop -x /usr/bin/Xvfb 9 | else 10 | lein do clean, midje $* 11 | fi -------------------------------------------------------------------------------- /deploy_heroku.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | heroku git:remote --app stonecutter 6 | heroku maintenance:on 7 | heroku buildpacks:clear 8 | heroku buildpacks:set https://github.com/heroku/heroku-buildpack-clojure 9 | heroku buildpacks:add --index 1 https://github.com/heroku/heroku-buildpack-nodejs 10 | git push heroku master 11 | heroku maintenance:off 12 | 13 | -------------------------------------------------------------------------------- /assets/stylesheets/components/_settings.scss: -------------------------------------------------------------------------------- 1 | .settings { 2 | margin-bottom: 45px; 3 | } 4 | 5 | .settings__list { 6 | @include reset-list; 7 | } 8 | 9 | .settings__item { 10 | @include font-size($paragraph_font_size); 11 | margin-top: 0; 12 | margin-bottom: 10px; 13 | line-height: 1.3; 14 | } 15 | 16 | .settings__link { 17 | text-decoration: none; 18 | } 19 | -------------------------------------------------------------------------------- /test-var/var/test-client-credentials-file.yml: -------------------------------------------------------------------------------- 1 | - name: Yellow File Party 2 | client-id: 7I6DTMPXGESEJ2AEEWVGZF2B5AOFGK6D 3 | client-secret: ABH7ZKGVMKQQA3OITBOXPBVSZMMD5NGS 4 | url: "http://yellowfile.org" 5 | 6 | - name: Blue File Party 7 | client-id: F7ZFMTRJLS5FF6DTWMBCGWA7CXEQTH24 8 | client-secret: ISLS5HVYP65QE6OROI43FLHFFOBGISOG 9 | url: "http://bluefile.org" 10 | -------------------------------------------------------------------------------- /test-resources/test-client-credentials.yml: -------------------------------------------------------------------------------- 1 | - name: Green Resource Party 2 | client-id: ZW76L2MGCVMQBN44VYRE7MS5JOZMUE2Z 3 | client-secret: WUSB7HHQIYEZNIGZ4HT4BSHEEAYCCKV 4 | url: "http://greenresource.org" 5 | 6 | - name: Red Resource Party 7 | client-id: M4DPY7IO5KZ77KRHMXYTACUZEEZEK4FH 8 | client-secret: VZ27BQ5GWLWXVFQQBPGDIPY4QTDEUZNM 9 | url: "https://redresource.org" 10 | -------------------------------------------------------------------------------- /assets/stylesheets/components/_welcome.scss: -------------------------------------------------------------------------------- 1 | .welcome { 2 | @include width-from($smallish_device) { 3 | padding-top: 0.65rem; 4 | } 5 | &__title { 6 | @include h1; 7 | } 8 | &__intro { 9 | @include font-size(17px); 10 | margin-top: 0; 11 | line-height: 1.3; 12 | @include width-from($medium_device) { 13 | @include font-size(21px); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/stonecutter/util/ring.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.util.ring 2 | (:require [stonecutter.session :as session])) 3 | 4 | (defn complete-uri-of [request] 5 | (str (:uri request) 6 | (when (:query-string request) "?") 7 | (:query-string request))) 8 | 9 | (defn preserve-session [response request] 10 | (-> response 11 | (session/replace-session-with (:session request)))) 12 | -------------------------------------------------------------------------------- /src-cljs/stonecutter/js/controller/client_translations.cljs: -------------------------------------------------------------------------------- 1 | (ns stonecutter.js.controller.client_translations 2 | (:require [taoensso.tower :as tower]) 3 | (:require-macros [taoensso.tower :as tower-macros])) 4 | 5 | (def ^:private tconfig 6 | {:fallback-locale :en 7 | :compiled-dictionary (tower-macros/dict-compile "lang/client_translations.clj")}) 8 | 9 | (def t (tower/make-t tconfig)) 10 | -------------------------------------------------------------------------------- /test/stonecutter/run_browser_tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | npm run gulp -- build 3 | lein cljs-build 4 | if [ -z "$DISPLAY" ] 5 | then 6 | start-stop-daemon --start -b -x /usr/bin/Xvfb -- :1 -screen 0 1280x1024x16 7 | DISPLAY=:1 lein do clean, midje stonecutter.browser.* $* 8 | start-stop-daemon --stop -x /usr/bin/Xvfb 9 | else 10 | lein do clean, midje stonecutter.browser.* $* 11 | fi -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /lib 3 | /classes 4 | /checkouts 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | .hgignore 12 | .hg/ 13 | .DS_Store 14 | .idea 15 | public 16 | node_modules 17 | npm-debug.log 18 | *.iml 19 | *.swp 20 | */.vagrant/* 21 | ops/roles/nginx/files/secure** 22 | /test-tmp 23 | .dir-locals.el 24 | .cljs_rhino_repl 25 | /data 26 | test/stonecutter/browser/screenshots/*.png -------------------------------------------------------------------------------- /ops/roles/ferm/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install ferm 3 | apt: name=ferm state=present 4 | 5 | - name: Add ferm config directory 6 | file: path=/etc/ferm state=directory 7 | owner=root group=root mode=0700 8 | 9 | - name: add the ferm.conf file to /etc/ferm 10 | copy: src=ferm.conf dest=/etc/ferm/ferm.conf 11 | owner=root group=root mode=0700 12 | 13 | - name: run ferm 14 | command: ferm /etc/ferm/ferm.conf 15 | -------------------------------------------------------------------------------- /docs/oauth_specs_decisions.md: -------------------------------------------------------------------------------- 1 | # Decisions Log 2 | 3 | ## Introduction 4 | This file is to track decisions made when designing Stonecutter to comply with OAuth2.0 specifications. 5 | 6 | ### Decisions and Discussion notes 7 | 8 | #### Date July 20, 2015 9 | To register with Stonecutter, a registering client must provide a full url to their site (e.g. "http://stonecutter-client.herokuapp.com") 10 | - the url must include "http://" or "https://" prefix 11 | -------------------------------------------------------------------------------- /test/stonecutter/test/util/image.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.test.util.image 2 | (:require [midje.sweet :refer :all] 3 | [stonecutter.util.image :as i])) 4 | 5 | (tabular 6 | (fact "the correct file type is extracted from the image encoding" 7 | (i/picture-type ?image) => ?filetype) 8 | ?image ?filetype 9 | "data:image/jpeg;base64,ABCDEFGHIJKL" "JPEG" 10 | "data:image/png;base64,MNOPQRSTUVWXYZ" "PNG") -------------------------------------------------------------------------------- /assets/stylesheets/components/_modal.scss: -------------------------------------------------------------------------------- 1 | .modal { 2 | &__content { 3 | margin-bottom: 30px; 4 | } 5 | &__title { 6 | @include h1; 7 | margin-bottom: 32px; 8 | } 9 | } 10 | .modal-buttons { 11 | @include columns; 12 | padding-top: 20px; 13 | padding-bottom: 20px; 14 | &__primary { 15 | @include column-two-thirds($medium_device); 16 | } 17 | &__secondary { 18 | @include column-one-third($medium_device); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /resources/log4j.dev: -------------------------------------------------------------------------------- 1 | # Root logger option 2 | log4j.rootLogger=DEBUG, stdout 3 | log4j.logger.org.apache.http=WARN 4 | log4j.logger.org.eclipse.jetty=WARN 5 | 6 | # Direct log messages to stdout 7 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 8 | log4j.appender.stdout.Target=System.out 9 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 10 | log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c:%L - %m%n 11 | -------------------------------------------------------------------------------- /test/stonecutter/test/util/time.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.test.util.time 2 | (:require [stonecutter.util.time :as time])) 3 | 4 | (defprotocol SettableClock 5 | (update-time [this f])) 6 | 7 | (deftype StubClock [time-atom] 8 | time/Clock 9 | (now-in-millis [this] 10 | @time-atom) 11 | SettableClock 12 | (update-time [this f] 13 | (swap! time-atom f))) 14 | 15 | (defn new-stub-clock [start-time-millis] 16 | (StubClock. (atom start-time-millis))) -------------------------------------------------------------------------------- /assets/stylesheets/components/_verify_email_address.scss: -------------------------------------------------------------------------------- 1 | .verify-email-address { 2 | margin-bottom: 2rem; 3 | &__content { 4 | margin-top: -2rem; 5 | padding: 1.5rem; 6 | border: 1px solid darken($error_color,8%); 7 | border-top: 0; 8 | border-radius: 0 0 3px 3px; 9 | } 10 | } 11 | 12 | .verify__title { 13 | margin-top: 0; 14 | @include font-size(20px); 15 | font-weight: bold; 16 | line-height: 1.2; 17 | color: $default_text_color; 18 | } -------------------------------------------------------------------------------- /assets/stylesheets/utilities/_responsive.scss: -------------------------------------------------------------------------------- 1 | @mixin width-from($width) { 2 | @media screen and (min-width: $width) 3 | { 4 | @content; 5 | } 6 | } 7 | 8 | @mixin width-between($min_width, $max_width) { 9 | @media only screen and (min-width: $min_width) and (max-width: $max_width) 10 | { 11 | @content; 12 | } 13 | } 14 | 15 | @mixin width-to($width) { 16 | @media 17 | only screen and (max-width: $width) 18 | { 19 | @content; 20 | } 21 | } -------------------------------------------------------------------------------- /ops/roles/mongo/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: import mongo key 3 | apt_key: keyserver=keyserver.ubuntu.com id=7F0CEB10 4 | 5 | - shell: 6 | echo "deb http://repo.mongodb.org/apt/ubuntu "$(lsb_release -sc)"/mongodb-org/3.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.0.list 7 | creates=/etc/apt/sources.list.d/mongodb-org-3.0.list 8 | 9 | - name: install mongo 10 | apt: name=mongodb-org update_cache=yes 11 | 12 | - name: start mongo 13 | service: name=mongod state=started -------------------------------------------------------------------------------- /test/stonecutter/test/util/ring.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.test.util.ring 2 | (:require [midje.sweet :refer :all] 3 | [ring.mock.request :as mock-request] 4 | [stonecutter.util.ring :as util-ring])) 5 | 6 | (tabular 7 | (fact "complete-uri rebuilds the requested uri from a ring request map, not including the host name" 8 | (util-ring/complete-uri-of (mock-request/request :get ?uri)) => ?uri) 9 | ?uri 10 | "/no-query" 11 | "/query?a=1" 12 | "/multiple-query?a=1&b=2" 13 | "relative") 14 | -------------------------------------------------------------------------------- /ops/deploy_vagrant.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ssh vagrant@192.168.50.61 < (mock/request :get "/blah") handler) => :site 12 | (-> (mock/request :get "/api/blah") handler) => :api)) 13 | -------------------------------------------------------------------------------- /assets/jade/mixins/_flash_message.jade: -------------------------------------------------------------------------------- 1 | mixin flashMessage(messageType, modifierClasses) 2 | - modifiers = "" 3 | - modifierClasses = modifierClasses || [] 4 | 5 | each cssClass in modifierClasses 6 | - modifiers += " flash-message--" + cssClass 7 | 8 | .flash-message(class="flash-message--#{messageType} #{modifiers}") 9 | block 10 | 11 | mixin flashMessageWithCljContainer(cljClass, funcClass, messageType, modifierClasses) 12 | div(class="#{cljClass} #{funcClass}") 13 | +flashMessage(messageType, modifierClasses) 14 | block -------------------------------------------------------------------------------- /assets/jade/mixins/_helpers.jade: -------------------------------------------------------------------------------- 1 | mixin javascriptIncludeTag(path, type) 2 | - type = type || 'text/javascript' 3 | script(src="#{javascriptsBase}/#{path}.js", type="#{type}") 4 | 5 | mixin stylesheetLinkTag(path, media) 6 | link(href="#{stylesheetsBase}/#{path}.css", media="#{media}", rel="stylesheet", type="text/css") 7 | 8 | mixin imageIncludeTag(path, cssClasses, attrs) 9 | - cssClasses = cssClasses || '' 10 | - attrs = attrs || {} //&attributes({ name:'foo', value:'bar', foo:'bad' }) 11 | img(src="#{imagesBase}/#{path}", class="#{cssClasses}")&attributes(attrs) -------------------------------------------------------------------------------- /src/stonecutter/routes.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.routes 2 | (:require [clojure.tools.logging :as log] 3 | [scenic.routes :as scenic] 4 | [bidi.bidi :as bidi])) 5 | 6 | (def routes (scenic/load-routes-from-file "routes.txt")) 7 | 8 | (defn path [action & params] 9 | (try 10 | (apply bidi/path-for routes action params) 11 | (catch Exception e 12 | (log/warn (format "Key: '%s' probably does not match a route.\n%s" action e)) 13 | (throw (Exception. (format "Error constructing url for action '%s', with params '%s'" action params)))))) 14 | -------------------------------------------------------------------------------- /assets/stylesheets/dcent_theme.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Setup theme colors 3 | // 4 | $color_1: #1f1f1f; 5 | $color_2: #fff; 6 | // add more colors if you like... 7 | 8 | // 9 | // These all exist within the app - only change their value :) 10 | // 11 | $header_background_color: $color_1; 12 | $header_logo_image_path: 'd-cent.svg'; 13 | // 14 | $tab_bg_color: $color_2; 15 | $tab_bg_color_active: $color_2; 16 | $tab_color_active: $color_1; 17 | $tab_link_bg_colour_hover: rgba($color_2, 0.5); 18 | $tab_link_colour_hover: darken($color_1,10%); 19 | 20 | @import 'application'; // do not delete! 21 | -------------------------------------------------------------------------------- /assets/jade/_admin_navigation.jade: -------------------------------------------------------------------------------- 1 | a.header-nav__link.clj--profile__link(href="./profile", data-l8n="content:admin/nav-profile") !Profile 2 | a.header-nav__link.clj--apps-list__link.func--show-apps__link(href="./apps", data-l8n="content:admin/nav-apps") !Apps 3 | a.header-nav__link.clj--user-list__link.func--show-users__link(href="./admin/users", data-l8n="content:admin/nav-users") !Users 4 | a.header-nav__link.clj--invite__link.func--invite__link(href="./invite", data-l8n="content:admin/nav-invite-user") !Invite 5 | a.header-nav__link.clj--sign-out__link.func--sign-out__link(href="./sign-in", data-l8n="content:profile/sign-out-link") !Sign out -------------------------------------------------------------------------------- /assets/jade/mixins/_profile.jade: -------------------------------------------------------------------------------- 1 | mixin authorisedAppItemEmpty() 2 | li.app__item.clj--authorised-app__list-item--empty 3 | .app__title(data-l8n="content:applications/list-empty") !No applications use your Profile Card 4 | 5 | mixin authorisedAppItem(appName) 6 | li.app__item.clj--authorised-app__list-item 7 | .app__title.clj--client-name #{appName} 8 | .app__remove 9 | a.button.button--remove.clj--app-item__unshare-link.func--app-item__unshare-link(href="./unshare-profile-card") 10 | i.fa.fa-remove.button__icon 11 | span.button__text(data-l8n="content:applications/unshare-button") !Unshare Card 12 | -------------------------------------------------------------------------------- /config/stonecutter_ansible.env: -------------------------------------------------------------------------------- 1 | --- 2 | HOST: "0.0.0.0" 3 | PORT: "5000" 4 | BASE_URL: "" 5 | CLIENT_CREDENTIALS_FILE_PATH: "" 6 | APP_NAME: "" 7 | HEADER_BG_COLOR: "" 8 | HEADER_FONT_COLOR: "" 9 | HEADER_FONT_COLOR_HOVER: "" 10 | STATIC_RESOURCES_DIR_PATH: "" 11 | LOGO_FILE_NAME: "" 12 | FAVICON_FILE_NAME: "" 13 | ADMIN_FIRST_NAME: "" 14 | ADMIN_LAST_NAME: "" 15 | ADMIN_LOGIN: "" 16 | ADMIN_PASSWORD: "" 17 | PASSWORD_RESET_EXPIRY: "" 18 | OPEN_ID_CONNECT_ID_TOKEN_LIFETIME_MINUTES: "" 19 | INVITE_EXPIRY: "" 20 | RSA_KEYPAIR_FILE_PATH: "" 21 | EMAIL_SCRIPT_PATH: "" 22 | EMAIL_SERVICE_PROVIDER: "" 23 | EMAIL_DOMAIN_NAME: "" 24 | MAILGUN_API_KEY: "" -------------------------------------------------------------------------------- /src/stonecutter/util/gen_key_pair.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.util.gen-key-pair 2 | (:require [stonecutter.jwt :as jwt])) 3 | 4 | (defn -main [& args] 5 | (let [key-id (first args) 6 | key-pair (jwt/generate-rsa-key-pair key-id)] 7 | (println) 8 | (println) 9 | (println "JWK public key for client:") 10 | (println "==========================") 11 | (println (jwt/key-pair->json key-pair)) 12 | (println) 13 | (println) 14 | (println "JWK including private key for stonecutter:") 15 | (println "==========================================") 16 | (println (jwt/key-pair->json key-pair :include-private-key)))) 17 | -------------------------------------------------------------------------------- /assets/jade/forgot-password-confirmation.jade: -------------------------------------------------------------------------------- 1 | extends layout/layout 2 | 3 | block vars 4 | - bodyClass = "func--forgotten-password-confirmation-page" 5 | - pageTitle = "!Forgotten password - email sent" 6 | - pageTitleDataL8n = "content:forgot-password-confirmation/title" 7 | 8 | block headerScripts 9 | 10 | block content 11 | .single-column 12 | h1.page-title(data-l8n="content:forgot-password-confirmation/page-header") !Email sent 13 | p(data-l8n="content:forgot-password-confirmation/helper-text") !Assuming you gave us the correct email address, we sent you a link! 14 | a.button(href="/", data-l8n="content:forgot-password-confirmation/home-link") !Return home 15 | -------------------------------------------------------------------------------- /src/stonecutter/db/confirmation.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.db.confirmation 2 | (:require [clauth.store :as cl-store] 3 | [stonecutter.db.mongo :as sm])) 4 | 5 | (defn store! [confirmation-store login confirmation-id] 6 | (cl-store/store! confirmation-store :confirmation-id {:login login :confirmation-id confirmation-id})) 7 | 8 | (defn fetch [confirmation-store confirmation-id] 9 | (cl-store/fetch confirmation-store confirmation-id)) 10 | 11 | (defn revoke! [confirmation-store confirmation-id] 12 | (cl-store/revoke! confirmation-store confirmation-id)) 13 | 14 | (defn retrieve-by-user-email [confirmation-store login] 15 | (first (sm/query confirmation-store {:login login}))) 16 | -------------------------------------------------------------------------------- /assets/stylesheets/utilities/_positioning.scss: -------------------------------------------------------------------------------- 1 | @mixin clearfix { 2 | zoom:1; 3 | &:before, 4 | &:after{ 5 | display:table; 6 | content:""; 7 | } 8 | &:after{ 9 | clear:both; 10 | } 11 | } 12 | 13 | @mixin min-height($height) { 14 | min-height: $height; 15 | height: auto !important; 16 | height: $height; 17 | } 18 | 19 | @mixin size($width,$height:$width) { 20 | width: $width; 21 | height: $height; 22 | } 23 | 24 | @mixin vertical-middle-me() { 25 | position: absolute; 26 | top: 50%; 27 | transform: translateY(-50%); 28 | } 29 | 30 | @mixin middle-me() { 31 | position: absolute; 32 | top: 50%; 33 | left: 50%; 34 | transform: translate(-50%, -50%); 35 | } -------------------------------------------------------------------------------- /assets/stylesheets/utilities/_backgrounds.scss: -------------------------------------------------------------------------------- 1 | @function image-url($file) { 2 | @return url("../images/#{$file}"); 3 | } 4 | 5 | @mixin background-image-retina($file, $type, $width, $height) { 6 | background-image: image-url($file + '.' + $type); 7 | @media only screen and (-webkit-min-device-pixel-ratio: 2), 8 | only screen and (-moz-min-device-pixel-ratio: 2), 9 | only screen and (-o-min-device-pixel-ratio: 2/1), 10 | only screen and (min-device-pixel-ratio: 2), 11 | only screen and (min-resolution: 192dpi), 12 | only screen and (min-resolution: 2dppx){ 13 | & { 14 | background-image: image-url($file + '@2x.' + $type); 15 | background-size: $width $height; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /test/stonecutter/test/view/forgotten_password_confirmation.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.test.view.forgotten-password-confirmation 2 | (:require [midje.sweet :refer :all] 3 | [stonecutter.view.forgotten-password-confirmation :as fpc] 4 | [net.cgrand.enlive-html :as html] 5 | [stonecutter.routes :as r] 6 | [stonecutter.test.view.test-helpers :as th])) 7 | 8 | (fact 9 | (th/test-translations "Forgotten password email sent confirmation" fpc/forgotten-password-confirmation)) 10 | 11 | (fact "Forgotten password confirmation page has content" 12 | (-> (fpc/forgotten-password-confirmation {}) 13 | (html/select [:.func--forgotten-password-confirmation-page])) =not=> empty?) 14 | -------------------------------------------------------------------------------- /ops/roles/nginx/templates/stonecutter.j2: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | return 301 {{ site_address }}$request_uri; 4 | } 5 | 6 | server { 7 | listen 443 ssl; 8 | 9 | ssl_certificate /etc/nginx/ssl/stonecutter.crt; 10 | ssl_certificate_key /etc/nginx/ssl/stonecutter.key; 11 | 12 | ssl_session_cache shared:SSL:32m; 13 | ssl_session_timeout 10m; 14 | 15 | ssl_dhparam /etc/nginx/cert/dhparam.pem; 16 | ssl_protocols TLSv1.2 TLSv1.1 TLSv1; 17 | 18 | location / { 19 | proxy_pass http://localhost:5000; 20 | proxy_set_header Host $host; 21 | proxy_set_header X-Real-IP $remote_addr; 22 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 23 | proxy_set_header X-Forwarded-Proto $scheme; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /assets/stylesheets/components/_search-row.scss: -------------------------------------------------------------------------------- 1 | .search-row { 2 | position: relative; 3 | margin-top: 0; 4 | margin-bottom: 1rem; 5 | &__icon { 6 | position: absolute; 7 | top: 12px; 8 | left: 0; 9 | @include font-size(20px); 10 | } 11 | &__input { 12 | width: 100%; 13 | padding: 0.75rem 1.5rem 0.75rem 1.65rem; 14 | background: transparent; 15 | @include font-size(20px); 16 | font-weight: bold; 17 | border: none; 18 | border-bottom: 2px solid $light_grey; 19 | &:focus { 20 | outline: 0; 21 | border-color: $medium_cyan; 22 | } 23 | &:-webkit-autofill { 24 | -webkit-box-shadow: 0 0 0 1000px white inset; 25 | -webkit-text-fill-color: $dark_cyan; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/stonecutter/util/time.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.util.time 2 | (:require [clj-time.core :as t] 3 | [clj-time.format :as f] 4 | [clj-time.coerce :as c])) 5 | 6 | (def sec 1000) 7 | (def minute (* 60 sec)) 8 | (def hour (* 60 minute)) 9 | (def day (* 24 hour)) 10 | 11 | (defn- -now-in-millis [] 12 | (-> (t/now) c/to-long)) 13 | 14 | (defprotocol Clock 15 | (now-in-millis [this])) 16 | 17 | (deftype JodaClock [] 18 | Clock 19 | (now-in-millis [this] 20 | (-now-in-millis))) 21 | 22 | (defn new-clock [] 23 | (JodaClock. )) 24 | 25 | (defn now-plus-hours-in-millis [clock plus-hours] 26 | (-> (now-in-millis clock) 27 | (c/from-long) 28 | (t/plus (t/hours plus-hours)) 29 | c/to-long)) 30 | 31 | (def to-epoch c/to-epoch) 32 | -------------------------------------------------------------------------------- /assets/jade/profile-deleted.jade: -------------------------------------------------------------------------------- 1 | extends layout/layout 2 | 3 | block vars 4 | - bodyClass = "func--profile-deleted-page" 5 | - pageTitle = "!Profile deleted" 6 | - pageTitleDataL8n = "content:profile-deleted/title" 7 | 8 | block headerScripts 9 | 10 | block content 11 | .three-fourths 12 | h1.page-title(data-l8n="content:profile-deleted/page-header") 13 | | !Your Profile has been deleted 14 | 15 | .card-demo 16 | .card-demo__deleted 17 | 18 | p 19 | span(data-l8n="content:profile-deleted/intro") !All your Profile Cards and data have been completely removed. 20 | 21 | a.button.button--primary.func--profile-deleted-next__button.clj--profile-deleted-next__button(href="./register", data-l8n="content:profile-deleted/action-button") !Register 22 | -------------------------------------------------------------------------------- /resources/client-credentials.yml: -------------------------------------------------------------------------------- 1 | - name: Test-Client Party App 2 | client-id: TESTCLIENTPARTYAPPID123 3 | client-secret: TESTCLIENTPARTYAPPSECRET123 4 | url: "http://localhost:4000" 5 | 6 | - name: Local Journey Test Client 7 | client-id: LOCALJOURNEYTEST 8 | client-secret: SECRET 9 | url: "http://192.168.50.70:4000" 10 | 11 | - name: Local Mooncake 12 | client-id: LOCALMOONCAKE 13 | client-secret: MOONCAKESECRET 14 | url: "http://192.168.50.71:4000" 15 | 16 | - name: Heroku Secure Client App 17 | client-id: JPIJJLNTNODGGXW7UHIIUOESBGTZUW2C 18 | client-secret: F6AE4ILQPOGWIKJKJS4NP43UH47ZV5JC 19 | url: "https://stonecutter-client.herokuapp.com" 20 | 21 | - name: Test Objective8 22 | client-id: OBJECTIVE8 23 | client-secret: SECRET 24 | url: "http://192.168.50.50:8080" 25 | 26 | 27 | -------------------------------------------------------------------------------- /deploy_prod.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ssh $REMOTE_USER@$SERVER_IP <> (vh/load-template-with-lang "public/delete-app.html" request) 15 | (set-form-action (r/path :delete-app :app-id (get-in request [:params :app-id]))) 16 | (set-cancel-link (r/path :show-apps-list)) 17 | vh/add-anti-forgery 18 | vh/remove-work-in-progress)) 19 | -------------------------------------------------------------------------------- /src/stonecutter/view/reset_password.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.view.reset-password 2 | (:require [net.cgrand.enlive-html :as html] 3 | [stonecutter.routes :as r] 4 | [stonecutter.view.view-helpers :as vh] 5 | [stonecutter.view.change-password :as cp])) 6 | 7 | (defn set-form-action [enlive-m forgotten-password-id] 8 | (html/at enlive-m [:form] (html/set-attr :action (r/path :reset-password :forgotten-password-id forgotten-password-id)))) 9 | 10 | (defn reset-password-form [request] 11 | (let [err (get-in request [:context :errors]) 12 | forgotten-password-id (get-in request [:params :forgotten-password-id] "no-id-provided")] 13 | (-> 14 | (vh/load-template-with-lang "public/reset-password.html" request) 15 | (set-form-action forgotten-password-id) 16 | vh/add-anti-forgery 17 | (cp/add-errors err)))) 18 | -------------------------------------------------------------------------------- /assets/stylesheets/core/_base.scss: -------------------------------------------------------------------------------- 1 | .skip-link { 2 | padding: 1em; 3 | position: absolute; 4 | top: -60px; 5 | left: 1em; 6 | z-index: 1000; 7 | color: $darker_grey; 8 | transition: top 250ms ease-in-out; 9 | &:focus { 10 | top: 1em; 11 | background: #fff; 12 | } 13 | &:hover { 14 | outline: auto 5px -webkit-focus-ring-color; 15 | } 16 | } 17 | .main-content { 18 | max-width: $large_device; 19 | margin: 0 auto; 20 | padding-left: $main-content--side-padding; 21 | padding-right: $main-content--side-padding; 22 | padding-bottom: 1rem; 23 | @include width-from($medium_device) { 24 | padding-bottom: 2rem; 25 | } 26 | } 27 | .max-medium { 28 | max-width: $medium_device; 29 | } 30 | .middle-container { 31 | padding: 0 1rem; 32 | margin: 0 auto 3rem auto; 33 | max-width: $large_device; 34 | &--max-medium { 35 | max-width: $medium_device; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /assets/jade/authorise-failure.jade: -------------------------------------------------------------------------------- 1 | extends layout/layout 2 | 3 | block vars 4 | - bodyClass = "func--authorise-failure-page" 5 | - pageTitle = "!Authorise failed" 6 | - pageTitleDataL8n = "content:authorise-fail/title" 7 | 8 | block headerScripts 9 | 10 | block content 11 | .column.three-fourths 12 | h1.page-title 13 | span(data-l8n="content:authorise-fail/page-intro") !You have not signed in to 14 | = " " 15 | span.clj--client-name Green Party Voting 16 | p 17 | span.clj--client-name Green Party Voting 18 | = " " 19 | span(data-l8n="content:authorise-fail/page-intro-ending") !can not have access to your Profile Card and cannot sign in. 20 | 21 | a.button.button--secondary.clj--redirect-to-client-home__link.func--redirect-to-client-home__link(href="#{demoAppURL}") 22 | | Return to 23 | = " " 24 | span.clj--client-name Green Party Voting 25 | -------------------------------------------------------------------------------- /src/stonecutter/db/invitations.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.db.invitations 2 | (:require [clauth.store :as cl-store] 3 | [stonecutter.db.mongo :as sm] 4 | [stonecutter.util.uuid :as uuid] 5 | [stonecutter.db.expiry :as e] 6 | [stonecutter.util.time :as time])) 7 | 8 | (defn generate-invite-id! [invite-store email clock expiry-days id-generation-fn] 9 | (let [invite-id (id-generation-fn) 10 | expiry (* expiry-days time/day)] 11 | (e/store-with-expiry! invite-store clock :invite-id {:email email :invite-id invite-id} expiry) 12 | invite-id)) 13 | 14 | (defn fetch-by-id [invite-store id] 15 | (first (sm/query invite-store {:invite-id id}))) 16 | 17 | (defn fetch-by-email [invite-store email] 18 | (first (sm/query invite-store {:email email}))) 19 | 20 | (defn remove-invite! [invite-store invite-id] 21 | (cl-store/revoke! invite-store invite-id)) 22 | 23 | -------------------------------------------------------------------------------- /assets/jade/confirm-email-expired.jade: -------------------------------------------------------------------------------- 1 | extends layout/layout 2 | 3 | block vars 4 | - bodyClass = "func--confirm-email-expired-page" 5 | - pageTitle = "!Confirmation link expired" 6 | - pageTitleDataL8n = "content:confirm-email-expired/title" 7 | 8 | block headerScripts 9 | 10 | block content 11 | .single-column 12 | h1.page-title(data-l8n="content:confirm-email-expired/page-header") !Your confirmation email link has expired 13 | p(data-l8n="content:confirm-email-expired/helper-text-1") !It's been a while since you created your account. 14 | p(data-l8n="content:confirm-email-expired/helper-text-1") !You need to resend the confirmation email to verify your address: 15 | form.resend-confirmation-form(action="/confirm-email-resent", method="post") 16 | button.button.button--full-width-smallish-devices(type="submit", data-l8n="content:confirm-email-expired/resend-action") !Resend confirmation 17 | -------------------------------------------------------------------------------- /assets/stylesheets/components/_admin-app-item.scss: -------------------------------------------------------------------------------- 1 | @mixin admin-app-box { 2 | margin-bottom: 1rem; 3 | padding: 1.3rem; 4 | border: 1px solid $light_grey; 5 | border-radius: 3px; 6 | } 7 | 8 | .admin-app-list { 9 | @include reset-list; 10 | margin-bottom: 0; 11 | } 12 | .admin-app-item { 13 | @include admin-app-box; 14 | position: relative; 15 | &__title { 16 | margin-top: 0; 17 | margin-bottom: 0; 18 | @include font-size($h3_font_size--small); 19 | font-weight: 500; 20 | line-height: 1.2; 21 | letter-spacing: -0.02em; 22 | color: $default_text_color; 23 | @include width-from($medium_device) { 24 | @include font-size($h3_font_size--medium); 25 | } 26 | } 27 | &__delete { 28 | position: absolute; 29 | top: 15px; 30 | right: 15px; 31 | @include size(32px); 32 | } 33 | } 34 | 35 | 36 | .admin-add-app-form { 37 | @include admin-app-box; 38 | } -------------------------------------------------------------------------------- /config/stonecutter.env: -------------------------------------------------------------------------------- 1 | HOST=0.0.0.0 2 | PORT=5000 3 | BASE_URL=https://sso.dcentproject.eu 4 | CLIENT_CREDENTIALS_FILE_PATH=/var/config/clients.yml 5 | APP_NAME="Stonecutter" 6 | HEADER_BG_COLOR=#1F1F1F 7 | HEADER_FONT_COLOR=#1F1F1F 8 | HEADER_FONT_COLOR_HOVER=#1F1F1F 9 | LOGO_FILE_NAME=logo.jpg 10 | FAVICON_FILE_NAME=favicon.ico 11 | STATIC_RESOURCES_DIR_PATH=/data/stonecutter/static 12 | ADMIN_FIRST_NAME=Jane 13 | ADMIN_LAST_NAME=Doe 14 | ADMIN_LOGIN=jane@example.com 15 | ADMIN_PASSWORD=password 16 | PASSWORD_RESET_EXPIRY=24 17 | OPEN_ID_CONNECT_ID_TOKEN_LIFETIME_MINUTES=10 18 | INVITE_EXPIRY=7 19 | RSA_KEYPAIR_FILE_PATH=/var/config/rsa-keypair.json 20 | EMAIL_SCRIPT_PATH=/var/stonecutter/email_service/send_mail.sh 21 | EMAIL_SERVICE_PROVIDER=mailgun 22 | EMAIL_DOMAIN_NAME= 23 | MAILGUN_API_KEY=api: 24 | MONGO_URI=mongodb://localhost:27017/stonecutter 25 | MONGO_DB_NAME=stonecutter -------------------------------------------------------------------------------- /assets/stylesheets/application.scss: -------------------------------------------------------------------------------- 1 | @import "utilities"; 2 | @import "variables"; 3 | 4 | @import "../../node_modules/font-awesome/scss/font-awesome"; 5 | 6 | @import "core/reset"; 7 | @import "core/base"; 8 | @import "core/grid"; 9 | @import "core/typography"; 10 | @import "core/forms"; 11 | @import "core/buttons"; 12 | 13 | 14 | @import "components/header"; 15 | @import "components/header-nav"; 16 | @import "components/authorise_buttons"; 17 | @import "components/authorise_text"; 18 | @import "components/authorised_apps"; 19 | @import "components/settings"; 20 | @import "components/card"; 21 | @import "components/profile_card_description"; 22 | @import "components/modal"; 23 | @import "components/flash_message"; 24 | @import "components/verify_email_address"; 25 | @import "components/user-list"; 26 | @import "components/search-row"; 27 | @import "components/welcome"; 28 | @import "components/toggle"; 29 | @import "components/admin-app-item"; -------------------------------------------------------------------------------- /ops/roles/stonecutter_application_config/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: create config folder 3 | file: path="/var/stonecutter/config" state=directory 4 | 5 | - name: copy application config file 6 | template: src=stonecutter_config.j2 dest=/var/stonecutter/config/stonecutter.env 7 | 8 | - name: copy rsa keypair file 9 | copy: src={{ RSA_KEYPAIR_FILE_PATH }} dest="/var/stonecutter/config/rsa-keypair.json" 10 | 11 | - name: copy client credentials file 12 | copy: src={{ CLIENT_CREDENTIALS_FILE_PATH }} dest="/var/stonecutter/config/clients.yml" 13 | 14 | - name: create static folder 15 | file: path="/data/stonecutter/static" state=directory 16 | 17 | - name: copy logo 18 | copy: src={{ STATIC_RESOURCES_DIR_PATH }}/{{ LOGO_FILE_NAME }} dest="/data/stonecutter/static/logo.svg" 19 | 20 | - name: copy favicon 21 | copy: src={{ STATIC_RESOURCES_DIR_PATH }}/{{ FAVICON_FILE_NAME }} dest="/data/stonecutter/static/favicon.ico" -------------------------------------------------------------------------------- /ops/roles/ferm/files/ferm.conf: -------------------------------------------------------------------------------- 1 | # Ferm script for configuring iptables. 2 | 3 | table filter { 4 | 5 | chain INPUT { 6 | # Set the default policy to ACCEPT to avoid getting 7 | # accidentally locked out. 8 | policy ACCEPT; 9 | 10 | # Connection tracking. 11 | mod state state INVALID DROP; 12 | mod state state (ESTABLISHED RELATED) ACCEPT; 13 | 14 | # Allow local connections. 15 | interface lo ACCEPT; 16 | 17 | # Allow ssh connections. 18 | proto tcp dport ssh ACCEPT; 19 | 20 | # Allow http(s) connections. 21 | proto tcp dport (https http) ACCEPT; 22 | 23 | # Ansible specified rules. 24 | 25 | # Because the default policy is to ACCEPT we DROP 26 | # everything that comes through to this stage. 27 | DROP; 28 | } 29 | 30 | chain fail2ban-ssh; 31 | 32 | # Outgoing connections are not limited. 33 | chain OUTPUT policy ACCEPT; 34 | 35 | # This is not a router. 36 | chain FORWARD policy DROP; 37 | } 38 | -------------------------------------------------------------------------------- /src-cljs/stonecutter/js/dom/common.cljs: -------------------------------------------------------------------------------- 1 | (ns stonecutter.js.dom.common 2 | (:require [dommy.core :as d]) 3 | (:require-macros [dommy.core :as dm] 4 | [stonecutter.translation :as t])) 5 | 6 | (def translations (t/load-client-translations)) 7 | 8 | (defn get-lang [] 9 | (keyword (.getAttribute (dm/sel1 :html) "lang"))) 10 | 11 | (defn add-class! [selector css-class] 12 | (d/add-class! (dm/sel1 selector) css-class)) 13 | 14 | (defn remove-class! [selector css-class] 15 | (d/remove-class! (dm/sel1 selector) css-class)) 16 | 17 | (defn set-text! [selector message] 18 | (d/set-text! (dm/sel1 selector) message)) 19 | 20 | (defn focus-on-element! [sel] 21 | (when-let [e (dm/sel1 sel)] 22 | (.focus e))) 23 | 24 | (defn prevent-default-submit! [submitEvent] 25 | (.preventDefault submitEvent)) 26 | 27 | (defn get-value [selector] 28 | (d/value (dm/sel1 selector))) 29 | 30 | (defn get-file [selector] 31 | (.item (.-files (dm/sel1 selector)) 0)) -------------------------------------------------------------------------------- /src/stonecutter/db/expiry.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.db.expiry 2 | (:require [stonecutter.util.time :as time] 3 | [clauth.store :as cl-store] 4 | [stonecutter.db.mongo :as mongo])) 5 | 6 | (defn expire-or-return-doc [store clock id doc] 7 | (if (and doc (> (:_expiry doc) (time/now-in-millis clock))) 8 | (dissoc doc :_expiry) 9 | (do (cl-store/revoke! store id) nil))) 10 | 11 | (defn fetch-with-expiry [store clock id] 12 | (->> (cl-store/fetch store id) 13 | (expire-or-return-doc store clock id))) 14 | 15 | (defn query-with-expiry [store clock kw query] 16 | (let [docs (mongo/query store query)] 17 | (doall (->> docs 18 | (map #(expire-or-return-doc store clock (kw %) %)) 19 | (remove nil?))))) 20 | 21 | (defn store-with-expiry! [store clock kw doc relative-expiry] 22 | (let [expiry (+ (time/now-in-millis clock) relative-expiry)] 23 | (->> (assoc doc :_expiry expiry) 24 | (cl-store/store! store kw)))) 25 | -------------------------------------------------------------------------------- /src/stonecutter/view/authorise_failure.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.view.authorise-failure 2 | (:require [traduki.core :as t] 3 | [net.cgrand.enlive-html :as html] 4 | [stonecutter.routes :as r] 5 | [stonecutter.view.view-helpers :as vh])) 6 | 7 | 8 | (defn set-redirect-to-client-home-link [params enlive-m] 9 | (html/at enlive-m 10 | [:.clj--redirect-to-client-home__link] (html/set-attr :href (:callback-uri-with-error params)))) 11 | 12 | (defn set-client-name [client-name enlive-m] 13 | (html/at enlive-m 14 | [:.clj--client-name] (html/content client-name))) 15 | 16 | (defn show-authorise-failure [request] 17 | (let [client-name (get-in request [:context :client-name]) 18 | params (:params request)] 19 | (->> (vh/load-template-with-lang "public/authorise-failure.html" request) 20 | (set-redirect-to-client-home-link params) 21 | (set-client-name client-name) 22 | vh/remove-work-in-progress))) 23 | -------------------------------------------------------------------------------- /src-cljs/stonecutter/js/controller/user_list.cljs: -------------------------------------------------------------------------------- 1 | (ns stonecutter.js.controller.user-list 2 | (:require [ajax.core :refer [POST]] 3 | [dommy.core :as d]) 4 | (:require-macros [dommy.core :as dm])) 5 | 6 | ;(defn handler [response] 7 | ; (.log js/console (str "successful response: " status " " status-text))) 8 | 9 | (defn error-handler [{:keys [status status-text]}] 10 | (.log js/console (str "something bad happened: " status " " status-text))) 11 | 12 | (defn anti-forgery [] 13 | 
 (d/value (dm/sel1 :#__anti-forgery-token))) 14 | 15 | (defn update-role [e] 16 | (let [checked (.-checked (.-target e)) 17 | login
 (.-id (.-target e))] 18 | (POST "/admin/set-user-trustworthiness" 19 | {:params {"login" login
 20 | "trust-toggle" checked} 21 | :headers {:X-CSRF-Token (anti-forgery)} 
 22 | :format :json 23 | ;:handler handler
 24 | :error-handler error-handler}))) 25 | -------------------------------------------------------------------------------- /assets/jade/confirm-email-resent.jade: -------------------------------------------------------------------------------- 1 | extends layout/layout 2 | 3 | block vars 4 | - bodyClass = "func--confirm-email-resent-page" 5 | - pageTitle = "!Confirmation email resent" 6 | - pageTitleDataL8n = "content:confirm-email-resent/title" 7 | 8 | block headerScripts 9 | 10 | block content 11 | .single-column 12 | h1.page-title(data-l8n="content:confirm-email-resent/page-header") !Confirmation email resent 13 | 14 | 15 | +flashMessageWithCljContainer('clj--flash-message-container','func--flash-message-container', 'success') 16 | p.flash-message__title 17 | span(data-l8n="content:flash/confirm-email-sent-pre-email-address") !A confirmation email has been sent to: 18 | = " " 19 | span.clj--email-address !email@example.com 20 | p.flash-message__title(data-l8n="content:flash/confirm-email-sent-post-email-address") !Please follow the instructions to confirm your email address. 21 | 22 | a.button(href="/profile", data-l8n="content:confirm-email-resent/action-link") !View profile -------------------------------------------------------------------------------- /assets/stylesheets/components/_header.scss: -------------------------------------------------------------------------------- 1 | $header_background_color: $light_grey !default; 2 | $header_logo_image_path: 'logo.svg' !default; 3 | $header--height: 80px; 4 | 5 | .header { 6 | @include clearfix; 7 | min-height: $header--height; 8 | margin-bottom: 0.25rem; 9 | background-color: $header_background_color; 10 | @include width-from($medium_device) { 11 | margin-bottom: 0.5rem; 12 | } 13 | &__inner { 14 | position: relative; 15 | min-height: $header--height; 16 | padding: 1rem 1rem 0.5rem 1rem; 17 | margin-bottom: 0; 18 | @include width-from($medium_device) { 19 | height: 100px; 20 | } 21 | } 22 | 23 | &__logo { 24 | width: 100%; 25 | height: 30px; 26 | margin-bottom: 0; 27 | background: image-url($header_logo_image_path) no-repeat 50% 50%; 28 | background-size: contain; 29 | @include width-from($medium_device) { 30 | @include vertical-middle-me; 31 | background-position: 0 50%; 32 | width: 50%; 33 | height: 45px; 34 | } 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /src/stonecutter/session.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.session) 2 | 3 | (defn request->user-login [request] 4 | (get-in request [:session :user-login])) 5 | 6 | (defn request->access-token [request] 7 | (get-in request [:session :access_token])) 8 | 9 | (defn request->return-to [request] 10 | (get-in request [:session :return-to])) 11 | 12 | (defn request->profile-photo [request] 13 | (get-in request [:params :profile-photo])) 14 | 15 | (defn request->first-name [request] 16 | (get-in request [:params :first-name])) 17 | 18 | (defn request->last-name [request] 19 | (get-in request [:params :last-name])) 20 | 21 | (defn set-user-login [response user-login] 22 | (assoc-in response [:session :user-login] user-login)) 23 | 24 | (defn set-user-role [response user-role] 25 | (assoc-in response [:session :role] user-role)) 26 | 27 | (defn set-access-token [response access-token] 28 | (assoc-in response [:session :access_token] access-token)) 29 | 30 | (defn replace-session-with [response existing-session] 31 | (assoc response :session existing-session)) 32 | -------------------------------------------------------------------------------- /assets/stylesheets/components/_header-nav.scss: -------------------------------------------------------------------------------- 1 | $tab_bg_color: transparent !default; 2 | $tab_bg_color_active: transparent !default; 3 | $tab_color_active: $dark_cyan !default; 4 | $tab_link_bg_colour_hover: rgba($white, 0.5) !default; 5 | $tab_link_colour: $darker_grey; 6 | $tab_link_colour_hover: $medium_cyan !default; 7 | 8 | 9 | .header-nav { 10 | @include clearfix; 11 | position: relative; 12 | z-index: 1; 13 | max-width: $max_site_width; 14 | margin: 0 auto; 15 | list-style: none; 16 | text-align: center; 17 | @include width-from($medium_device) { 18 | @include vertical-middle-me; 19 | right: 0; 20 | text-align: right; 21 | } 22 | 23 | &__link { 24 | position: relative; 25 | display: inline-block; 26 | text-align: center; 27 | padding: 10px 20px; 28 | text-decoration: none; 29 | font-weight: bold; 30 | @include font-size(13px); 31 | color: $tab_link_colour; 32 | transition: color 250ms ease; 33 | &:hover { 34 | cursor: pointer; 35 | color: $tab_link_colour_hover; 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/stonecutter/db/client_seed.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.db.client-seed 2 | (:require [clj-yaml.core :as yaml] 3 | [clojure.java.io :as io] 4 | [stonecutter.db.client :as client])) 5 | 6 | (defn load-client-credentials-from-string [s] 7 | (yaml/parse-string s)) 8 | 9 | (defn load-client-credentials-from-resource [resource-name] 10 | (-> resource-name 11 | io/resource 12 | slurp 13 | load-client-credentials-from-string)) 14 | 15 | (defn load-client-credentials-from-file [file-path] 16 | (-> file-path 17 | slurp 18 | load-client-credentials-from-string)) 19 | 20 | (defn load-client-credentials [resource-or-file] 21 | (if (io/resource resource-or-file) 22 | (load-client-credentials-from-resource resource-or-file) 23 | (load-client-credentials-from-file resource-or-file))) 24 | 25 | (defn load-client-credentials-and-store-clients [client-store resource-or-file] 26 | (do (client/delete-clients! client-store) 27 | (client/store-clients-from-map client-store (load-client-credentials resource-or-file)))) 28 | -------------------------------------------------------------------------------- /src/stonecutter/controller/common.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.controller.common 2 | (:require [ring.util.response :as response] 3 | [stonecutter.routes :as r] 4 | [stonecutter.db.token :as token] 5 | [stonecutter.session :as session])) 6 | 7 | (defn sign-in-user 8 | ([response token-store user] 9 | (sign-in-user response token-store user {})) 10 | ([response token-store user existing-session] 11 | (-> response 12 | (session/replace-session-with existing-session) 13 | (session/set-user-login (:login user)) 14 | (session/set-user-role (:role user)) 15 | (session/set-access-token (token/generate-login-access-token token-store user))))) 16 | 17 | (defn sign-in-to-index 18 | ([token-store user] 19 | (sign-in-to-index token-store user {})) 20 | ([token-store user existing-session] 21 | (-> (response/redirect (r/path :index)) 22 | (sign-in-user token-store user existing-session)))) 23 | 24 | (defn signed-in? [request] 25 | (and (session/request->user-login request) (session/request->access-token request))) 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stonecutter_frontend", 3 | "version": "1.0.0", 4 | "description": "Build all the things", 5 | "main": "server.js", 6 | "engines": { 7 | "node": "0.12.7" 8 | }, 9 | "dependencies": { 10 | "font-awesome": "latest", 11 | "gulp": "^3.8.10", 12 | "gulp-autoprefixer": "^1.0.1", 13 | "del": "^1.2.0", 14 | "gulp-concat": "^2.4.1", 15 | "gulp-if": "^1.2.5", 16 | "gulp-imagemin": "^1.2.1", 17 | "gulp-jade": "^0.9.0", 18 | "gulp-minify-css": "^0.3.11", 19 | "gulp-replace": "^0.5.3", 20 | "gulp-sass": "^2.0.3", 21 | "gulp-util": "^3.0.1", 22 | "gulp-html5-lint": "^1.0.1", 23 | "jade": "*", 24 | "run-sequence": "^1.0.1" 25 | }, 26 | "devDependencies": { 27 | "browser-sync": "^2.7.13", 28 | "express": "^4.10.1", 29 | "gulp-express": "^0.1.0", 30 | "gulp-gh-pages": "^0.5.0", 31 | "gulp-nodemon": "^1.0.4" 32 | }, 33 | "author": "Chris Cheshire", 34 | "license": "MIT", 35 | "scripts": { 36 | "gulp": "gulp", 37 | "postinstall": "./npm-postinstall.sh" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /assets/jade/error-500.jade: -------------------------------------------------------------------------------- 1 | extends layout/layout 2 | 3 | block vars 4 | - bodyClass = "func--error-500-page" 5 | - pageTitle = "!Error-500" 6 | - pageTitleDataL8n = "content:error-500/title" 7 | 8 | block headerScripts 9 | 10 | block content 11 | .single-column 12 | h1.page-title.clj--error-page-header(data-l8n="content:error-500/page-header") !Sorry something went wrong 13 | p.clj--error-page-intro(data-l8n="content:error-500/page-intro") !An unexpected error has occurred, please try again. We apologise for any inconvenience. 14 | .clj--error-page-content 15 | h2(data-l8n="content:error-default/return-to-link-header") !What do I do now? 16 | ul 17 | li 18 | span(data-l8n="content:error-default/return-to-link-before") !Please return to the 19 | =" " 20 | a.clj--error-return-home__link(href="./", title="!Return to Home", data-l8n="content:error-default/return-to-link attr/title:error-default/return-to-link-title-attr") !Home page 21 | =" " 22 | span(data-l8n="content:error-default/return-to-link-after") !now. 23 | -------------------------------------------------------------------------------- /assets/stylesheets/utilities/_shapes.scss: -------------------------------------------------------------------------------- 1 | @mixin triangle($direction:"up", $size:5px, $color:red) { 2 | 3 | width: 0; 4 | height: 0; 5 | 6 | @if $direction == "up" { 7 | border-left: $size solid transparent; 8 | border-right: $size solid transparent; 9 | border-bottom: $size solid $color; 10 | } 11 | @if $direction == "down" { 12 | border-top: $size solid $color; 13 | border-left: $size solid transparent; 14 | border-right: $size solid transparent; 15 | } 16 | @if $direction == "right" { 17 | border-top: $size solid transparent; 18 | border-bottom: $size solid transparent; 19 | border-left: $size solid $color; 20 | } 21 | @if $direction == "left" { 22 | border-top: $size solid transparent; 23 | border-right: $size solid $color; 24 | border-bottom: $size solid transparent; 25 | } 26 | 27 | } 28 | 29 | @mixin rounded_rectangle($width: 1px, $height: $width, $radius: 1px) { 30 | width: $width; 31 | height: $height; 32 | border-radius: $radius; 33 | } 34 | 35 | @mixin circle($size) { 36 | height: $size; 37 | width: $size; 38 | border-radius: $size/2; 39 | } -------------------------------------------------------------------------------- /ops/roles/stonecutter_application_config/templates/stonecutter_config.j2: -------------------------------------------------------------------------------- 1 | HOST={{ HOST }} 2 | PORT={{ PORT }} 3 | BASE_URL={{ BASE_URL }} 4 | APP_NAME={{ APP_NAME }} 5 | HEADER_BG_COLOR={{ HEADER_BG_COLOR }} 6 | HEADER_FONT_COLOR={{ HEADER_FONT_COLOR }} 7 | HEADER_FONT_COLOR_HOVER={{ HEADER_FONT_COLOR_HOVER }} 8 | ADMIN_FIRST_NAME={{ ADMIN_FIRST_NAME }} 9 | ADMIN_LAST_NAME={{ ADMIN_LAST_NAME }} 10 | ADMIN_LOGIN={{ ADMIN_LOGIN }} 11 | ADMIN_PASSWORD={{ ADMIN_PASSWORD }} 12 | PASSWORD_RESET_EXPIRY={{ PASSWORD_RESET_EXPIRY }} 13 | OPEN_ID_CONNECT_ID_TOKEN_LIFETIME_MINUTES={{ OPEN_ID_CONNECT_ID_TOKEN_LIFETIME_MINUTES }} 14 | INVITE_EXPIRY={{ INVITE_EXPIRY }} 15 | EMAIL_SERVICE_PROVIDER={{ EMAIL_SERVICE_PROVIDER }} 16 | EMAIL_DOMAIN_NAME={{ EMAIL_DOMAIN_NAME }} 17 | MAILGUN_API_KEY={{ MAILGUN_API_KEY }} 18 | 19 | # DO NOT REMOVE THE FOLLOWING VARIABLES 20 | EMAIL_SCRIPT_PATH=/var/stonecutter/email_service/send_mail.sh 21 | CLIENT_CREDENTIALS_FILE_PATH=/var/config/clients.yml 22 | LOGO_FILE_NAME=logo.svg 23 | FAVICON_FILE_NAME=favicon.ico 24 | STATIC_RESOURCES_DIR_PATH=/data/stonecutter/static 25 | RSA_KEYPAIR_FILE_PATH=/var/config/rsa-keypair.json 26 | -------------------------------------------------------------------------------- /assets/stylesheets/components/_authorise_text.scss: -------------------------------------------------------------------------------- 1 | .authorise-text { 2 | margin-bottom: 30px; 3 | &__description { 4 | @include font-size($paragraph_font_size); 5 | &:last-child { 6 | margin-bottom: 0; 7 | } 8 | &--allowed { 9 | color: $dark_cyan; 10 | font-weight: bold; 11 | } 12 | &--disallowed { 13 | color: $dark_pink; 14 | font-weight: bold; 15 | } 16 | } 17 | &__list { 18 | @include reset-list(); 19 | } 20 | &__item { 21 | margin-bottom: 15px; 22 | @include font-size($paragraph_font_size); 23 | &:last-child { 24 | margin-bottom: 0; 25 | } 26 | } 27 | &__info { 28 | margin-top: 0.5rem; 29 | padding-left: 2rem; 30 | @include font-size(14px); 31 | font-weight: 100; 32 | color: $dark_cyan; 33 | } 34 | &__icon { 35 | margin-right: 8px; 36 | &--allowed { 37 | color: $dark_cyan; 38 | } 39 | &--disallowed { 40 | color: $dark_pink; 41 | } 42 | } 43 | } 44 | 45 | .authorise-terms { 46 | //border-top: 1px solid $light_grey; 47 | padding-top: 30px; 48 | } 49 | .authorise-terms__link { 50 | color: $dark_cyan; 51 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 ThoughtWorks Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/auth_flow_diagram.txt: -------------------------------------------------------------------------------- 1 | This file is the text use on http://bramp.github.io/js-sequence-diagrams 2 | to generate the diagram.svg 3 | 4 | Title: Stonecutter Auth Journey 07/08/2015 5 | User->Client: :GET /login 6 | #Client is stonecutter-client and stonecutter-oauth.client 7 | Client-->StoneCutter: :GET /authorisation\n{:client-id \n:response_type "code"\n:redirect_uri } 8 | StoneCutter->>Clauth: clauth.endpoint/authorization-handler\n:auto-approver false\n:user-session-required-redirect\n:authorization-form 9 | Clauth->StoneCutter: ?code= 10 | Note over StoneCutter: preserves session\n{:user-login\n:access_token} 11 | StoneCutter->Client: :GET :status 302\nto: 12 | Client->StoneCutter: :POST /api/token\n{:grant_type :redirect_uri :code :client_id :client_secret} 13 | StoneCutter->Clauth: token-request-handler 14 | Clauth->StoneCutter: json {:access_token :token_type "bearer"} 15 | StoneCutter->Client: json with keys {:access_token :token_type \n:user-email :user-id :user-email-confirmed :role} 16 | Note over Client: Redirects to /voting 17 | Client->User:session {:access-token :user\n:user-email-confirmed :role} 18 | -------------------------------------------------------------------------------- /src-cljs/stonecutter/js/dom/change_password.cljs: -------------------------------------------------------------------------------- 1 | (ns stonecutter.js.dom.change-password 2 | (:require [stonecutter.js.dom.common :as dom])) 3 | 4 | (def change-password-form-element-selector :.clj--change-password__form) 5 | 6 | (def field-invalid-class :form-row--invalid) 7 | (def field-valid-class :form-row--valid) 8 | 9 | (def selectors 10 | {:current-password {:input :.clj--current-password__input 11 | :form-row :.clj--current-password 12 | :validation :.clj--current-password__validation} 13 | :new-password {:input :.clj--new-password__input 14 | :form-row :.clj--new-password 15 | :validation :.clj--new-password__validation}}) 16 | 17 | (defn input-selector [field-key] 18 | (get-in selectors [field-key :input])) 19 | 20 | (defn form-row-selector [field-key] 21 | (get-in selectors [field-key :form-row])) 22 | 23 | (defn validation-selector [field-key] 24 | (get-in selectors [field-key :validation])) 25 | 26 | (defn get-translated-message [key] 27 | (-> dom/translations :change-password-form key)) 28 | 29 | (defn get-value [field-key] 30 | (dom/get-value (input-selector field-key))) 31 | -------------------------------------------------------------------------------- /test-cljs/stonecutter/test/unit/translations.cljs: -------------------------------------------------------------------------------- 1 | (ns stonecutter.test.unit.translations 2 | (:require [cemerick.cljs.test] 3 | [stonecutter.js.dom.register-form :as rfd] 4 | [stonecutter.js.dom.common :as dom] 5 | [stonecutter.js.controller.client_translations :as ct]) 6 | (:require-macros [cemerick.cljs.test :refer [deftest is testing run-tests are]])) 7 | 8 | 9 | (deftest about-handling-client-side-translations 10 | (testing "if given an unsupported language then falls back to english" 11 | (with-redefs [dom/get-lang (constantly :xx)] 12 | (is (= (ct/t (dom/get-lang) :index/register-first-name-blank-validation-message) "First name cannot be blank")))) 13 | (testing "if lang is updated then returns a translated message" 14 | (with-redefs [dom/get-lang (constantly :en)] 15 | (is (not (contains? (ct/t (dom/get-lang) :index/register-email-address-blank-validation-message) "Finnish")))) 16 | (with-redefs [dom/get-lang (constantly :fi)] 17 | (is (= (ct/t (dom/get-lang) :index/register-email-address-blank-validation-message) "Email address cannot be blank in Finnish"))))) -------------------------------------------------------------------------------- /test/stonecutter/test/translation.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.test.translation 2 | (:require [midje.sweet :refer :all] 3 | [stonecutter.translation :as t])) 4 | 5 | (facts "can load translations from a string" 6 | (fact "basic example" 7 | (t/load-translations-from-string "a-key: Hello") => {:a-key "Hello"}) 8 | (fact "with nested keys" 9 | (t/load-translations-from-string 10 | "a:\n 11 | hello: Hello\n 12 | goodbye: Goodbye\n") => {:a {:hello "Hello" :goodbye "Goodbye"}})) 13 | 14 | (facts "can load translations from a file" 15 | (t/load-translations-from-file "test-translations.yml") => {:a {:hello "Hello" :goodbye "Goodbye"}}) 16 | 17 | (facts "about getting locale from requests" 18 | (fact "request with no locales set defaults to :en" 19 | (t/get-locale-from-request {}) => :en) 20 | (fact "request with session locale set, always take session locale above others" 21 | (t/get-locale-from-request {:session {:locale :fi} :locale :en}) => :fi) 22 | (fact "request can take locale if no session locale is set" 23 | (t/get-locale-from-request {:session {:locale nil} :locale :fr}) => :fr)) -------------------------------------------------------------------------------- /ops/roles/docker/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # - script: get_docker.sh 3 | 4 | - name: install pip 5 | apt: name=python-pip state=present 6 | 7 | # required to prevent bug: 8 | # https://github.com/ansible/ansible/issues/10810 9 | - name: ensure pip is upgraded 10 | command: easy_install -U pip 11 | 12 | # latest version of docker-py breaks all the things: 13 | # https://github.com/ansible/ansible-modules-core/issues/1227 14 | - name: install docker-py 15 | pip: name=docker-py version=1.1.0 state=present 16 | 17 | - name: add docker apt key 18 | apt_key: keyserver=hkp://p80.pool.sks-keyservers.net:80 id=36A1D7869245C8950F966E92D8576A8BA88D21E9 19 | 20 | - name: add docker apt repo 21 | apt_repository: repo='deb https://get.docker.com/ubuntu docker main' state=present 22 | 23 | - name: install lxc-docker 24 | apt: name=lxc-docker state=latest 25 | 26 | - name: restart docker service 27 | service: name=docker state=restarted 28 | 29 | - name: make db directory 30 | file: path=/data/db state=directory 31 | 32 | - name: make backup directory 33 | file: path=/data/backup state=directory 34 | 35 | - name: docker mongo 36 | docker: 37 | name: mongo 38 | image: mongo:3 39 | volumes: 40 | - /data/db:/data/db 41 | - /data/backup:/data/backup 42 | -------------------------------------------------------------------------------- /ops/roles/nginx/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: install nginx 3 | apt: name=nginx state=present 4 | 5 | - name: install fail2ban 6 | apt: name=fail2ban state=present 7 | 8 | - name: create SSL folder 9 | file: path="/etc/nginx/ssl" state=directory 10 | 11 | - name: copy SSL cert 12 | copy: src={{ cert_location }} dest="/etc/nginx/ssl/stonecutter.crt" 13 | 14 | - name: copy SSL key 15 | copy: src={{ cert_key_location }} dest="/etc/nginx/ssl/stonecutter.key" 16 | 17 | - name: Create directory for DH parameter file 18 | file: path=/etc/nginx/cert state=directory mode=0755 19 | 20 | - name: Create DH parameters file 21 | command: openssl dhparam 2048 -out /etc/nginx/cert/dhparam.pem 22 | args: 23 | creates: /etc/nginx/cert/dhparam.pem 24 | 25 | - name: copy over stonecutter nginx config 26 | template: src="stonecutter.j2" dest="/etc/nginx/sites-available/stonecutter" mode=0644 27 | 28 | - name: create symbolic link to nginx helsinki config 29 | file: src="/etc/nginx/sites-available/stonecutter" dest="/etc/nginx/sites-enabled/stonecutter" state=link 30 | 31 | - name: remove default nginx symbolic link from sites-enabled 32 | file: path="/etc/nginx/sites-enabled/default" state=absent 33 | 34 | - name: restart nginx 35 | service: name=nginx state=restarted 36 | -------------------------------------------------------------------------------- /src/stonecutter/view/unshare_profile_card.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.view.unshare-profile-card 2 | (:require [traduki.core :as t] 3 | [net.cgrand.enlive-html :as html] 4 | [stonecutter.routes :as r] 5 | [stonecutter.view.view-helpers :as vh])) 6 | 7 | (defn set-client-id [client-id enlive-m] 8 | (html/at enlive-m 9 | [:.clj--client-id__input] (html/set-attr :value client-id))) 10 | 11 | (defn set-cancel-link [enlive-m] 12 | (html/at enlive-m 13 | [:.clj--unshare-profile-card-cancel__link] (html/set-attr :href (r/path :show-profile)))) 14 | 15 | (defn set-form-action [enlive-m] 16 | (html/at enlive-m 17 | [:.clj--unshare-profile-card__form] (html/set-attr :action (r/path :unshare-profile-card)))) 18 | 19 | (defn set-client-name [client-name enlive-m] 20 | (html/at enlive-m 21 | [:.clj--client-name] (html/content client-name))) 22 | 23 | (defn unshare-profile-card [request] 24 | (let [client-id (get-in request [:context :client :client-id]) 25 | client-name (get-in request [:context :client :name])] 26 | (->> (vh/load-template-with-lang "public/unshare-profile-card.html" request) 27 | (set-client-name client-name) 28 | set-form-action 29 | (set-client-id client-id) 30 | set-cancel-link 31 | vh/add-anti-forgery))) 32 | -------------------------------------------------------------------------------- /src/stonecutter/lein.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.lein 2 | (:require [stonecutter.config :as config] 3 | [stonecutter.view.view-helpers :as vh] 4 | [stonecutter.db.storage :as s] 5 | [stonecutter.util.time :as time] 6 | [stonecutter.email :as email] 7 | [stonecutter.db.client-seed :as client-seed] 8 | [stonecutter.admin :as admin] 9 | [stonecutter.jwt :as jwt] 10 | [stonecutter.handler :as h])) 11 | 12 | (defonce lein-app nil) 13 | 14 | (defn lein-ring-init 15 | "Function called when running app with 'lein ring server'" 16 | [] 17 | (let [config-m (config/create-config) 18 | clock (time/new-clock) 19 | stores-m (s/create-in-memory-stores nil) 20 | email-sender (email/bash-sender-factory nil) 21 | token-generator (jwt/create-generator clock (jwt/load-key-pair (config/rsa-keypair-file-path config-m)) 22 | (config/base-url config-m))] 23 | (vh/disable-template-caching!) 24 | (admin/create-admin-user config-m (s/get-user-store stores-m)) 25 | (client-seed/load-client-credentials-and-store-clients (s/get-client-store stores-m) (config/client-credentials-file-path config-m)) 26 | (alter-var-root #'lein-app (constantly (h/create-app config-m clock stores-m email-sender token-generator true))))) 27 | -------------------------------------------------------------------------------- /docs/conventions.md: -------------------------------------------------------------------------------- 1 | # Coding Conventions 2 | 3 | ## Namespaces 4 | 5 | ### Prefer :as over :refer 6 | 7 | Good: 8 | ``` 9 | (ns mynamespace 10 | (:require [first.dependency :as fd])) 11 | 12 | (fd/do-thing) 13 | ``` 14 | 15 | Less good: 16 | ``` 17 | (ns mynamespace 18 | (:require [first.dependency :refer [do-thing]])) 19 | 20 | (do-thing) 21 | ``` 22 | 23 | ### Prefix clauth aliases with cl- 24 | 25 | Good: 26 | ``` 27 | (ns mynamespace 28 | (:require [clauth.user :as cl-user])) 29 | ``` 30 | 31 | Bad: 32 | ``` 33 | (ns mynamespace 34 | (:require [clauth.user :as user])) 35 | ``` 36 | 37 | ### List external dependencies at start, and internal ones at end 38 | 39 | Good: 40 | ``` 41 | (ns stonecutter.namespace 42 | (:require [clauth.token :as cl-token] 43 | [net.cgrand.enlive-html :as html] 44 | [stonecutter.helper :as helper] 45 | [stonecutter.db.storage :as storage])) 46 | ``` 47 | 48 | Bad: 49 | ``` 50 | (ns stonecutter.namespace 51 | (:require 52 | [clauth.token :as cl-token] 53 | [stonecutter.helper :as helper] 54 | [net.cgrand.enlive-html :as html] 55 | [stonecutter.db.storage :as storage])) 56 | ``` 57 | 58 | ## Code Structure 59 | 60 | ### Put route handler functions into controller namespaces 61 | 62 | 63 | ## Tests 64 | 65 | ### Create global defs for css selectors 66 | -------------------------------------------------------------------------------- /src-cljs/stonecutter/js/dom/change_profile_form.cljs: -------------------------------------------------------------------------------- 1 | (ns stonecutter.js.dom.change-profile-form 2 | (:require [stonecutter.js.dom.common :as dom])) 3 | 4 | (def field-invalid-class :form-row--invalid) 5 | 6 | (def change-profile-details-form-element-selector :.clj--change-profile-details__form) 7 | 8 | (def selectors 9 | {:change-first-name {:input :.clj--change-first-name__input 10 | :form-row :.clj--first-name 11 | :validation :.clj--change-first-name__validation} 12 | :change-last-name {:input :.clj--change-last-name__input 13 | :form-row :.clj--last-name 14 | :validation :.clj--change-last-name__validation} 15 | :change-profile-picture {:input :.clj--upload-picture__input 16 | :form-row :.clj--upload-picture 17 | :validation :.clj--upload-picture__validation}}) 18 | 19 | (defn input-selector [field-key] 20 | (get-in selectors [field-key :input])) 21 | 22 | (defn form-row-selector [field-key] 23 | (get-in selectors [field-key :form-row])) 24 | 25 | (defn get-value [field-key] 26 | (dom/get-value (input-selector field-key))) 27 | 28 | (defn get-file [field-key] 29 | (dom/get-file (input-selector field-key))) 30 | 31 | (defn validation-selector [field-key] 32 | (get-in selectors [field-key :validation])) 33 | -------------------------------------------------------------------------------- /assets/jade/forgot-password.jade: -------------------------------------------------------------------------------- 1 | extends layout/layout 2 | 3 | block vars 4 | - bodyClass = "func--forgot-password-page" 5 | - pageTitle = "!Forgot password" 6 | - pageTitleDataL8n = "content:forgot-password/title" 7 | 8 | block headerScripts 9 | 10 | block content 11 | .single-column 12 | 13 | +flashMessageWithCljContainer('clj--flash-message-container','func--flash-message-container', 'fail') 14 | p.flash-message__title(data-l8n="content:flash/expired-password-reset") !Reset url no longer exists 15 | 16 | h1.page-title Forgot password 17 | 18 | form.login-form.clj--forgotten-password__form(action="./forgot-password-confirmation", method="post") 19 | p.form-row.clj--forgotten-password-email 20 | label.form-row__label(for="email") 21 | span.form-row__title(data-l8n="content:forgot-password/email-label") 22 | | !Email address 23 | span.form-row__validation.clj--forgotten-password-email__validation(data-l8n="content:forgot-password/email-address-invalid-validation-message") 24 | | !Enter a valid email address 25 | input.form-row__input.clj--forgotten-password-email__input.func--email__input(type="email", id="email", name="email") 26 | button.button.button--full-width-smallish-devices.func--send-forgotten-password-email__button(type="submit", data-l8n="content:forgot-password/submit") !Send change password link 27 | -------------------------------------------------------------------------------- /test/stonecutter/test/db/confirmation.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.test.db.confirmation 2 | (:require [midje.sweet :refer :all] 3 | [stonecutter.db.storage :as s] 4 | [stonecutter.db.confirmation :as confirmation] 5 | [stonecutter.db.storage :as storage] 6 | [stonecutter.db.mongo :as m])) 7 | 8 | (facts "about storage of confirmations" 9 | (let [confirmation-store (m/create-memory-store)] 10 | (fact "can store a confirmation" 11 | (confirmation/store! confirmation-store "user@email.com" "confirmation-id") 12 | => {:login "user@email.com" 13 | :confirmation-id "confirmation-id"}) 14 | (fact "can retrieve a confirmation once it has been stored" 15 | (confirmation/store! confirmation-store "user12@email.com" "confirmation-id12") 16 | (confirmation/fetch confirmation-store "confirmation-id12") 17 | => {:login "user12@email.com" 18 | :confirmation-id "confirmation-id12"}))) 19 | 20 | (fact "can retrieve confirmation by user login" 21 | (let [confirmation-store (m/create-memory-store) 22 | email "test@email.com" 23 | confirmation (confirmation/store! confirmation-store email "confirmation-id")] 24 | (confirmation/retrieve-by-user-email confirmation-store email) => confirmation)) 25 | -------------------------------------------------------------------------------- /src-cljs/stonecutter/js/dom/upload_photo.cljs: -------------------------------------------------------------------------------- 1 | (ns stonecutter.js.dom.upload-photo 2 | (:require [dommy.core :as d] 3 | [stonecutter.validation :as v] 4 | [stonecutter.js.dom.common :as dom] 5 | [stonecutter.js.controller.change-profile-form :as cpfc]) 6 | (:require-macros [dommy.core :as dm])) 7 | 8 | (def profile-card-photo__input :.clj--card-photo-input) 9 | (def profile-card-photo__selector :.clj--card-photo) 10 | (def profile-card-photo__form :.clj--card-photo-upload) 11 | (def profile-card-photo__error-container :.clj--profile-image-error-container) 12 | (def profile-card-photo__error-text :.clj--profile-image-error-text) 13 | 14 | (defn submit-form! [selector] 15 | (.submit (dm/sel1 selector))) 16 | 17 | (defn upload-image [e] 18 | (let [image (dom/get-file profile-card-photo__input) 19 | error (v/validate-profile-picture image)] 20 | (if (and image (not error)) 21 | (submit-form! profile-card-photo__form) 22 | (do (d/remove-attr! (dm/sel1 profile-card-photo__error-container) :hidden) 23 | (d/set-text! (dm/sel1 profile-card-photo__error-text) 24 | (get-in cpfc/error-to-message [:change-profile-picture error])))))) 25 | 26 | (defn show-button [e] 27 | (d/remove-attr! (dm/sel1 profile-card-photo__form) :hidden)) 28 | 29 | (defn hide-button [e] 30 | (d/set-attr! (dm/sel1 profile-card-photo__form) :hidden "hidden")) -------------------------------------------------------------------------------- /assets/stylesheets/components/_flash_message.scss: -------------------------------------------------------------------------------- 1 | $flash-message--border-radius: 3px; 2 | 3 | .flash-message { 4 | position: relative; 5 | padding: 16px 16px 16px 38px; 6 | margin-top: 1rem; 7 | margin-bottom: 2rem; 8 | border-radius: $flash-message--border-radius; 9 | &:before { 10 | display: block; 11 | position: absolute; 12 | top: 18px; 13 | left: 14px; 14 | font-family: 'FontAwesome'; 15 | } 16 | &--small-bottom-margin { 17 | margin-bottom: 1rem; 18 | } 19 | &--success { 20 | background-color: $success_color; 21 | border: 1px solid darken($success_color,20%); 22 | //color: #13423E; 23 | color: darken($success_color,65%); 24 | &:before { 25 | content: '\f058'; 26 | } 27 | } 28 | &--fail { 29 | background-color: $error_color; 30 | border: 1px solid darken($error_color,8%); 31 | color: $error_color--dark; 32 | &:before { 33 | content: '\f06a'; 34 | } 35 | } 36 | &--no-round-bottom { 37 | border-radius: $flash-message--border-radius $flash-message--border-radius 0 0; 38 | } 39 | .flash-message__title { 40 | margin-top: 0; 41 | margin-bottom: 0.25rem; 42 | line-height: 1.2; 43 | white-space: pre-line; 44 | font-weight: bold; 45 | @include font-size(17px); 46 | word-wrap: break-word; 47 | &--not-bold { 48 | font-weight: normal; 49 | } 50 | &:last-child { 51 | margin-bottom: 0; 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /assets/jade/email-demo.jade: -------------------------------------------------------------------------------- 1 | extends layout/layout-email 2 | 3 | block vars 4 | - subjectTitle = "Hello World" 5 | 6 | block inlineStyles 7 | block inlineIE7Styles 8 | block inlineOutlookStyles 9 | 10 | block content 11 | table(width="100%", cellpadding="0", cellspacing="0", border="0", align="center") 12 | tr 13 | td(valign="top", style="vertical-align: top;") 14 | p(style="font-family:#{font_family}; font-size: 16px; color: #{basicTextColor}; margin: 0 0 16px 0;") Hi Chris 15 | p(style="font-family:#{font_family}; font-size: 16px; color: #{basicTextColor}; margin: 0 0 16px 0;") Click this link to confirm your email address: 16 | p(style="font-family:#{font_family}; font-size: 16px; color: #{basicTextColor}; margin: 0 0 16px 0;") 17 | a(href="#" style="font-family:#{font_family}; font-size: 16px; color: #{basicLinkColor};") http://www.linkgoeshere.com 18 | p(style="font-family:#{font_family}; font-size: 16px; color: #{basicTextColor}; margin: 0 0 16px 0;") 19 | br 20 | | Thanks 21 | br 22 | br 23 | | #{appName} Admin 24 | 25 | block footerContent 26 | table 27 | tr 28 | td(style="font-family:#{font_family}; font-size: 10px; color: #{footerTextColor}; margin: 0;") 29 | | This was not me, 30 | = " " 31 | a(href="#", style="font-family:#{font_family}; font-size: 10px; color: #{basicLinkColor};") delete account 32 | = "." 33 | -------------------------------------------------------------------------------- /assets/jade/profile-created.jade: -------------------------------------------------------------------------------- 1 | extends layout/layout 2 | 3 | block vars 4 | - bodyClass = "func--profile-created-page" 5 | - pageTitle = "!Your Profile Card has been created" 6 | - pageTitleDataL8n = "content:profile-created/title" 7 | 8 | block headerScripts 9 | 10 | block content 11 | .three-fourths 12 | h1.page-title(data-l8n="content:profile-created/page-header") 13 | | !Your Profile Card has been created 14 | 15 | +flashMessageWithCljContainer('clj--flash-message-container','func--flash-message-container', 'success', ['small-bottom-margin']) 16 | p.flash-message__title 17 | span(data-l8n="content:flash/confirm-email-sent-pre-email-address") !A confirmation email has been sent to: 18 | = " " 19 | span.clj--email-address !email@example.com 20 | p.flash-message__title.flash-message__title--not-bold(data-l8n="content:flash/confirm-email-sent-post-email-address") !Please follow the instructions to confirm your email address. 21 | 22 | .card-demo 23 | .card-demo__created 24 | 25 | p.clj-wip 26 | span(data-l8n="content:profile-created/intro") !Your Profile Card can be used to sign in to supported applications like 27 | = " " 28 | span.clj--client-name !Green Party Voting. 29 | 30 | a.button.button--primary.func--profile-created-next__button.clj--profile-created-next__button(href="./profile", data-l8n="content:profile-created/action-button-default") !View your profile 31 | -------------------------------------------------------------------------------- /src/stonecutter/admin.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.admin 2 | (:require [stonecutter.config :as config] 3 | [stonecutter.validation :as v] 4 | [clojure.tools.logging :as log] 5 | [stonecutter.db.user :as u])) 6 | 7 | (defn create-admin-user [config-m user-store] 8 | (let [admin-first-name (config/admin-first-name config-m) 9 | admin-last-name (config/admin-last-name config-m) 10 | admin-login (config/admin-login config-m) 11 | admin-password (config/admin-password config-m) 12 | duplication-checker (partial u/user-exists? user-store) 13 | errors (or (v/validate-registration-email admin-login duplication-checker) 14 | (v/validate-password-format admin-password))] 15 | (when (and admin-login admin-password) 16 | (case errors 17 | :duplicate (log/info "Admin account already exists.") 18 | :invalid (throw (Exception. "INVALID ADMIN LOGIN DETAILS - please check that admin login is in the correct format")) 19 | :too-short (throw (Exception. "ADMIN PASSWORD is too short - please check that admin password is in the correct format")) 20 | :too-long (throw (Exception. "ADMIN PASSWORD is too long - please check that admin password is in the correct format")) 21 | nil (u/store-admin! 22 | user-store 23 | admin-first-name 24 | admin-last-name 25 | admin-login 26 | admin-password))))) 27 | -------------------------------------------------------------------------------- /src/stonecutter/controller/stylesheets.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.controller.stylesheets 2 | (:require [ring.util.response :as r] 3 | [garden.core :as garden] 4 | [stonecutter.config :as config])) 5 | 6 | (defn header-bg-color-css [config-m] 7 | (when-let [header-bg-color (config/header-bg-color config-m)] 8 | (garden/css {:pretty-print? false} 9 | [:.header {:background-color header-bg-color}]))) 10 | 11 | (defn logo-file-name [config-m] 12 | (if (and (config/static-resources-dir-path config-m) 13 | (config/logo-file-name config-m)) 14 | (str "/" (config/logo-file-name config-m)) 15 | "../images/logo.svg")) 16 | 17 | (defn header-logo-css [config-m] 18 | (let [logo-file-name (logo-file-name config-m)] 19 | (garden/css {:pretty-print? false} 20 | [:.header__logo {:background-image (str "url(" logo-file-name ")")}]))) 21 | 22 | (defn header-font-color-css [config-m] 23 | (garden/css {:pretty-print? false} 24 | [:.header-nav__link {:color (config/header-font-color config-m)} 25 | [:&:hover {:color (config/header-font-color-hover config-m)}]])) 26 | 27 | (defn generate-theme-css [config-m] 28 | (str 29 | (header-bg-color-css config-m) 30 | (header-logo-css config-m) 31 | (header-font-color-css config-m))) 32 | 33 | (defn theme-css [request] 34 | (let [config-m (get-in request [:context :config-m])] 35 | (-> (r/response (generate-theme-css config-m)) 36 | (r/content-type "text/css")))) 37 | -------------------------------------------------------------------------------- /test/stonecutter/test/config.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.test.config 2 | (:require [midje.sweet :refer :all] 3 | [stonecutter.config :as c] 4 | [clojure.java.io :as io])) 5 | 6 | 7 | (fact "get-env throws an exception when the requested key isn't in the env-vars set" 8 | (c/get-env {:env-key "env-var"} :some-key-that-isnt-in-env-vars) => (throws Exception)) 9 | 10 | (tabular 11 | (fact "secure? is true by default" 12 | (c/secure? {:secure ?secure-env-value}) => ?return-value) 13 | ?secure-env-value ?return-value 14 | "true" true 15 | "asdf" true 16 | "" true 17 | nil true 18 | "false" false) 19 | 20 | (fact "about to-env" 21 | (c/to-env :some-config) => "SOME_CONFIG") 22 | 23 | (fact "can generate config line in file" 24 | (c/gen-config-line {:some-config 1} [:some-config "This is a piece of config"]) 25 | => "# This is a piece of config\nSOME_CONFIG=1" 26 | (c/gen-config-line {:some-config 1} [:some-other-config "Some other description"]) 27 | => "# Some other description\n# SOME_OTHER_CONFIG=") 28 | 29 | (fact "can generate config file" 30 | (c/gen-config! {:var-a "blah" :var-b "dave"} [:var-a "Var a description" :var-b "Var b description"] "test-resources/config.env") 31 | (slurp "test-resources/config.env") => "# Var a description\nVAR_A=blah\n\n# Var b description\nVAR_B=dave" 32 | (io/delete-file "test-resources/config.env")) 33 | 34 | 35 | -------------------------------------------------------------------------------- /assets/jade/delete-app.jade: -------------------------------------------------------------------------------- 1 | extends layout/layout 2 | 3 | block vars 4 | - bodyClass = "func--delete-client-page" 5 | - pageTitle = "!Delete your app" 6 | - pageTitleDataL8n = "content:delete-app/title" 7 | 8 | block content 9 | .single-column 10 | .modal__content.clj-modal-contents.js-modal 11 | .modal__header 12 | h2.modal__title(data-l8n="content:delete-app/modal-delete-title") 13 | | !Are you sure you want to delete your app? 14 | .modal__body 15 | p(data-l8n="content:delete-app/modal-delete-lede") 16 | | !If you delete your app all your data will be removed. 17 | .modal-buttons 18 | .modal-buttons__primary 19 | form.delete-app.clj--delete-app__form.func--delete-app__form(action="./profile-deleted",method="post") 20 | button.button.button--full-width.func--delete-app__button(type="submit", data-l8n="content:delete-app/modal-delete-ok-button") !Delete app 21 | .modal-buttons__secondary 22 | a.button.button--full-width.button--link.clj--delete-app-cancel__link.func--delete-app-cancel__link(href="./profile", data-l8n="content:delete-app/modal-delete-cancel-button") !Cancel 23 | 24 | block footerScripts 25 | if staticMode 26 | script. 27 | var form = document.getElementsByClassName('delete-app')[0]; 28 | form.addEventListener("submit", function (e) { 29 | e.preventDefault(); 30 | var formAction = form.getAttribute('action') + "?"; 31 | window.location = formAction; 32 | }); 33 | -------------------------------------------------------------------------------- /assets/jade/delete-account.jade: -------------------------------------------------------------------------------- 1 | extends layout/layout 2 | 3 | block vars 4 | - bodyClass = "func--delete-account-page" 5 | - pageTitle = "!Delete your account" 6 | - pageTitleDataL8n = "content:delete-account/title" 7 | 8 | block content 9 | .single-column 10 | .modal__content.clj-modal-contents.js-modal 11 | .modal__header 12 | h2.modal__title(data-l8n="content:delete-account/modal-delete-title") 13 | | !Are you sure you want to delete your account? 14 | .modal__body 15 | p(data-l8n="content:delete-account/modal-delete-lede") 16 | | !If you delete your account all your data will be removed. 17 | .modal-buttons 18 | .modal-buttons__primary 19 | form.delete-account.clj--delete-account__form(action="./profile-deleted",method="post") 20 | button.button.button--full-width.func--delete-account__button(type="submit", data-l8n="content:delete-account/modal-delete-ok-button") !Delete account 21 | .modal-buttons__secondary 22 | a.button.button--full-width.button--link.clj--delete-account-cancel__link(href="./profile", data-l8n="content:delete-account/modal-delete-cancel-button") !Cancel 23 | 24 | block footerScripts 25 | if staticMode 26 | script. 27 | var form = document.getElementsByClassName('delete-account')[0]; 28 | form.addEventListener("submit", function (e) { 29 | e.preventDefault(); 30 | var formAction = form.getAttribute('action') + "?"; 31 | window.location = formAction; 32 | }); 33 | -------------------------------------------------------------------------------- /src/stonecutter/view/error.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.view.error 2 | (:require [net.cgrand.enlive-html :as html] 3 | [stonecutter.view.view-helpers :as vh] 4 | [stonecutter.routes :as r])) 5 | 6 | (defn modify-error-translation-keys [enlive-map error-page-key] 7 | (html/at enlive-map 8 | [:body] (html/set-attr :class (str "func--" error-page-key "-page")) 9 | [:title] (html/set-attr :data-l8n (str "content:" error-page-key "/title")) 10 | [:.clj--error-page-header] (html/set-attr :data-l8n (str "content:" error-page-key "/page-header")) 11 | [:.clj--error-page-intro] (html/set-attr :data-l8n (str "content:" error-page-key "/page-intro")))) 12 | 13 | (defn set-return-to-link [enlive-m path] 14 | (html/at enlive-m [:.clj--error-return-home__link] (html/set-attr :href path))) 15 | 16 | (defn internal-server-error [request] 17 | (-> (vh/load-template-with-lang "public/error-500.html" request) 18 | (set-return-to-link (r/path :index)))) 19 | 20 | (defn not-found-error [request] 21 | (-> (internal-server-error request) (modify-error-translation-keys "error-404"))) 22 | 23 | (defn csrf-error [request] 24 | (-> (internal-server-error request) (modify-error-translation-keys "error-csrf"))) 25 | 26 | (defn forbidden-error [request] 27 | (-> (internal-server-error request) (modify-error-translation-keys "error-forbidden"))) 28 | 29 | (defn account-nonexistent [request] 30 | (-> (internal-server-error request) (modify-error-translation-keys "error-account-nonexistent"))) 31 | -------------------------------------------------------------------------------- /ops/deploy_snap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ssh $REMOTE_USER@$SERVER_IP "mkdir -p /var/stonecutter/config" 4 | ssh $REMOTE_USER@$SERVER_IP "mkdir -p /data/stonecutter/static" 5 | scp stonecutter_$SNAP_STAGE_NAME.env $REMOTE_USER@$SERVER_IP:~/stonecutter.env 6 | scp rsa-keypair_$SNAP_STAGE_NAME.json $REMOTE_USER@$SERVER_IP:~/rsa-keypair.json 7 | scp clients.yml $REMOTE_USER@$SERVER_IP:~/clients.yml 8 | scp logo.svg $REMOTE_USER@$SERVER_IP:~/logo.svg 9 | scp dcent-favicon.ico $REMOTE_USER@$SERVER_IP:~/dcent-favicon.ico 10 | ssh $REMOTE_USER@$SERVER_IP <nodejs 24 | file: src=/usr/bin/nodejs dest=/usr/bin/node state=link 25 | 26 | - name: Node.js | Install packages 27 | npm: name={{item}} global=yes 28 | with_items: 29 | - npm 30 | - gulp 31 | 32 | - name: download firefox 33 | get_url: url=https://ftp.mozilla.org/pub/firefox/releases/38.0/linux-x86_64/en-US/firefox-38.0.tar.bz2 dest=/opt/ 34 | 35 | - name: extract firefox 36 | unarchive: src=/opt/firefox-38.0.tar.bz2 dest=/opt/ copy=no 37 | 38 | - name: link firefox binary to path 39 | file: src=/opt/firefox/firefox dest=/usr/bin/firefox state=link 40 | 41 | - name: install xvfb 42 | apt: name=xvfb state=present 43 | 44 | - name: install phantomjs 45 | apt: name=phantomjs state=present 46 | 47 | - name: Install node dependencies 48 | npm: path=/var/stonecutter 49 | -------------------------------------------------------------------------------- /resources/lang/en-client.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | index: 4 | register-first-name-blank-validation-message: First name cannot be blank 5 | register-first-name-too-long-validation-message: First name must be shorter 6 | register-last-name-blank-validation-message: Last name cannot be blank 7 | register-last-name-too-long-validation-message: Last name must be shorter 8 | register-email-address-blank-validation-message: Email address cannot be blank 9 | register-email-address-invalid-validation-message: Enter a valid email address 10 | register-email-address-duplicate-validation-message: User already exists 11 | register-email-address-too-long-validation-message: Email address is too long 12 | register-password-blank-validation-message: Password cannot be blank 13 | register-password-too-long-validation-message: Password is too long 14 | register-password-too-short-validation-message: Password should be at least 8 characters long 15 | 16 | change-password-form: 17 | current-password-invalid-validation-message: Current password is incorrect 18 | new-password-blank-validation-message: New password cannot be blank 19 | new-password-too-long-validation-message: New password is too long 20 | new-password-too-short-validation-message: New password should be at least 8 characters long 21 | new-password-unchanged-validation-message: New password must be different to current password 22 | 23 | upload-profile-picture: 24 | picture-too-large-validation-message: Please select an image with a size smaller than 5MB. 25 | picture-not-image-validation-message: Please select an image file. -------------------------------------------------------------------------------- /config/rsa-keypair.json: -------------------------------------------------------------------------------- 1 | {"kty":"RSA","kid":"9319248-amy","n":"nKuOVCSDxJ7sUOn4ld-FiE0c_Q6uM6EfAQfC5zkWdwxNGsxxoLUnO4em6jMj1lmHcNqIz20pXOOFatkazZXfTHqwt1U0AfenmNz-YjB3PRt1XCbf2I40O71p6xrwCZ0mENO3-WmilU6KdVlew_YKsCUKNQMsJlB7Eg8b03KzgtaQwxj_M-nd8qK2wesMSYP0BTi7nFIbmlKRU5MGnkiUERqGrPT8nS6W_CsaCvP8KztY4uEPrRnymtzxu1oamAuRE2vrk6kJazF-JQZMJ_hiWhh3q-RFzmoNegFfOXuF_9MXbfAfbJJtaUT6723IkQdnOqNTN68WunINCD2G9EaaJQ","e":"AQAB","d":"d2RJb5fwDqupnTPgK56OLUPOvo4sdM3pSai3f-vCUfC-5Zg9U2IZxsTmj31vZw8NJxRyw9gFeHMunjFauqbm52psNxooKwOm5dgrx3n1tEyzkAlT9rzP02LST8dUe48Vjic6AUjeDHgxDaKQlDdxB7ECaUnEoGIel7GJ3AaCBEb38MYxibfY4NzK98W45lRMKLFCNAR-UpeSGl5m9ghBrBBQCfr5WwCgr2ywNBGc8P7sVIS74f0q1h_nboPmDGrFAVhdwQQsPlyQsI1zkuFcySqGGUzfOjwKpMjggMfnv1WkDmkdHNYhKs_ToDh6VBkKKYOlZ5Y4EgGP0AaIXg2R9Q","p":"69jxIS80FLKpe5OR_TMjeCox7Xwnu7il22RrYiOyhMgL4v9HJhLhCHuiXoJdNqRA-hrqkXZbyxRnjW-0VAZbgmfBQkjsxqrEwaBzZqYws9t-rdCMBfjycAaK5HShEwJxIkFMszBBRH8xQOPAyC3KFJ32ZCzhpTnbYErWPkB5aBM","q":"qg6lW0dyVQSL1Md3-s3e7kUC8in-SmDRDfH7UqCNW4fZTe_b1zzLj83EtkKjM6Mzm95far9PdSOmJV0FwRKT9yxRezqpP2bYs6eZFS-Twi9w1OdmnphDZ2Ef__8bP1SOTfQmlIe2Od722cJLLqaLi_-EpSGny7nq-v47vOmXq-c","dp":"vlAtI-4HRLx27vVB1OOzN4A1EcIh_eUeUU9TJ7pOj78teFsuepa5aDL77u-bcEBS_n9B0WeE4vbwKL1-8l6-8RfwZyCZkCa05tomxSpOGpSAQJoMo7UjTSq6YWeLot9Rk6bF0cKaqD9K62XdZqssaj6dWHCvFLj_QVCiWVGFpmk","dq":"JRCEIdz0omsXPw0qrVNpn20TM0zLy9JGPk_bgyrTJHv7dO0ucT8i7vjeQwtLHtoXSYWyLkX8P3_BmqsnsekmrtzN9ZnXgaYc3StORjvUbKKCaST2TOLK7iFRJC6p9aesTHlGX2Ek6oAaNWQPlt9d0umiM4ueUtz2xjXbN1WCZUk","qi":"R3xeyB012jbU6wQPkYiSEpN-iRW1joF4GCA0gIJwMcHYDS2l1fzV5uIVyIoOyvhmdCIL-GO_EmbpUSFo9wSC7_K9VuZM3nodY2xpa3UxP05x4qA72E9aDMvfWQc1FdvIZg77YP0_pduLAT9WDHD1K-PRyhTYPDpuUJ05dZj0Gho"} -------------------------------------------------------------------------------- /ops/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | VAGRANTFILE_API_VERSION = "2" 5 | digital_ocean_memory = 1024 6 | 7 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 8 | 9 | config.vm.box = "ubuntu/trusty64" 10 | 11 | config.vm.define "development", primary: true do |dev| 12 | dev.vm.network "private_network", ip: "192.168.50.60" 13 | dev.vm.network :forwarded_port, guest: 22, host: 2222, id: "ssh", auto_correct: false 14 | dev.vm.network :forwarded_port, guest: 5000, host: 5000, id: "app", auto_correct: false 15 | 16 | dev.vm.synced_folder "../", "/var/stonecutter" 17 | 18 | dev.vm.provider :virtualbox do |vbox| 19 | vbox.customize ["modifyvm", :id, "--memory", 2048] 20 | end 21 | 22 | dev.vm.provision "ansible" do |ansible| 23 | ansible.playbook = "development_playbook.yml" 24 | ansible.inventory_path = "development.inventory" 25 | ansible.sudo = true 26 | end 27 | end 28 | 29 | config.vm.define "dob_vm", primary: true do |dob| 30 | dob.vm.network "private_network", ip: "192.168.50.61" 31 | dob.vm.network :forwarded_port, guest: 22, host: 2223, id: "ssh", auto_correct: false 32 | 33 | dob.vm.provider "virtualbox" do |vbox| 34 | vbox.customize ["modifyvm", :id, "--memory", digital_ocean_memory, "--cpus", 1] 35 | end 36 | 37 | dob.vm.provision "ansible" do |ansible| 38 | ansible.playbook = "dob_vm_playbook.yml" 39 | ansible.inventory_path = "dob_vm.inventory" 40 | ansible.sudo = true 41 | end 42 | 43 | dob.vm.synced_folder "../", "/var/stonecutter" 44 | end 45 | 46 | end 47 | -------------------------------------------------------------------------------- /test-resources/test-key.json: -------------------------------------------------------------------------------- 1 | {"kty":"RSA","kid":"test-key","n":"jwbc0t66Ik9T9JEhcmaanmd_zk21_I4WGHFS2B7UEIIXQYtzenr83qBiebGnYkiwIyAEX-_XE0Hqnof3EWKqGNjZUm9U0r3vLiXAi1OKgnr-_C2-Q3MebmfczfYvmenhyVUErd4HL_ILSg4iOOiN2pkRNBN7deYUpCr0swuW8zSIjejNTwDS_xXInBTiFsl26iT6Hc1DKX6QD1rJUr6ZC7mQYXTK_Dk0tmgkmn2-9k5pkKVo9xRfOi4l72jy_Imf1BeoAMRn-fzOdApNglgWS6RxJRszXjumOGY72OscyF22WHmbwQI9lovF3q8J3326sJH7fSVNVc0I_rzqUL8k0w","e":"AQAB","d":"U91WRl9LDSsus00WK5p2N0PA8RsoBrrZweNRDGCnQDbHpCs8vyi2dWPd2jWNTFgKz83KQubDWgtgoyxedtc_neopI-kb96ZfRNPmHswRf4jXUs8PrUUnJt3H3wznxHwbZI5xe_GgjUCD1hyLfIsAApmWOM7jqbILkGePrByzmk7mO2QoTDu48B3HL7RV5p9JPSq8cjK77Fb0gcZ1dkVMA3sX5kN9TIUC0Na_sjo2BsXe-HD9oF3rgEnMaVMedOhCXrRBagHTedMoijzfoAYtsob-_Xt1vHzelNE5LVpVlFHQH0-12wHazVZroQyMlEb_FOXFdmXvmD2EwooGJ8P-QQ","p":"0GEl5uCQk30jGlPOmyr-e8-IypC0g-zON8DANYLiRxrfOh1XcQn-HQnMMVjksembMakmnWh2CWj3stpMBKByYOs6LTtH-mpPgS853KA--XIQ7_75-6wLpt5KwAOb9Uh68hvsNuJC6R8pThcVP8ciq33s3QbTw37llOTlQtQonpk","q":"r7Zg3fR5U7UQ4HL-Q_hcy6RnM0aKTaw3CoTWfEcP3gUp_VyiWcJ2rhy69ss31xUYOfekzpX55WC53noHe0Un9Vfwo13i-7oAeo9Pe4k-g5H_QXxKZIsvatzCk0QBTFXCtPygcGPn0lpwBl9h2JXya4ErMTGt4m1SAlfwRYvQ3ks","dp":"oOYiif9kIz5A2JdVtOKh1aAOE7tgZ212Xf5ra-ZAKn9JVmpJlnMM0Ac8r-3fyLCEsPfXZTu_yMxQVr3QbNIm_0ciiMJ5dZaZBTseBomFlr7I7UeIZxgVdye1uEYRKnho1oFRB5_14mO5VR7lvXVaA-kb-B7JbO6S_0Eu9Uc0hiE","dq":"aDyLDmCfMPczAdN13yCQ_AWVayenmBhXtpfDHYqScSUjGbhAX3srKHLGvu0jPSa4bnroe90gl-BfowlFMu19nOAEUwW5R5e5_PrTLffm6-pKQLDY-PhQstYvX2lyU7R0gyVyj_nUZkdcOYuP4gph-0BvPQm5m586jUVZtggRai0","qi":"RGyusuU5JY3mpESLP_KGcUM0aG93pohX-2o9c-fRGceCVWvwC8nhmTjLJl0m-KzVpNlQmhcl5AViORXZRgvSW8HjC2kAQx9kMeDISXSnOUn3X96eMTZ1vYAECMqMBvIJRqMqdaJFw8hMY_2bQlTNjMLM02993fO4lWZX-ccJc4g"} 2 | -------------------------------------------------------------------------------- /test/stonecutter/test/helper.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.test.helper 2 | (:require [midje.sweet :refer :all] 3 | [ring.util.response :as response] 4 | [stonecutter.helper :as h] 5 | [stonecutter.helper :as helper] 6 | [net.cgrand.enlive-html :as html])) 7 | 8 | (fact "disabling caching should add the correct headers" 9 | (let [r (-> (response/response "a-response") h/disable-caching)] 10 | (get-in r [:headers "Pragma"]) => "no-cache" 11 | (get-in r [:headers "Cache-Control"]) => "no-cache, no-store, must-revalidate" 12 | (get-in r [:headers "Expires"]) => "0") 13 | 14 | (fact "if response is nil, then should return nil" 15 | (h/disable-caching nil) => nil)) 16 | 17 | (defn get-link-href [enlive-m] 18 | (-> enlive-m (html/select [:link]) first :attrs :href)) 19 | 20 | (defn get-response-enlive-m [response] 21 | (-> response :body html/html-snippet)) 22 | 23 | (fact "Enlive response injects the app name anywhere where class is clj--app-name" 24 | (let [html "

" 25 | enlive-m (html/html-snippet html)] 26 | (-> (helper/enlive-response enlive-m {:context {:config-m {:app-name "My App"} :translator {}}}) 27 | get-response-enlive-m 28 | (html/select [:h1]) first html/text) => "My App" 29 | (-> (helper/enlive-response enlive-m {:context {:config-m {:app-name "My App"} :translator {}}}) 30 | get-response-enlive-m 31 | (html/select [:span]) first html/text) => "My App")) 32 | -------------------------------------------------------------------------------- /src-cljs/stonecutter/js/dom/register_form.cljs: -------------------------------------------------------------------------------- 1 | (ns stonecutter.js.dom.register-form 2 | (:require [stonecutter.js.dom.common :as dom])) 3 | 4 | (def register-form-element-selector :.clj--register__form) 5 | 6 | (def field-invalid-class :form-row--invalid) 7 | (def field-valid-class :form-row--valid) 8 | 9 | (def selectors 10 | {:registration-first-name {:input :.clj--registration-first-name__input 11 | :form-row :.clj--registration-first-name 12 | :validation :.clj--registration-first-name__validation} 13 | :registration-last-name {:input :.clj--registration-last-name__input 14 | :form-row :.clj--registration-last-name 15 | :validation :.clj--registration-last-name__validation} 16 | :registration-email {:input :.clj--registration-email__input 17 | :form-row :.clj--registration-email 18 | :validation :.clj--registration-email__validation} 19 | :registration-password {:input :.clj--registration-password__input 20 | :form-row :.clj--registration-password 21 | :validation :.clj--registration-password__validation}}) 22 | 23 | (defn form-row-selector [field-key] 24 | (get-in selectors [field-key :form-row])) 25 | 26 | (defn input-selector [field-key] 27 | (get-in selectors [field-key :input])) 28 | 29 | (defn validation-selector [field-key] 30 | (get-in selectors [field-key :validation])) 31 | 32 | (defn get-value [field-key] 33 | (dom/get-value (input-selector field-key))) 34 | 35 | -------------------------------------------------------------------------------- /assets/jade/admin-invite-user.jade: -------------------------------------------------------------------------------- 1 | extends layout/layout 2 | 3 | block vars 4 | - bodyClass = "func--admin-invite-user-page" 5 | - pageTitle = "!Sign in" 6 | - pageTitleDataL8n = "content:admin-invite-user/title" 7 | 8 | block headerScripts 9 | 10 | block headerContent 11 | include _admin_navigation 12 | 13 | block content 14 | 15 | .single-column 16 | 17 | +flashMessageWithCljContainer('clj--flash-message-container','func--flash-message-container', 'success') 18 | p.flash-message__title 19 | span.clj--invited-email.func--invited-email !someone@somewhere.com 20 | = " " 21 | span.flash-message-text(data-l8n="content:admin-invite-user/invited-flash-success") !was successfully added 22 | 23 | h1.page-title(data-l8n="content:admin-invite-user/page-title") 24 | | !Invite a user 25 | form.login-form(action="./invite-sent", method="post", role="form") 26 | p.form-row.clj--invite-user-email 27 | label.form-row__label(for="email") 28 | span.form-row__title(data-l8n="content:admin-invite-user/email-address") 29 | | !Email address 30 | span.form-row__validation.clj--invite-user-email__validation(data-l8n="content:admin-invite-user/invite-email-address-invalid-validation-message") 31 | | !Enter a valid email address 32 | input.form-row__input.clj--email__input.func--email__input(type="email", id="email", name="email") 33 | 34 | button.button.button--full-width-smallish-devices.func--invite-user__button(type="submit", data-l8n="content:admin-invite-user/invite-button") 35 | | !Invite 36 | 37 | block footerScripts 38 | -------------------------------------------------------------------------------- /src/stonecutter/view/delete_account.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.view.delete-account 2 | (:require [traduki.core :as t] 3 | [net.cgrand.enlive-html :as html] 4 | [stonecutter.routes :as r] 5 | [stonecutter.view.view-helpers :as vh])) 6 | 7 | (defn set-form-action [path enlive-m] 8 | (html/at enlive-m [:.clj--delete-account__form] (html/set-attr :action path))) 9 | 10 | (defn set-cancel-link [path enlive-m] 11 | (html/at enlive-m [:.clj--delete-account-cancel__link] (html/set-attr :href path))) 12 | 13 | (defn set-register-link [path enlive-m] 14 | (html/at enlive-m [:.clj--profile-deleted-next__button] (html/set-attr :href path))) 15 | 16 | (defn delete-account-confirmation [request] 17 | (->> (vh/load-template-with-lang "public/delete-account.html" request) 18 | (set-form-action (r/path :delete-account)) 19 | (set-cancel-link (r/path :show-profile)) 20 | vh/add-anti-forgery 21 | vh/remove-work-in-progress)) 22 | 23 | (defn profile-deleted [request] 24 | (->> (vh/load-template-with-lang "public/profile-deleted.html" request) 25 | (set-register-link (r/path :index)) 26 | vh/remove-work-in-progress)) 27 | 28 | (defn email-confirmation-delete-account [request] 29 | (let [confirmation-id (get-in request [:params :confirmation-id] "missing-confirmation-id")] 30 | (->> (vh/load-template-with-lang "public/delete-account.html" request) 31 | (set-form-action (r/path :show-confirmation-delete 32 | :confirmation-id confirmation-id)) 33 | (set-cancel-link (r/path :index)) 34 | vh/add-anti-forgery 35 | vh/remove-work-in-progress))) 36 | -------------------------------------------------------------------------------- /assets/stylesheets/core/_typography.scss: -------------------------------------------------------------------------------- 1 | @mixin h1 { 2 | margin-top: 1.25rem; 3 | margin-bottom: 1.25rem; 4 | @include font-size($h1_font-size--small); 5 | font-weight: bold; 6 | line-height: 1.2; 7 | letter-spacing: -0.025em; 8 | color: $default_text_color; 9 | @include width-from($medium_device) { 10 | margin-top: 1.6rem; 11 | margin-bottom: 1.6rem; 12 | @include font-size($h1_font_size--medium); 13 | } 14 | } 15 | @mixin h2 { 16 | margin-top: 0; 17 | @include font-size($h2_font_size--small); 18 | font-weight: 500; 19 | line-height: 1.2; 20 | letter-spacing: -0.02em; 21 | color: $default_text_color; 22 | @include width-from($medium_device) { 23 | @include font-size($h2_font_size--medium); 24 | } 25 | } 26 | 27 | .page-title { 28 | @include h1; 29 | } 30 | 31 | h2 { 32 | @include h2; 33 | } 34 | 35 | p { 36 | @include font-size($paragraph_font_size); 37 | margin-top: 0; 38 | line-height: 1.3; 39 | } 40 | 41 | .no-bullet { 42 | list-style: none; 43 | padding-left: 0; 44 | } 45 | 46 | .highlight { 47 | &--dark-pink { 48 | color: $dark_pink; 49 | } 50 | &--dark-cyan { 51 | color: $dark_cyan; 52 | } 53 | } 54 | 55 | .def-list { 56 | margin-top: 1rem; 57 | margin-bottom: 1rem; 58 | text-align: left; 59 | &__title { 60 | margin-bottom: 3px; 61 | color: $dark_grey; 62 | @include width-from($smallish_device) { 63 | margin-bottom: 7px; 64 | float: left; 65 | } 66 | } 67 | &__data { 68 | margin-bottom: 10px; 69 | margin-left: 0; 70 | color: $medium_grey; 71 | @include width-from($smallish_device) { 72 | margin-bottom: 7px; 73 | margin-left: 150px; 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /assets/jade/layout/layout.jade: -------------------------------------------------------------------------------- 1 | include ../mixins/_helpers 2 | include ../mixins/_view_helpers 3 | include ../mixins/_flash_message 4 | include ../mixins/_profile 5 | include ../mixins/_admin 6 | 7 | - var bodyClass = "" 8 | - var pageTitle = "Stonecutter" 9 | - var pageTitleDataL8n = "content:default/title" 10 | - demoAppURL = demoAppURL || "http://thoughtworksinc.github.io/stonecutter-client" 11 | 12 | block vars 13 | 14 | doctype 15 | html(lang="en") 16 | head 17 | meta(charset='utf-8') 18 | meta(http-equiv='X-UA-Compatible', content='IE=edge') 19 | title(data-l8n="#{pageTitleDataL8n}") #{pageTitle} 20 | meta(name="viewport", content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no") 21 | meta(name="format-detection", content="telephone=no") 22 | link(rel="shortcut icon" href="/stonecutter-favicon.ico") 23 | script(type="text/javascript"). 24 | document.getElementsByTagName('html')[0].className += ' js'; 25 | block headerMeta 26 | 27 | +stylesheetLinkTag("application","screen") 28 | +stylesheetLinkTag("theme","screen") 29 | 30 | block headerScripts 31 | 32 | body(class="#{bodyClass}") 33 | a.skip-link(href="#main") Skip navigation 34 | .header(role="banner") 35 | .header__inner.middle-container 36 | .header__logo 37 | span.visuallyhidden.clj--app-name Stonecutter 38 | nav.header-nav.clj--header-nav(role="navigation") 39 | block headerContent 40 | .main-content#main(role="main" tabindex="-1") 41 | block columns 42 | .columns 43 | block content 44 | script. 45 | document.addEventListener("touchstart", function(){}, true); 46 | block footerScripts 47 | -------------------------------------------------------------------------------- /resources/lang/fi-client.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | index: 4 | register-first-name-blank-validation-message: First name cannot be blank in Finnish 5 | register-first-name-too-long-validation-message: First name must be shorter in Finnish 6 | register-last-name-blank-validation-message: Last name cannot be blank in Finnish 7 | register-last-name-too-long-validation-message: Last name must be shorter in Finnish 8 | register-email-address-blank-validation-message: Email address cannot be blank in Finnish 9 | register-email-address-invalid-validation-message: Enter a valid email address in Finnish 10 | register-email-address-duplicate-validation-message: User already exists in Finnish 11 | register-email-address-too-long-validation-message: Email address is too long in Finnish 12 | register-password-blank-validation-message: Password cannot be blank in Finnish 13 | register-password-too-long-validation-message: Password is too long in Finnish 14 | register-password-too-short-validation-message: Password should be at least 8 characters long in Finnish 15 | 16 | change-password-form: 17 | current-password-invalid-validation-message: Current password is incorrect in Finnish 18 | new-password-blank-validation-message: New password cannot be blank in Finnish 19 | new-password-too-long-validation-message: New password is too long in Finnish 20 | new-password-too-short-validation-message: New password should be at least 8 characters long in Finnish 21 | new-password-unchanged-validation-message: New password must be different to current password in Finnish 22 | 23 | upload-profile-picture: 24 | picture-too-large-validation-message: Please select an image with a size smaller than 5MB. in Finnish 25 | picture-not-image-validation-message: Please select an image file. in Finnish -------------------------------------------------------------------------------- /ops/roles/nginx/files/stonecutter.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA6UHomRFXnkfZyuly3GlBFWLZg53P6z3CPt2KE7Pu42vLXNBb 3 | QDBkY8LNko1yFLqsHTIcgqk05mVPRqar9rycUUCHJx364hc65KMDJxElTax/QeDB 4 | PFWqSZfQzIHlNasctCpd2SUzihpaCUkCUMAKEUmnQQq29Ck/7870+55l7Xo7wrW8 5 | p9Qlu7+rWFvW5wZ96fk+U6PuFiIYAJ40Hi3N32Wvea1R466fOLAzBrnIsGzHzYD1 6 | TZkVp5BYas3lVhopENXTlssv2ZoyxHE/uGdffdLxXXSUffaXMQF8hYXkexzK+g19 7 | uCo1LLOUgIH2Xzu2nghjEupepEqkTdNY4gxTQQIDAQABAoIBAQDd5m9qPo9F6kCM 8 | wm0ctZzOxYz7osPLnKMPvx2+BKy7+S4ri6NjeyXlqcp7IshmY/echrGMs3+5tqMm 9 | KSTqwx9KRMLOOb0UHNlP7KvxHGSKchFWEISD61LmU+C0zNXKqo/R7YP+MV/If60r 10 | rCLhwu9Q5uqP+6t0t1E1x4JTZKq+sHrXLRWZF7MW7uKl1MS19KvUSWhEVhC7MvQo 11 | lXPvsajUMHIMKewc1bAMe91assZZN/wXbd5QT7BBDtguYt9QA+mzikZ1KkopD1rL 12 | GT++79zq4n1UeawG0BE4wK3qbLnnweFcOZ5HiLQirOyftPmwAw+84qrAiwKBvYO1 13 | T5Pmu6bhAoGBAPqbhn7+FmyJg2BnBkANJLi/63GQeM4mwGf6PJfzxfWuDZ7r26ga 14 | 3yVhVFEc2a0TP4d70bwNezgo95LmgqEF/wOy9DFMkolAjc7DYqxZVc2fdHOeWjbr 15 | dexuUT0/5Lr8wCxRt9Wy/GUWLMRZDdyAGWMMCbNCjsnEGfTg5/PuSeF1AoGBAO5G 16 | z2aiFxaVSqZCg8GXwnD+PqkOf3fd0zuLY2PTM5q1V5ytRaCGr7UXgUo05uDwBTpG 17 | ++1Nj2Fv+5FfSycyZ9Mjzfc07ISV30xAoslfzzDe8CzkoPhe9Dq+BB/KOtgww+id 18 | 076ym+6bcgwXtloFg9bs2nbOzGOCSH6qgYid3IUdAoGAcps7Z8olUR+WIDkdR597 19 | Iq5KKxZJ2OUp6qMqoMcPyen+OqZcPsWDNSIMoEeUWK1LyVbbtKdpqWY6ykh2htMq 20 | K3PkbsM45GHMODlsX6s/LRj19YX+dc06kZRlvKACp2y9Kcd5TdZbzJLWiDi9uRAw 21 | C/bOXNdcW3M995n/GbPov50CgYEAgsgk4B1JdOC+V2Ecti5Yz/IMbHgDqMP9Q6pv 22 | BASRR3IPQ3SrSb+DQ29CTHua2Y2EIQeDES3H6+AuAQ1/z2TQLchyLSRESUWFiXHU 23 | p5jJvPYwd0OJwqlDfdZ7pwM1pyGk9dFivkGEasOxJkgBk4mBYn8gLaO19Uw2BCgL 24 | 7vNObVUCgYBFlLGfUBZ0H5V7fomwpHa4cDjbb/eyUSfBUf/tGyY8gD1D7DXWphBH 25 | vQw+sBQxWO+JTT0gj2qf6+ZajfdwDYwJJ5D9u0yy+fR0AR6rngymiCouvf7JO2In 26 | nemATzx0OA+w2LZHcD7yHYUAYHIaJTnyy1hnFvpOKD5JfmAwSyUkUw== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /src/stonecutter/view/confirmation_sign_in.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.view.confirmation-sign-in 2 | (:require [traduki.core :as t] 3 | [net.cgrand.enlive-html :as html] 4 | [stonecutter.routes :as r] 5 | [stonecutter.view.view-helpers :as vh])) 6 | 7 | (def error-translations 8 | {:credentials {:confirmation-invalid "content:confirmation-sign-in-form/invalid-credentials-validation-message"}}) 9 | 10 | (defn add-invalid-credentials-error [err enlive-m] 11 | (if (contains? err :credentials) 12 | (let [error-translation (get-in error-translations [:credentials (:credentials err)])] 13 | (-> enlive-m 14 | (html/at [:.clj--validation-summary] (html/add-class "validation-summary--show")) 15 | (html/at [:.clj--validation-summary__item] (html/set-attr :data-l8n (or error-translation "content:confirmation-sign-in-form/unknown-error"))))) 16 | (vh/remove-element enlive-m [:.clj--validation-summary]))) 17 | 18 | (defn set-confirmation-id [params enlive-m] 19 | (html/at enlive-m 20 | [:.clj--confirmation-id__input] (html/set-attr :value (:confirmation-id params)))) 21 | 22 | (defn set-form-action [path enlive-m] 23 | (html/at enlive-m [:form] (html/set-attr :action path))) 24 | 25 | (defn set-forgotten-password-link [enlive-m] 26 | (html/at enlive-m [:.clj--forgot-password] (html/set-attr :href (r/path :show-forgotten-password-form)))) 27 | 28 | (defn confirmation-sign-in-form [request] 29 | (->> (vh/load-template-with-lang "public/confirmation-sign-in.html" request) 30 | (set-form-action (r/path :confirmation-sign-in)) 31 | set-forgotten-password-link 32 | (set-confirmation-id (:params request)) 33 | (add-invalid-credentials-error (get-in request [:context :errors])) 34 | vh/add-anti-forgery)) 35 | -------------------------------------------------------------------------------- /test/stonecutter/test/db/expiry.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.test.db.expiry 2 | (:require [midje.sweet :refer :all] 3 | [stonecutter.db.mongo :as m] 4 | [clauth.store :as cl-store] 5 | [stonecutter.db.expiry :as e] 6 | [stonecutter.test.util.time :as test-time])) 7 | 8 | (fact "storing record with expiry" 9 | (let [clock (test-time/new-stub-clock 100) 10 | store (m/create-memory-store) 11 | doc {:name "craig"}] 12 | (e/store-with-expiry! store clock :name doc 2000) 13 | (cl-store/fetch store "craig") => {:name "craig" :_expiry 2100})) 14 | 15 | (tabular 16 | (fact "about fetch-with-expiry" 17 | (let [clock (test-time/new-stub-clock ?time) 18 | store (m/create-memory-store)] 19 | (cl-store/store! store :name {:name "craig" :_expiry 100}) 20 | (e/fetch-with-expiry store clock "craig") => ?result 21 | (cl-store/entries store) => ?store-contents)) 22 | ?time ?result ?store-contents 23 | 99 {:name "craig"} [{:name "craig" :_expiry 100}] 24 | 100 nil [] 25 | 101 nil [] 26 | ) 27 | 28 | (tabular 29 | (fact "about query-with-expiry" 30 | (let [clock (test-time/new-stub-clock ?time) 31 | store (m/create-memory-store)] 32 | (cl-store/store! store :name {:name "craig" :_expiry 100}) 33 | (e/query-with-expiry store clock :name {:name "craig"}) => ?result 34 | (cl-store/entries store) => ?store-contents)) 35 | ?time ?result ?store-contents 36 | 99 [{:name "craig"}] [{:name "craig" :_expiry 100}] 37 | 100 [] [] 38 | 101 [] []) -------------------------------------------------------------------------------- /ops/roles/nginx/files/stonecutter.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIE3DCCA8SgAwIBAgIJAKgZaeGnsuPVMA0GCSqGSIb3DQEBBQUAMIGkMQswCQYD 3 | VQQGEwJFTjEVMBMGA1UECBMMVGhvdWdodFdvcmtzMRUwEwYDVQQHEwxUaG91Z2h0 4 | V29ya3MxFTATBgNVBAoTDFRob3VnaHRXb3JrczEVMBMGA1UECxMMVGhvdWdodFdv 5 | cmtzMREwDwYDVQQDEwhURVNUIEtFWTEmMCQGCSqGSIb3DQEJARYXZC1jZW50QHRo 6 | b3VnaHR3b3Jrcy5jb20wHhcNMTUwNjE4MTQyNTQ0WhcNMTYwNjE3MTQyNTQ0WjCB 7 | pDELMAkGA1UEBhMCRU4xFTATBgNVBAgTDFRob3VnaHRXb3JrczEVMBMGA1UEBxMM 8 | VGhvdWdodFdvcmtzMRUwEwYDVQQKEwxUaG91Z2h0V29ya3MxFTATBgNVBAsTDFRo 9 | b3VnaHRXb3JrczERMA8GA1UEAxMIVEVTVCBLRVkxJjAkBgkqhkiG9w0BCQEWF2Qt 10 | Y2VudEB0aG91Z2h0d29ya3MuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB 11 | CgKCAQEA6UHomRFXnkfZyuly3GlBFWLZg53P6z3CPt2KE7Pu42vLXNBbQDBkY8LN 12 | ko1yFLqsHTIcgqk05mVPRqar9rycUUCHJx364hc65KMDJxElTax/QeDBPFWqSZfQ 13 | zIHlNasctCpd2SUzihpaCUkCUMAKEUmnQQq29Ck/7870+55l7Xo7wrW8p9Qlu7+r 14 | WFvW5wZ96fk+U6PuFiIYAJ40Hi3N32Wvea1R466fOLAzBrnIsGzHzYD1TZkVp5BY 15 | as3lVhopENXTlssv2ZoyxHE/uGdffdLxXXSUffaXMQF8hYXkexzK+g19uCo1LLOU 16 | gIH2Xzu2nghjEupepEqkTdNY4gxTQQIDAQABo4IBDTCCAQkwHQYDVR0OBBYEFO6h 17 | N3IzD1aP9uRyZ7cgWp6zXweRMIHZBgNVHSMEgdEwgc6AFO6hN3IzD1aP9uRyZ7cg 18 | Wp6zXweRoYGqpIGnMIGkMQswCQYDVQQGEwJFTjEVMBMGA1UECBMMVGhvdWdodFdv 19 | cmtzMRUwEwYDVQQHEwxUaG91Z2h0V29ya3MxFTATBgNVBAoTDFRob3VnaHRXb3Jr 20 | czEVMBMGA1UECxMMVGhvdWdodFdvcmtzMREwDwYDVQQDEwhURVNUIEtFWTEmMCQG 21 | CSqGSIb3DQEJARYXZC1jZW50QHRob3VnaHR3b3Jrcy5jb22CCQCoGWnhp7Lj1TAM 22 | BgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQCs+H9WeFqhUvltOz2JfqiR 23 | 29EenyyYRRz469HGRoOasmude4YwAyldDryOK0/D4EN6YceJVUdLxEpilhip3RbT 24 | fKJKmDkluCVx+JaFijkTbekAFMIGJS+YJk2XLvemwaNo5XUez3/QtCIYE3a88iEe 25 | W8xLeCMQwgxp2qwU7XG5nerI6lwSeqqDup0VMtsq1gbAYquFF6LzHjrj8aZdxbkW 26 | npKNMk35Qybgum8hUObxGKUGs5kFYwXxWyknXfqktNNTb7CU8ED4khstpJmouMqT 27 | QkWj1LRFJUdvZ4QOSC77X0DsO6FLRW1uNv8BG6l75EC/iRvU0+pSCtFQ/0hOqOmL 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /test/stonecutter/test/db/invitations.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.test.db.invitations 2 | (:require [midje.sweet :refer :all] 3 | [stonecutter.db.invitations :as i] 4 | [stonecutter.db.mongo :as m] 5 | [clauth.store :as cl-store] 6 | [stonecutter.util.time :as time] 7 | [stonecutter.test.util.time :as test-time] 8 | [stonecutter.util.uuid :as uuid])) 9 | 10 | (def invitation-store (m/create-memory-store)) 11 | (def test-clock (test-time/new-stub-clock 0)) 12 | 13 | (background 14 | (before :facts (cl-store/reset-store! invitation-store))) 15 | 16 | (facts "can store an invite with generated unique invite-id" 17 | (let [email-1 "user@usersemail.co.uk" 18 | email-2 "user2@usersemail.co.uk" 19 | expiry-days 7 20 | invite-id-1 (i/generate-invite-id! invitation-store email-1 test-clock expiry-days uuid/uuid) 21 | invite-id-2 (i/generate-invite-id! invitation-store email-2 test-clock expiry-days uuid/uuid)] 22 | (i/fetch-by-id invitation-store invite-id-1) => (just {:email email-1 :invite-id invite-id-1 23 | :_expiry (* expiry-days time/day)}) 24 | invite-id-1 =not=> invite-id-2)) 25 | 26 | (fact "can delete an invite" 27 | (let [email-1 "user@usersemail.co.uk" 28 | expiry-days 7 29 | invite-id-1 (i/generate-invite-id! invitation-store email-1 test-clock expiry-days uuid/uuid)] 30 | (i/fetch-by-id invitation-store invite-id-1) => (just {:email email-1 :invite-id invite-id-1 31 | :_expiry (* expiry-days time/day)}) 32 | (i/remove-invite! invitation-store invite-id-1) 33 | (i/fetch-by-id invitation-store invite-id-1) => nil)) -------------------------------------------------------------------------------- /assets/jade/unshare-profile-card.jade: -------------------------------------------------------------------------------- 1 | extends layout/layout 2 | 3 | block vars 4 | - bodyClass = "func--unshare-profile-card-page" 5 | - pageTitle = "!Unshare your Profile Card" 6 | - pageTitleDataL8n = "content:unshare/title" 7 | 8 | block content 9 | .single-column 10 | .modal__content.clj-modal-contents.js-modal 11 | .modal__header 12 | h2.modal__title 13 | span(data-l8n="content:unshare/modal-unshare-title") !Are you sure you want to unshare your Profile Card with 14 | = " " 15 | span.clj--client-name Green Party 16 | |? 17 | .modal__body 18 | p 19 | span(data-l8n="content:unshare/modal-unshare-lede-begin") !If you unshare you will need to approve 20 | = " " 21 | span.clj--client-name Green Party 22 | = " " 23 | span(data-l8n="content:unshare/modal-unshare-lede-end") !again to sign in. 24 | .modal-buttons 25 | .modal-buttons__primary 26 | form.unshare-card.clj--unshare-profile-card__form(action="./profile",method="post") 27 | input.clj--client-id__input(type="hidden" name="client_id" value="1234") 28 | button.button.button--full-width.func--unshare-profile-card__button(type="submit", data-l8n="content:unshare/modal-unshare-ok-button") !Unshare Profile Card 29 | .modal-buttons__secondary 30 | a.button.button--full-width.button--secondary.clj--unshare-profile-card-cancel__link(href="./profile", data-l8n="content:unshare/modal-unshare-cancel-button") !Cancel 31 | 32 | block footerScripts 33 | if staticMode 34 | script. 35 | var form = document.getElementsByClassName('unshare-card')[0]; 36 | form.addEventListener("submit", function (e) { 37 | e.preventDefault(); 38 | var formAction = form.getAttribute('action') + "?"; 39 | window.location = formAction; 40 | }); 41 | -------------------------------------------------------------------------------- /test/stonecutter/integration/kerodon/steps.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.integration.kerodon.steps 2 | (:require [kerodon.core :as k] 3 | [stonecutter.integration.kerodon.kerodon-selectors :as ks] 4 | [stonecutter.integration.kerodon.kerodon-checkers :as kc] 5 | [stonecutter.routes :as r] 6 | [stonecutter.db.invitations :as i])) 7 | 8 | 9 | (defn register 10 | ([state email password] 11 | (register state "dummy first" "dummy last" email password)) 12 | ([state first-name last-name email password] 13 | (-> state 14 | (k/visit (r/path :index)) 15 | (kc/check-and-fill-in ks/registration-first-name-input first-name) 16 | (kc/check-and-fill-in ks/registration-last-name-input last-name) 17 | (kc/check-and-fill-in ks/registration-email-input email) 18 | (kc/check-and-fill-in ks/registration-password-input password) 19 | (kc/check-and-press ks/registration-submit)))) 20 | 21 | (defn accept-invite [state first-name last-name password invite-store email clock expiry-days] 22 | (let [invite-id (i/generate-invite-id! invite-store email clock expiry-days (constantly "asdf"))] 23 | (-> state 24 | (k/visit (r/path :accept-invite :invite-id invite-id)) 25 | (kc/check-and-fill-in ks/registration-first-name-input first-name) 26 | (kc/check-and-fill-in ks/registration-last-name-input last-name) 27 | (kc/check-and-fill-in ks/registration-password-input password) 28 | (kc/check-and-press ks/registration-submit)))) 29 | 30 | (defn sign-in [state email password] 31 | (-> state 32 | (k/visit (r/path :index)) 33 | (kc/check-and-fill-in ks/sign-in-email-input email) 34 | (kc/check-and-fill-in ks/sign-in-password-input password) 35 | (kc/check-and-press ks/sign-in-submit))) 36 | 37 | (defn sign-out [state] 38 | (-> state 39 | (k/visit (r/path :sign-out)))) 40 | -------------------------------------------------------------------------------- /config/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/stylesheets/_variables.scss: -------------------------------------------------------------------------------- 1 | $white: #fff; 2 | $black: #000; 3 | 4 | $light_grey: #EEEEEE; 5 | $light_pink: #FFEAFC; 6 | $light_cyan: #D3FFFD; 7 | $super_light_cyan: #e9fffe; 8 | 9 | $medium_grey: #AAAAAA; 10 | $medium_cyan: #00D3CA; 11 | $medium_pink: #af59a2; 12 | $medium_pink_dull: #FFC0DF; 13 | 14 | $dark_grey: #666666; 15 | $dark_cyan: #007E84; 16 | $dark_pink: #9E2E8D; 17 | $dark_salmon: #fe706a; 18 | $dark_coral: #ff5959; 19 | 20 | $darker_grey: #222222; 21 | $darker_cyan: #00444A; 22 | $darker_pink: #690055; 23 | 24 | $card_background: #404040; 25 | 26 | $text_grey: #333; 27 | 28 | $default_text_color: rgba(black,0.8); 29 | $background_color: $white; 30 | 31 | $error_color: $medium_pink_dull; 32 | $error_color--dark: darken($error_color,75%); 33 | $success_color: #BAE6E2; 34 | 35 | 36 | $link_colour: $default_text_color !default; 37 | $link_hover_colour: $link_colour !default; 38 | $link_visited_colour: $link_colour !default; 39 | $link_active_colour: $link_colour !default; 40 | 41 | // 42 | 43 | $small_device: 320px; 44 | $smallish_device: 480px; 45 | $medium_device: 640px; 46 | $large_device: 768px; 47 | $extra_large_device: 960px; 48 | $monster_large_device: 1220px; 49 | 50 | // 51 | 52 | $grid_gutter: 15px; 53 | 54 | $box--border-radius: 0px; 55 | 56 | $button--height: 64px; 57 | 58 | $main-content--side-padding: 1rem; 59 | 60 | 61 | // 62 | 63 | $max_site_width: $medium_device; 64 | $main_nav_height: 50px; 65 | 66 | // 67 | 68 | $heading_title_font_family: sans-serif; 69 | $para_font_family: sans-serif; 70 | $serif_font_family: Georgia, Times, 'Times New Roman', serif; 71 | 72 | // 73 | 74 | $h1_font-size--small: 26px; 75 | $h1_font_size--medium: 30px; 76 | $h2_font_size--small: 20px; 77 | $h2_font_size--medium: 24px; 78 | $h3_font_size--small: 18px; 79 | $h3_font_size--medium: 20px; 80 | $paragraph_font_size: 16px; 81 | $input_font_size: 18px; 82 | $button_font_size: 18px; 83 | 84 | 85 | -------------------------------------------------------------------------------- /src/stonecutter/helper.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.helper 2 | (:require [ring.util.response :as r] 3 | [net.cgrand.enlive-html :as html] 4 | [stonecutter.translation :as t] 5 | [stonecutter.view.view-helpers :as vh] 6 | [stonecutter.config :as config] 7 | [clojure.java.io :as io])) 8 | 9 | (defn copy [uri file] 10 | (let [new-file-path (str "resources/public/" file)] 11 | (io/make-parents new-file-path) 12 | (with-open [in (io/input-stream uri) 13 | out (io/output-stream new-file-path)] 14 | (io/copy in out)))) 15 | 16 | (defn update-app-name [enlive-m request] 17 | (let [app-name (config/app-name (get-in request [:context :config-m]))] 18 | (-> enlive-m 19 | (html/at [:.clj--app-name] (html/content app-name))))) 20 | 21 | 22 | (defn set-favicon [enlive-m request] 23 | (if-let [favicon-file-name (config/favicon-file-name (get-in request [:context :config-m]))] 24 | (-> enlive-m 25 | (html/at [[:link (html/attr= :rel "shortcut icon")]] (html/set-attr :href (str "/" favicon-file-name)))) 26 | enlive-m)) 27 | 28 | (defn update-attr [attr f & args] 29 | (fn [node] 30 | (apply update-in node [:attrs attr] f args))) 31 | 32 | (defn prepend-to-attr [attr s] 33 | (fn [node] 34 | (update-in node [:attrs attr] #(str s %)))) 35 | 36 | (defn enlive-response [enlive-m request] 37 | (-> enlive-m 38 | (update-app-name request) 39 | (set-favicon request) 40 | (t/context-translate request) 41 | vh/remove-work-in-progress 42 | vh/enlive-to-str 43 | r/response 44 | (r/content-type "text/html"))) 45 | 46 | (defn disable-caching [response] 47 | (when response 48 | (-> response 49 | (assoc-in [:headers "Cache-Control"] "no-cache, no-store, must-revalidate") 50 | (assoc-in [:headers "Pragma"] "no-cache") 51 | (assoc-in [:headers "Expires"] "0")))) -------------------------------------------------------------------------------- /src/stonecutter/view/profile_created.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.view.profile-created 2 | (:require [traduki.core :as t] 3 | [net.cgrand.enlive-html :as html] 4 | [stonecutter.view.view-helpers :as vh] 5 | [stonecutter.routes :as r] 6 | [stonecutter.session :as session])) 7 | 8 | (def from-app-translation-tag "content:profile-created/action-button-from-app") 9 | (def default-translation-tag "content:profile-created/action-button-default") 10 | 11 | (defn set-button [enlive-m transformation] 12 | (html/at enlive-m 13 | [:.clj--profile-created-next__button] 14 | transformation)) 15 | 16 | (defn set-next-link 17 | [next-url enlive-m] 18 | (set-button enlive-m (html/set-attr :href next-url))) 19 | 20 | (defn set-button-text 21 | [from-app enlive-m] 22 | (set-button enlive-m (html/set-attr :data-l8n (if from-app 23 | from-app-translation-tag 24 | default-translation-tag)))) 25 | (defn get-next-url [from-app request] 26 | (if from-app (session/request->return-to request) (r/path :show-profile))) 27 | 28 | (defn set-flash-message [request enlive-m] 29 | (let [{:keys [flash-type email-address]} (:flash request)] 30 | (if (and email-address (= flash-type :confirm-email-sent)) 31 | (html/at enlive-m 32 | [:.clj--email-address] (html/content email-address)) 33 | (vh/remove-element enlive-m [:.clj--flash-message-container])))) 34 | 35 | 36 | (defn profile-created [request] 37 | (let [from-app (get-in request [:params :from-app]) 38 | next-url (get-next-url from-app request)] 39 | (->> (vh/load-template-with-lang "public/profile-created.html" request) 40 | (set-next-link next-url) 41 | (set-flash-message request) 42 | (set-button-text from-app) 43 | vh/remove-work-in-progress))) 44 | -------------------------------------------------------------------------------- /test/stonecutter/browser/common.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.browser.common 2 | (:require [clj-webdriver.taxi :as wd])) 3 | 4 | 5 | ;; COMMON 6 | (def stonecutter-index-page-body ".func--index-page") 7 | (def stonecutter-user-list-page-body ".func--admin-user-list-page") 8 | (def stonecutter-accept-invite-page-body ".func--accept-invite-page") 9 | (def stonecutter-sign-in-email-input ".func--sign-in-email__input") 10 | (def stonecutter-sign-in-password-input ".func--sign-in-password__input") 11 | (def stonecutter-sign-in-button ".func--sign-in__button") 12 | (def stonecutter-register-first-name-input ".func--registration-first-name__input") 13 | (def stonecutter-register-last-name-input ".func--registration-last-name__input") 14 | (def stonecutter-register-email-input ".func--registration-email__input") 15 | (def stonecutter-register-password-input ".func--registration-password__input") 16 | (def stonecutter-register-create-profile-button ".func--create-profile__button") 17 | (def stonecutter-trust-toggle ".clj--user-item__toggle") 18 | 19 | (defn input-register-credentials-and-submit [] 20 | (wd/input-text stonecutter-register-first-name-input "Journey") 21 | (wd/input-text stonecutter-register-last-name-input "Test") 22 | (wd/input-text stonecutter-register-password-input "password123!!!") 23 | (wd/click stonecutter-register-create-profile-button)) 24 | 25 | (defn wait-for-selector [selector] 26 | (try 27 | (wd/wait-until #(not (empty? (wd/css-finder selector))) 5000) 28 | (catch Exception e 29 | (prn (str ">>>>>>>>>> Selector could not be found: " selector)) 30 | (prn "========== PAGE SOURCE ==========") 31 | (prn (wd/page-source)) 32 | (prn "========== END PAGE SOURCE ==========") 33 | (throw e)))) 34 | 35 | (def localhost "localhost:5439") 36 | (defn accept-invite [id] 37 | (str localhost "/accept-invite/" id)) 38 | 39 | (def user-list-page (str localhost "/admin/users")) 40 | ;;______________ -------------------------------------------------------------------------------- /assets/jade/change-email.jade: -------------------------------------------------------------------------------- 1 | extends layout/layout 2 | 3 | block vars 4 | - bodyClass = "func--change-email-page" 5 | - pageTitle = "!Change your email address" 6 | - pageTitleDataL8n = "content:change-email-form/title" 7 | 8 | block headerScripts 9 | 10 | block headerContent 11 | a.header-nav__link(href="./profile", data-l8n="content:profile/profile-link") !Profile 12 | a.header-nav__link.clj--sign-out__link.func--sign-out__link(href="./sign-out", data-l8n="content:profile/sign-out-link") !Sign out 13 | 14 | block content 15 | .single-column 16 | h1.page-title(data-l8n="content:change-email-form/page-header") 17 | | !Change your email address 18 | form.clj--change-email__form(action="#", method="post", role="form") 19 | 20 | p.form-row.clj--email-address 21 | label.form-row__label(for="email-address") 22 | span.form-row__title(data-l8n="content:change-email-form/email-address-label") 23 | | !New email address 24 | span.form-row__validation.clj--new-email__validation(data-l8n="content:change-email-form/new-email-blank-validation-message") 25 | | !Enter a new email address 26 | input.form-row__input.func--change-email__input(type="text", id="email-address", name="email-address") 27 | 28 | .button-actions 29 | .button-actions__primary 30 | button.button.button--full-width.clj--change-email__button.func--change-email__button(type="submit" data-l8n="content:change-email-form/form-button") 31 | | !Save changes 32 | .button-actions__secondary 33 | a.button.button--full-width.button--link.clj--change-email-cancel__link.func--change-email-cancel__link(href="./profile") 34 | span(data-l8n="content:change-email-form/cancel-button-1") !Cancel 35 | =" " 36 | span.visuallyhidden(data-l8n="content:change-email-form/cancel-button-2") !changing my email address 37 | 38 | block footerScripts 39 | -------------------------------------------------------------------------------- /ops/roles/mail/templates/send_mail.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | DIR=$( cd "$( dirname "$BASH_SOURCE[0]}" )" && pwd ) 3 | PROVIDER_DIR=$DIR/providers 4 | VERBOSE=0 5 | GREEN='\033[0;36m' 6 | NC='\033[0m' # No Color 7 | 8 | if [[ ! -d $PROVIDER_DIR ]]; 9 | then 10 | echo "Provider directory $PROVIDER_DIR does not exist" 11 | exit 1 12 | fi 13 | 14 | SUPPORTED_EMAIL_PROVS=($( ls -A $PROVIDER_DIR )) 15 | 16 | if [[ ${{ '{#' }}SUPPORTED_EMAIL_PROVS[@]} -eq 0 ]] 17 | then 18 | echo "Provider directory $PROVIDER_DIR is empty" 19 | echo 'Please provide a provider' 20 | exit 1 21 | fi 22 | 23 | provider=$EMAIL_SERVICE_PROVIDER 24 | 25 | log () { 26 | if [[ $VERBOSE -eq 1 ]]; then 27 | printf "${GREEN}$@${NC}" 28 | echo "" 29 | fi 30 | } 31 | 32 | usage() { 33 | cat < (th/check-redirects-to "/path") 16 | (-> response :session :user-login) => "user@user.com" 17 | (-> response :session :access_token) =not=> nil 18 | (-> response :session :access_token) => (-> (cl-store/entries token-store) first :token) 19 | (-> response :session :other) => "value") 20 | 21 | (fact "if response is not supplied then defaults to home redirect" 22 | (let [token-store (m/create-memory-store) 23 | user {:login "user@user.com"}] 24 | (common/sign-in-to-index token-store user) => (th/check-redirects-to (r/path :index))))) 25 | 26 | (fact "signed-in? returns true only when user-login and access_token are in the session" 27 | (tabular 28 | (common/signed-in? ?request) => ?expected-result 29 | ?request ?expected-result 30 | {:session {:user-login ...user-login... :access_token ...token...}} truthy 31 | {:session {:user-login nil :access_token ...token...}} falsey 32 | {:session {:user-login ...user-login... :access_token nil}} falsey 33 | {:session {:user-login nil :access_token nil}} falsey 34 | {:session {}} falsey 35 | {:session nil} falsey 36 | {} falsey)) 37 | -------------------------------------------------------------------------------- /assets/jade/reset-password.jade: -------------------------------------------------------------------------------- 1 | extends layout/layout 2 | 3 | block vars 4 | - bodyClass = "func--reset-password-page" 5 | - pageTitle = "!Reset your password" 6 | - pageTitleDataL8n = "content:reset-password-form/title" 7 | 8 | block headerScripts 9 | 10 | block headerContent 11 | a.header-nav__link(href="./profile", data-l8n="content:profile/profile-link") !Profile 12 | a.header-nav__link.clj--sign-out__link.func--sign-out__link(href="./sign-out", data-l8n="content:profile/sign-out-link") !Sign out 13 | 14 | block content 15 | .single-column 16 | h1.page-title(data-l8n="content:reset-password-form/page-header") 17 | | !Reset your password 18 | form.reset-password.clj--reset-password__form(action="./reset-password-post", method="post", role="form") 19 | .validation-summary.clj--validation-summary 20 | .validation-summary__title 21 | ul.validation-summary__list 22 | li.validation-summary__item.clj--validation-summary__item(data-l8n="content:change-password-form/new-password-too-short-validation-message") 23 | | !Validation summary message 24 | 25 | p.form-row.clj--new-password 26 | label.form-row__label(for="new-password") 27 | span.form-row__title(data-l8n="content:reset-password-form/new-password") 28 | | !New password 29 | input.form-row__input.func--new-password__input(type="password", id="new-password", name="new-password") 30 | 31 | button.button.button--full-width-smallish-devices.func--reset-password__button(type="submit" data-l8n="content:reset-password-form/form-button") 32 | | !Reset password 33 | 34 | block footerScripts 35 | if staticMode 36 | script. 37 | var form = document.getElementsByClassName('reset-password')[0]; 38 | form.addEventListener("submit", function (e) { 39 | e.preventDefault(); 40 | var formAction = form.getAttribute('action') + "?"; 41 | window.location = formAction; 42 | }); 43 | -------------------------------------------------------------------------------- /src/stonecutter/view/forgotten_password.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.view.forgotten-password 2 | (:require [stonecutter.view.view-helpers :as vh] 3 | [net.cgrand.enlive-html :as html] 4 | [stonecutter.routes :as r])) 5 | 6 | (def error-translations 7 | {:email {:invalid "content:forgot-password/email-address-invalid-validation-message" 8 | :too-long "content:forgot-password/email-address-too-long-validation-message" 9 | :non-existent "content:forgot-password/email-address-non-existent-validation-message"}}) 10 | 11 | (defn set-form-action [enlive-m] 12 | (-> enlive-m 13 | (html/at [:.clj--forgotten-password__form] (html/do-> (html/set-attr :action (r/path :send-forgotten-password-email)) 14 | (html/set-attr :method "post"))))) 15 | 16 | (defn add-error-class [enlive-m field-row-selector] 17 | (html/at enlive-m field-row-selector (html/add-class "form-row--invalid"))) 18 | 19 | (defn add-email-error [err enlive-m] 20 | (if (contains? err :email) 21 | (let [error-translation (get-in error-translations [:email (:email err)])] 22 | (-> enlive-m 23 | (add-error-class [:.clj--forgotten-password-email]) 24 | (html/at [:.clj--forgotten-password-email__validation] (html/set-attr :data-l8n (or error-translation "content:forgot-password/unknown-error"))))) 25 | (vh/remove-element enlive-m [:.clj--forgotten-password-email__validation]))) 26 | 27 | (defn set-email-input [email enlive-m] 28 | (html/at enlive-m 29 | [:.clj--forgotten-password-email__input] (html/set-attr :value email))) 30 | 31 | (defn forgotten-password-form [request] 32 | (->> (vh/load-template-with-lang "public/forgot-password.html" request) 33 | set-form-action 34 | (add-email-error (get-in request [:context :errors])) 35 | (set-email-input (get-in request [:params :email])) 36 | (vh/set-flash-message request :expired-password-reset) 37 | vh/add-anti-forgery)) 38 | -------------------------------------------------------------------------------- /test/stonecutter/test/view/error.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.test.view.error 2 | (:require [midje.sweet :refer :all] 3 | [net.cgrand.enlive-html :as html] 4 | [stonecutter.helper :as helper] 5 | [stonecutter.translation :as t] 6 | [stonecutter.routes :as r] 7 | [stonecutter.test.view.test-helpers :as th] 8 | [stonecutter.view.error :as e])) 9 | (let [request {:params {} :error-m {} :session {:locale :fi}}] 10 | (fact "modify error translation keys updates the data-l8n tags of the correct elements" 11 | (let [modified-error-enlive-map (e/modify-error-translation-keys (e/internal-server-error request) "oops-error")] 12 | modified-error-enlive-map => (th/has-attr? [:body] 13 | :class "func--oops-error-page") 14 | modified-error-enlive-map => (th/has-attr? [:title] 15 | :data-l8n "content:oops-error/title") 16 | modified-error-enlive-map => (th/has-attr? [:.clj--error-page-header] 17 | :data-l8n "content:oops-error/page-header") 18 | modified-error-enlive-map => (th/has-attr? [:.clj--error-page-intro] 19 | :data-l8n "content:oops-error/page-intro"))) 20 | 21 | (tabular 22 | (facts "there are no missing translations" 23 | (let [translator (t/translations-fn t/translation-map) 24 | page (-> (?error-page-fn request) (helper/enlive-response {:translator translator}) :body)] 25 | page => th/no-untranslated-strings)) 26 | ?error-page-fn 27 | e/account-nonexistent 28 | e/csrf-error 29 | e/forbidden-error 30 | e/internal-server-error 31 | e/not-found-error) 32 | 33 | (fact "home page link is set correctly" 34 | (-> (e/not-found-error request) (html/select [:.clj--error-return-home__link]) first :attrs :href) => (r/path :index))) 35 | -------------------------------------------------------------------------------- /assets/jade/confirmation-sign-in.jade: -------------------------------------------------------------------------------- 1 | extends layout/layout 2 | 3 | block vars 4 | - bodyClass = "func--confirmation-sign-in-page" 5 | - pageTitle = "!Sign in" 6 | - pageTitleDataL8n = "content:confirmation-sign-in-form/title" 7 | 8 | block headerScripts 9 | 10 | block headerContent 11 | 12 | block content 13 | .single-column 14 | h1.page-title.visuallyhidden(data-l8n="content:confirmation-sign-in-form/page-header") 15 | | !Sign in 16 | form.login-form(action="authorise", method="post", role="form") 17 | .validation-summary.clj--validation-summary 18 | .validation-summary__title 19 | ul.validation-summary__list 20 | li.validation-summary__item.clj--validation-summary__item(data-l8n="content:confirmation-sign-in-form/invalid-credentials-validation-message") 21 | | !Invalid email address or password 22 | input.clj--confirmation-id__input(type="hidden", id="confirmation-id", name="confirmation-id") 23 | p.form-row.clj--sign-in-password 24 | label.form-row__label(for="password") 25 | span.form-row__title(data-l8n="content:confirmation-sign-in-form/password") 26 | | !Password 27 | input.form-row__input.func--password__input(type="password", id="password", name="password") 28 | .button-actions 29 | .button-actions__primary 30 | button.button.button--full-width.func--sign-in__button(type="submit", data-l8n="content:confirmation-sign-in-form/sign-in") 31 | | !Sign in to Confirm Email 32 | .button-actions__secondary 33 | a.button.button--link.clj--forgot-password(href="#", data-l8n="content:confirmation-sign-in-form/forgot-password") !Forgot password? 34 | 35 | block footerScripts 36 | if staticMode 37 | script. 38 | var form = document.getElementsByClassName('login-form')[0]; 39 | form.addEventListener("submit", function (e) { 40 | e.preventDefault(); 41 | var formAction = form.getAttribute('action') + "?"; 42 | window.location = formAction; 43 | }); 44 | -------------------------------------------------------------------------------- /assets/stylesheets/utilities/_typography.scss: -------------------------------------------------------------------------------- 1 | @mixin font-face($family, $filename) { 2 | @font-face { 3 | font-family: '#{$family}'; 4 | src: url('fonts/#{$filename}.woff2') format('woff2'), 5 | url('fonts/#{$filename}.woff') format('woff'), 6 | url('fonts/#{$filename}.ttf') format('truetype'); 7 | @content; 8 | } 9 | } 10 | 11 | @mixin sentenceCase() { 12 | text-transform: lowercase; 13 | &:first-letter { 14 | text-transform: uppercase; 15 | } 16 | } 17 | 18 | @mixin no-select() { 19 | -webkit-touch-callout: none; 20 | user-select: none; 21 | } 22 | 23 | @mixin ellipsis() { 24 | white-space: nowrap; 25 | overflow: hidden; 26 | text-overflow: ellipsis; 27 | } 28 | 29 | @mixin hide-icon-text() { 30 | text-indent: -9999px; 31 | i { 32 | text-indent: 0; 33 | } 34 | } 35 | 36 | // Visually Hidden 37 | @mixin visuallyhidden { 38 | position: absolute; 39 | overflow: hidden; 40 | clip: rect(0 0 0 0); 41 | margin: -1px; 42 | padding: 0; 43 | width: 1px; 44 | height: 1px; 45 | border: 0; 46 | } 47 | .visuallyhidden { 48 | @include visuallyhidden; 49 | } 50 | @function strip-units($number) { 51 | @return $number / ($number * 0 + 1); 52 | } 53 | @function calc-rem($size, $base-font-size: 16) { 54 | $remSize: strip-units($size) / $base-font-size; 55 | @return #{$remSize}rem; 56 | } 57 | @mixin font-size($size: 16px) { 58 | font-size: $size; 59 | font-size: calc-rem($size); 60 | } 61 | @mixin line-height($line: 15px) { 62 | line-height: $line; 63 | line-height: calc-rem($line); 64 | } 65 | @mixin font($size, $line_height, $family, $weight: null) { 66 | font: $weight #{$size/$line_height} $family; 67 | } 68 | 69 | 70 | 71 | @mixin font-smoothing($value: on) { 72 | @if $value == on { 73 | -webkit-font-smoothing: antialiased; 74 | -moz-osx-font-smoothing: grayscale; 75 | } @else { 76 | -webkit-font-smoothing: subpixel-antialiased; 77 | -moz-osx-font-smoothing: auto; 78 | } 79 | } 80 | 81 | 82 | @mixin reset-list() { 83 | list-style-type: none; 84 | padding-left: 0; 85 | margin-bottom: 0; 86 | } -------------------------------------------------------------------------------- /src/stonecutter/view/invite_user.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.view.invite-user 2 | (:require [stonecutter.view.view-helpers :as vh] 3 | [stonecutter.routes :as r] 4 | [net.cgrand.enlive-html :as html])) 5 | 6 | (def email-errors 7 | {:invalid "content:admin-invite-user/invite-email-address-invalid-validation-message" 8 | :duplicate "content:admin-invite-user/invite-email-address-duplicate-validation-message" 9 | :invited "content:admin-invite-user/invite-email-address-invited-validation-message" 10 | :blank "content:admin-invite-user/invite-email-address-blank-validation-message"}) 11 | 12 | (defn set-form-action [enlive-m] 13 | (html/at enlive-m [:form] (html/set-attr :action (r/path :send-invite)))) 14 | 15 | (defn set-flash-message [enlive-m request] 16 | (let [email-address (get-in request [:flash :email-address])] 17 | (if email-address 18 | (html/at enlive-m [:.clj--invited-email] (html/content email-address)) 19 | (vh/remove-element enlive-m [:.func--flash-message-container])))) 20 | 21 | (defn add-email-errors [enlive-m err] 22 | (if-let [invitation-email-error (:invitation-email err)] 23 | (let [error-translation (get email-errors invitation-email-error)] 24 | (-> enlive-m 25 | (vh/add-error-class [:.clj--invite-user-email]) 26 | (html/at [:.clj--invite-user-email__validation] (html/set-attr :data-l8n (or error-translation "content:admin-invite-user/invite-unknown-error"))))) 27 | enlive-m)) 28 | 29 | (defn set-invite-email-input [enlive-m params] 30 | (html/at enlive-m 31 | [:.clj--email__input] (html/set-attr :value (:email params)))) 32 | 33 | (defn invite-user [request] 34 | (let [error-m (get-in request [:context :errors])] 35 | (-> (vh/load-template-with-lang "public/admin-invite-user.html" request) 36 | vh/remove-work-in-progress 37 | vh/set-admin-links 38 | set-form-action 39 | (set-invite-email-input (:params request)) 40 | (add-email-errors error-m) 41 | (set-flash-message request) 42 | vh/add-anti-forgery))) 43 | -------------------------------------------------------------------------------- /src/stonecutter/db/storage.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.db.storage 2 | (:require [stonecutter.db.mongo :as m] 3 | [ring.middleware.session.memory :as mem-session])) 4 | 5 | (defn create-mongo-stores [db conn db-name] 6 | {:auth-code-store (m/create-auth-code-store db) 7 | :user-store (m/create-user-store db) 8 | :client-store (m/create-client-store db) 9 | :token-store (m/create-token-store db) 10 | :confirmation-store (m/create-confirmation-store db) 11 | :session-store (m/create-session-store db) 12 | :forgotten-password-store (m/create-forgotten-password-store db) 13 | :invitation-store (m/create-invitation-store db) 14 | :profile-picture-store (m/create-profile-picture-store conn db-name)}) 15 | 16 | (defn create-in-memory-stores [conn] 17 | {:auth-code-store (m/create-memory-store) 18 | :user-store (m/create-memory-store) 19 | :client-store (m/create-memory-store) 20 | :token-store (m/create-memory-store) 21 | :confirmation-store (m/create-memory-store) 22 | :forgotten-password-store (m/create-memory-store) 23 | :session-store (mem-session/memory-store) 24 | :invitation-store (m/create-memory-store) 25 | :profile-picture-store (m/create-profile-picture-store conn "stonecutter")}) 26 | 27 | (defn get-auth-code-store [store-m] 28 | (:auth-code-store store-m)) 29 | 30 | (defn get-user-store [store-m] 31 | (:user-store store-m)) 32 | 33 | (defn get-profile-picture-store [store-m] 34 | (:profile-picture-store store-m)) 35 | 36 | (defn get-client-store [store-m] 37 | (:client-store store-m)) 38 | 39 | (defn get-token-store [store-m] 40 | (:token-store store-m)) 41 | 42 | (defn get-confirmation-store [store-m] 43 | (:confirmation-store store-m)) 44 | 45 | (defn get-forgotten-password-store [store-m] 46 | (:forgotten-password-store store-m)) 47 | 48 | (defn get-session-store [store-m] 49 | (:session-store store-m)) 50 | 51 | (defn get-invitation-store [store-m] 52 | (:invitation-store store-m)) 53 | -------------------------------------------------------------------------------- /assets/jade/user-list.jade: -------------------------------------------------------------------------------- 1 | extends layout/layout 2 | 3 | block vars 4 | - bodyClass = "func--admin-user-list-page" 5 | - pageTitle = "!User list" 6 | - pageTitleDataL8n = "content:admin-user-list/title" 7 | 8 | block headerScripts 9 | 10 | block headerContent 11 | include _admin_navigation 12 | 13 | block content 14 | .single-column 15 | 16 | +flashMessageWithCljContainer('clj--flash-message-container','func--flash-message-container', 'success') 17 | p.flash-message__title 18 | span.clj--flash-message-login !Email 19 | = " " 20 | span.clj--flash-message-text !Success Flash Message. 21 | 22 | h1(data-l8n="content:admin-user-list/page-title") !User list 23 | form(action="#", method="post", role="search") 24 | p.search-row.clj-wip 25 | label.search-row__label(for="search") 26 | i.fa.fa-search.search-row__icon 27 | span.visuallyhidden(data-l8n="content:admin-user-list/label-search") 28 | | !Search 29 | input.search-row__input(type="text", id="search", name="search", placeholder="") 30 | 31 | ul.user-list.clj--user-list 32 | +userItem("Shadi Erlendr", "Shadi.Erlendr@gmail.com", false, true) 33 | +userItem("Horatius Vilhelm", "Horatius.Vilhelm@live.com", false, true) 34 | +userItem("Kepa Othello", "Kepa.Othello@amazon.com", true, true) 35 | +userItem("Kevyn Hildred", "Kevyn.Hildred@hotmail.com", false, false) 36 | +userItem("Wallis Fran", "Wallis.Fran@live.com", true, false) 37 | +userItem("Ricki Robbie", "Ricki.Robbie@gmail.com", true, false) 38 | +userItem("Lindsay Claude", "Lindsay.Claude@google.com", false, false) 39 | +userItem("Kenzie Mackenzie", "Kenzie.Mackenzie@email-address.com", false, false) 40 | +userItem("Kayden Andy", "Kayden.Andy@apple.com", true, false) 41 | +userItem("Rene Phoenix", "Rene.Phoenix@mobileme.com", false, false) 42 | +userItem("Odell Indigo", "Odell.Indigo@yahoo.com", true, false) 43 | +userItem("Sammy Laurie", "Sammy.Laurie@gmail.com", false, false) 44 | +userItem("Averill Bernie", "Averill.Bernie@gmail.com", false, false) 45 | -------------------------------------------------------------------------------- /src/stonecutter/view/change_email.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.view.change-email 2 | (:require [net.cgrand.enlive-html :as html] 3 | [stonecutter.routes :as r] 4 | [stonecutter.view.view-helpers :as vh])) 5 | 6 | (def form-row-error-css-class "form-row--invalid") 7 | 8 | (def error-translations 9 | {:new-email {:blank "content:change-email-form/new-email-blank-validation-message" 10 | :duplicate "content:change-email-form/new-email-duplicate-validation-message" 11 | :invalid "content:change-email-form/new-email-invalid-validation-message" 12 | :unchanged "content:change-email-form/new-email-unchanged-validation-message"}}) 13 | 14 | (defn add-error-class [enlive-m field-row-selector] 15 | (html/at enlive-m field-row-selector (html/add-class form-row-error-css-class))) 16 | 17 | (defn add-change-email-error [enlive-m err] 18 | (if-let [change-email-error (:new-email err)] 19 | (let [error-translation (get-in error-translations [:new-email change-email-error])] 20 | (-> enlive-m 21 | (vh/add-error-class [:.clj--email-address]) 22 | (html/at [:.clj--new-email__validation] (html/set-attr :data-l8n (or error-translation "content:change-email-form/change-email-unknown-error"))))) 23 | enlive-m)) 24 | 25 | (defn set-cancel-link [enlive-m] 26 | (html/at enlive-m [:.clj--change-email-cancel__link] (html/set-attr :href (r/path :show-profile)))) 27 | 28 | (defn add-change-email-errors [enlive-m err] 29 | (if (empty? err) 30 | enlive-m 31 | (add-change-email-error enlive-m err))) 32 | 33 | (defn change-email-form [request] 34 | (let [err (get-in request [:context :errors]) 35 | library-m (vh/load-template-with-lang "public/library.html" request)] 36 | (-> (vh/load-template-with-lang "public/change-email.html" request) 37 | (vh/display-admin-navigation-links request library-m) 38 | (add-change-email-errors err) 39 | (vh/set-form-action [:.clj--change-email__form] (r/path :change-email)) 40 | set-cancel-link 41 | vh/add-anti-forgery 42 | vh/remove-work-in-progress))) 43 | 44 | -------------------------------------------------------------------------------- /test/stonecutter/test/view/authorise_failure.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.test.view.authorise-failure 2 | (:require [midje.sweet :refer :all] 3 | [net.cgrand.enlive-html :as html] 4 | [stonecutter.test.view.test-helpers :as th] 5 | [stonecutter.view.authorise-failure :refer [show-authorise-failure]] 6 | [stonecutter.translation :as t] 7 | [stonecutter.helper :as helper])) 8 | 9 | (fact "authorise should return some html" 10 | (let [page (-> (th/create-request) 11 | show-authorise-failure)] 12 | (html/select page [:body]) =not=> empty?)) 13 | 14 | (fact "work in progress should be removed from page" 15 | (let [page (-> (th/create-request) show-authorise-failure)] 16 | page => th/work-in-progress-removed)) 17 | 18 | (fact "there are no missing translations" 19 | (let [translator (t/translations-fn t/translation-map) 20 | page (-> (th/create-request) show-authorise-failure (helper/enlive-response {:translator translator}) :body)] 21 | page => th/no-untranslated-strings)) 22 | 23 | (fact "redirect-uri from session is set as return to client link" 24 | (let [translator (t/translations-fn t/translation-map) 25 | page (-> (th/create-request translator) 26 | (assoc-in [:params :callback-uri-with-error] "redirect-uri?error=access_denied") 27 | show-authorise-failure)] 28 | (-> page (html/select [:.func--redirect-to-client-home__link]) first :attrs :href) => "redirect-uri?error=access_denied")) 29 | 30 | (fact "client name is injected" 31 | (let [client-name "CLIENT_NAME" 32 | client-name-is-correct-fn (fn [element] (= (html/text element) client-name)) 33 | client-name-elements (-> (th/create-request) 34 | (assoc-in [:context :client-name] client-name) 35 | show-authorise-failure 36 | (html/select [:.clj--client-name]))] 37 | client-name-elements =not=> empty? 38 | client-name-elements => (has every? client-name-is-correct-fn))) 39 | -------------------------------------------------------------------------------- /src/stonecutter/translation.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.translation 2 | (:require [clj-yaml.core :as yaml] 3 | [clojure.java.io :as io] 4 | [clojure.tools.logging :as log] 5 | [traduki.core :as t] 6 | [taoensso.tower :as tower] 7 | [stonecutter.util.map :as map])) 8 | 9 | (defn load-translations-from-string [s] 10 | (yaml/parse-string s)) 11 | 12 | (defn load-translations-from-file [file-name] 13 | (-> file-name 14 | io/resource 15 | slurp 16 | load-translations-from-string)) 17 | 18 | 19 | (defn translation-map [lang] 20 | (map/deep-merge 21 | (load-translations-from-file (str "lang/" lang ".yml")) 22 | (load-translations-from-file (str "lang/" lang "-client.yml")))) 23 | 24 | (defn config-translation [] 25 | {:dictionary {:en (translation-map "en") 26 | :fi (translation-map "fi") 27 | } 28 | :dev-mode? false 29 | :fallback-locale :en 30 | :log-missing-translation-fn (fn [{:keys [locales ks ns] :as args}] 31 | (log/warn (str "Missing translation! locales: " locales 32 | ", keys: " ks ", namespace: " ns)))}) 33 | 34 | (defmacro load-client-translations [] 35 | (load-translations-from-file "lang/en-client.yml")) 36 | 37 | (defn translations-fn [translation-map] 38 | (fn [translation-key] 39 | (let [key1 (keyword (namespace translation-key)) 40 | key2 (keyword (name translation-key)) 41 | translation (get-in translation-map [key1 key2])] 42 | (when-not translation (log/warn (str "No translation found for " translation-key))) 43 | translation))) 44 | 45 | (defn get-locale-from-request [request] 46 | (if-let [session-locale (get-in request [:session :locale])] 47 | session-locale 48 | (get request :locale :en))) 49 | 50 | (defn context-translate [enlive-m request] 51 | (t/translate (partial (tower/make-t (config-translation)) (get-locale-from-request request)) enlive-m)) 52 | 53 | (defn has-locale [locale] 54 | ((keyword locale) (:dictionary (config-translation))) 55 | ) 56 | -------------------------------------------------------------------------------- /src/stonecutter/view/authorise.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.view.authorise 2 | (:require [traduki.core :as t] 3 | [net.cgrand.enlive-html :as html] 4 | [stonecutter.routes :as r] 5 | [stonecutter.view.view-helpers :as vh] 6 | [stonecutter.view.profile :as profile])) 7 | 8 | (defn set-form-action [enlive-m] 9 | (html/at enlive-m [:.clj--authorise__form] (html/set-attr :action (r/path :authorise-client)))) 10 | 11 | (defn set-cancel-link [params enlive-m] 12 | (let [cancel-link (str (r/path :show-authorise-failure) 13 | "?client_id=" (:client_id params) 14 | "&redirect_uri=" (:redirect_uri params))] 15 | (html/at enlive-m [:.clj--authorise-cancel__link] (html/set-attr :href cancel-link)))) 16 | 17 | (defn set-hidden-params [params enlive-m] 18 | (-> enlive-m 19 | (html/at [:.clj--authorise-client-id__input] (html/set-attr :value (:client_id params))) 20 | (html/at [:.clj--authorise-response-type__input] (html/set-attr :value (:response_type params))) 21 | (html/at [:.clj--authorise-redirect-uri__input] (html/set-attr :value (:redirect_uri params))) 22 | (html/at [:.clj--authorise-scope__input] (html/set-attr :value (:scope params))))) 23 | 24 | (defn set-client-name [client-name enlive-m] 25 | (html/at enlive-m 26 | [:.clj--client-name] (html/content client-name))) 27 | 28 | (defn set-hidden-clauth-csrf-token [csrf-token enlive-m] 29 | (-> enlive-m 30 | (html/at [:.clj--authorise-csrf__input] (html/set-attr :value csrf-token)))) 31 | 32 | (defn authorise-form [request] 33 | (let [params (:params request) 34 | client-name (get-in request [:context :client :name]) 35 | csrf-token (or (request :csrf-token) ((request :session {}) :csrf-token))] 36 | (->> (vh/load-template-with-lang "public/authorise.html" request) 37 | set-form-action 38 | (set-cancel-link params) 39 | (set-client-name client-name) 40 | (set-hidden-params params) 41 | (#(profile/add-profile-card % request)) 42 | (set-hidden-clauth-csrf-token csrf-token) 43 | vh/add-anti-forgery 44 | vh/remove-work-in-progress))) 45 | -------------------------------------------------------------------------------- /src/stonecutter/util/image.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.util.image 2 | (:require [image-resizer.util :as resizer-u] 3 | [image-resizer.core :as resizer] 4 | [pantomime.mime :as mime] 5 | [clojure.string :as str]) 6 | (:import (java.io ByteArrayInputStream ByteArrayOutputStream) 7 | (javax.imageio ImageIO) 8 | (org.apache.commons.io IOUtils) 9 | (org.apache.commons.codec.binary Base64) 10 | (java.awt.image BufferedImage))) 11 | 12 | (def image-height 150) 13 | (def image-width 150) 14 | 15 | (defn picture-data [picture] 16 | (last (str/split picture #","))) 17 | 18 | (defn picture-type [picture] 19 | (->> picture 20 | (re-find #"/(\w{3,});") 21 | last 22 | str/upper-case)) 23 | 24 | (defn remove-transparency [transparent-image] 25 | (let [opaque-image (BufferedImage. image-width image-height BufferedImage/TYPE_3BYTE_BGR) 26 | input-rgb (.getRGB transparent-image 0 0 image-width image-height nil 0 image-width)] 27 | (.setRGB opaque-image 0 0 image-width image-height input-rgb 0 image-width) 28 | opaque-image)) 29 | 30 | (defn buffered-image->input-stream [buffered-image content-type] 31 | (let [os (ByteArrayOutputStream.) 32 | file-extension (mime/extension-for-name content-type) 33 | opaque-image (remove-transparency buffered-image)] 34 | (ImageIO/write opaque-image (.substring file-extension 1) os) 35 | (ByteArrayInputStream. (.toByteArray os)))) 36 | 37 | (defn resize-and-crop-image [file] 38 | (let [image (resizer-u/buffered-image file) 39 | dimensions (resizer/dimensions image) 40 | width (first dimensions) 41 | height (last dimensions)] 42 | (if (< width height) 43 | (-> image 44 | (resizer/resize-to-width image-width) 45 | (resizer/crop-to-height image-height)) 46 | (-> image 47 | (resizer/resize-to-height image-height) 48 | (resizer/crop-to-width image-width))))) 49 | 50 | (defn encode-base64 [file] 51 | (->> file 52 | .getInputStream 53 | IOUtils/toByteArray 54 | Base64/encodeBase64 55 | (map char) 56 | (apply str "data:" (.getContentType file) ";base64,"))) -------------------------------------------------------------------------------- /assets/stylesheets/components/_toggle.scss: -------------------------------------------------------------------------------- 1 | $toggle__handle--size: 22px; 2 | $toggle--size: $toggle__handle--size * 2; 3 | 4 | .toggle { 5 | @include clearfix; 6 | position: relative; 7 | user-select: none; 8 | &__title { 9 | float: left; 10 | height: $toggle__handle--size; 11 | line-height: $toggle__handle--size; 12 | padding-right: 0.5rem; 13 | @include font-size(10px); 14 | //font-weight: bold; 15 | color: $dark_grey; 16 | } 17 | &__switch { 18 | position: relative; 19 | float: left; 20 | display: block; 21 | width: $toggle--size; 22 | height: $toggle__handle--size; 23 | background-color: rgba($black,0.1); 24 | border-radius: $toggle__handle--size; 25 | transition: background-color 250ms ease-in-out; 26 | 27 | } 28 | &__handle { 29 | position: absolute; 30 | top: 1px; 31 | left: 1px; 32 | display: block; 33 | @include circle($toggle__handle--size - 2); 34 | background-color: $white; 35 | transition: transform 250ms ease-in-out; 36 | } 37 | &__handle-icon { 38 | @include middle-me; 39 | width: 100%; 40 | height: 100%; 41 | line-height: 1.5; 42 | text-align: center; 43 | @include font-size(14px); 44 | color: $dark_grey; 45 | transition: color 250ms ease-in-out; 46 | border-radius: inherit; 47 | &:after { 48 | display: block; 49 | position: absolute; 50 | top: 0; 51 | left: 0; 52 | content: '\f09c'; 53 | background-color: $white; 54 | @include size(100%); 55 | font-family: FontAwesome; 56 | text-align: center; 57 | opacity: 0; 58 | border-radius: inherit; 59 | } 60 | } 61 | &__input { 62 | position: absolute; 63 | top: 0; 64 | left: 0; 65 | width: 100%; 66 | height: 100%; 67 | z-index: 1; 68 | opacity: 0; 69 | &:checked ~ .toggle__switch .toggle__handle { 70 | transform: translateX($toggle__handle--size); 71 | } 72 | &:checked ~ .toggle__switch .toggle__handle .toggle__handle-icon { 73 | color: $medium_cyan; 74 | &:after { 75 | opacity: 1; 76 | } 77 | } 78 | &:checked ~ .toggle__switch { 79 | background-color: $medium_cyan; 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /assets/jade/admin-sign-in.jade: -------------------------------------------------------------------------------- 1 | extends layout/layout 2 | 3 | block vars 4 | - bodyClass = "func--admin-sign-in-page" 5 | - pageTitle = "!Sign in" 6 | - pageTitleDataL8n = "content:admin-sign-in-form/title" 7 | 8 | block headerScripts 9 | block headerContent 10 | block content 11 | 12 | .single-column 13 | h1.page-title(data-l8n="content:admin-sign-in-form/page-header") 14 | | !Sign in 15 | form.login-form(action="./user-list", method="post", role="form") 16 | .validation-summary.clj--validation-summary 17 | .validation-summary__title 18 | ul.validation-summary__list 19 | li.validation-summary__item.clj--validation-summary__item(data-l8n="content:admin-sign-in-form/invalid-credentials-validation-message") 20 | | !Invalid email address or password 21 | p.form-row.clj--sign-in-email 22 | label.form-row__label(for="email") 23 | span.form-row__title(data-l8n="content:admin-sign-in-form/email-address") 24 | | !Email address 25 | span.form-row__validation.clj--sign-in-email__validation(data-l8n="content:admin-sign-in-form/email-address-invalid-validation-message") 26 | | !Enter a valid email address 27 | input.form-row__input.clj--email__input.func--email__input(type="email", id="email", name="email") 28 | p.form-row.clj--sign-in-password 29 | label.form-row__label(for="password") 30 | span.form-row__title(data-l8n="content:admin-sign-in-form/password") 31 | | !Password 32 | span.form-row__validation.clj--sign-in-password__validation(data-l8n="content:admin-sign-in-form/password-invalid-validation-message") 33 | | !Cannot be blank 34 | input.form-row__input.func--password__input(type="password", id="password", name="password") 35 | .button-actions 36 | .button-actions__primary 37 | button.button.button--full-width.func--sign-in__button(type="submit", data-l8n="content:admin-sign-in-form/sign-in") 38 | | !Sign in 39 | .button-actions__secondary 40 | a.button.button--link.button--full-width.clj--forgot-password.func--forgot-password__button(href="#", data-l8n="content:admin-sign-in-form/forgot-password") !Forgot password? 41 | 42 | block footerScripts 43 | -------------------------------------------------------------------------------- /docs/CONFIG.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | The following environment variables can be passed to the application. 4 | 5 | ## Required: 6 | 7 | - **RSA_KEYPAIR_FILE_PATH** - location of json file containing RSA private keypair for Open ID Connect. 8 | 9 | ## Optional: 10 | 11 | - **HOST** - defaults to localhost 12 | - **PORT** - defaults to 5000 13 | - **BASE_URL** - your application URI or IP address. Defaults to localhost:5000 14 | - **CLIENT_CREDENTIALS_FILE_PATH** - location of the yml fle containing the clients your instance of Stonecutter will communicate with. See *config/clients.yml* for an example. Defaults to client-credentials.yml 15 | - **APP_NAME** - shown on the index page. Defaults to "Stonecutter" 16 | - **PASSWORD_RESET_EXPIRY** - time in hours before reset password e-mail expires. Defaults to 24. 17 | - **OPEN_ID_CONNECT_ID_TOKEN_LIFETIME_MINUTES** - time in minutes before OpenID Connect token expires. Defaults to 10. 18 | - **INVITE_EXPIRY** - time in days before invite e-mail expires. Defaults to 7. 19 | - **MONGO_URI** - URI for the MongoDB database. Set automatically when using Docker. Defaults to "mongodb://localhost:27017/stonecutter" 20 | - **MONGO_DB_NAME** - name of the MongoDB database. Defaults to stonecutter. 21 | 22 | ### Customisations for the header 23 | 24 | - **HEADER_BG_COLOR** - defaults to #eee 25 | - **HEADER_FONT_COLOR** - defaults to #222. 26 | - **HEADER_FONT_COLOR_HOVER** - defaults to #00d3ca. 27 | - **STATIC_RESOURCES_DIR_PATH** - the directory containing the custom logo and favicon 28 | - **LOGO_FILE_NAME** - defaults to the Stonecutter logo 29 | - **FAVICON_FILE_NAME** - defaults to the Stonecutter icon 30 | 31 | ### Details used when setting up an admin user 32 | 33 | - **ADMIN_FIRST_NAME** - defaults to "Mighty" 34 | - **ADMIN_LAST_NAME** - defaults to "Admin" 35 | - **ADMIN_LOGIN** - required for the creation of an admin user. Must be a valid email, have a length < 254 characters and must not already exist as a user. 36 | - **ADMIN_PASSWORD** - required for the creation of an admin user. Must have a length between 8 and 254 characters - **please change this password after starting the application!** 37 | 38 | ### Email configuration 39 | 40 | - **EMAIL_SERVICE_PROVIDER** - used when deploying using ansible to select the email script. 41 | -------------------------------------------------------------------------------- /test/stonecutter/test/view/delete_app.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.test.view.delete-app 2 | (:require [midje.sweet :refer :all] 3 | [stonecutter.routes :as r] 4 | [net.cgrand.enlive-html :as html] 5 | [stonecutter.test.view.test-helpers :as th] 6 | [stonecutter.translation :as t] 7 | [stonecutter.helper :as helper] 8 | [stonecutter.view.delete-app :refer [delete-app-confirmation]])) 9 | 10 | (defn assoc-app-id [request id] 11 | (assoc-in request [:params :app-id] id)) 12 | 13 | 14 | (facts "about delete-app-confirmation page" 15 | (fact "should return some html" 16 | (let [page (-> (th/create-request) 17 | (assoc-app-id "blah") 18 | delete-app-confirmation)] 19 | (html/select page [:body]) =not=> empty?)) 20 | 21 | (fact "work in progress should be removed from page" 22 | (let [page (-> (th/create-request) 23 | (assoc-app-id "blah") 24 | delete-app-confirmation)] 25 | page => th/work-in-progress-removed)) 26 | 27 | (fact "there are no missing translations" 28 | (let [translator (t/translations-fn 29 | t/translation-map) 30 | page (-> (th/create-request) 31 | (assoc-app-id "blah") 32 | delete-app-confirmation 33 | (helper/enlive-response {:translator translator}) :body)] 34 | page => th/no-untranslated-strings)) 35 | 36 | (fact "form posts to correct endpoint" 37 | (let [page (-> (th/create-request) 38 | (assoc-app-id "blah") 39 | delete-app-confirmation)] 40 | (-> page (html/select [:form]) first :attrs :action) => (r/path :delete-app :app-id "blah"))) 41 | 42 | (fact "cancel link should go to correct endpoint" 43 | (let [page (-> (th/create-request) 44 | (assoc-app-id "blah") 45 | delete-app-confirmation)] 46 | (-> page (html/select [:.clj--delete-app-cancel__link]) first :attrs :href) => (r/path :show-apps-list)))) 47 | -------------------------------------------------------------------------------- /assets/jade/routes.jade: -------------------------------------------------------------------------------- 1 | extends layout/layout 2 | 3 | block vars 4 | - bodyClass = "func--index-page" 5 | - pageTitle = "!Index" 6 | //- pageTitleDataL8n = "" 7 | 8 | block headerScripts 9 | style. 10 | .demo { 11 | padding-top: 40px; 12 | } 13 | .demo-nav { 14 | margin-bottom: 40px; 15 | list-style: none; 16 | } 17 | .demo-item { 18 | display: block; 19 | margin: 0 0 15px 0; 20 | color: #007E84; 21 | font-size: 20px; 22 | text-decoration: none; 23 | } 24 | .demo-item:hover { 25 | text-decoration: underline; 26 | } 27 | .demo-item:last-child { 28 | margin-bottom: 0; 29 | } 30 | 31 | block content 32 | 33 | .demo 34 | .middle-container 35 | 36 | h2 Main 37 | nav.demo-nav 38 | a.demo-item(href="./library") Library 39 | a.demo-item(href="./") Home 40 | a.demo-item(href="./change-password") Change password 41 | a.demo-item(href="./change-email") Change email address 42 | a.demo-item(href="./change-profile") Change profile details 43 | a.demo-item(href="./profile-created") Profile created 44 | a.demo-item(href="./profile-deleted") Profile deleted 45 | a.demo-item(href="./forgot-password") Forgot password 46 | a.demo-item(href="./forgot-password-confirmation") Forgot password confirmation 47 | a.demo-item(href="./reset-password") Reset password 48 | a.demo-item(href="./authorise") Authorise 49 | a.demo-item(href="./authorise-failure") Authorise failure 50 | a.demo-item(href="./profile") Profile 51 | a.demo-item(href="./unshare-profile-card") Unshare Profile Card No JS 52 | a.demo-item(href="./delete-account") Delete account No JS 53 | a.demo-item(href="./confirm-email-expired") Email confirmation expired 54 | a.demo-item(href="./confirm-email-resent") Confirm email resent 55 | 56 | h2 Emails 57 | nav.demo-nav 58 | a.demo-item(href="./email-demo") WIP html email 59 | 60 | h2 Admin 61 | nav.demo-nav 62 | a.demo-item(href="./admin/sign-in") Sign in 63 | a.demo-item(href="./admin/user-list") User list 64 | a.demo-item(href="./admin/apps") Add apps 65 | a.demo-item(href="./admin/invite") Invite user 66 | -------------------------------------------------------------------------------- /assets/stylesheets/components/_user-list.scss: -------------------------------------------------------------------------------- 1 | .user-list { 2 | @include reset-list; 3 | margin-left: -$main-content--side-padding; 4 | margin-right: -$main-content--side-padding; 5 | padding-left: $main-content--side-padding; 6 | padding-right: $main-content--side-padding; 7 | } 8 | 9 | .user-item { 10 | position: relative; 11 | display: table; 12 | width: 100%; 13 | min-height: $button--height; 14 | padding: 1rem 95px 1rem 0; 15 | border-bottom: 1px solid $light_grey; 16 | &:hover { 17 | border-color: $medium_cyan; 18 | } 19 | &__text { 20 | display: table-cell; 21 | vertical-align: middle; 22 | } 23 | &__name { 24 | color: $dark_grey; 25 | @include font-size($paragraph_font_size); 26 | line-height: 1.3; 27 | } 28 | &__email-address { 29 | color: $medium_grey; 30 | word-break: break-all; 31 | @include font-size(12px); 32 | &__icon { 33 | padding-right: 0.2rem; 34 | } 35 | } 36 | &__action-list { 37 | @include clearfix; 38 | position: absolute; 39 | top: 0; 40 | right: 0; 41 | } 42 | &__toggle-trust { 43 | height: $button--height; 44 | padding-top: 21px; 45 | float: left; 46 | } 47 | &__trust-button { 48 | display: block; 49 | @include circle(22px); 50 | float: left; 51 | margin-top: 21px; 52 | margin-left: 0.75rem; 53 | padding: 0; 54 | -webkit-appearance: none; 55 | -moz-appearance: none; 56 | background-color: $dark_cyan; 57 | @include font-size(14px); 58 | color: white; 59 | text-align: center; 60 | border: 0; 61 | transition: background 250ms ease, color 250ms ease, box-shadow 250ms ease, transform 250ms ease; 62 | outline: 0; 63 | .js & { 64 | display: none; //hide if JS class is present on html 65 | } 66 | &:focus, &:hover { 67 | background-color: lighten($dark_cyan, 1%); 68 | box-shadow: 0 19px 60px -10px rgba(0,0,0,.35); 69 | transform: translateY(-2px); 70 | } 71 | &:active { 72 | background-color: darken($dark_cyan, 5%); 73 | outline: 0; 74 | } 75 | } 76 | &__button { 77 | &:hover { 78 | background-color: $white; 79 | } 80 | &--approved { 81 | color: $dark_cyan; 82 | } 83 | &--unapproved { 84 | color: $medium_grey; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /test/stonecutter/test/email.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.test.email 2 | (:require [midje.sweet :refer :all] 3 | [stonecutter.email :as email])) 4 | 5 | (def test-state (atom nil)) 6 | 7 | (defn reset-test-state! [] 8 | (reset! test-state nil)) 9 | 10 | (defprotocol EmailRecorder 11 | (last-sent-email [this]) 12 | (reset-emails! [this])) 13 | 14 | (defrecord TestEmailSender [email-store] 15 | email/EmailSender 16 | (send-email! [this email-address subject body] 17 | (swap! email-store (partial cons {:email email-address :subject subject :body body}))) 18 | EmailRecorder 19 | (last-sent-email [this] 20 | (first @email-store)) 21 | (reset-emails! [this] 22 | (reset! email-store []))) 23 | 24 | (defn create-test-email-sender [] 25 | (TestEmailSender. (atom []))) 26 | 27 | (defn test-email-renderer [subject body] 28 | (constantly 29 | {:subject subject :body body})) 30 | 31 | (fact "renders and sends an email generated using the specified template" 32 | (against-background 33 | (email/get-confirmation-renderer) => (test-email-renderer ...subject... ...body...)) 34 | (let [email-sender (create-test-email-sender)] 35 | (email/send! email-sender :confirmation ...email-address... ...email-data-map...) 36 | (let [sent-email (last-sent-email email-sender)] 37 | (:email sent-email) => ...email-address... 38 | (:subject sent-email) => ...subject... 39 | (:body sent-email) => ...body...))) 40 | 41 | (fact "testing the forgotten password renderer" 42 | (let [app-name "MyTestApp" 43 | base-url "base-url" 44 | email-data {:app-name app-name :base-url base-url :forgotten-password-id "forgotten-password-id"} 45 | email (email/forgotten-password-renderer email-data)] 46 | 47 | (:subject email) => (str "Reset password for " app-name) 48 | (:body email) => (contains (str base-url "/reset-password/forgotten-password-id")))) 49 | 50 | (fact "testing the changed password confirmation renderer" 51 | (let [app-name "MyTestApp" 52 | admin-email "admin@email.com" 53 | email-data {:app-name app-name :admin-email admin-email} 54 | email (email/changed-password-renderer email-data)] 55 | 56 | (:subject email) => (contains app-name) 57 | (:body email) => (contains admin-email))) 58 | -------------------------------------------------------------------------------- /src/stonecutter/middleware.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.middleware 2 | (:require [clojure.tools.logging :as log] 3 | [ring.util.response :as r] 4 | [ring.middleware.file :as ring-mf] 5 | [stonecutter.translation :as translation] 6 | [stonecutter.routes :as routes] 7 | [stonecutter.controller.user :as user] 8 | [stonecutter.helper :as helper] 9 | [stonecutter.config :as config] 10 | [stonecutter.controller.common :as common])) 11 | 12 | (defn wrap-error-handling [handler err-handler dev-mode?] 13 | (if-not dev-mode? 14 | (fn [request] 15 | (try 16 | (handler request) 17 | (catch Exception e 18 | (log/error e e) 19 | (err-handler request)))) 20 | handler)) 21 | 22 | (defn wrap-handle-403 [handler error-403-handler] 23 | (fn [request] 24 | (let [response (handler request)] 25 | (if (= (:status response) 403) 26 | (error-403-handler request) 27 | response)))) 28 | 29 | (defn wrap-config [handler config-m] 30 | (fn [request] 31 | (-> request 32 | (assoc-in [:context :config-m] config-m) 33 | handler))) 34 | 35 | (defn wrap-handlers-except [handlers wrap-function exclusions] 36 | (into {} (for [[k v] handlers] 37 | [k (if (k exclusions) v (wrap-function v))]))) 38 | 39 | (defn wrap-disable-caching [handler] 40 | (fn [request] 41 | (-> request 42 | handler 43 | helper/disable-caching))) 44 | 45 | (defn wrap-signed-in [handler] 46 | (fn [request] 47 | (if (common/signed-in? request) 48 | (handler request) 49 | (r/redirect (routes/path :index))))) 50 | 51 | (defn wrap-custom-static-resources [handler config-m] 52 | (if-let [static-resources-dir-path (config/static-resources-dir-path config-m)] 53 | (do (log/info (str "All resources in " static-resources-dir-path " are now being served as static resources")) 54 | (ring-mf/wrap-file handler static-resources-dir-path)) 55 | handler)) 56 | 57 | (defn wrap-just-these-handlers [handlers-m wrap-function inclusions] 58 | (into {} (for [[k v] handlers-m] 59 | [k (if (k inclusions) (wrap-function v) v)]))) 60 | 61 | (defn wrap-authorised [handler authorisation-checker] 62 | (fn [request] 63 | (when (authorisation-checker request) 64 | (handler request)))) 65 | 66 | -------------------------------------------------------------------------------- /test-resources/test.vcf: -------------------------------------------------------------------------------- 1 | BEGIN:VCARD 2 | VERSION:3.0 3 | N:Lasty;Frank;;; 4 | FN:Frank Lasty 5 | PHOTO;TYPE=JPEG;ENCODING=B:/9j/4AAQSkZJRgABAgAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCACWAJYDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD2ou+T87fnSeY/99vzpD94/WkpiHeY/wDfb86PMf8Avt+dNooAd5j/AN9vzo8x/wC+3502igB3mP8A32/OjzH/AL7fnTaKAHeY/wDfb86PMf8Avt+dNooAd5j/AN9vzo8x/wC+3502igB3mP8A32/OjzH/AL7fnTaKAHeY/wDfb86PMf8Avt+dNooAd5j/AN9vzo8x/wC+3502igB3mP8A32/Oim0UAKfvH60lKfvH60lABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFACn7x+tJSn7x+tJQAUUUUAFFFFABRRVPUNShsI8v80h+6g6n/AOtQBcorjrnV7y5Y5lMa/wB2PiqouJlORNID6hzQB3dFcrZ67cwMFmPnR98/eH410tvcRXUIlhbcp/T60AS0UUUAFFFFABRRRQAUUUUAKfvH60lKfvH60lABRRRQAUUUUAV767SytWmfkjhV9T6Vxk80lxM0srbnY5JrR127NxemFT8kPy/Vu/8Ah+FZVABRRRTAKu6bfvYXIbkxNw6+o9frVKigDvlZXRXUgqwyCO4paxfD12ZIHtXPzR/Mv+7/APr/AJ1tUgCiiigAooooAKKKKAFP3j9aSlP3j9aSgAooooAKiuZhb20sx/gUt9alrL1+Xy9MK/8APRwv9f6UAcqzFmLMcknJNJRRTAKKKKACiiigC5pdx9m1GF8/KW2t9DxXZ1wFd3by+dbRS/30DfmKQElFFFABRRRQAUUUUAKfvH60lKfvH60lABRRRQAVheJWxFbJ6sx/LH+Nbtc/4m+9a/R/6UAYFFFFMAooooAKKKKACuy0ht+k25/2SPyJFcbXX6H/AMgiD6t/6EaQGhRRRQAUUUUAFFFFACn7x+tJSn7x+tJQAUUUUAFYXiZcx2zehYfnj/Ct2srxBFv0zcP+Wbhv6f1oA5WiiimAUUUUAFFFFABXY6OuzSbcexP5kmuOruraLybWGLuiBT+ApAS0UUUAFFFFABRRRQAp+8frSUp+8frSUAFFFFABUV1CLi1lhP8AGpA9j2qWigDgSCpIIwRwRSVp65afZ78yKP3c3zD69/8AH8azKYBRRRQAUUUUAW9Mg+06jDHjK7tzfQc12lYfh202RPdMOX+VPp3/AF/lW5SAKKKKACiiigAooooAU/eP1pKU/eP1pKACiiigAooooAq39mt9aNEeG6q3oa42WJ4JWikUq6nBBrvKo6jpcV+mT8koHyuP5GgDjqKt3Om3dqxDwsV/vIMiqwRicBWJ9AKYDat6fYvf3IjXIQcu3oP8ans9FurlgXUwx92Yc/gK6e1tYrOERQrgdz3J9TSAkRFjjVEACqMADsKdRRQAUUUUAFFFFABRRRQAp+8frSUUUAFFFFABRRRQAUUUUAFGB6UUUAFFFFABRRRQAUUUUAFFFFABRRRQB//Z 6 | EMAIL:valid@email.com 7 | REV:20121201T134211Z 8 | END:VCARD -------------------------------------------------------------------------------- /test-resources/avatar-encoding.txt: -------------------------------------------------------------------------------- 1 | data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJYAAACWCAIAAACzY+a1AAAG7klEQVR42u2da1MaSRiF8///RRBkQC7iFRWV9Uq0diMiisSAch8w+3XXb3t0EBPKqHGBOS851imrhJ7u4zzVl7e7p+fD3d3dP/ox/PPvh97tt2arLdnVh9tvf7faHcmuhFAIJSGUhFAIhVAIJSGUhFAIdReEUBJCSQiFUDdCCCUhlIRQCCUhlIRQEkIhlIRQEkJJCIVQEkJJCCUhFEJJCCUhlIRQCCUhlIRQEkIhlITQJ7U7brPVrtcbN7V64ew89+l4Z29/IPyJD/EVEiAZEgshkTpuF1guy1e5o+PllXRwNhIIOvg9JO9DJEAyJAZKXCiE/tc8qFAobm3vhCOxQDAcnI3OOnMvCAmQDIk3t7K40MtBCH3jV61eb2xuO9F4IOSEwq/A+15IPBNycCEuRyamKVpFiJt+ki9EY0nA+CV4QyAhZIKs7FK0hxD3Gj3fzt7Bu8k9yxIZ3o9zDII0hhC3uFZvoPVDMzgqfp6QIbJF5uYoGkOIW5xez4yc34AiMkcRQjguNZvt5dW1mVBkHPweKUZQBBpUIRxL8Le2sYnAbnz8HqOOyOrahqGQ0QzC/YPcCMcvr45uUJwQjrL+FYrnY+r/XugXUaiJumgA4fVNLRJNTJKfJxSKooVwBFVwaSU9sSZ0qDlF0fwVkR3h55P8q9Oe4xzaRGFACP9HFNFqO9G4X/w8wQB5jMGLsN1xs3/s+tKEDjWnsME8ZcOLsFK9nkAU+MZIEWaE8JdHMen1DAM/TzBDO64hRXj1tUJSBQcVEZaE8Bd6wY3MNg8/T7DE2SMyIqw1GhOei3njfA2MCeGbquDO7gEbP08wRlgRGRH6Mp32xik3IXxd+cKZ77HgCzHiaaEohC/HEu7a+iYnP0+r6Q2YFMKfqtFshcIRZoSoiDAphD+f1M6f0raiA4QwKYQ/nZFZXcsw8/O0srpONVNDhdANhef4ESJAFMLndX5RoppUe2GyDVaF8NmIfp+f32OMv88TILIgdLu9+YVlKwiTqSUYFsJhzcw6VhCiO1RDOqyLUtlERzjoDovnJSH8QYe5Iyv8PMGwEP4QTlCt0b95Hd8VwqexTCyRsoUQhklGNBQIAZFwjffVEQ1JXEGB8GulOtZHzsb0GBtsC2FfhbMi+ez2s/PdsC2EfeWOjm3x8wTbQvi4a3tnzyJC2GboDv1HaDGioIorGBB2F5ZWLCKEbYZVJ/8RIrqai89bRDgXTzKEhhQIfX8C7X0KR2JCeK9u7/ZjMGwRYSAYhnkh7PRubwNmEcK8EN4f6GRudu3pKQuCo6L8R3hTq9tFCPNCKIRCKIRCKIRCqAk2t2s3qNAEWz+0Dyi0N46wZ269d7AVUQj7c6TReNIiwmgsoTlSD2E3mVqyiPBhW776wofhzPLKmkWEsK3hTH/jxXZ2xyJC2NbGi772DnIWEcK2tj89PmJ/cmpxEyJsC2Ffl1+uDD3WNIgoYFsIn84qsbghn+T0EgqEiO7DkZi5jTOwLYRPccXSStoWQp7D81mebDK3oZtkKzfRU74n+YKhQSmswrCe8v1B1zc1QyMaWOV5kQwLwm7vNhpLWEHoROMMaxR0RwdtZLasIIRVHR30jP76nDfRHcIkrOrcGcMBPskOYEaEbrcXTxo49wImeU7varEdKbt/+IkfIUzy3DE6hJXqNXlbGgiG2d7fxHa8ejeZWmRGmJhfZHt5E91LDg5y1G3pYY6rFWVEiMEe7dohjFGNRUkRMh+yDmOEr8BjfO1W6bIcCDp8AxkHxtjuFSlCzkENLHG+hZL0FZT507Mg02QbzMAS4Y3iRdjuuLEE0WE0MEP7Rmbe1zGfnBZ4XscMM5x3qcX9Xns3tUhxsBdswIwQvkdfriq+HzULA7BBe4vYEaL72cru+osQBmh7QQMI7xcRGy0fT2hD0TDAfH8MIIROC0VfmlMUSvjaV5MI3W5vO7s74UUoFIdCqZZ2DSOEmq32hI+dRXEolP/OmEF4v4JRq0cm1SmioBrBgTLThhAqf7nC+GKsu9yQOYooczx1NoUI0S2eX5Sic4kxUUS2yBxFkEcRhhF6ixhnxfNoLDlyivf8YklkzrkcMT0IPYpo6BLziyOkiKyQIbI1x88kQq9Fvak10uuZkVBEJsgKGdpqP20j7FfHjnt0/OfcQ6P6DpbeVbgcmXRswjOP0GtUK9Xrze1sPJEKhJw3Lk4hGRLjksxWFpdbbDynB6HXqLrd3kWpvH/4aWFpdSbkfJwJz4QiXiX7Xvjw4SsHyZD44rIMeG3L9W9KEA6qI2BUqjelyzIaRlSv1OJyYn5hIPyJD/EVEiAZEluvfNOGcFAjoWarU2+0avXGkPAhvvLSTM2/PG0If08JoRBKQigJ4e+u/wCjy48Y0wHqrwAAAABJRU5ErkJggg== -------------------------------------------------------------------------------- /test/stonecutter/test/db/forgotten_password.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.test.db.forgotten-password 2 | (:require [midje.sweet :refer :all] 3 | [clauth.store :as cl-store] 4 | [stonecutter.db.mongo :as m] 5 | [stonecutter.db.forgotten-password :as fp] 6 | [stonecutter.test.util.time :as test-time] 7 | [stonecutter.util.time :as time])) 8 | 9 | (def test-clock (test-time/new-stub-clock 0)) 10 | 11 | (facts "about storing id for user" 12 | (let [forgotten-password-store (m/create-memory-store) 13 | forgotten-password-id "abcdefggasdf" 14 | login "bob@burgers.com" 15 | expiry-hours 7] 16 | (fp/store-id-for-user! forgotten-password-store test-clock forgotten-password-id login expiry-hours) 17 | (cl-store/entries forgotten-password-store) => [{:forgotten-password-id forgotten-password-id 18 | :login login 19 | :_expiry (* expiry-hours time/hour)}])) 20 | 21 | (facts "about fetching forgotten password doc by id" 22 | (fact "can fetch single doc by username" 23 | (let [forgotten-password-store (m/create-memory-store) 24 | forgotten-password-id "asdf" 25 | login "bob@burgers.com"] 26 | (fp/store-id-for-user! forgotten-password-store test-clock forgotten-password-id login 240) 27 | (fp/forgotten-password-doc-by-login forgotten-password-store test-clock login) => {:forgotten-password-id forgotten-password-id 28 | :login login} 29 | )) 30 | (fact "if there are no matching docs then nil is returned" 31 | (let [forgotten-password-store (m/create-memory-store)] 32 | (fp/forgotten-password-doc-by-login forgotten-password-store test-clock "a@a.com") => nil)) 33 | (fact "if there are multiple matching docs then exception is thrown" 34 | (let [forgotten-password-store (m/create-memory-store) 35 | login "bob@burgers.com"] 36 | (fp/store-id-for-user! forgotten-password-store test-clock "id1" login 24) 37 | (fp/store-id-for-user! forgotten-password-store test-clock "id2" login 24) 38 | (fp/forgotten-password-doc-by-login forgotten-password-store test-clock login) => (throws Exception)))) -------------------------------------------------------------------------------- /assets/stylesheets/core/_grid.scss: -------------------------------------------------------------------------------- 1 | $grid_gutter: 10px; 2 | 3 | // Grid system 4 | // 5 | // Create rows with `.columns` to clear the floated columns and outdent the 6 | // padding on `.column`s with negative margin for alignment. 7 | 8 | @mixin columns { 9 | @include clearfix; 10 | margin-right: -$grid_gutter; 11 | margin-left: -$grid_gutter; 12 | } 13 | @mixin column($breakpoint: $medium_device) { 14 | padding-right: $grid_gutter; 15 | padding-left: $grid_gutter; 16 | @include width-to($breakpoint) { 17 | float: none; 18 | width: 100%; 19 | margin-bottom: $grid_gutter; 20 | } 21 | @include width-from($breakpoint) { 22 | float: left; 23 | } 24 | } 25 | 26 | @mixin single-column { 27 | width: 100%; 28 | float: none; 29 | padding-right: $grid_gutter; 30 | padding-left: $grid_gutter; 31 | } 32 | 33 | .columns { 34 | @include columns; 35 | } 36 | 37 | // Base class for every column (requires a column width from below) 38 | .column { 39 | @include column; 40 | } 41 | 42 | @mixin column-one-half($breakpoint:$medium_device) { 43 | $breakpoint: if($breakpoint, $breakpoint, 100px); 44 | @include column($breakpoint); 45 | width: 50%; 46 | } 47 | @mixin column-one-third($breakpoint:$medium_device) { 48 | @include column($breakpoint); 49 | width: 33.333333%; 50 | } 51 | @mixin column-two-thirds($breakpoint:$medium_device) { 52 | @include column($breakpoint); 53 | width: 66.666667%; 54 | } 55 | 56 | @mixin column-one-fourth($breakpoint:$medium_device) { 57 | @include column($breakpoint); 58 | width: 25%; 59 | } 60 | @mixin column-three-fourths($breakpoint:$medium_device) { 61 | @include column($breakpoint); 62 | width: 75%; 63 | } 64 | 65 | 66 | // Column widths 67 | .one-third { 68 | @include column-one-third; 69 | } 70 | 71 | .two-thirds { 72 | @include column-two-thirds; 73 | } 74 | 75 | .one-fourth { 76 | @include column-one-fourth; 77 | } 78 | 79 | .one-half { 80 | @include column-one-half; 81 | } 82 | 83 | .three-fourths { 84 | @include column-three-fourths; 85 | } 86 | 87 | .one-fifth { 88 | width: 20%; 89 | } 90 | 91 | .four-fifths { 92 | width: 80%; 93 | } 94 | 95 | // Single column hack 96 | .single-column { 97 | @include single-column; 98 | } 99 | 100 | // Equal width columns via table sorcery. 101 | .table-column { 102 | display: table-cell; 103 | width: 1%; 104 | padding-right: $grid_gutter; 105 | padding-left: $grid_gutter; 106 | vertical-align: top; 107 | } -------------------------------------------------------------------------------- /src/stonecutter/jwt.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.jwt 2 | (:require [clojure.tools.logging :as log] 3 | [stonecutter.routes :as routes] 4 | [stonecutter.util.time :as t]) 5 | (:import [org.jose4j.jwk JsonWebKey$Factory JsonWebKey$OutputControlLevel RsaJwkGenerator JsonWebKeySet] 6 | [org.jose4j.jws JsonWebSignature AlgorithmIdentifiers] 7 | [org.jose4j.jwt JwtClaims NumericDate] 8 | [org.jose4j.jwx HeaderParameterNames])) 9 | 10 | (defn generate-rsa-key-pair [key-id] 11 | (doto (RsaJwkGenerator/generateJwk 2048) 12 | (.setKeyId key-id))) 13 | 14 | (defn json->key-pair [json-string] (JsonWebKey$Factory/newJwk json-string)) 15 | 16 | (defn key-pair->json 17 | ([key-pair] 18 | (key-pair->json key-pair nil)) 19 | 20 | ([key-pair flag] 21 | (if (= flag :include-private-key) 22 | (.toJson key-pair JsonWebKey$OutputControlLevel/INCLUDE_PRIVATE) 23 | (.toJson key-pair)))) 24 | 25 | (defn json-web-key->json-web-key-set [json-web-key] 26 | (.toJson (JsonWebKeySet. [json-web-key]))) 27 | 28 | (defn load-key-pair [path] 29 | (try 30 | (-> (slurp path) json->key-pair) 31 | (catch Exception e 32 | (log/error e "Invalid RSA key file provided. App startup aborted.") 33 | (throw (Exception. "App startup aborted"))))) 34 | 35 | (defn set-additional-claims [jwt-claims claims-m] 36 | (doseq [[claim value] claims-m] (.setClaim jwt-claims (name claim) value))) 37 | 38 | (defn create-generator [clock rsa-key-pair issuer] 39 | (fn [sub aud token-lifetime-minutes additional-claims] 40 | (let [now (t/now-in-millis clock) 41 | expiration-time (+ now (* token-lifetime-minutes 60 1000)) 42 | jwt-claims (doto (JwtClaims.) 43 | (.setIssuer issuer) 44 | (.setAudience aud) 45 | (.setExpirationTime (NumericDate/fromMilliseconds expiration-time)) 46 | (.setIssuedAt (NumericDate/fromMilliseconds now)) 47 | (.setSubject sub) 48 | (set-additional-claims additional-claims)) 49 | jws (doto (JsonWebSignature.) 50 | (.setPayload (.toJson jwt-claims)) 51 | (.setKey (.getPrivateKey rsa-key-pair)) 52 | (.setKeyIdHeaderValue (.getKeyId rsa-key-pair)) 53 | (.setAlgorithmHeaderValue AlgorithmIdentifiers/RSA_USING_SHA256) 54 | (.setHeader HeaderParameterNames/JWK_SET_URL (str issuer (routes/path :jwk-set))))] 55 | (.getCompactSerialization jws)))) 56 | -------------------------------------------------------------------------------- /test/stonecutter/test/view/confirmation_sign_in.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.test.view.confirmation-sign-in 2 | (:require [midje.sweet :refer :all] 3 | [net.cgrand.enlive-html :as html] 4 | [stonecutter.routes :as r] 5 | [stonecutter.test.view.test-helpers :as th] 6 | [stonecutter.translation :as t] 7 | [stonecutter.view.confirmation-sign-in :refer [confirmation-sign-in-form]] 8 | [stonecutter.helper :as helper])) 9 | 10 | (facts "about confirmation sign in form" 11 | (let [page (-> (th/create-request) confirmation-sign-in-form)] 12 | (fact "confirmation sign-in-form should return some html" 13 | page => (th/element-exists? [:form])) 14 | 15 | (fact "there are no missing translations" 16 | (th/test-translations "confirmation sign in form" confirmation-sign-in-form)) 17 | 18 | (fact "work in progress should be removed from page" 19 | page => th/work-in-progress-removed) 20 | 21 | (fact "form should post to correct endpoint" 22 | page => (th/has-form-action? (r/path :confirmation-sign-in))) 23 | 24 | (fact "confirmation id should be set in form" 25 | (let [confirmation-id "confirmation-123" 26 | page (-> (th/create-request {} nil {:confirmation-id confirmation-id}) confirmation-sign-in-form)] 27 | page => (th/has-attr? [:.clj--confirmation-id__input] :value confirmation-id))) 28 | 29 | (fact "forgotten-password button should link to correct page" 30 | page => (th/has-attr? [:.clj--forgot-password] 31 | :href (r/path :show-forgotten-password-form))))) 32 | 33 | (facts "about displaying errors" 34 | (facts "when password is invalid" 35 | (let [errors {:credentials :confirmation-invalid} 36 | page (-> (th/create-request {} errors {}) confirmation-sign-in-form)] 37 | 38 | (fact "credentials validation element is present" 39 | page => (th/element-exists? [:.validation-summary--show])) 40 | 41 | (fact "correct error message is displayed" 42 | page => (th/has-attr? [:.clj--validation-summary__item] 43 | :data-l8n "content:confirmation-sign-in-form/invalid-credentials-validation-message")) 44 | 45 | (fact "invalid value is not preserved in input field" 46 | (-> page (html/select [:.func--password__input]) first :attrs :value) => empty?)))) 47 | -------------------------------------------------------------------------------- /test/stonecutter/test/view/unshare_profile_card.clj: -------------------------------------------------------------------------------- 1 | (ns stonecutter.test.view.unshare-profile-card 2 | (:require [midje.sweet :refer :all] 3 | [net.cgrand.enlive-html :as html] 4 | [stonecutter.test.view.test-helpers :as th] 5 | [stonecutter.translation :as t] 6 | [stonecutter.routes :as r] 7 | [stonecutter.view.unshare-profile-card :refer [unshare-profile-card]] 8 | [stonecutter.helper :as helper])) 9 | 10 | (fact "should return some html" 11 | (let [page (-> (th/create-request) 12 | unshare-profile-card)] 13 | (html/select page [:body]) =not=> empty?)) 14 | 15 | (fact "work in progress should be removed from page" 16 | (let [page (-> (th/create-request) unshare-profile-card)] 17 | page => th/work-in-progress-removed)) 18 | 19 | (fact "there are no missing translations" 20 | (let [translator (t/translations-fn t/translation-map) 21 | page (-> (th/create-request) unshare-profile-card (helper/enlive-response {:translator translator}) :body)] 22 | page => th/no-untranslated-strings)) 23 | 24 | (fact "form posts to correct endpoint" 25 | (let [page (-> (th/create-request) unshare-profile-card)] 26 | (-> page (html/select [:form]) first :attrs :action) => (r/path :unshare-profile-card))) 27 | 28 | (fact "client_id is included in the form as a hidden parameter" 29 | (let [client-id-element (-> (th/create-request) 30 | (assoc-in [:context :client] {:client-id "SOME_CLIENT_ID"}) 31 | unshare-profile-card 32 | (html/select [:.clj--client-id__input]) 33 | first)] 34 | (-> client-id-element :attrs :value) => "SOME_CLIENT_ID" 35 | (-> client-id-element :attrs :type) => "hidden")) 36 | 37 | (fact "client name is injected" 38 | (let [client-name "CLIENT_NAME" 39 | client-name-elements (-> (th/create-request) 40 | (assoc-in [:context :client] {:name client-name}) 41 | unshare-profile-card 42 | (html/select [:.clj--client-name])) 43 | client-name-is-correct-fn (fn [element] (= (html/text element) client-name))] 44 | client-name-elements =not=> empty? 45 | client-name-elements => (has every? client-name-is-correct-fn))) 46 | 47 | (fact "cancel link should go to correct endpoint" 48 | (let [page (-> (th/create-request) unshare-profile-card)] 49 | (-> page (html/select [:.clj--unshare-profile-card-cancel__link]) first :attrs :href) => (r/path :show-profile))) 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # stonecutter 2 | 3 | A D-CENT project: an easily deployable oauth server for small organisations. 4 | 5 | > **There has been an update to the deployment script `deploy_prod.sh`. If you have deployed an instance of Stonecutter 6 | to an Ubuntu server using Ansible, you will need to rerun the last two steps of the deployment: 7 | [Configure with ansible](https://github.com/d-cent/stonecutter/blob/master/docs/UBUNTU.md#configure-with-ansible) and 8 | [Deploy application to the server](https://github.com/d-cent/stonecutter/blob/master/docs/UBUNTU.md#deploy-application-to-the-server).** 9 | 10 | ## Development VM 11 | 12 | You can develop and run the application in a VM to ensure that the correct versions of Stonecutter's dependencies 13 | are installed. You will need [VirtualBox][], [Vagrant][] and [Ansible][] installed. 14 | 15 | First, clone the repository. 16 | 17 | Navigate to the ops/ directory of the project and run: 18 | 19 | vagrant up development 20 | 21 | The first time this is run, it will provision and configure a new VM. 22 | 23 | When the VM has started, access the virtual machine by running: 24 | 25 | vagrant ssh 26 | 27 | The source folder will be located at `/var/stonecutter`. 28 | 29 | After initial setup, navigate to the source directory and build the assets with: 30 | 31 | cd /var/stonecutter 32 | gulp build 33 | 34 | [Vagrant]: https://www.vagrantup.com 35 | [Ansible]: http://docs.ansible.com/ansible/intro_installation.html 36 | [VirtualBox]: https://www.virtualbox.org/ 37 | 38 | ### Running 39 | 40 | To start the app, run: 41 | 42 | ./start_app_vm.sh 43 | 44 | To start a web server for the application in development mode, run: 45 | 46 | lein ring server-headless 47 | 48 | NB: running the application like this will save users into an in memory cache that will be destroyed as soon as the app is shutdown. 49 | 50 | ### Running test suite 51 | 52 | To run all tests, use this command: 53 | 54 | ``` 55 | lein test 56 | ``` 57 | 58 | Commands and aliases can be found in the project.clj file. 59 | 60 | ### Running the prototype 61 | 62 | Simply type: 63 | 64 | ``` 65 | gulp server 66 | ``` 67 | 68 | 69 | ## Architecture 70 | 71 | The Continuous Delivery build and deployment architecture is documented [here](https://docs.google.com/a/thoughtworks.com/drawings/d/1FZ35v27_pBym_NqzLbqVP_TwnHVBHNwFQss_Lzbs1bU/edit?usp=sharing). 72 | 73 | The Hosting Architecture is documented [here](https://docs.google.com/a/thoughtworks.com/drawings/d/1mgdxe0Q0uYZloZLFLvlwyKRx4iUangEAn4aV2qm-zWs/edit?usp=sharing). 74 | 75 | 76 | ## Deployment 77 | 78 | To deploy using Docker, see [here](docs/DOCKER.md). 79 | 80 | To deploy to an Ubuntu server using Ansible, see [here](docs/UBUNTU.md). --------------------------------------------------------------------------------