Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision

Target

Select target project
  • uffd/uffd
  • rixx/uffd
  • thies/uffd
  • leona/uffd
  • enbewe/uffd
  • strifel/uffd
  • thies/uffd-2
7 results
Select Git revision
Show changes
Showing
with 910 additions and 64 deletions
/**
* @popperjs/core v2.0.4 - MIT License
*/
"use strict";!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e=e||self).Popper={})}(this,(function(e){function t(e){return{width:(e=e.getBoundingClientRect()).width,height:e.height,top:e.top,right:e.right,bottom:e.bottom,left:e.left,x:e.left,y:e.top}}function n(e){return"[object Window]"!=={}.toString.call(e)?(e=e.ownerDocument)?e.defaultView:window:e}function r(e){return{scrollLeft:(e=n(e)).pageXOffset,scrollTop:e.pageYOffset}}function o(e){return e instanceof n(e).Element}function i(e){return e instanceof n(e).HTMLElement}function a(e){return e?(e.nodeName||"").toLowerCase():null}function s(e,o,s){void 0===s&&(s=!1),e=t(e);var f={scrollLeft:0,scrollTop:0},p={x:0,y:0};return s||("body"!==a(o)&&(f=o!==n(o)&&i(o)?{scrollLeft:o.scrollLeft,scrollTop:o.scrollTop}:r(o)),i(o)&&((p=t(o)).x+=o.clientLeft,p.y+=o.clientTop)),{x:e.left+f.scrollLeft-p.x,y:e.top+f.scrollTop-p.y,width:e.width,height:e.height}}function f(e){return{x:e.offsetLeft,y:e.offsetTop,width:e.offsetWidth,height:e.offsetHeight}}function p(e){return"html"===a(e)?e:e.parentNode||e.host||document.ownerDocument||document.documentElement}function c(e){return n(e).getComputedStyle(e)}function u(e,t){void 0===t&&(t=[]);var r=function e(t){if(0<=["html","body","#document"].indexOf(a(t)))return t.ownerDocument.body;if(i(t)){var n=c(t);if(/auto|scroll|overlay|hidden/.test(n.overflow+n.overflowY+n.overflowX))return t}return e(p(t))}(e);return r=(e="body"===a(r))?n(r):r,t=t.concat(r),e?t:t.concat(u(p(r)))}function d(e){var t;return!i(e)||!(t=e.offsetParent)||void 0!==window.InstallTrigger&&"fixed"===c(t).position?null:t}function l(e){var t=n(e);for(e=d(e);e&&0<=["table","td","th"].indexOf(a(e));)e=d(e);return e&&"body"===a(e)&&"static"===c(e).position?t:e||t}function m(e){var t=new Map,n=new Set,r=[];return e.forEach((function(e){t.set(e.name,e)})),e.forEach((function(e){n.has(e.name)||function e(o){n.add(o.name),[].concat(o.requires||[],o.requiresIfExists||[]).forEach((function(r){n.has(r)||(r=t.get(r))&&e(r)})),r.push(o)}(e)})),r}function h(e){var t;return function(){return t||(t=new Promise((function(n){Promise.resolve().then((function(){t=void 0,n(e())}))}))),t}}function v(e){return e.split("-")[0]}function g(){for(var e=arguments.length,t=Array(e),n=0;n<e;n++)t[n]=arguments[n];return!t.some((function(e){return!(e&&"function"==typeof e.getBoundingClientRect)}))}function b(e){void 0===e&&(e={});var t=e.defaultModifiers,n=void 0===t?[]:t,r=void 0===(e=e.defaultOptions)?H:e;return function(e,t,i){function a(){c.forEach((function(e){return e()})),c=[]}void 0===i&&(i=r);var p={placement:"bottom",orderedModifiers:[],options:Object.assign({},H,{},r),modifiersData:{},elements:{reference:e,popper:t},attributes:{},styles:{}},c=[],d=!1,v={state:p,setOptions:function(i){return a(),p.options=Object.assign({},r,{},p.options,{},i),p.scrollParents={reference:o(e)?u(e):[],popper:u(t)},i=function(e){var t=m(e);return A.reduce((function(e,n){return e.concat(t.filter((function(e){return e.phase===n})))}),[])}([].concat(p.options.modifiers.filter((function(e){return!n.find((function(t){return t.name===e.name}))})),n.map((function(e){return Object.assign({},e,{},p.options.modifiers.find((function(t){return t.name===e.name})))})))),p.orderedModifiers=i.filter((function(e){return e.enabled})),p.orderedModifiers.forEach((function(e){var t=e.name,n=e.options;n=void 0===n?{}:n,"function"==typeof(e=e.effect)&&(t=e({state:p,name:t,instance:v,options:n}),c.push(t||function(){}))})),v.update()},forceUpdate:function(){if(!d){var e=p.elements,t=e.reference;if(g(t,e=e.popper))for(p.rects={reference:s(t,l(e),"fixed"===p.options.strategy),popper:f(e)},p.reset=!1,p.placement=p.options.placement,p.orderedModifiers.forEach((function(e){return p.modifiersData[e.name]=Object.assign({},e.data)})),t=0;t<p.orderedModifiers.length;t++)if(!0===p.reset)p.reset=!1,t=-1;else{var n=p.orderedModifiers[t];e=n.fn;var r=n.options;r=void 0===r?{}:r,n=n.name,"function"==typeof e&&(p=e({state:p,options:r,name:n,instance:v})||p)}}},update:h((function(){return new Promise((function(e){v.forceUpdate(),e(p)}))})),destroy:function(){a(),d=!0}};return g(e,t)?(v.setOptions(i).then((function(e){!d&&i.onFirstUpdate&&i.onFirstUpdate(e)})),v):v}}function y(e){return 0<=["top","bottom"].indexOf(e)?"x":"y"}function x(e){var t=e.reference,n=e.element,r=(e=e.placement)?v(e):null;e=e?e.split("-")[1]:null;var o=t.x+t.width/2-n.width/2,i=t.y+t.height/2-n.height/2;switch(r){case"top":o={x:o,y:t.y-n.height};break;case"bottom":o={x:o,y:t.y+t.height};break;case"right":o={x:t.x+t.width,y:i};break;case"left":o={x:t.x-n.width,y:i};break;default:o={x:t.x,y:t.y}}if(null!=(r=r?y(r):null))switch(i="y"===r?"height":"width",e){case"start":o[r]=Math.floor(o[r])-Math.floor(t[i]/2-n[i]/2);break;case"end":o[r]=Math.floor(o[r])+Math.ceil(t[i]/2-n[i]/2)}return o}function w(e){var t,r=e.popper,o=e.popperRect,i=e.placement,a=e.offsets,s=e.position,f=e.gpuAcceleration,p=e.adaptive,c=window.devicePixelRatio||1;e=Math.round(a.x*c)/c||0,c=Math.round(a.y*c)/c||0;var u=a.hasOwnProperty("x");a=a.hasOwnProperty("y");var d,m="left",h="top";if(p){var v=l(r);v===n(r)&&(v=r.ownerDocument.documentElement),"top"===i&&(h="bottom",c-=v.clientHeight-o.height,c*=f?1:-1),"left"===i&&(m="right",e-=v.clientWidth-o.width,e*=f?1:-1)}return r=Object.assign({position:s},p&&F),f?Object.assign({},r,((d={})[h]=a?"0":"",d[m]=u?"0":"",d.transform=2>(window.devicePixelRatio||1)?"translate("+e+"px, "+c+"px)":"translate3d("+e+"px, "+c+"px, 0)",d)):Object.assign({},r,((t={})[h]=a?c+"px":"",t[m]=u?e+"px":"",t.transform="",t))}function O(e){return e.replace(/left|right|bottom|top/g,(function(e){return I[e]}))}function M(e){return e.replace(/start|end/g,(function(e){return N[e]}))}function D(e,t){var n=!(!t.getRootNode||!t.getRootNode().host);if(e.contains(t))return!0;if(n)do{if(t&&e.isSameNode(t))return!0;t=t.parentNode||t.host}while(t);return!1}function j(e){return Object.assign({},e,{left:e.x,top:e.y,right:e.x+e.width,bottom:e.y+e.height})}function E(e,o){if("viewport"===o)e=j({width:(e=n(e)).innerWidth,height:e.innerHeight,x:0,y:0});else if(i(o))e=t(o);else{var a=e.ownerDocument.documentElement;e=n(a),o=r(a),(a=s(a.ownerDocument.documentElement,e)).height=Math.max(a.height,e.innerHeight),a.width=Math.max(a.width,e.innerWidth),a.x=-o.scrollLeft,a.y=-o.scrollTop,e=j(a)}return e}function k(e,t,r){return t="clippingParents"===t?function(e){var t=u(e),n=0<=["absolute","fixed"].indexOf(c(e).position)&&i(e)?l(e):e;return o(n)?t.filter((function(e){return o(e)&&D(e,n)})):[]}(e):[].concat(t),(r=(r=[].concat(t,[r])).reduce((function(t,r){var o=E(e,r),s=n(r=i(r)?r:e.ownerDocument.documentElement),f=i(r)?c(r):{};parseFloat(f.borderTopWidth);var p=parseFloat(f.borderRightWidth)||0,u=parseFloat(f.borderBottomWidth)||0,d=parseFloat(f.borderLeftWidth)||0;f="html"===a(r);var l=r.clientWidth+p,m=r.clientHeight+u;return u=r.clientTop,p=r.clientLeft>d?p:f?s.innerWidth-l:r.offsetWidth-l,s=f?s.innerHeight-m:r.offsetHeight-m,r=r.clientLeft,t.top=Math.max(o.top+u,t.top),t.right=Math.min(o.right-p,t.right),t.bottom=Math.min(o.bottom-s,t.bottom),t.left=Math.max(o.left+r,t.left),t}),E(e,r[0]))).width=r.right-r.left,r.height=r.bottom-r.top,r.x=r.left,r.y=r.top,r}function P(e){return Object.assign({},{top:0,right:0,bottom:0,left:0},{},e)}function L(e,t){return t.reduce((function(t,n){return t[n]=e,t}),{})}function W(e,n){void 0===n&&(n={});var r=n;n=void 0===(n=r.placement)?e.placement:n;var i=r.boundary,a=void 0===i?"clippingParents":i,s=void 0===(i=r.rootBoundary)?"viewport":i;i=void 0===(i=r.elementContext)?"popper":i;var f=r.altBoundary,p=void 0!==f&&f;r=P("number"!=typeof(r=void 0===(r=r.padding)?0:r)?r:L(r,T));var c=e.elements.reference;f=e.rects.popper,a=k(o(p=e.elements[p?"popper"===i?"reference":"popper":i])?p:e.elements.popper.ownerDocument.documentElement,a,s),p=x({reference:s=t(c),element:f,strategy:"absolute",placement:n}),f=j(Object.assign({},f,{},p)),s="popper"===i?f:s;var u={top:a.top-s.top+r.top,bottom:s.bottom-a.bottom+r.bottom,left:a.left-s.left+r.left,right:s.right-a.right+r.right};if(e=e.modifiersData.offset,"popper"===i&&e){var d=e[n];Object.keys(u).forEach((function(e){var t=0<=["right","bottom"].indexOf(e)?1:-1,n=0<=["top","bottom"].indexOf(e)?"y":"x";u[e]+=d[n]*t}))}return u}function B(e,t,n){return void 0===n&&(n={x:0,y:0}),{top:e.top-t.height-n.y,right:e.right-t.width+n.x,bottom:e.bottom-t.height+n.y,left:e.left-t.width-n.x}}function R(e){return["top","right","bottom","left"].some((function(t){return 0<=e[t]}))}var T=["top","bottom","right","left"],q=T.reduce((function(e,t){return e.concat([t+"-start",t+"-end"])}),[]),S=[].concat(T,["auto"]).reduce((function(e,t){return e.concat([t,t+"-start",t+"-end"])}),[]),A="beforeRead read afterRead beforeMain main afterMain beforeWrite write afterWrite".split(" "),H={placement:"bottom",modifiers:[],strategy:"absolute"},C={passive:!0},F={top:"auto",right:"auto",bottom:"auto",left:"auto"},I={left:"right",right:"left",bottom:"top",top:"bottom"},N={start:"end",end:"start"},_=[{name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(e){var t=e.state,r=e.instance,o=(e=e.options).scroll,i=void 0===o||o,a=void 0===(e=e.resize)||e,s=n(t.elements.popper),f=[].concat(t.scrollParents.reference,t.scrollParents.popper);return i&&f.forEach((function(e){e.addEventListener("scroll",r.update,C)})),a&&s.addEventListener("resize",r.update,C),function(){i&&f.forEach((function(e){e.removeEventListener("scroll",r.update,C)})),a&&s.removeEventListener("resize",r.update,C)}},data:{}},{name:"popperOffsets",enabled:!0,phase:"read",fn:function(e){var t=e.state;t.modifiersData[e.name]=x({reference:t.rects.reference,element:t.rects.popper,strategy:"absolute",placement:t.placement})},data:{}},{name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(e){var t=e.state,n=e.options;e=void 0===(e=n.gpuAcceleration)||e,n=void 0===(n=n.adaptive)||n,e={placement:v(t.placement),popper:t.elements.popper,popperRect:t.rects.popper,gpuAcceleration:e},t.styles.popper=Object.assign({},t.styles.popper,{},w(Object.assign({},e,{offsets:t.modifiersData.popperOffsets,position:t.options.strategy,adaptive:n}))),null!=t.modifiersData.arrow&&(t.styles.arrow=Object.assign({},t.styles.arrow,{},w(Object.assign({},e,{offsets:t.modifiersData.arrow,position:"absolute",adaptive:!1})))),t.attributes.popper=Object.assign({},t.attributes.popper,{"data-popper-placement":t.placement})},data:{}},{name:"applyStyles",enabled:!0,phase:"write",fn:function(e){var t=e.state;Object.keys(t.elements).forEach((function(e){var n=t.styles[e]||{},r=t.attributes[e]||{},o=t.elements[e];i(o)&&a(o)&&(Object.assign(o.style,n),Object.keys(r).forEach((function(e){var t=r[e];!1===t?o.removeAttribute(e):o.setAttribute(e,!0===t?"":t)})))}))},effect:function(e){var t=e.state,n={position:"absolute",left:"0",top:"0",margin:"0"};return Object.assign(t.elements.popper.style,n),function(){Object.keys(t.elements).forEach((function(e){var r=t.elements[e],o=Object.keys(t.styles.hasOwnProperty(e)?Object.assign({},t.styles[e]):n);e=t.attributes[e]||{},o=o.reduce((function(e,t){var n;return Object.assign({},e,((n={})[String(t)]="",n))}),{}),i(r)&&a(r)&&(Object.assign(r.style,o),Object.keys(e).forEach((function(e){return r.removeAttribute(e)})))}))}},requires:["computeStyles"]},{name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(e){var t=e.state,n=e.name,r=void 0===(e=e.options.offset)?[0,0]:e,o=(e=S.reduce((function(e,n){var o=t.rects,i=v(n),a=0<=["left","top"].indexOf(i)?-1:1,s="function"==typeof r?r(Object.assign({},o,{placement:n})):r;return o=(o=s[0])||0,s=((s=s[1])||0)*a,i=0<=["left","right"].indexOf(i)?{x:s,y:o}:{x:o,y:s},e[n]=i,e}),{}))[t.placement],i=o.y;t.modifiersData.popperOffsets.x+=o.x,t.modifiersData.popperOffsets.y+=i,t.modifiersData[n]=e}},{name:"flip",enabled:!0,phase:"main",fn:function(e){var t=e.state,n=e.options;if(e=e.name,!t.modifiersData[e]._skip){var r=n.fallbackPlacements,o=n.padding,i=n.boundary,a=n.rootBoundary,s=void 0===(n=n.flipVariations)||n,f=v(n=t.options.placement);r=r||(f!==n&&s?function(e){if("auto"===v(e))return[];var t=O(e);return[M(e),t,M(t)]}(n):[O(n)]);var p=function(e,t){var n=new Set;return e.filter((function(e){if(e=t(e),!n.has(e))return n.add(e),!0}))}([n].concat(r).reduce((function(e,n){return"auto"===v(n)?e.concat(function(e,t){void 0===t&&(t={});var n=t.boundary,r=t.rootBoundary,o=t.padding,i=t.flipVariations,a=t.placement.split("-")[1],s=(a?i?q:q.filter((function(e){return e.split("-")[1]===a})):T).reduce((function(t,i){return t[i]=W(e,{placement:i,boundary:n,rootBoundary:r,padding:o})[v(i)],t}),{});return Object.keys(s).sort((function(e,t){return s[e]-s[t]}))}(t,{placement:n,boundary:i,rootBoundary:a,padding:o,flipVariations:s})):e.concat(n)}),[]),(function(e){return e}));r=t.rects.reference,n=t.rects.popper;var c=new Map;f=!0;for(var u=p[0],d=0;d<p.length;d++){var l=p[d],m=v(l),h="start"===l.split("-")[1],g=0<=["top","bottom"].indexOf(m),b=g?"width":"height",y=W(t,{placement:l,boundary:i,rootBoundary:a,padding:o});if(h=g?h?"right":"left":h?"bottom":"top",r[b]>n[b]&&(h=O(h)),b=O(h),(m=[0>=y[m],0>=y[h],0>=y[b]]).every((function(e){return e}))){u=l,f=!1;break}c.set(l,m)}if(f)for(r=function(e){var t=p.find((function(t){if(t=c.get(t))return t.slice(0,e).every((function(e){return e}))}));if(t)return u=t,"break"},n=s?3:1;0<n&&"break"!==r(n);n--);t.placement!==u&&(t.modifiersData[e]._skip=!0,t.placement=u,t.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}},{name:"preventOverflow",enabled:!0,phase:"main",fn:function(e){var t=e.state,n=e.options;e=e.name;var r=n.mainAxis,o=void 0===r||r;r=void 0!==(r=n.altAxis)&&r;var i=n.tether;i=void 0===i||i;var a=n.tetherOffset,s=void 0===a?0:a;n=W(t,{boundary:n.boundary,rootBoundary:n.rootBoundary,padding:n.padding}),a=v(t.placement);var p=t.placement.split("-")[1],c=!p,u=y(a);a="x"===u?"y":"x";var d=t.modifiersData.popperOffsets,l=t.rects.reference,m=t.rects.popper,h="function"==typeof s?s(Object.assign({},t.rects,{placement:t.placement})):s;if(s={x:0,y:0},o){var g="y"===u?"top":"left",b="y"===u?"bottom":"right",x="y"===u?"height":"width";o=d[u];var w=d[u]+n[g],O=d[u]-n[b],M=i?-m[x]/2:0,D="start"===p?l[x]:m[x];p="start"===p?-m[x]:-l[x],m=t.elements.arrow,m=i&&m?f(m):{width:0,height:0};var j=t.modifiersData["arrow#persistent"]?t.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0};g=j[g],b=j[b],m=Math.max(0,Math.min(Math.abs(l[x]-m[x]),m[x])),j=t.modifiersData.offset?t.modifiersData.offset[t.placement][u]:0,D=t.modifiersData.popperOffsets[u]+(c?l[x]/2-M-m-g-h:D-m-g-h)-j,c=t.modifiersData.popperOffsets[u]+(c?-l[x]/2+M+m+b+h:p+m+b+h)-j,i=Math.max(i?Math.min(w,D):w,Math.min(o,i?Math.max(O,c):O)),t.modifiersData.popperOffsets[u]=i,s[u]=i-o}r&&(r=d[a],i=Math.max(r+n["x"===u?"top":"left"],Math.min(r,r-n["x"===u?"bottom":"right"])),t.modifiersData.popperOffsets[a]=i,s[a]=i-r),t.modifiersData[e]=s},requiresIfExists:["offset"]},{name:"arrow",enabled:!0,phase:"main",fn:function(e){var t,n=e.state;e=e.name;var r=n.elements.arrow,o=n.modifiersData.popperOffsets,i=v(n.placement),a=y(i);if(i=0<=["left","right"].indexOf(i)?"height":"width",r){var s=n.modifiersData[e+"#persistent"].padding;r=f(r),o=Math.max(s["y"===a?"top":"left"],Math.min(n.rects.popper[i]/2-r[i]/2+((n.rects.reference[i]+n.rects.reference[a]-o[a]-n.rects.popper[i])/2-(o[a]-n.rects.reference[a])/2),n.rects.popper[i]-r[i]-s["y"===a?"bottom":"right"])),n.modifiersData[e]=((t={})[a]=o,t)}},effect:function(e){var t=e.state,n=e.options;e=e.name;var r=n.element;r=void 0===r?"[data-popper-arrow]":r,n=void 0===(n=n.padding)?0:n,("string"!=typeof r||(r=t.elements.popper.querySelector(r)))&&D(t.elements.popper,r)&&(t.elements.arrow=r,t.modifiersData[e+"#persistent"]={padding:P("number"!=typeof n?n:L(n,T))})},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]},{name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(e){var t=e.state;e=e.name;var n=t.rects.reference,r=t.rects.popper,o=t.modifiersData.preventOverflow,i=W(t,{elementContext:"reference"}),a=W(t,{altBoundary:!0});n=B(i,n),r=B(a,r,o),o=R(n),a=R(r),t.modifiersData[e]={referenceClippingOffsets:n,popperEscapeOffsets:r,isReferenceHidden:o,hasPopperEscaped:a},t.attributes.popper=Object.assign({},t.attributes.popper,{"data-popper-reference-hidden":o,"data-popper-escaped":a})}}],U=b({defaultModifiers:_});e.createPopper=U,e.defaultModifiers=_,e.popperGenerator=b,Object.defineProperty(e,"__esModule",{value:!0})}));
//# sourceMappingURL=popper.min.js.map
.footer {
position: fixed;
bottom: 0;
width: 100%;
height: 2em;
line-height: 2em;
background-color: #f5f5f5;
}
.narrow-card {
background: #f7f7f7;
box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
padding: 30px;
}
.qrcode {
background: #ffffff;
}
@media (prefers-color-scheme: dark) {
.footer {
background-color: #23282d;
}
.narrow-card {
background: #292e33;
}
a {
color: #3395ff;
background-color: transparent
}
a:hover {
color: #0d82ff
}
.btn-link {
color: #3395ff;
}
.btn-link:hover {
color: #0d82ff;
}
.btn-link.disabled,
.btn-link:disabled {
color: #3395ff;
}
/* Dark theme breaks spinners (appears as full ring without animation) */
.spinner-border {
display: inline-block;
width: 2rem;
height: 2rem;
vertical-align: text-bottom;
border: 0.25em solid currentColor;
border-right-color: transparent;
border-radius: 50%;
-webkit-animation: spinner-border .75s linear infinite;
animation: spinner-border .75s linear infinite;
}
.spinner-border-sm {
width: 1rem;
height: 1rem;
border-width: 0.2em;
}
}
.footer {
position: fixed;
bottom: 0;
width: 100%;
height: 2em;
line-height: 2em;
background-color: #f5f5f5;
}
class CleanupTask:
def __init__(self):
self.handlers = []
def handler(self, func):
self.handlers.append(func)
return func
def delete_by_attribute(self, attribute):
def decorator(cls):
@self.handler
def handler():
cls.query.filter(getattr(cls, attribute)).delete()
return cls
return decorator
def run(self):
for handler in self.handlers:
handler()
cleanup_task = CleanupTask()
import random import random
import subprocess
import base64 import base64
from datetime import timedelta, datetime from datetime import timedelta, datetime
import io import io
...@@ -16,23 +15,23 @@ def register_template_helper(app): ...@@ -16,23 +15,23 @@ def register_template_helper(app):
@app.template_filter() @app.template_filter()
def qrcode_svg(content, **attrs): #pylint: disable=unused-variable def qrcode_svg(content, **attrs): #pylint: disable=unused-variable
img = qrcode.make(content, image_factory=qrcode.image.svg.SvgPathImage, border=0) img = qrcode.make(content, image_factory=qrcode.image.svg.SvgPathImage, border=4)
svg = img.get_image() svg = img.get_image()
for key, value, in attrs.items(): for key, value, in attrs.items():
svg.set(key, value) svg.set(key, value)
buf = io.BytesIO() buf = io.BytesIO()
img.save(buf) img.save(buf)
return Markup(buf.getvalue().decode().replace('<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n', '')) return Markup(
buf.getvalue().decode()
.replace('<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n', '')
.replace(' id="qr-path" ', ' ')
.replace('<svg ', '<svg class="qrcode" ')
)
@app.template_filter() @app.template_filter()
def datauri(data, mimetype='text/plain'): #pylint: disable=unused-variable def datauri(data, mimetype='text/plain'): #pylint: disable=unused-variable
return Markup('data:%s;base64,%s'%(mimetype, base64.b64encode(data.encode()).decode())) return Markup('data:%s;base64,%s'%(mimetype, base64.b64encode(data.encode()).decode()))
@app.url_defaults
def static_version_inject(endpoint, values): #pylint: disable=unused-variable
if endpoint == 'static':
values['v'] = app.jinja_env.globals['gitversion']['longhash'] #pylint: disable=no-member
app.jinja_env.trim_blocks = True app.jinja_env.trim_blocks = True
app.jinja_env.lstrip_blocks = True app.jinja_env.lstrip_blocks = True
...@@ -42,7 +41,3 @@ def register_template_helper(app): ...@@ -42,7 +41,3 @@ def register_template_helper(app):
app.add_template_global(min, name='min') app.add_template_global(min, name='min')
app.add_template_global(max, name='max') app.add_template_global(max, name='max')
app.add_template_global(equalto, name='equalto') app.add_template_global(equalto, name='equalto')
# get git commit
git_output = subprocess.check_output(['git', "log", "-g", "-1", "--pretty=%H#%h#%d#%s"]).decode('UTF-8').split('#', 3)
app.jinja_env.globals['gitversion'] = {'hash': git_output[1], 'longhash': git_output[0], 'branch': git_output[2], 'msg': git_output[3]} #pylint: disable=no-member
{% extends 'base.html' %} {% extends 'base.html' %}
{% block body %} {% block body %}
<form action="{{ url_for("selfservice.forgot_password") }}" method="POST">
<div class="row mt-2 justify-content-center"> <div class="row mt-2 justify-content-center">
<div class="col-lg-6 col-md-10" style="background: #f7f7f7; box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); padding: 30px;"> <div class="col-lg-6 col-md-10 narrow-card">
<div class="text-center"> <div class="text-center">
<img alt="branding logo" src="{{ config.get("BRANDING_LOGO_URL") }}" class="col-lg-8 col-md-12" > <img alt="branding logo" src="{{ config.get("BRANDING_LOGO_URL") }}" class="col-lg-8 col-md-12" >
</div> </div>
<div class="col-12"> <div class="col-12">
<h2 class="text-center">Forgot password</h2> <h2 class="text-center">{{ _('Access Denied') }}</h2>
</div> </div>
<div class="form-group col-12"> <div class="form-group col-12">
<label for="user-loginname">Login Name</label> <p>
<input type="text" class="form-control" id="user-loginname" name="loginname" required="required" tabindex = "1"> {% if description %}
</div> {{ description|safe }}
<div class="form-group col-12"> {% else %}
<label for="user-mail">Mail Address</label> {{ _("You don't have the permission to access this page.") }}
<input type="text" class="form-control" id="user-mail" name="mail" required="required" tabindex = "2"> {% endif %}
</div> </p>
<div class="form-group col-12">
<button type="submit" class="btn btn-primary btn-block" tabindex = "3">Send password reset mail</button>
</div> </div>
</div> </div>
</div> </div>
</form>
{% endblock %} {% endblock %}
...@@ -4,13 +4,14 @@ ...@@ -4,13 +4,14 @@
{% block head %} {% block head %}
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>{% block title %}uffd{% endblock %}</title> <title>{% block title %}{{ config['SITE_TITLE'] }}{% endblock %}</title>
<link href="{{ url_for('static', filename="bootstrap/bootstrap.min.css") }}" rel="stylesheet"> <link href="{{ url_for('static', filename="bootstrap/bootstrap.min.css") }}" rel="stylesheet">
<link href="{{ url_for('static', filename="bootstrap/bootstrap-prefers-dark-color-only.min.css") }}" rel="stylesheet">
<link href="{{ url_for('static', filename="fa/css/all.css") }}" rel="stylesheet"> <link href="{{ url_for('static', filename="fa/css/all.css") }}" rel="stylesheet">
<link href="{{ url_for('static', filename="style.css") }}" rel="stylesheet"> <link href="{{ url_for('static', filename="style-1.css") }}" rel="stylesheet">
<script src="{{ url_for('static', filename="jquery/jquery-3.4.1.min.js") }}"></script> <script src="{{ url_for('static', filename="jquery/jquery-3.4.1.min.js") }}"></script>
<script src="{{ url_for('static', filename="popper/popper.min.js") }}"></script> <script src="{{ url_for('static', filename="popper/popper-1.16.0.min.js") }}"></script>
<script src="{{ url_for('static', filename="bootstrap/bootstrap.min.js") }}"></script> <script src="{{ url_for('static', filename="bootstrap/bootstrap.min.js") }}"></script>
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/png"> <link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/png">
...@@ -35,13 +36,13 @@ ...@@ -35,13 +36,13 @@
{%- endmacro %} {%- endmacro %}
<nav class="navbar navbar-expand-md navbar-dark bg-dark static-top" > <nav class="navbar navbar-expand-md navbar-dark bg-dark static-top" >
<a class="navbar-brand" href="{{ url_for('index') }}">uffd</a> <a class="navbar-brand" href="{{ url_for('index') }}">{{ config['SITE_TITLE'] }}</a>
{% if getnavbar() %} {% if getnavbar() or request.user or request.endpoint != 'session.login' or config['LANGUAGES']|length > 1 %}
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#baseNavbar" aria-controls="baseNavbar" aria-expanded="false" aria-label="Toggle navigation"> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#baseNavbar" aria-controls="baseNavbar" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="baseNavbar"> <div class="collapse navbar-collapse" id="baseNavbar">
{% if getnavbar() %}
<ul class="navbar-nav mr-auto"> <ul class="navbar-nav mr-auto">
{% for n in getnavbar() if not n.group %} {% for n in getnavbar() if not n.group %}
{{ navbaricon(n) }} {{ navbaricon(n) }}
...@@ -67,14 +68,46 @@ ...@@ -67,14 +68,46 @@
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
{% if is_valid_session() %} {% endif %}
{% if request.user or request.endpoint != 'session.login' or config['LANGUAGES']|length > 1 %}
<ul class="navbar-nav ml-auto"> <ul class="navbar-nav ml-auto">
{% if config['LANGUAGES']|length > 1 %}
<li class="nav-item">
<form class="language-switch py-2 pr-1" method="POST" style="margin-left: -5px;" action="{{ url_for('setlang') }}">
<input type="hidden" name="ref" value="{{ request.full_path }}">
<select name="lang" class="bg-dark" style="border: 0px; color: rgba(255, 255, 255, 0.5);" onchange="$('.language-switch').submit()">
{% for identifier, name in config['LANGUAGES'].items() %}
<option value="{{ identifier }}" {{ 'selected' if identifier == get_locale() }}>{{ name }}</option>
{% endfor %}
</select>
<noscript>
<button type="submit" class="bg-dark py-0 pl-0" style="border: 0px; color: rgba(255, 255, 255, 0.5);">{{_('Change')}}</button>
</noscript>
</form>
</li>
{% endif %}
{% if request.user %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for("session.deviceauth") }}">
<span aria-hidden="true" class="fas fa-mobile-alt" title="{{_('Authorize Device Login')}}"></span>
<span class="d-inline d-md-none">{{_('Device Login')}}</span>
</a>
</li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{{ url_for("session.logout") }}"> <a class="nav-link" href="{{ url_for("session.logout") }}">
<span aria-hidden="true" class="fa fa-sign-out-alt"></span> <span aria-hidden="true" class="fa fa-sign-out-alt"></span>
Logout {{_("Logout")}}
</a>
</li>
{% elif request.endpoint != 'session.login' %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for("session.login", ref=request.full_path) }}">
<span aria-hidden="true" class="fa fa-sign-in-alt"></span>
{{ _("Login") }}
</a> </a>
</li> </li>
{% endif %}
</ul> </ul>
{% endif %} {% endif %}
</div> </div>
...@@ -82,31 +115,32 @@ ...@@ -82,31 +115,32 @@
</nav> </nav>
{% endblock navbar %} {% endblock navbar %}
<div class="container mt-2"> {% block main %}
<main role="main" class="container mt-3">
<div class="row"> <div class="row">
{% for message in get_flashed_messages() %} {% for category, message in get_flashed_messages(with_categories=true) %}
<div class="alert alert-primary col-12" role="alert">{{ message }}</div> <div class="col-12">
<div class="alert alert-{{ 'danger' if category == 'error' else 'warning' if category == 'warning' else 'primary' }}" role="alert">{{ message }}</div>
</div>
{% endfor %} {% endfor %}
</div> </div>
</div>
<main role="main" class="container">
{% block body %} {% block body %}
{% endblock body %} {% endblock body %}
<!-- spacer for floating footer -->
<div class="mb-5"></div>
</main> </main>
{% endblock main %}
<!-- spacer for floating footer -->
<div class="mb-5"></div>
<footer class="footer"> <footer class="footer">
{% block footer %} {% block footer %}
<div class="container"> <div class="container-fluid">
<ul class="list-inline"> <ul class="list-inline">
{% for link in config["FOOTER_LINKS"]%} {% for link in config["FOOTER_LINKS"]%}
<li class="list-inline-item"><a href="{{ link.url }}">{{ link.title }}</a></li> <li class="list-inline-item"><a href="{{ link.url }}">{{ link.title }}</a></li>
{% endfor %} {% endfor %}
<li class="list-inline-item float-right"> <li class="list-inline-item float-right">
<a href="https://git.cccv.de/infra/uffd/uffd/">Sourcecode</a> <a href="https://git.cccv.de/uffd/uffd/">{{_("About uffd")}}</a>
<a target="_blank" href="https://git.cccv.de/infra/uffd/uffd/-/commit/{{ gitversion.longhash }}"><span title="{{ gitversion.branch }} {{ gitversion.hash }}: {{ gitversion.msg }}" data-toggle="tooltip">Version: {{ gitversion.hash }}</span></a>
</li> </li>
</ul> </ul>
</div> </div>
......
{% extends 'base.html' %}
{% block main %}
<main role="main" class="container mt-3">
<div class="row justify-content-center">
<div class="col-lg-6 col-md-10 px-0">
<div class="row">
{% for category, message in get_flashed_messages(with_categories=true) %}
<div class="col-12">
<div class="alert alert-{{ 'danger' if category == 'error' else 'warning' if category == 'warning' else 'primary' }}" role="alert">{{ message }}</div>
</div>
{% endfor %}
</div>
</div>
</div>
<div class="row justify-content-center">
<div class="col-lg-6 col-md-10 narrow-card">
<div class="text-center">
<img alt="branding logo" src="{{ config.get("BRANDING_LOGO_URL") }}" class="col-lg-8 col-md-12" >
</div>
{% block body %}
{% endblock body %}
</div>
</div>
</main>
{% endblock main %}
{% extends 'base.html' %}
{% block body %}
<div class="row">
<div class="col">
<p class="text-right">
<a class="btn btn-primary" href="{{ url_for("group.show") }}">
<i class="fa fa-plus" aria-hidden="true"></i> {{_("New")}}
</a>
</p>
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">{{_("GID")}}</th>
<th scope="col">{{_("Name")}}</th>
<th scope="col">{{_("Description")}}</th>
</tr>
</thead>
<tbody>
{% for group in groups|sort(attribute="unix_gid") %}
<tr id="group-{{ group.id }}">
<th scope="row">
{{ group.unix_gid }}
</th>
<td>
<a href="{{ url_for("group.show", id=group.id) }}">
{{ group.name }}
</a>
</td>
<td>
{{ group.description or '' }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
{% extends 'base.html' %}
{% block body %}
<form action="{{ url_for("group.update", id=group.id) }}" method="POST" autocomplete="off">
<div class="align-self-center">
<div class="clearfix pb-2 col">
<div class="float-sm-right">
<button type="submit" class="btn btn-primary"><i class="fa fa-save" aria-hidden="true"></i> {{_("Save")}}</button>
<a href="{{ url_for("group.index") }}" class="btn btn-secondary">{{_("Cancel")}}</a>
{% if group.id %}
<a href="{{ url_for("group.delete", id=group.id) }}" onClick='return confirm({{_("Are you sure?")|tojson}});' class="btn btn-danger"><i class="fa fa-trash" aria-hidden="true"></i> {{_("Delete")}}</a>
{% else %}
<a href="#" class="btn btn-danger disabled"><i class="fa fa-trash" aria-hidden="true"></i> {{_("Delete")}}</a>
{% endif %}
</div>
</div>
<div class="form-group col">
<label for="group-gid">{{_("Group ID")}}</label>
{% if not group.id %}
<input type="number" class="form-control" id="group-gid" name="unix_gid" value="" placeholder="Automatically chosen if empty">
{% else %}
<input type="number" class="form-control" id="group-gid" name="unix_gid" value="{{ group.unix_gid }}" readonly>
{% endif %}
</div>
<div class="form-group col">
<label for="group-loginname">{{_("Name")}}</label>
<input type="text" class="form-control" id="group-loginname" name="name" minlength=1 maxlength=32 pattern="[a-z0-9_-]*" value="{{ group.name or '' }}" {{ 'readonly' if group.id }}>
<small class="form-text text-muted">
{{_('At least one and at most 32 lower-case characters, digits, dashes ("-") or underscores ("_"). <b>Cannot be changed later!</b>')|safe}}
</small>
</div>
<div class="form-group col">
<label for="group-description">{{_("Description")}}</label>
<textarea class="form-control" id="group-description" name="description" rows="5">{{ group.description or '' }}</textarea>
<small class="form-text text-muted">
</small>
</div>
{% if group.id %}
<div class="col">
<span>{{_("Members")}}:</span>
<ul class="row">
{% for member in group.members|sort(attribute='loginname') %}
<li class="col-12 col-xs-6 col-sm-4 col-md-3 col-lg-2"><a href="{{ url_for("user.show", id=member.id) }}">{{ member.loginname }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
</form>
{% endblock %}
{% extends 'base.html' %}
{% block body %}
<div class="btn-toolbar mb-2">
<a class="btn btn-primary ml-auto" href="{{ url_for("invite.new") }}"><i class="fa fa-plus" aria-hidden="true"></i> {{_('New')}}</a>
</div>
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th scope="col">{{_('Link')}}</th>
<th scope="col">{{_('Created by')}}</th>
<th scope="col">{{_('Permissions')}}</th>
<th scope="col">{{_('Usages')}}</th>
<th scope="col">{{_('Status')}}</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for invite in invites|sort(attribute='created', reverse=True)|sort(attribute='active', reverse=True) %}
<tr>
<td>
{% if invite.creator == request.user and invite.active %}
<a href="{{ url_for('invite.use', invite_id=invite.id, token=invite.token) }}"><code>{{ invite.short_token }}</code></a>
<button type="button" class="btn btn-link btn-sm p-0 copy-clipboard" data-copy="{{ url_for('invite.use', invite_id=invite.id, token=invite.token, _external=True) }}" title="{{_('Copy link to clipboard')}}"><i class="fas fa-clipboard"></i></button>
<button type="button" class="btn btn-link btn-sm p-0" data-toggle="modal" data-target="#modal-{{ invite.id }}-qrcode" title="{{_('Show link as QR code')}}"><i class="fas fa-qrcode"></i></button>
{% else %}
<code>{{ invite.short_token }}</code>
{% endif %}
</td>
<td>
{% if not invite.creator %}
{{ '<deleted user>' }}
{% else %}
{{ invite.creator.loginname }}
{% endif %}
</td>
<td>
{{ _('Signup') if invite.allow_signup }}{{ ', ' if invite.allow_signup and invite.roles }}
{% for role in invite.roles %}{{ ', ' if loop.index != 1 }}<i class="fas fa-key"></i>&thinsp;{{ role.name }}{% endfor %}
</td>
<td>
<span style="white-space: nowrap;">{{ invite.signups|selectattr('completed')|list|length }} <i class="fas fa-users" title="{{ _('user signups') }}"></i></span>,
<span style="white-space: nowrap;">{{ invite.grants|length }} <i class="fas fa-key" title="role grants"></i></span>
</td>
<td>
{% if invite.disabled %}
{{_('Disabled')}}
{% elif invite.voided %}
{{_('Voided')}}
{% elif invite.expired %}
{{_('Expired')}}
{% elif not invite.permitted %}
{{_('Invalid, unpermitted creator')}}
{% elif not invite.active %}
{{_('Invalid')}}
{% elif invite.single_use %}
{{ _('Valid once, expires %(expiry_date)s', expiry_date=invite.valid_until|dateformat) }}
{% else %}
{{ _('Valid, expires %(expiry_date)s', expiry_date=invite.valid_until|dateformat) }}
{% endif %}
</td>
<td class="text-right">
<button type="button" class="btn btn-link btn-sm p-0" data-toggle="modal" data-target="#modal-{{ invite.id }}"><i class="fas fa-ellipsis-h"></i></button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% for invite in invites %}
<div class="modal" tabindex="-1" id="modal-{{ invite.id }}">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{_('Invite Link Details')}}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<ul class="list-unstyled">
<li><b>{{_('Type:')}}</b> {% if invite.single_use %}{{_('Single-use')}}{% else %}{{_('Multi-use')}}{% endif %}</li>
<li><b>{{_('Created:')}}</b> {{ invite.created|datetimeformat }}</li>
<li><b>{{_('Expires:')}}</b> {{ invite.valid_until|datetimeformat }}</li>
<li><b>{{_('Permissions:')}}</b>
<ul>
{% if invite.allow_signup %}
<li>{{_('Link allows account registration')}}</li>
{% else %}
<li>{{_('No account registration allowed')}}</li>
{% endif %}
{% for role in invite.roles %}
<li>{{_('Link grants users the role "%(name)s"', name=role.name)}}</li>
{% endfor %}
</ul>
</li>
<li><b>Usages:</b>
{% if not invite.signups and not invite.grants %}
{{_('Never used')}}
{% else %}
<ul>
{% for signup in invite.signups if signup.completed %}
<li>{{_('Registration of user <a href="%(user_url)s">%(user_name)s</a>', user_url=url_for('user.show', id=signup.user.id)|e, user_name=signup.user.loginname|e)|safe}}</li>
{% endfor %}
{% for grant in invite.grants if grant.user %}
<li>{{_('Roles granted to <a href="%(user_url)s">%(user_name)s</a>', user_url=url_for('user.show', id=grant.user.id)|e, user_name=grant.user.loginname|e)|safe}}</li>
{% endfor %}
</ul>
{% endif %}
</li>
</ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
{% if invite.active %}
<form action="{{ url_for('invite.disable', invite_id=invite.id) }}" method="POST">
<button type="submit" class="btn btn-primary">{{_('Disable Link')}}</button>
</form>
{% elif invite.creator == request.user and not invite.expired and invite.permitted %}
<form action="{{ url_for('invite.reset', invite_id=invite.id) }}" method="POST">
<button type="submit" class="btn btn-primary">{{_('Reenable Link')}}</button>
</form>
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
{% for invite in invites if invite.creator == request.user %}
<div class="modal" tabindex="-1" id="modal-{{ invite.id }}-qrcode">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{_('Invite')}}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
{{ url_for('invite.use', invite_id=invite.id, token=invite.token, _external=True)|qrcode_svg(width='100%', height='100%') }}
</div>
</div>
</div>
</div>
{% endfor %}
<script>
$(".copy-clipboard").on("click", function() {
navigator.clipboard.writeText($(this).data("copy"));
});
</script>
{% endblock %}
{% extends 'base.html' %}
{% block body %}
<form action="{{ url_for("invite.new_submit") }}" method="POST" autocomplete="off" class="form">
<div class="form-group">
<label for="single-use">{{_('Link Type')}}</label>
<select class="form-control" id="single-use" name="single-use">
<option value="1" {{ 'selected' if request.values.get('single-use', '1') == '1' }}>{{_('Valid for a single successful use')}}</option>
<option value="0" {{ 'selected' if request.values.get('single-use', '1') == '0' }}>{{_('Multi-use')}}</option>
</select>
</div>
<div class="form-group">
<label for="valid-until">{{_('Valid Until')}}</label>
<input class="form-control" type="datetime-local" id="valid-until" name="valid-until" value="{{ request.values.get('valid-until') or (datetime.now() + timedelta(hours=36)).replace(hour=23, minute=59).isoformat(timespec='minutes') }}" min="{{ datetime.now().isoformat(timespec='minutes') }}" max="{{ (datetime.now() + timedelta(days=config['INVITE_MAX_VALID_DAYS'])).isoformat(timespec='minutes') }}">
<small class="text-muted">{{_('Must be within the next %(max_valid_days)d days', max_valid_days=config['INVITE_MAX_VALID_DAYS'])}}</small>
</div>
{% if allow_signup %}
<div class="form-group">
<label for="allow-signup">{{_('Account Registration')}}</label>
<select class="form-control" id="allow-signup" name="allow-signup">
<option value="1" {{ 'selected' if request.values.get('allow-signup', '1') == '1' }}>{{_('Link allows account registration')}}</option>
<option value="0" {{ 'selected' if request.values.get('allow-signup', '1') == '0' }}>{{_('No account registration allowed')}}</option>
</select>
</div>
{% else %}
<input type="hidden" name="allow-signup" value="0">
{% endif %}
{% if roles %}
<div class="form-group">
<label for="valid-until">{{_('Granted Roles')}}</label>
<table class="table table-sm">
<thead>
<tr>
<th scope="col"></th>
<th scope="col">{{_('Name')}}</th>
<th scope="col">{{_('Description')}}</th>
</tr>
</thead>
<tbody>
{% for role in roles|sort(attribute="name") if not role.is_default %}
<tr>
<td>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="role-{{ role.id }}" name="role-{{ role.id }}" value="1" {{ 'checked' if 'role-%d'%role.id in request.values }}>
</div>
</td>
<td>{{ role.name }}</td>
<td>{{ role.description }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
<button type="submit" class="btn btn-primary"><i class="fa fa-save" aria-hidden="true"></i> {{_('Create Link')}}</button>
<a href="{{ url_for("invite.index") }}" class="btn btn-secondary">{{_('Cancel')}}</a>
</form>
{% endblock %}
{% extends 'base_narrow.html' %}
{% block body %}
<div class="col-12 mb-3">
<h2 class="text-center">{{_('Invite Link')}}</h2>
</div>
{% if not request.user %}
<p>{{_('Welcome to the %(org_name)s Single-Sign-On!', org_name=config.ORGANISATION_NAME)}}</p>
{% endif %}
{% if invite.roles and invite.allow_signup %}
<p>{{_('With this link you can register a new user account with the following roles or add the roles to an existing account:')}}</p>
{% elif invite.roles %}
<p>{{_('With this link you can add the following roles to an existing account:')}}</p>
{% elif invite.allow_signup %}
<p>{{_('With this link you can register a new user account.')}}</p>
{% endif %}
{% if invite.roles %}
<ul>
{% for role in invite.roles %}
<li>{{ role.name }}{% if role.description %}: {{ role.description }}{% endif %}</li>
{% endfor %}
</ul>
{% endif %}
{% if request.user %}
{% if invite.roles %}
<form method="POST" action="{{ url_for("invite.grant", invite_id=invite.id, token=invite.token) }}" class="mb-2">
<button type="submit" class="btn btn-primary btn-block">{{_('Add the roles to your account now')}}</button>
</form>
<a href="{{ url_for("session.logout", ref=url_for("session.login", ref=request.full_path)) }}" class="btn btn-secondary btn-block">{{_('Logout and switch to a different account')}}</a>
{% endif %}
{% if invite.allow_signup %}
<a href="{{ url_for("session.logout", ref=url_for("invite.signup_start", invite_id=invite.id, token=invite.token)) }}" class="btn btn-secondary btn-block">{{_('Logout to register a new account')}}</a>
{% endif %}
{% else %}
{% if invite.allow_signup %}
<a href="{{ url_for("invite.signup_start", invite_id=invite.id, token=invite.token) }}" class="btn btn-primary btn-block">{{_('Register a new account')}}</a>
{% endif %}
{% if invite.roles %}
<a href="{{ url_for("session.login", ref=request.full_path) }}" class="btn btn-primary btn-block">{{_('Login and add the roles to your account')}}</a>
{% endif %}
{% endif %}
{% endblock %}
...@@ -3,50 +3,41 @@ ...@@ -3,50 +3,41 @@
{% block body %} {% block body %}
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<p class="text-right">
<a class="btn btn-primary" href="{{ url_for("mail.show") }}">
<i class="fa fa-plus" aria-hidden="true"></i> {{_('New')}}
</a>
</p>
<table class="table table-striped table-sm"> <table class="table table-striped table-sm">
<thead> <thead>
<tr> <tr>
<th scope="col">name</th> <th scope="col">{{_('Name')}}</th>
<th scope="col">receiving address</th> <th scope="col">{{_('Receiving addresses')}}</th>
<th scope="col">destinations</th> <th scope="col">{{_('Destinations')}}</th>
<th scope="col">
<p class="text-right">
<a class="btn btn-primary" href="{{ url_for("mail.show") }}">
<i class="fa fa-plus" aria-hidden="true"></i> New
</a>
</p>
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for mail in mails|sort(attribute="uid") %} {% for mail in mails|sort(attribute="uid") %}
<tr id="mail-{{ mail.uid }}"> <tr id="mail-{{ mail.id }}">
<th scope="row"> <th scope="row">
<a href="{{ url_for("mail.show", uid=mail.uid) }}"> <a href="{{ url_for("mail.show", mail_id=mail.id) }}">
{{ mail.uid }} {{ mail.uid }}
</a> </a>
</th> </th>
<td> <td>
<ul> <ul class="m-0">
{% for i in mail.receivers %} {% for i in mail.receivers %}
<li>{{ i }}</li> <li>{{ i }}</li>
{% endfor %} {% endfor %}
</ul> </ul>
</td> </td>
<td> <td>
<ul> <ul class="m-0">
{% for i in mail.destinations %} {% for i in mail.destinations %}
<li>{{ i }}</li> <li>{{ i }}</li>
{% endfor %} {% endfor %}
</ul> </ul>
</td> </td>
<td>
<p class="text-right">
<a href="{{ url_for("mail.show", uid=mail.uid) }}" class="btn btn-primary">
<i class="fa fa-edit" aria-hidden="true"></i> Edit
</a>
</p>
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
......
{% extends 'base.html' %} {% extends 'base.html' %}
{% block body %} {% block body %}
<form action="{{ url_for("mail.update", uid=mail.uid) }}" method="POST"> <form action="{{ url_for("mail.update", mail_id=mail.id) }}" method="POST" autocomplete="off">
<div class="align-self-center"> <div class="align-self-center">
<div class="form-group col"> <div class="form-group col">
<label for="mail-name">Name</label> <label for="mail-name">{{_('Name')}}</label>
<input type="text" class="form-control" id="mail-name" name="mail-uid" {% if mail.uid %} value="{{ mail.uid }}" readonly {% else %} value=""{% endif %}> <input type="text" class="form-control" id="mail-name" name="mail-uid" {% if mail.id %} value="{{ mail.uid }}" readonly {% else %} value=""{% endif %}>
<small class="form-text text-muted"> <small class="form-text text-muted">
</small> </small>
</div> </div>
<div class="form-group col"> <div class="form-group col">
<label for="mail-receivers">Receiving addresses</label> <label for="mail-receivers">{{_('Receiving addresses')}}</label>
<textarea rows="10" class="form-control" id="mail-receivers" name="mail-receivers">{{ mail.receivers|join('\n') }}</textarea> <textarea rows="10" class="form-control" id="mail-receivers" name="mail-receivers">{{ mail.receivers|join('\n') }}</textarea>
<small class="form-text text-muted"> <small class="form-text text-muted">
One address per line {{_('One address pattern (local+ext@domain, local@domain, local, @domain) per line. Only lower-case ASCII letters, digits and symbols.')}}
</small> </small>
</div> </div>
<div class="form-group col"> <div class="form-group col">
<label for="mail-destinations">Destinations</label> <label for="mail-destinations">{{_('Destinations')}}</label>
<textarea rows="10" class="form-control" id="mail-destinations" name="mail-destinations">{{ mail.destinations|join('\n') }}</textarea> <textarea rows="10" class="form-control" id="mail-destinations" name="mail-destinations">{{ mail.destinations|join('\n') }}</textarea>
<small class="form-text text-muted"> <small class="form-text text-muted">
One address per line {{_('One address per line')}}
</small> </small>
</div> </div>
<div class="form-group col"> <div class="form-group col">
<button type="submit" class="btn btn-primary"><i class="fa fa-save" aria-hidden="true"></i> Save</button> <button type="submit" class="btn btn-primary"><i class="fa fa-save" aria-hidden="true"></i> {{_('Save')}}</button>
<a href="{{ url_for("mail.index") }}" class="btn btn-secondary">Cancel</a> <a href="{{ url_for("mail.index") }}" class="btn btn-secondary">{{_('Cancel')}}</a>
{% if mail.uid %} {% if mail.id %}
<a href="{{ url_for("mail.delete", uid=mail.uid) }}" class="btn btn-danger"><i class="fa fa-trash" aria-hidden="true"></i> Delete</a> <a href="{{ url_for("mail.delete", mail_id=mail.id) }}" class="btn btn-danger"><i class="fa fa-trash" aria-hidden="true"></i> {{_('Delete')}}</a>
{% else %} {% else %}
<a href="#" class="btn btn-danger disabled"><i class="fa fa-trash" aria-hidden="true"></i> Delete</a> <a href="#" class="btn btn-danger disabled"><i class="fa fa-trash" aria-hidden="true"></i> {{_('Delete')}}</a>
{% endif %} {% endif %}
</div> </div>
</div> </div>
......
...@@ -3,14 +3,6 @@ ...@@ -3,14 +3,6 @@
{% block body %} {% block body %}
<h1>OAuth2.0 Authorization Error</h1> <h1>OAuth2.0 Authorization Error</h1>
<p><b>Error: {{ error }}</b> {{ '(' + error_description + ')' if error_description else '' }}</p> <p><b>Error: {{ error }}</b> {{ '(' + error_description + ')' if error_description else '' }}</p>
{% if args %}
<p>Parameters:</p>
<ul>
{% for key, value in args.items() %}
<li>{{ key }}={{ value }}</li>
{% endfor %}
</ul>
{% endif %}
<hr> <hr>
......
{% extends 'base.html' %} {% extends 'base_narrow.html' %}
{% block body %} {% block body %}
<div class="row mt-2 justify-content-center"> <div class="col-12">
<div class="col-lg-6 col-md-10" style="background: #f7f7f7; box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); padding: 30px;"> <h2 class="text-center">{{_('Logout')}}</h2>
<div class="text-center"> </div>
<img alt="branding logo" src="{{ config.get("BRANDING_LOGO_URL") }}" class="col-lg-8 col-md-12" >
</div>
<div class="col-12">
<h2 class="text-center">Logout</h2>
</div>
<div class="col-12"> <div class="col-12">
<noscript> <noscript>
<div class="alert alert-warning" role="alert">Javascript is required for automatic logout</div> <div class="alert alert-warning" role="alert">{{_('Javascript is required for automatic logout')}}</div>
</noscript> </noscript>
<p>While you successfully logged out of the Single-Sign-On service, you may still be logged in on these services:</p> <p>{{_('While you successfully logged out of the Single-Sign-On service, you may still be logged in on these services:')}}</p>
<ul> <ul>
{% for client in clients if client.logout_urls %} {% for client in clients if client.logout_uris %}
<li class="client" data-urls='{{ client.logout_urls|tojson }}'> <li class="client" data-urls='{{ client.logout_uris_json }}'>
{{ client.client_id }} {{ client.service.name }}
<span class="status-active spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span> <span class="status-active spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
<i class="status-success fas fa-check d-none"></i> <i class="status-success fas fa-check d-none"></i>
<i class="status-failed fas fa-exclamation d-none"></i> <i class="status-failed fas fa-exclamation d-none"></i>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
<p> <p>
Please wait until you have been automatically logged out of all services or make sure of this yourself. {{_('Please wait until you have been automatically logged out of all services or make sure of this yourself.')}}
</p> </p>
<button id="retry-button" class="btn btn-block btn-primary d-none" disabled> <button id="retry-button" class="btn btn-block btn-primary d-none" disabled>
<span id="cont-text">Logging you out on all services ...</span> <span id="cont-text">{{_('Logging you out on all services ...')}}</span>
</button> </button>
<a href="{{ request.values.get('ref') or '/' }}" class="btn btn-block btn-secondary"> <a href="{{ request.values.get('ref') or '/' }}" class="btn btn-block btn-secondary">
<span>Skip this and continue</span> <span>{{_('Skip this and continue')}}</span>
</a> </a>
</div>
</div>
</div> </div>
<script> <script>
...@@ -60,7 +53,6 @@ function logout_services() { ...@@ -60,7 +53,6 @@ function logout_services() {
}); });
}); });
p = p.then(function () { p = p.then(function () {
console.log('done', elem);
elem.find('.status-active').addClass('d-none'); elem.find('.status-active').addClass('d-none');
elem.find('.status-success').removeClass('d-none'); elem.find('.status-success').removeClass('d-none');
elem.removeClass('client'); elem.removeClass('client');
...@@ -68,22 +60,20 @@ function logout_services() { ...@@ -68,22 +60,20 @@ function logout_services() {
.catch(function (err) { .catch(function (err) {
elem.find('.status-active').addClass('d-none'); elem.find('.status-active').addClass('d-none');
elem.find('.status-failed').removeClass('d-none'); elem.find('.status-failed').removeClass('d-none');
console.log(err);
throw err; throw err;
}); });
all_promises.push(p); all_promises.push(p);
}); });
Promise.allSettled(all_promises).then(function (results) { Promise.allSettled(all_promises).then(function (results) {
console.log(results);
for (result of results) { for (result of results) {
if (result.status == 'rejected') if (result.status == 'rejected')
throw result.reason; throw result.reason;
} }
$('#cont-text').text('Done, redirecting ...'); $('#cont-text').text({{_('Done, redirecting ...')|tojson}});
window.location = {{ (request.values.get('ref') or '/')|tojson }}; window.location = {{ (request.values.get('ref') or '/')|tojson }};
}).catch(function (err) { }).catch(function (err) {
$("#retry-button").prop('disabled', false); $("#retry-button").prop('disabled', false);
$('#cont-text').text('Log out failed on some services. Retry?'); $('#cont-text').text({{_('Log out failed on some services. Retry?')|tojson}});
}); });
} }
......
...@@ -3,40 +3,29 @@ ...@@ -3,40 +3,29 @@
{% block body %} {% block body %}
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<table class="table table-striped"> <p class="text-right">
<a class="btn btn-primary" href="{{ url_for("role.new") }}">
<i class="fa fa-plus" aria-hidden="true"></i> {{_("New")}}
</a>
</p>
<table class="table table-striped table-sm">
<thead> <thead>
<tr> <tr>
<th scope="col">id</th> <th scope="col">{{_("Name")}}</th>
<th scope="col">name</th> <th scope="col">{{_("Description")}}</th>
<th scope="col">description</th>
<th scope="col">
<p class="text-right">
<a class="btn btn-primary" href="{{ url_for("role.show") }}">
<i class="fa fa-plus" aria-hidden="true"></i> New
</a>
</p>
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for role in roles|sort(attribute="name") %} {% for role in roles|sort(attribute="name") %}
<tr id="role-{{ role.id }}"> <tr id="role-{{ role.id }}">
<td>
{{ role.id }}
</td>
<th scope="row"> <th scope="row">
{{ role.name }} <a href="{{ url_for("role.show", roleid=role.id) }}">
{{ role.name or _('<empty name>') }}
</a>
</th> </th>
<td> <td>
{{ role.description }} {{ role.description }}
</td> </td>
<td>
<p class="text-right">
<a href="{{ url_for("role.show", roleid=role.id) }}" class="btn btn-primary">
<i class="fa fa-edit" aria-hidden="true"></i> Edit
</a>
</p>
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
......
{% extends 'base.html' %}
{% block body %}
{% if role.locked %}
<div class="alert alert-warning" role="alert">
{{_("Name, moderator group, included roles and groups of this role are managed externally.")}} <a href="{{ url_for("role.unlock", roleid=role.id) }}" class="alert-link">Unlock this role</a> to edit them at the risk of having your changes overwritten.
</div>
{% endif %}
<form action="{{ url_for("role.update", roleid=role.id) }}" method="POST" autocomplete="off">
<div class="align-self-center">
<div class="clearfix pb-2"><div class="float-sm-right">
<button type="submit" class="btn btn-primary"><i class="fa fa-save" aria-hidden="true"></i> {{_("Save")}}</button>
<a href="{{ url_for("role.index") }}" class="btn btn-secondary">{{_("Cancel")}}</a>
{% if role.id %}
{% if not role.is_default %}
<a href="{{ url_for("role.set_default", roleid=role.id) }}" onClick='return confirm({{_("All non-service users will be removed as members from this role and get its permissions implicitly. Are you sure?")|tojson}});' class="btn btn-secondary">{{_("Set as default")}}</a>
{% else %}
<a href="{{ url_for("role.unset_default", roleid=role.id) }}" onClick='return confirm({{_("Are you sure?")|tojson}});' class="btn btn-secondary">{{_("Unset as default")}}</a>
{% endif %}
<a href="{{ url_for("role.delete", roleid=role.id) }}" onClick='return confirm({{_("Are you sure?")|tojson}});' class="btn btn-danger {{ 'disabled' if role.locked }}"><i class="fa fa-trash" aria-hidden="true"></i> {{_("Delete")}}</a>
{% else %}
<a href="#" class="btn btn-secondary disabled">{{_("Set as default")}}</a>
<a href="#" class="btn btn-danger disabled"><i class="fa fa-trash" aria-hidden="true"></i> {{_("Delete")}}</a>
{% endif %}
</div></div>
<ul class="nav nav-tabs pt-2 border-0" id="tablist" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="settings-tab" data-toggle="tab" href="#settings" role="tab" aria-controls="settings" aria-selected="true">{{_("Settings")}}</a>
</li>
<li class="nav-item">
<a class="nav-link" id="roles-tab" data-toggle="tab" href="#roles" role="tab" aria-controls="roles" aria-selected="false">{{_("Included roles")}} <span class="badge badge-pill badge-secondary">{{ role.included_roles|length }}</span></a>
</li>
<li class="nav-item">
<a class="nav-link" id="groups-tab" data-toggle="tab" href="#groups" role="tab" aria-controls="groups" aria-selected="false">{{_("Included groups")}} <span class="badge badge-pill badge-secondary">{{ role.groups|length }}</span></a>
</li>
</ul>
<div class="tab-content border mb-2 pt-2" id="tabcontent">
<div class="tab-pane fade show active" id="settings" role="tabpanel" aria-labelledby="settings-tab">
<div class="form-group col">
<label for="role-name">{{_("Role Name")}}</label>
<input type="text" class="form-control" id="role-name" name="name" value="{{ role.name or '' }}" {{ 'disabled' if role.locked }}>
<small class="form-text text-muted">
</small>
</div>
<div class="form-group col">
<label for="role-description">{{_("Description")}}</label>
<textarea class="form-control" id="role-description" name="description" rows="5">{{ role.description or '' }}</textarea>
<small class="form-text text-muted">
</small>
</div>
<div class="form-group col">
<label for="moderator-group">{{_("Moderator Group")}}</label>
<select class="form-control" id="moderator-group" name="moderator-group" {{ 'disabled' if role.locked }}>
<option value="" class="text-muted">{{_("No Moderator Group")}}</option>
{% for group in groups %}
<option value="{{ group.id }}" {{ 'selected' if group == role.moderator_group }}>{{ group.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group col">
<span>{{_("Moderators")}}:</span>
<ul class="row">
{% for moderator in role.moderator_group.members %}
<li class="col-12 col-xs-6 col-sm-4 col-md-3 col-lg-2"><a href="{{ url_for("user.show", id=moderator.id) }}">{{ moderator.loginname }}</a></li>
{% endfor %}
</ul>
</div>
<div class="form-group col">
<span>{{_("Members")}}:</span>
<ul class="row">
{% for member in role.members|sort(attribute='loginname') %}
<li class="col-12 col-xs-6 col-sm-4 col-md-3 col-lg-2"><a href="{{ url_for("user.show", id=member.id) }}">{{ member.loginname }}</a></li>
{% endfor %}
</ul>
</div>
</div>
<div class="tab-pane fade" id="roles" role="tabpanel" aria-labelledby="roles-tab">
<div class="form-group col">
<span>{{_("Roles to include groups from recursively")}}</span>
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col"></th>
<th scope="col">{{_("name")}}</th>
<th scope="col">{{_("description")}}</th>
<th scope="col">{{_("currently includes groups")}}</th>
</tr>
</thead>
<tbody>
{% for r in roles|sort(attribute="name")|sort(attribute='name') %}
<tr id="include-role-{{ r.id }}">
<td>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="include-role-{{ r.id }}-checkbox" name="include-role-{{ r.id }}" value="1" aria-label="enabled"
{% if r == role or role.locked %} disabled{% endif %}
{% if r in role.included_roles %} checked{% endif %}>
</div>
</td>
<td>
<a href="{{ url_for("role.show", roleid=r.id) }}">
{{ r.name }}
</a>
</td>
<td>
{{ r.description }}
</td>
<td>
{% for group in r.groups_effective|sort(attribute='name') %}
<a href="{{ url_for("group.show", id=group.id) }}">{{ group.name }}</a>{{ ', ' if not loop.last }}
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="tab-pane fade" id="groups" role="tabpanel" aria-labelledby="groups-tab">
<div class="form-group col">
<span>{{_("Included groups")}}</span>
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col"></th>
<th scope="col">{{_("name")}}</th>
<th scope="col">{{_("description")}}</th>
<th scope="col">{{_("2FA required")}}</th>
</tr>
</thead>
<tbody>
{% for group in groups|sort(attribute="name") %}
<tr id="group-{{ group.id }}">
<td>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="group-{{ group.id }}-checkbox" name="group-{{ group.id }}" value="1" aria-label="enabled" {% if group in role.groups %}checked{% endif %} {{ 'disabled' if role.locked }}>
</div>
</td>
<td>
<a href="{{ url_for("group.show", id=group.id) }}">
{{ group.name }}
</a>
</td>
<td>
{{ group.description }}
</td>
<td>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="group-mfa-{{ group.id }}-checkbox" name="group-mfa-{{ group.id }}" value="1" aria-label="enabled" {% if group in role.groups and role.groups[group].requires_mfa %}checked{% endif %} {{ 'disabled' if role.locked }}>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</form>
{% endblock %}
...@@ -3,27 +3,23 @@ ...@@ -3,27 +3,23 @@
{% block body %} {% block body %}
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<table class="table table-striped"> <table class="table table-striped table-sm">
<thead> <thead>
<tr> <tr>
<th scope="col">gid</th> <th scope="col">{{_('Name')}}</th>
<th scope="col">name</th> <th scope="col">{{_('Description')}}</th>
<th scope="col">description</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for group in groups|sort(attribute="gid") %} {% for role in roles|sort(attribute='name') %}
<tr id="group-{{ group.gid }}"> <tr id="role-{{ role.id }}">
<th scope="row"> <th scope="row">
{{ group.gid }} <a href="{{ url_for('rolemod.show', role_id=role.id) }}">
</th> {{ role.name or '<empty name>' }}
<td>
<a href="{{ url_for("group.show", gid=group.gid) }}">
{{ group.name }}
</a> </a>
</td> </th>
<td> <td>
{{ group.description }} {{ role.description }}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
......