From 9800858fb358b3f03bb546a9ad54dc7e9bcf5f57 Mon Sep 17 00:00:00 2001 From: 132ikl <132@ikl.sh> Date: Sat, 20 Apr 2019 19:28:01 -0400 Subject: [PATCH 1/4] Add latest shortlink functionality, disable browser cache for redirects, fix baseurl retrieval --- config.yml | 6 ++++++ liteshort.py | 56 +++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 46 insertions(+), 16 deletions(-) diff --git a/config.yml b/config.yml index 4abcb47..bc83ccd 100644 --- a/config.yml +++ b/config.yml @@ -54,6 +54,12 @@ site_domain: # Default: unset subdomain: +# String: URL which takes you to the most recent short URL's destination +# Short URLs cannot be created with this string if set +# Default: l +latest: 'l' + # Boolean: Show link to project repository on GitHub at bottom right corner of page # Default: true show_github_link: true + diff --git a/liteshort.py b/liteshort.py index 865da3a..c57d590 100644 --- a/liteshort.py +++ b/liteshort.py @@ -3,7 +3,7 @@ # This software is license under the MIT license. It should be included in your copy of this software. # A copy of the MIT license can be obtained at https://mit-license.org/ -from flask import Flask, current_app, flash, g, jsonify, redirect, render_template, request, send_from_directory, url_for +from flask import Flask, current_app, flash, g, jsonify, make_response, redirect, render_template, request, send_from_directory, url_for import bcrypt import os import random @@ -22,13 +22,13 @@ def load_config(): req_options = {'admin_username': 'admin', 'database_name': "urls", 'random_length': 4, 'allowed_chars': 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_', 'random_gen_timeout': 5, 'site_name': 'liteshort', 'site_domain': None, 'show_github_link': True, - 'secret_key': None, 'disable_api': False, 'subdomain': '' + 'secret_key': None, 'disable_api': False, 'subdomain': '', 'latest': 'l' } config_types = {'admin_username': str, 'database_name': str, 'random_length': int, 'allowed_chars': str, 'random_gen_timeout': int, 'site_name': str, 'site_domain': (str, type(None)), 'show_github_link': bool, 'secret_key': str, - 'disable_api': bool, 'subdomain': (str, type(None)) + 'disable_api': bool, 'subdomain': (str, type(None)), 'latest': (str, type(None)) } for option in req_options.keys(): @@ -62,7 +62,7 @@ def authenticate(username, password): def check_long_exist(long): query = query_db('SELECT short FROM urls WHERE long = ?', (long,)) for i in query: - if i and (len(i['short']) <= current_app.config["random_length"]): # Checks if query if pre-existing URL is same as random length URL + if i and (len(i['short']) <= current_app.config["random_length"]) and i['short'] != current_app.config['latest']: # Checks if query if pre-existing URL is same as random length URL return i['short'] return False @@ -103,7 +103,7 @@ def generate_short(rq): return response(rq, None, 'Timeout while generating random short URL') short = ''.join(random.choice(current_app.config['allowed_chars']) for i in range(current_app.config['random_length'])) - if not check_short_exist(short): + if not check_short_exist(short) and short != app.config['latest']: return short @@ -114,6 +114,14 @@ def get_long(short): return None +def get_baseUrl(): + if current_app.config['site_domain']: + # TODO: un-hack-ify adding the protocol here + return 'https://' + current_app.config['site_domain'] + '/' + else: + return request.base_url + + def list_shortlinks(): result = query_db('SELECT * FROM urls', (), False, None) result = nested_list_to_dict(result) @@ -123,7 +131,7 @@ def list_shortlinks(): def nested_list_to_dict(l): d = {} for nl in l: - d[nl[0]] = nl[1] + d[nl[0]] = nl[1] return d @@ -143,14 +151,25 @@ def response(rq, result, error_msg="Error: Unknown error"): else: if result and result is not True: flash(result, 'success') - return render_template("main.html") elif not result: flash(error_msg, 'error') - return render_template("main.html") return render_template("main.html") +def set_latest(long): + if app.config['latest']: + if query_db('SELECT short FROM urls WHERE short = ?', (current_app.config['latest'],)): + get_db().cursor().execute("UPDATE urls SET long = ? WHERE short = ?", + (long, current_app.config['latest'])) + else: + get_db().cursor().execute("INSERT INTO urls (long,short) VALUES (?, ?)", + (long, current_app.config['latest'])) + + def validate_short(short): + if short == app.config['latest']: + return response(request, None, + 'Short URL cannot be the same as a special URL ({})'.format(short)) for char in short: if char not in current_app.config['allowed_chars']: return response(request, None, @@ -209,10 +228,12 @@ def main(): def main_redir(url): long = get_long(url) if long: - return redirect(long, 301) - flash('Short URL "' + url + '" doesn\'t exist', 'error') - redirect_site = url_for('main') - return redirect(redirect_site) + resp = make_response(redirect(long, 301)) + else: + flash('Short URL "' + url + '" doesn\'t exist', 'error') + resp = make_response(redirect(url_for('main'))) + resp.headers.set('Cache-Control', 'no-store, must-revalidate') + return resp @app.route('/', methods=['POST'], subdomain=app.config['subdomain']) @@ -228,7 +249,7 @@ def main_post(): else: return result if get_long(short) == request.form['long']: - return response(request, (('https://' + app.config['site_domain'] + '/') or request.base_url) + short, + return response(request, get_baseUrl() + short, 'Error: Failed to return pre-existing non-random shortlink') else: short = generate_short(request) @@ -237,12 +258,15 @@ def main_post(): 'Short URL already taken') long_exists = check_long_exist(request.form['long']) if long_exists and not request.form.get('short'): - # TODO: un-hack-ify adding the protocol here - return response(request, (('https://' + app.config['site_domain'] + '/') or request.base_url) + long_exists, + set_latest(request.form['long']) + get_db().commit() + return response(request, get_baseUrl() + long_exists, 'Error: Failed to return pre-existing random shortlink') get_db().cursor().execute('INSERT INTO urls (long,short) VALUES (?,?)', (request.form['long'], short)) + set_latest(request.form['long']) get_db().commit() - return response(request, (('https://' + app.config['site_domain'] + '/') or request.base_url) + short, + + return response(request, get_baseUrl() + short, 'Error: Failed to generate') elif request.form.get('api'): if current_app.config['disable_api']: From 332d82e97ace82b295344467de6f8199cbf5458b Mon Sep 17 00:00:00 2001 From: Kevin Alberts Date: Sat, 8 Jun 2019 19:45:41 +0200 Subject: [PATCH 2/4] Add Inter-Actief styling --- static/favicon.ico | Bin 15086 -> 270398 bytes static/ia-logo.svg | 3 + static/ia.css | 196 ++++++++++++++++++++++++++++++++++++ static/ictsv-logo-white.svg | 192 +++++++++++++++++++++++++++++++++++ static/ictsv-logo.svg | 189 ++++++++++++++++++++++++++++++++++ templates/main.html | 92 ++++++++++------- 6 files changed, 633 insertions(+), 39 deletions(-) create mode 100644 static/ia-logo.svg create mode 100644 static/ia.css create mode 100644 static/ictsv-logo-white.svg create mode 100644 static/ictsv-logo.svg diff --git a/static/favicon.ico b/static/favicon.ico index 4d8381ca157bcbc4c86fcef5a5b3be7c75d47285..d1f8f7f6403cb743e99210abae32b7c720f939c5 100644 GIT binary patch literal 270398 zcmeHQ1$-1o7mwfscMa}P+zJGDDDJdKf#MXGLPBwGi6vL<+W`B>BG zpXQ(P6!7-;_KguEM(j9o;>1soAVEUwBJIYF8;|<-?fVnXVwr3-Zzgd{ z0Xi2pY}l~pnKNg?mEbd5b=}VU`{WE>~^hH2wa}N^n8_r&tX3Ute(-!TNOuKgN zT13Z=9Y>(ORHEk1PfjU7a-l+n3f0)PYu7m?`An*=kt0X`fi=hEzet|L7{uw;t=m*7 znkfl8a^%Rh%9Sg(#W{@RvgRMh6iAXJN&3LRzy-qotR$b=tLw#!7jMCT-8m-vC#XY4 zU*-1g+mGzgOX+kP>m^H;?9P@gTd_|%V*Z0G3Si%fQMhp7s+TTZy00{kIjS!tB;+#Q z1=DdwaNEfGjvqgM<(oHeTz+S9L~C2{h=_<+Uw{4e;MlQaC$ZfvGu7x6z}P3o+x`e! z{GoOl?Ljqa)EH=V8au)d_wd{oE?l^&<@rk(+YK8wgyzbX%RB=(8YF|lq2H4MdOdpt ze$eul_P%f@#*A5meIT^^pCE$8*4d0ZU=qncCy2 zX(Aw-0+|1?N|Y$kTFeJ}d_Q{h=w-8J&Bn;?xUn+EK6UEU8NKKKpMU-t=I`&H-B_eG zv5}$x?f-cDucybH7q|KD-Mja}!NCX7S1x@896EF;RPXsucL4B09q(;Fo z1Rl`qm-J9z_xcC=#+|RKTD58oeemGHbG>LUv`cT0nKET6h%qqV0mYNVz~5N!(?j<> zSl|J@e(4^tfB*htum?zMU~=0;4L(s?-o1PGv0k(nw_Cb&=`N;^CY!@#$VAxSCWkHf z3^5Pr^$ouxS78Uo-8&%cFH6DpPjmY#TjSiXU%x-d&!{1p-9+j$3cweaU+vnpd)wj# zt<&J|Dgy6-KYZ3ngC{`mCdI{z7jFo%P_5}MZjasp$>*rSNp7MSNdbEA`SQyzYwM|h zD)8USmoMMWnl)<~`b0jfx%z{@#RGckVGCnGeSrVL5?m3Vb7WxyQ24LE{<cI&|nT9c%Dgy=X7AOJ|UPfPntI(Vu60vEJv<%LZ8BJK^{6;lr2n z<;&NQxAB0l?+$na(ai=>pg;YN9XmFdH~RC8FZSCs-Me?6CFTLWzTxZsG31d!yp0Fx zK0ST<^o3q@7q@%;`t?V+gZc3`2E5`6`BQ>MjT$i_10*}R*|TRa#T!%tp2h?C`oRwO zu6Q2m^-XsGybF^(u=&Ra1$^L(^DBxOr5DdpyTo_&w`R?nki>}-7vO{RAF-S}ckW(2 z=`U`(eEIT&KK9J~GCBp|2jI)Sd-q-y^MD@TvB@f-+a?g zZ@#1RKlNeZ!i8HQ6VJ=p$i#pzq-T1M0kscZtT$!LmhHtFOLl0$)5#yg|bz z@ig=R>hLxoc>6fW!u%(H1mqW(H~Kq)FOH|)zGN#E`Clyj(tk0LPCisT&W;svdCPCv zvgNGa{6}X1*aAFm+qP{VC&t4NS?kxYKS^);Q~NXqSFT(M123C00B{z~Y4VMNGieIQ z$}+-EtyrEsc^bffUT3_O^s7>($`5!$8v$VO$^YSVi<`BdrBrORBb3vCcQ*Se!_shBZiChOU==QtMl zk9Z%n-T>QJu4DiS6DCae`0-;V?qKAT1bZ5fwgA4^=hML-Vudzs+DzKLd-r7}Y_rau zJ$nzaUgi`pUc5Ta1^$@Ltod5>K`g)yEcgSX`H#EEDV(La@&|zZCf=e&i;fchdjF9< z=7b3oR)SA?@dxCM{W&$wPxwQh3w_LoO6`S0J@kA3{{82}hYw!{Un_OsbDb%FH;4yN zW52!5>3xtieCAK!4)ZW5PiV@UNQ=4&|YZl=KhQ%T%&nee%iGG)pFh)1FvqA}_+7d{{eb)@y*^wQ)v9m1 zcI|rMj>3d3D&o`sg?GuyQN82f1GY~op#O9@I=|@gZ}Lk%apJ@!AiKxCXFFmQ&^dkk z_U%WUIdkT=9<&v=NoxW6aqHV}za5GFj%0(n{go@t!4KTekL+<>GUNjfCOMJO&*RDTtE;LkJ0L=Zg@VPYl{Q2{Li}BU#TR02A4`ev> zUVhY5E|@A+s=}~EWx^JS&PI@t{0JEoH$EvL3yKBVa4WrdF?zd@0bYVGOEr7zm8Yd+ zeN7Bq*WQ;dUAh+?R~G(_HR(>>x^;VC%}uNaZAk_IJ)mEOHA!!pj2t=g59p!f(}NZ| zwdtKRXU@Vn`#jZ~_SF96%a`xLNAy6QXrXAcq=#3eNRfJw@n2_wwzm2}^X|xzBbV?F zSr_u)Sc(!rUS&R=J9i#*@7_HoWKi%2a-vY7LWbFY-codgqy4Liz)gFxj@p~W}W$r8(!Ez{i}74crA3}7Vapv!$o3wlRygY-f0?$&`7o(v5Qy^Ouc zqu*hm+mhw@@#A5v&{R%8uawL$)H5f{5B&MsQV zW%usizyE01fM}`@Wsh#_)~!2RQoCjdt*6L2m(?+6&^oRC4|L^-Rd1 ziKfs?9LAXpAYQ$C^}5pBQ-i0RtrPyC&tM)^ad$pyT^?uWIMAz`>ny%3 zI~dsAB!eF68dmr(C@5$dbR)BB%|Cj!SGH`~Du@lvgdPC&Qo^AJ&{I!(xY9Px{t2PO zc!j0;>}+3FuU>tUdaI08NkMk8&AVllB?LX@H15i{s8`TmS5D!Pci`L zh0O*(G3md?9-ouknB-_`&_t^`VTbXC8s%#Z<;!rAEr3pAp8 zNV8_m20#X&^;?LWRCK*K?qGhcTD9uK%DYu?aPaX=nKD&!9V6{a!awH_7WmiQJ|Hf8 zA9}~sl7b`yfDgl9R`?J1ktML*%BUp`^ll69*7foJ{?J|iQKJ0m)2Hv$s#S}8GI`7& z;#$mB0(ZUZfDY_W*!yB@P55EMhD~S1765NhXW^$Qzt%L+v%MTSa+I1sfBt^G`H$Ks z-Pypvz{xtlvS0rX_$4z_mf+QX0mly@2toeIEq@c=8l>rOw$ zjT^TOve$T8lM(!v1U?Ekv%-InhcAX)+)(~#9E_=Vg$fnw!Z*x)clbxCvUm@=1-rEN z4*JXt82)VcDTTY9^}q%uSdaNn1KF=%zn@s~2?!bVIf|{s41;OV5yS;54qYrJ_A$}} z7&vg?C{K|AVBT!ibKWY|_Mkz7R_k&8C-#OE&GJx;D|9+WLxa(ODViU|KU1kloo6nkn|E-dT44FjB;egQKH|tRVZTcApM2=RF8xalPwo?I{R`xB#;`_!pZS2@c=YURg$WG3EWn3n;N-E5UARr+|^8Q{f>7jNmU z8|T~vEbt@ZB^N_ns(Y;50T36cBMWrVst?#}b{#9#EJ5%rM*|T@SFHK>s z@OP&jyaQy$8Gsob7CCf=@884N`;bN)23aa2f&0a z0OEq&DpRIRO;4aBY{uf^4A7Moe|C=Me^^-9BkcX{p!1`-&5zW0LJwdG3;Pev|B#T7 ztLUqyevzeUqXi5;5Np5>g6=+gY!ZzK5FG7I#Jz_ ziMMt%XN&}=I~eqUX3)EkTC{bh-WfAy>_B`PkBQF?8@dG5t5@$xcFfM;sa4*6`}UoO z-`y6tuf%7OZ^3_Y5l8X5R`hVx4s<)_ut+C0`_!^!%RcZ4$b>HDsZ*!^hAvilHStxt zZunG6iFh(YS&^d%JdSu<*AVBb2hLMTmBv}g`XB>XA@o_#UpfPjOc%H@cTQ50-qGvA z8MY4eAI|7Wd!cQT0U$02*`|1e3_zeI;sRuV4%rx1e5*p1dZTgU#(i2e>-&gEE(sgBI5Da>zn33{L%aaSxL*!I%(hp=(EI~K7IN^+I#h) zy|~@UlP3pbE@ksTl6%uR0B;zBai_f@#z~8BdPjpS@&sZOw1J(H$E=?u1HhWegim^s zgLUZ8VY&zMo?BgR+qUgc*keX$!KaSeAQ=FBG8J_TAsq{2?j?hdtLBJzdqk@>_2I*Z zFDFf!v>Nj3GOWt_q=sb7m@&bQ_(BUfz2)xMvEw-U>MPY>qrouW8Xh`y=#myRcGL#F z!y;aMW9$pe+XC3*1L)br#XClB#0wdF?b@{m!uygV+?|CJze3J;y=v8}z3{$Iy3-!X zw>Yy5b{2Ov^3ynBAN(8lPu^qzxwmfJ%1nHAoS{d-KW!3w^UFihy@7)td`pz+-MjZf z$d+Czxek!cB;q*@CbeOY_n9r`%dH3$!$Iy%8N(MlC?c&9Y*RW#K zGGW4mVBDoWDmJK1#X-aw(Qxn=lso^H}7G7Kc)ZRAOF#rGiPotUApwoK7INuf$shg*r!&+ zd^cpQ8uCRB9cA71Q0aUYDFZkRrr}Bt0Ph}+PM$n@L+QEfYJEkD6se1I30Gqf$#Whn z_i%NgrE?5?zz%YiSClFz+|SOQJ$n^?9=P%cKza`c4jlMPpn*~}b+w)b4H}HZK55AP z&eeHh;Br2Q#an@u`{&iGSML@mP=G620NQ)tA7%>+_L#J0Ko4L$bVHIGnCDH@L==D= zGaX{WGvQkmcHJ+!cI`Sv)IBafAp;l-pG?nO-LsV}M>2pkY0@y^tBQ-k*BMF30F#a! zIdX%NJmhL!bPj>t7&8F#KGu9aHdhroo zX{gdXExB=#8d~X0 zI$9U#{iR^Rf?VkVV6LD{>h4*V(C8oND=;Yvxpn0+};s&IQ|v+m7a@7Vz-! z@TZUgj8h6XPpJpC?2||bLJJx@Y6EiE2%Nz@!XJ>Qj4#XmgpO1?*aGOT2S9vDd*Gr) zi`L;>!ldsMwZ;)PBu%mQJ#sWRwSptwMrE~dHS1-I0?-3W1l#4|TJe^nc3=y11a{Q< z*`kuVy&)fnuz|S4iVOh$wSv^e*sPru3Sh5|84wWAkd^x813-L|$M6Rb!~j_|?E>r{ronIYTSw<3E#UB7|CqNs zn5KixSYyxw%1S<9wBRkNHt76Ix^Rd~vjqIdl^!bjKq6mAQuNUZM&pn;apIDUv8mY| zl_-GwXL87($7#i1QtkW?e~6K}2YRSn=>g!)Ei?SGKbNA7mN3Kx>ZcOBnbk5z0g?gK zs#U8cEB8;t1v(EIKm*3eq;_|F;Sc1rmUCRHEqW7D8)374b|?Vb&vNA7M2aW0fYI3# zvhYXHiyz1ib+qXT&S0~&ps}1bFb4OqFEACmPMcBSvlaLQ%JcKjKW~%68*1jE`A_E% z#L1q^n+yu}m@U<$t=zh43~(2D2^qkbe9i{`u*Ur}Rp-u~C(_u<mDKs=-?YOSlz z)JuFYapJ`FywRU$eBndA+Tp{8!<^w0wepe-0KS}iV?4Zh8k3G4JI+&!rq0w$XRr$w zE?kG7s6;%`pJ!a*KfNI0(&#P&AU-AjgA95a{JJOPX-x9x&)?RWd8%gKJ9q9pfNn^6 zp6JgruFwNW)4zZJscQ0+Gj-3LIdcQ-lk)O3Ch)Mv%NagVD=)o;4H`6PJowMk<5I~U44lDYlqgZ6gIY9owqD2p zZ>CL~wyJCd%#0pQ0muN-{rKaLzdFk&YUPJr(kjGREa1^Zb*}T?h+kI-`ZurDqNy|W z!X7iCcJ10fIER9n+fylk{U>&}Zruhyd-jY88Nk}LYmdW6VQo*Prz`!&dY2k;gocLRREwvash4B`ty{P5$`k*=CvalKuU+H} zoz%)pcQ8{1;DM9ajV@=(l&K8jAnsR-r<|!bQU)*)dH{*tK*^(uQY`yQl`3_2hE8hc zMO=_ukO5TkC?cAjb(sR#e=DhL@s%z*<3j$}a(fZm|s8;|q=s(2O^-RmuMF|$Jd!R5I1&aAJF@?sr& z^UE*4EOQSdv$Q8sphk@v?M|FHp}Ri-;!VV6f6=U2vv!~#H!>*r;7`@BUq2?!V6+DB z*|X;`-eJwS0G>37-RLW10Ob*n@35o2K?^wXU-$0aNAe^Cz+CggyIXH9=qslU$b&DN zG5|Lwl!q0CKY)yL=FC|whc~pyGiubR6|e=!>0xAar@KC|$E*aO!0)u6ucJ1uT)A?; zTD5B5yMvHf)}tuk4LzVqtjM4?ZrpepGO+-UqM)0d&73)NzV++ZGvO1EWI^x`wGb$A za|d&C_&mNiWB}hGHm&Zl0qOi75fSmKUcGwFK~!#R0pR~A1LUww$N=bk0C{8x>=V*@ zJY$;($f5w^z*WSUhdMeNY6WlKzWp%Vi@DJQ0Imt)lc_(ge_GL4s-3-i_nrYyaPPbi530}3JO{V89-KplG_$qJ{2ogtV3rXsd=dt?ELxj;gCu8wAE2F z&5#uE89R3D48*2pA}$T|08Z!5ox6k~DQyQS#04sjcqk{dn&VRKkWJF)(W7Ugt1;|! z*Gx7T1+d=tfF1x7wgB))^AdWo!Mb$)2C10!I`kLgFc&J<4_>R zh!G>EJ$dp(cNqZ9f4qmE%914uS8+jd=FC|Pc%Rg2j!U&evY=tZhD`!{$21O$o0v#Z z0D1u3&Ye5=w-jG!1tS?i@#4iRqrcqf0pQIeOJHE&Qmtq#)efz}@DH~NexghnfYhWh z5DY#6E8^{!2|YC8O?rcZEkJw&kysKH$U71tHs}zkd8q|VxC7!n;t=FQ_80u4if zSg~TI#@pL&Vf?k=HK{iG^y%}vpPyeU!w}dpB0h-E-jo#?!13e9Z^9O!t>bQ+ISfJp z-|^$e&tt_WCDx)d>C>m@Ne{>iwh!etY}mkr41jC_1`HT50=#X?01Pt09iajLFTKeI zOln`&3P!$3^XAPO{(Bhtz1qxIl8wjoDxKul@U%&phSFc`!&@pawv3cS(Teu=FK;N4;Z$?P*g;p@~ z3l2ThLg*(q{wSaakbC09iMsn_65fHJ7dF?F0oa-;#!O3{I(0?ZfHGkVMC(k+k|jI) z`1o)mgND2#5&VHnptVn{byljKdGqG&fgS)?_L#<;+48Ug|0T!0@3gS~OYw#lF!%&r zNbz#z_1+k{5DTgEojZ4)YC&H|Z9pD;vqFUm4UIur6AL*MfDB+Y#f5jo8(P43?AUQB zUc7jTki#2l=b?KD_QMWFp{*P| za0g2~a^%QqEXV+e{=feE>n`X46qeI>Gmqy}fMfutPMx~KkPKjv=Tp@w47|Vn_S?RQ z$@fI<^;J&&@C|jlY}v9kox;G(?9mj!`7@Z#-g5Xu?L518@4gITa`gsXtXQ#zh{tt7 zZQ9DIpW>6iCsQZL0AhPIQ=6TaO##>f41)~tjU4_^GY_3XNDl=*iZaRWya6&o=1~ax zw>#COtuuAw9cD800C=*;GyoU5jsj!=m0=6`(it96GjI0n*;}}djpvnwE_RZsQ>QLq zK?XqMirBzg5gRbC=TXw^u`3jS3?S+C>(}q8Ij7~;O=kea1)1dv9v)oAyKC33fq(z~ zHxqgQ@CR@UcM`5*vw1M@DAIMv0CvfpqiW`(`M+z|uCqXjD;YqE5+#}+Jb3W3nzWT$ z_doyq^A7$=zBcs$6q#BcQAzdc)rZUF54H0>eE9GwWB}Pbf^Z6T61Go;*REZ=Uv1jT ztsnl$#z6*8sE1VsyPE{?3I1B`xhmFKFW%? zieliAN2u#u^#8ilc%*ts58+F_S$&x#lQLe`}a)v1Aq)*8|0E1 zm>`bX3Dqe8AN28lnV*uYU_p@co zX6gYk#v^KV2j}g0GiJ=#D|eo%nUC(DTefUD$lDuq(V|7aIdkUBEj4NDOx@^DU5r5t zwRp~~mmLb=4Juas`t`>+!yjtqJ$v@-)%fw_bL9^pQKCeJ{`~XLGiuV+nYtS`Y&a4= znf%z{QnM#&Q2^__FYerRS&#t`k6yic^)7tmwo(gU7VE_r#NW7a<8D^;01y{sd%}bX zO+6qMnMH*@rAd<}OITRgV`ugVwemt1@dA4BQ&d2c=_(Nqaau$~gzj<}8rKUKF5D-c zV47fN=Mp#Tam< zi=o(r^012V-kYFh%a)U9+*$a;UHBBPtm;0^y9 z1#$j-#KQgq-=t4EcI-IX83c@wcktlB^AH!A344r7mo7a>kRU-yBhb~v!7&9=q)3qw zwgAk?0Dk}d_cgeG#&z6tYA8HQ2_qmlfehi&n)c! z)CcIq|B3yFD?K#GVGF}2FcUtRXbh@UsWMcGXH1xJD1iHC9K?eDn#Nu4KiYea9653Y zGJwX$p|E32aNdcB`0Qs`k;A|iV29&gn>h?Z0q=qZ3zoii?HUs@0N4UPMqH{OgV0ts z8i>WS2>y`Y>AeP1`xK)P9p~x}CVQX^m=XS8bFv}>px6ZH%Tn4C4M=LcC_(qW%ZnE; zn2-U|IRtl-l6E_50t`g~#NkZcrAwFTEbKkRd#hHh+QXX+0D2Dv;g|gd3u8ciC|$aA zKSMFOiHuzeKn4&O{y;jiz6 z|6H}l|4>7M;xPy5^ zA2BY@s7VkzDK*|Ev*53E?%cV*%AY@fbI6KY!j7#$)~s2}!>3ISyl1DOyi^8E;O&u& zphKrYy8^HUV8R~&odKpzn>LSh<8&Z}ZcT!hzhhG~@_=YZA zv0}xZ{rdI$18>8Z9zA;Wk3AZp&u>CPLasuVx(sEyQCY|_(qbJm^_d+EBy8`~VgH$F zkG5K;FIu!{8+_#DchGG^fuW0$j}<*&iV<0%LWO}^6Bzq;eC$QJFn`Wv#W9)0u8H@!9FKRZ;>HW{`6tDWI-wer%uA-hEE$4S(t z0Aivhz}=_(kRe0nBQBz%deDRi(TIMbWBC~NC6i(EQW$3eGv;};frSjE1kT+D)#52< z>ZNlq*6d%ve+i>?)6oAcQ>IKGXXvD6UdR9+U{5ZuCc)uvC@t;zuz@^DXAU*-p}KLe zdAn@cvi)VtmTd(4_oQm#=4o}~Y?KN2rTJ=)f!zAxpL88$Km|OFj_&jq@!3nVatFJA z|NgV8RjaD?4&#lrB@1j%M!@d-matFDL##<(lP*V_!-O5w{PE{ z@#d`geUsK2`VY3w;qW^@4(kwCv1g?AC-{G94!USQCboDM^4kdP4}DeOKPeLE3d5N+ zZctFrI$QM6I*sNCbds*&ey*rbP{bO|g1P?-WIPdC;}pF;*u6YJU#6x?l`5yM1J6j~ zIdbGE2S0p=qti)`e@Dsyrb7n6Oi3-QA2*|RrcTzJw0@If4& za;)e9z%TRj>eZ_cQeyriwm?GE)dg=%;abmgNA2Mpa35#Qk+8!^s|2o|S_kO?K-X#n zD>8s>+qRu4RH#sGPo<|T{l@t?1LRXo#08@G?9jzr>k1y?GI(!`0h^A>hYlUO=x8qL z1;@JdH}1ckao^$U%}G2?^j)uBy>_hV0fdK#KZ71XH*`yB8B}z%nfW*Nprpfx57%7= zKxZ)e4?bB=;|}JQPeAD3rN=t5nU!-*^fgAn6+WmhLgriz%)o6NH28q8yk^ZBCSp?) zzYH2QXd+KC0PH{Uun!H6&ZBz$8+*Ym$m=V)xrc9Z#2DT{N#09`v*%!Lcl~~y;p*Qyk_jZ^40pNWt zNdJ-lNkgKZBJ{tR5DA>`n@ zptH#39RT;A-q8PM!WIQGfS2%5+T0oblQqd0&IG;3%&>`PX8+u>WlIS5blLiG(5Wx( z>bzs69O6+tNtZ4im-a#Mu~-K(fKXP(qFuXoLm&f4#PF0=v}=gNn+!4lX7m6c13V2G zfNWa;=xugpg?HTP2mFiHqc>Sa2iF5C!}%@eym|AOkU`V<1qKE#g+BmpY&5`gG2xfF zHod>O!@o+EJ$LTh-!*I2>@Jse8#Zh>S*f=5tOxd3gCGOo&W9-YFYcHzW2V6u8xt~s zb?erh#GSafT>djcKD@(x!HNvv@#Dw;j2t;~2IwkFHx{zLm3q!wrP_vF(62am#xVjv zIg0~)r1XLw028rk$rmhqIMxLYvSm=t;>%io$N(~7&aF|3m))s{-eKT7YcuX8>EzG} z^5Y%uw8u(W=%25qw+uNr8!ZofsMmyVwo@#O#sBcvv}x0RSc|!_&jQ~iY1OJ#klyqc z-hqRIgF`cB%vfFypU<2*^MKy{R-%2pJ#WRD$3iR=Ib>Hf4`QigUAc1Qb|vVjL>&VL z3|NSm4&3-`BOMy};A_H)?aW_){dJ4{b;@Bf*!%5MV$Aif19xC<=Rf!_HtcWak}o*D z$AQ{kzkdBG#DB^shyRR{2euET5fkyU-t?z40Ifst5jzRAbr?$n{+qYz-Df4*hfnr3 zyq^DQ4nYrK0QA3@@YfU(5%H=-i4tTB#ElF9{y=iVCleFzFti5z`s=SNaUM=jydm{x z6!~A*u3Z;GADc`0AB`_$&~*+RIB-FUwN!~ZU>n~P?=kVEc+W^M>^;fhmwB`jv~#Bp zK?bmT_3A^A0XU2e2wBZ9?zG2BS?FMl1HW+_F9bGFS+{Q8%8V=raS9heHzAdg=C~9t zBm-#BpuyLqgUiC4IDY*2&1%)EIkd;bIqiEE`s;2VN|r3yh3uH6#>mJp@Lx>C242pJ zEdXQyr=gR{jUEv37JO1wVnGH#bBSa@coQ3eZb|CI;fzqr-MPm?d9sBCuH4)=iQl6C zHE-U0SXfxtBNoPk_z%7$OQDb4*kwYOK0oBJn^>Sf-2-3?w8)VR0Pi$u;7f-Ixv{Hs zga66r*lRLHpAWqK8+Oq3PMkP#%~d)oS&sNGARwSK&Sc!kWFP}b2_N;7l%%6Obs~*HRf8^LwMGyQ)ejj&A5>@ zM;}Aczr7bOT)3VU836RqEeZyu=!2%U?K*kdH|6XpgFzvRmeu|e->&1d%7 z!+zL--o|*KJ>iHuSSsk=O=sl}hIzdUG5|??OpK3j;lhPmv0}q)kC&pSKYH}&RnekF z8$-s#rQMIMIR$xd7U+LpV1@q>yLldGdRe~TefM1-vcrweuX_9&{>g7Z4(l*JJA4_Z zTD*9%$D9F(zbRG`#VoQl4>(Tqf&J$WR(w)IC+Pz0len=3B)%(Kwrr#Q`}Z^96OjBu zz&5@Y_IOF(7}!H&;SA7-mALYb)=KhoQmj}p^7$i)7mw>e%a$$2;%xNN5e>C~lMD)L zbZ(x;A#K{U#TP7CuuBX2I%8T zG%8O*2G9^Pzz9dYp%wg#FTQAJR9f4@i(~*3CQO*i${lRhtXVtY6R?o2zD7&)LB9O3 z9ooQ3Z0YDcO8W`qt7{;N%EhN0raFM}2?{V7_FBjR*9F?!!>B zgN)9s>%2rsue0c~fvE>As(VB$2?XJ9e^HxAVrQS5Kx|1pbfXOhe4w>LoN6X?F-Zo1ci~Q$GqUXCpc51qXSSM{ zYr9!F2ar5##flZX(XZ;Jyx;L`fj_EA7@DQ=+!(n?1~7Z}>_1qzgV8zw9r>-0OXhWkx^x%t_xCRZ-#Rm|U%#%4Jc;bth7KJ% zhxEwgo?o27(WrUBf9W1Re8_|zAn_mKg3K~1t?l6D3%UCkR%8IM3%iZ?GKc;^Y~bPp z{or)4Bj~t&`}R{>-3O>0*s`354}&g|`hBK;uZ_WxNb-RXm}4~eSoniZ{5kZMt1+xt zv0^*;;Rs`a{&WXJT#(kZr%UjRT@ZA*;zQ=1A2H_x5ijtzTCzbpFNKGPKke47+ssUv zG8HoIdh8B~Lje2%{KNw9Q6F#?d5SYhR)?K7P=+*V(v(3=)FZ5n0nT9K@HXU5oJ?Xj*|Bqob?~KZQFJ_=_^BDfpo^W+Aox5efV~1$O`X4xBMUYXpyu< zRT_W2>%-oaV(HSQO!x#PK7c=vVE8G`rT|^M$!-|&=aXg4nza%99?rjT;et!PXYbv+ z_kw))7c5w?BlM3_gJ$$bVZQ4tFi04qluEA8?yMzblMFr7=?b9bQB@2dKKvI}Yyq%e z{|%o^MN}dnoei?$%-$6Do}u4<`|TXq9qDHEPtTU!g-{&b@{H2u zV3!;YnM6|$B&Ew;COrV?&K`XK{{4SS)6e+Fy2H?FnA$0Gi3FEKlFWo_*2Xq)` zVy=9*!3Scg^XJds(3AG!w&9n(y(t6aVDi&O3*WA#N|kCM9)CT*Aq%?zJ@hhsP~VCr z#vtLYUAvB|WjE*Scz*NEH`7Tr%+nZfjH@^NZxpVz2e=h|;z|dF#$fa2&CJLEV5?h4%p>Nvu_*xm8-*Z)x~#^2;%uFW z3w98?$=Quf@z1e?-<+|m_+>^+fWDvi&`dEZ1rjDqn4y3F{)?QQt7_!mxpU`noIfSw z(i(9bAcv^{f7ZI|V$m6_UcGt~j7Vb>C)*St-|2Ph*6ql`9RM*wPQyojCEGnUbZQLv z1ih}tn(Azw9XobhU}#F4$bCiu@&QeL8=d79rSl&=c<>_LBFG*+ zLxv1l#{s!b6h#zBks?J-_*-1(3cn~>hUS0HoH^@>y2i~X{M*(OZ1I$&sk3zj1_o~A zhUg}u77D;0O~Tf#TMr~1aA)~N$^3}za2By;s_{51cn3(JS3Di@7$PRv5+3Q#JI*Cb zmaK}{l)BzQckI}4EOFw*YWbpNeu!DP13(ruj(I|v9aNkGn0Hy>n|YC5XHaT)dYOg}4J$L`;M?;{905ZyE!{(`@4*9T8?#1@8Uyq?gRNMx;ux(#pZCm6;ei4a2d-DIUgNZmO+&f|(EU3M zT_oOh&%NOnFyEm=hpuV82GbZIu5NEygLvR|6Gz(=C{(CW_2A%OP0yfY|JJHit6}I7 zw`YH0G_VHyffggQJ_BsovgI`VFVOmH`u`CIgTr6&U$Q}i22If_Mm@y=ReROmgUI(S-W)II z&6~F}Ulf+c7IxVQ@ebD$enC`!3xM6xyS{z<{(`#~x9={}qro2(`atm!pQ1f{!d_K# z4?1z;#7+2|BYi*f&iN^udE-6Sf9TMm3)J)vPJUOR?>86vpSjRH_jj*Phk);YK@Oax zR;^k+Fh_5wu?FCs;UwOgE7H3o-;}nEJ?P?_CQX_G(8*Y$hF<248#f;J=+R>W=6()p z%XTl!RGuk7=g~9&_Si*1rp!E=g=g?A0Szi$#)VAM)MNy@!8t8ZTsW7bLZ|U zu@2LDf60<12g{Tx)7;O`F9n)6_kD>m;2lKoL5LNQ7W~qA(V|6%NnYnHACepaw*8ly zHf=f{`u&B-_c-r#bpn5HdJ{l=&M%=KJOk&k+pfrE!o$O#LBDD%e5|&{cxJ#IFP0O1 zFtc(_0jvWta2|+<{iq;(R(9*!wd>+JbLJch4Gq16zh~sX@8QFTuPBx*^mxy~ujqc{ zS%h_?FV3+g5#KLKB+r@eOD^yd&H_Hs;f6N==_3=CM>wjKefkGm_1B7qk_l=jAzpZZ}?WvbnBGGqj z;am6(^YV&H!?&-HF8qdw(!}2aJm^!n2pMY*z@yT{-$Hq>PvM^t0Pv_Zl#lurm5)lJ zynzBB@>iTD{uZ){)5PEQ-~j?4@|Vg8Y2t4on>bDUZ4d4*03v_IW$4=$Tu2L203O9G zk?o7?q;FerA>2XGHl{B`W7Y=))fm{_TW@6bun6* zsPZ9z2TH($01p5h?L?Kad{dzhAJ9E(2kpS$6P)-$ROW*vF_4N|KPBLnVuYZmjP*N! zid)ely2^qFP?5+^2UtGCsDDHtOL=RW6)8t>e-!#)VOt9UErbZLq*=@RTfRdq^#^{^ z=7$fIXoUw^-~o|vYnnW`buvel7u9JkF9oMkw&2#Z4jVwMuN2(&0zjocE&$^4)&=BKxM)#{oa~lm!&1!l0`d`V zdjW`=zjhW7%e1o$nZCCv(*LVrK!d!2Lfh^T}i;^aCW|A%F))!7VvInXF-e z6WHDY0RmV@)y@I{RIIbc4jl8!+UQ42K3xo+Mly{U{>fyw^bY@rj#?%H{hPjp$?6PT zC}Xd@01vWJzc`J)g~=$iF9$A^u~%MzhuEoK3>PMw(7w0~ecOTyX^!Aw4$9kti__>^ zn2bXIV`H%&VEF+%GLM`GtWx@T0lGa~wnNKErWVTPzX#8xI7t-Xwg~=RQ zzf%iH5E`&mUPzMz7bdIFzPJp1OTk0zz=bk$;KF1V+839hZz*_~9k@_N4qTY5Li^$} z^z8~<8Eg9%&T|1SFq366ew43SDz(;WtzQ;A zFa$mR$Y0hnqVj=Z7PvL#^QRM?Zgiwq~8omL3U1YD30g z$tAFq4Y*}W3M~IQ-2am)DHd!BlRXFo2(b3((*+=MvJ(LWuu#VOZveb33s4mLpTiST zm!i6FQ$`W=vH4Hs7F0hldmlv|G#i;_=|GSb9@RPehz=Ju{aG0*ZwoG@If92cC{J*4 z->C!QGWOs?8AtFiFWdDC;Nmp#w@_Z3 zCjPbsw@e)Y5cx}KLi_H*EmJRA{gfuu?=GAsm{6C{j5tmFErtu@OU!Poyciz!EmVLq z0)JC^NqCSbuTUOoQL9ae4LH*Lg}nHV$}6OWiB=nYN97gZ81z6}-$L4Z5z69k0sbM# QKkD;C&}YqmghhV+AB*ALeE+e5vzvlFM@9fNWI=dx& z$#1@MzVn@PzH{!q=bU?YoojI^H+ix{XWi=eI(M9NE}ISW?apn&){a*BnZum>E)Em% zp^EDO*~>DaqTc*{L(jknMmGmK;db~XTnwLtLtrS&Jc4@xf&>cKPlBN)Jbi8rB*h)I zUx)A8htMEysv~wA%RNJ63@0bV{u=fN;W1F!;`a4mkAcaPwv zI^VOYZO#42ur|ljt04v6x<5E~)%SZ{-*aIU_~Nhsmr-ExNh=23+gd-hU-@M# z$eU=*(zENAD}A#$^rOEPQ#aa{<#kUy0@{l|2);z03)PtuI&`9~jlYxDe&Ea7$UFt+ zBdvU`QqLHy58XQ&hh2mBleRL`&tlWQAz1`^4$NORUoIhI^`tddTGMJ3ovHl#Non0< zd5;@O9}bc`AzJSVY=^0^A325cXx%CO)Uje4XDwsU`^jC<0lK$+8As+Nh{o|awlThA zOgg%Lsg3p+zc|w`FlNJo3(?X3lkytN-EcbCSQaGI-++BI#K}Pz^<4S=Y>IPCu>BT$ z0kR;eXAIV-#-etP0)Kz7a%qj>Xz)eP?see%DQ)|T@0*m?TBSM#$b#eoh_+cvUgsh0 zpGjl*FJ-rY){Lv+Ul47#7n|mxR=E~OYokCq3zC73;hakK#4%W3Nm^sCN7SF+)@JgV zuz|Se%J)@T{$GKKAy${HI}dh4v`$i9=O68R^~jMpN82l4%YvkqF{u8ZUobM)fhKwn8xJ3a zTE0OXXybb`-UDa`g0k>H?a`%Nz|$23#kPvhrz*O$CDfFuJeALtb-FOUtlNrZ-PhKe zspLy-6@6btrSC z4c+I`=^Q$6sZ=1{nfbQ#>-ki^4_zv_jwRBI-Q;%am!`U@j`sS-x0K3yxLc}Kqc82K zh)#7@#@EuB4#)>CU(ubeqVujY?n0=Gq3#WJ$%XS_x)A0|p$^uT+ubv-N@(8p*7`V* zg&DBt|FpD%zcEDL!DfQq)6a!z(5!Rsbo$cwLS}t0o*@4r*q`!w8rQdkXy2CCx1TTV zrvOIL?sHz>`i8k0?galGOz8#CXwh}HasG$2|E{cWQjdD&ieVl!Qf{Cz>tBCAii*BB z`!VQ!C=+$shy1UpYyE#Y(vH4qXF)MM4(7Lpbfct`hOFHOyhin{>t4`(sq6k(Fh9MA zq@WRUJdLOiYuo1FV$ZgTwEBDAvuPtq zN6*%0;26jNyWd`aBX1>t0~`fsf!YC-^64$S@-Y2B}JvJ|8F`C4=L5BL$B z0=mYMSzvxIk@n~3S8@nNaj6aSJ-vv?`4AoBU7p_= zq?1JTl496S+4n(v-vp3c1b%#r%hvz@(K>B@)%w@9)iu^w%s;BNzOIF@!Z^r-?{gk` zUydN7I_9HiIZ1jb(Eaib&^>N-lJrh|CP8<^cRhKf1r&q7 zzE~U5x}WZcJS+vxSu_UUUun(32~bV^G2e&daQG%f+fx2nI2Yau5?C_t!KTzzE!U8efvv3PQzNCbgoK@W##_r+-o;YTCsG2&KQ0yeq$5k0-B zOiwA@)w8Q*dCyKaGrgm;qpdez%=C6l%k^e60pC}kE$D+lu~yg=%{_~6R-=e z2yOkT91iMpEGYdjX#dwbth0d5rzQuH(AuCgvdQ5jilB3W_S{vVJ?;_cfJec<8d}>P zfUBVsW`oXlS)vru9mO3a7#~pnJIC xqHmm~>0FdmO!Rtm7ynFi2#3@8yxg9^jsFui=x$yda{MC?enpUfa4!VT{SO7Wvo!z! diff --git a/static/ia-logo.svg b/static/ia-logo.svg new file mode 100644 index 0000000..f3d2274 --- /dev/null +++ b/static/ia-logo.svg @@ -0,0 +1,3 @@ + + +image/svg+xml \ No newline at end of file diff --git a/static/ia.css b/static/ia.css new file mode 100644 index 0000000..cfa200e --- /dev/null +++ b/static/ia.css @@ -0,0 +1,196 @@ +body{ + margin: 0 0 2rem; + background-color: #f4f4f4; + font-family: Open Sans, Arial, sans-serif; +} + +#top{ + background-color: #1D428A; + width: 100%; + display: flex; + justify-content: center; +} + +#header { + height: 6rem; + display: flex; + align-items: center; +} + +#logo img { + height: 3rem; +} + +#logo { + margin-right: 1rem; + align-items: center; +} + +#title { + color: white; + text-decoration: none; + font-family: "Humanst521 BT", "Open Sans", sans-serif; + font-size: 2.75rem; + line-height: 2.75rem; +} + +#middle { + width: 100%; + display: flex; + justify-content: center; +} + +#content { + min-height: 100px; + min-width: 200px; + margin: 1rem; + width: 600px; +} + +#box { + padding: 1rem; +} + +#box .form { + margin-bottom: 1rem; +} + +#box-title { + margin: 0 0 0.5rem 0; + font-family: "Humanst521 BT", "Open Sans", sans-serif; + font-size: 2.5rem; + font-weight: normal; +} + +.drop-shadow { + filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.5)); +} + +#long-url-box{ + display: flex; + align-items: stretch; +} + +#long-url-box .prepend { + background-color: #1D428A; + color: white; + font-family: "Humanst521 BT", "Open Sans", sans-serif; + font-size: 2rem; + font-weight: bold; + display: block; + padding: 0.5rem; + text-align: center; + z-index: 1; + + margin-top: 1rem; + margin-bottom: 1rem; +} + +#long-url { + width: 1%; + min-width: 40px; + flex: 1 1 auto; + font-size: 1.75rem; + color: black; + position: relative; + border: none; + padding: 0.5rem; + + margin-top: 1rem; + margin-bottom: 1rem; + + border-top: 1px solid rgba(0, 0, 0, 0.5); + border-bottom: 1px solid rgba(0, 0, 0, 0.5); +} + +#submit { + background-color: #077821; + color: white; + border: none; + font-size: 2rem; + cursor: pointer; + font-weight: normal; + padding: 0.5rem; + font-family: "Humanst521 BT", "Open Sans", sans-serif; + + margin-top: 1rem; + margin-bottom: 1rem; +} + +#submit:hover { + background-color: #099A2B; +} + +#custom-link-box .optional { + font-size: 1.25rem; + margin-top: 0.5em; + margin-bottom: 0.5rem; +} + +#custom-label { + display: flex; + align-items: stretch; +} + +#custom-label .url { + float: left; + background-color: #1D428A; + color: white; + + + font-family: "Humanst521 BT", "Open Sans", sans-serif; + font-size: 1.5rem; + font-weight: normal; + display: block; + padding: 0.5rem; + text-align: center; + z-index: 1; +} + +#custom-link { + width: 1%; + min-width: 40px; + flex: 1 1 auto; + font-size: 1.5rem; + color: black; + position: relative; + border: none; + padding: 0.5rem; + + border-top: 1px solid rgba(0, 0, 0, 0.5); + border-bottom: 1px solid rgba(0, 0, 0, 0.5); + border-right: 1px solid rgba(0, 0, 0, 0.5); +} + +.success { + color: #077821; + font-size: 1.25rem; +} + +.error { + color: #B8231F; + font-size: 1.25rem; +} + +.success span { + display: block; +} + +.success a { + color: #153065; + text-decoration: none; + font-family: monospace; + font-size: 1.25rem; + display: block; + + margin-top: 0.25rem; + padding: 0.25rem; + border: 1px solid rgba(0, 0, 0, 0.5); + background-color: white; + text-align: center; +} + +.success a:hover { + color: #2450A8; + text-decoration: underline; +} diff --git a/static/ictsv-logo-white.svg b/static/ictsv-logo-white.svg new file mode 100644 index 0000000..a4c1db7 --- /dev/null +++ b/static/ictsv-logo-white.svg @@ -0,0 +1,192 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/static/ictsv-logo.svg b/static/ictsv-logo.svg new file mode 100644 index 0000000..d1447bb --- /dev/null +++ b/static/ictsv-logo.svg @@ -0,0 +1,189 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/templates/main.html b/templates/main.html index 5ae9901..08daf1a 100644 --- a/templates/main.html +++ b/templates/main.html @@ -9,47 +9,61 @@ A copy of the MIT license can be obtained at https://mit-license.org/ {{ config.site_name }} - - - + + -
-

{{ config.site_name }}

-
-

- -

-

- -

-

- -

-
-
- {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - {% for category, message in messages %} - {% if category == 'success' %} -
- ✓ Shortlink successfully generated! Available at {{ message }} -
- {% elif category == 'error' %} -
- ✖ {{ message }} -
- {% endif %} - {% endfor %} - {% endif %} - {% endwith %} - {% if config.show_github_link %} -
- - - +
+ - {% endif %} +
+
+
+
+
+
+

Shorten URL

+
+
+ + + +
+ +
+
+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + {% if category == 'success' %} +
+ ✓ Short link successfully generated! + {{ message }} +
+ {% elif category == 'error' %} +
+ ✖ {{ message }} +
+ {% endif %} + {% endfor %} + {% endif %} + {% endwith %} +
+
+
- \ No newline at end of file + From 8288b3624e3a52f48d6385b0ae8f68b0f54539bf Mon Sep 17 00:00:00 2001 From: Kevin Alberts Date: Sun, 30 Jun 2019 15:57:11 +0200 Subject: [PATCH 3/4] Add admin panel to delete URLs from the browser. Add copyright notice. --- .gitignore | 3 ++ LICENSE | 2 +- config.default.yml | 69 +++++++++++++++++++++++++++++ config.yml | 65 ---------------------------- liteshort.py | 83 +++++++++++++++++++++++++++++++++-- static/admin.css | 74 ++++++++++++++++++++++++++++++++ static/ia.css | 35 +++++++++++++++ static/styles.css | 4 +- templates/admin.html | 100 +++++++++++++++++++++++++++++++++++++++++++ templates/login.html | 67 +++++++++++++++++++++++++++++ templates/main.html | 18 ++++++-- 11 files changed, 445 insertions(+), 75 deletions(-) create mode 100644 config.default.yml delete mode 100644 config.yml create mode 100644 static/admin.css create mode 100644 templates/admin.html create mode 100644 templates/login.html diff --git a/.gitignore b/.gitignore index b783f8b..fde1ef4 100644 --- a/.gitignore +++ b/.gitignore @@ -108,3 +108,6 @@ venv.bak/ # Databases *.db + +# Configuration files +config.yml diff --git a/LICENSE b/LICENSE index 46908db..77ba444 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2019 Steven Spangler +Copyright 2019 Steven Spangler, Kevin Alberts, I.C.T.S.V. Inter-Actief Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/config.default.yml b/config.default.yml new file mode 100644 index 0000000..c2acbff --- /dev/null +++ b/config.default.yml @@ -0,0 +1,69 @@ +# String: Username to make admin API requests +# Default: 'admin' +admin_username: 'admin' + +# String: Plaintext password to make admin API requests +# Safe to remove if admin_hashed_password is set +# Default: unset +#admin_password: + +# String: Hashed password (bcrypt) to make admin API requests - Preferred over plaintext, use securepass.sh to generate +# Please note that authentication takes noticeably longer than using plaintext password +# Don't include the : segment, just the hash +# Default: unset (required to start application) +admin_hashed_password: '' + +# Boolean: Disables API. If set to true, admin_password/admin_hashed_password do not need to be set. +# Default: false +disable_api: false + +# String: Secret key used for cookies (used for storage of messages) +# This should be a 12-16 character randomized string with letters, numbers, and symbols +# Default: unset (required to start application) +secret_key: '' + +# String: Filename of the URL database without extension +# Default: 'urls' +database_name: 'urls' + +# Integer: Length of random short URLs by default +# Default: 4 +random_length: 5 + +# String: Allowed URL characters +# Default: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_ +allowed_chars: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_©®‍‼⁉⃣™ℹ↔↕↖↗↘↙↩↪⌚⌛⌨⏏⏩⏪⏫⏬⏭⏮⏯⏰⏱⏲⏳⏸⏹⏺Ⓜ▪▫▶◀◻◼◽◾☀☁☂☃☄☎☑☔☕☘☝☠☢☣☦☪☮☯☸☹☺♀♂♈♉♊♋♌♍♎♏♐♑♒♓♟♠♣♥♦♨♻♾♿⚒⚓⚔⚕⚖⚗⚙⚛⚜⚠⚡⚪⚫⚰⚱⚽⚾⛄⛅⛈⛎⛏⛑⛓⛔⛩⛪⛰⛱⛲⛳⛴⛵⛷⛸⛹⛺⛽✂✅✈✉✊✋✌✍✏✒✔✖✝✡✨✳✴❄❇❌❎❓❔❕❗❣❤➕➖➗➡➰➿⤴⤵⬅⬆⬇⬛⬜⭐⭕〰〽㊗㊙️🀄🃏🅰🅱🅾🅿🆎🆑🆒🆓🆔🆕🆖🆗🆘🆙🆚🇦🇧🇨🇩🇪🇫🇬🇭🇮🇯🇰🇱🇲🇳🇴🇵🇶🇷🇸🇹🇺🇻🇼🇽🇾🇿🈁🈂🈚🈯🈲🈳🈴🈵🈶🈷🈸🈹🈺🉐🉑🌀🌁🌂🌃🌄🌅🌆🌇🌈🌉🌊🌋🌌🌍🌎🌏🌐🌑🌒🌓🌔🌕🌖🌗🌘🌙🌚🌛🌜🌝🌞🌟🌠🌡🌤🌥🌦🌧🌨🌩🌪🌫🌬🌭🌮🌯🌰🌱🌲🌳🌴🌵🌶🌷🌸🌹🌺🌻🌼🌽🌾🌿🍀🍁🍂🍃🍄🍅🍆🍇🍈🍉🍊🍋🍌🍍🍎🍏🍐🍑🍒🍓🍔🍕🍖🍗🍘🍙🍚🍛🍜🍝🍞🍟🍠🍡🍢🍣🍤🍥🍦🍧🍨🍩🍪🍫🍬🍭🍮🍯🍰🍱🍲🍳🍴🍵🍶🍷🍸🍹🍺🍻🍼🍽🍾🍿🎀🎁🎂🎄🎅🎆🎇🎈🎉🎊🎋🎌🎍🎎🎏🎐🎑🎒🎓🎖🎗🎙🎚🎛🎞🎟🎠🎡🎢🎣🎤🎥🎦🎧🎨🎩🎪🎫🎬🎭🎮🎯🎰🎱🎲🎳🎴🎵🎶🎷🎸🎹🎺🎻🎼🎽🎾🎿🏀🏁🏂🏃🏄🏅🏆🏇🏈🏉🏊🏋🏌🏍🏎🏏🏐🏑🏒🏓🏔🏕🏖🏗🏘🏙🏚🏛🏜🏝🏞🏟🏠🏡🏢🏣🏤🏥🏦🏧🏨🏩🏪🏫🏬🏭🏮🏯🏰🏳🏴🏵🏷🏸🏹🏺🏻🏼🏽🏾🏿🐀🐁🐂🐃🐄🐅🐆🐇🐈🐉🐊🐋🐌🐍🐎🐏🐐🐑🐒🐓🐔🐕🐖🐗🐘🐙🐚🐛🐜🐝🐞🐟🐠🐡🐢🐣🐤🐥🐦🐧🐨🐩🐪🐫🐬🐭🐮🐯🐰🐱🐲🐳🐴🐵🐶🐷🐸🐹🐺🐻🐼🐽🐾🐿👀👁👂👃👄👅👆👇👈👉👊👋👌👍👎👏👐👑👒👓👔👕👖👗👘👙👚👛👜👝👞👟👠👡👢👣👤👥👦👧👨👩👪👫👬👭👮👯👰👱👲👳👴👵👶👷👸👹👺👻👼👽👿💀💁💂💃💄💅💆💇💈💉💊💋💌💍💎💏💐💑💒💓💔💕💖💗💘💙💚💛💜💝💞💟💠💡💢💣💤💥💦💧💨💩💪💫💬💭💮💯💰💱💲💳💴💵💶💷💸💹💺💻💼💽💾💿📀📁📂📃📄📅📆📇📈📉📊📋📌📍📎📏📐📑📒📓📔📕📖📗📘📙📚📛📜📝📞📟📠📡📢📣📤📥📦📧📨📩📪📫📬📭📮📯📰📱📲📳📴📵📶📷📸📹📺📻📼📽📿🔀🔁🔂🔃🔄🔅🔆🔇🔈🔉🔊🔋🔌🔍🔎🔏🔐🔑🔒🔓🔔🔕🔖🔗🔘🔙🔚🔛🔜🔝🔞🔟🔠🔡🔢🔣🔤🔥🔦🔧🔨🔩🔪🔫🔬🔭🔮🔯🔰🔱🔲🔳🔴🔵🔶🔷🔸🔹🔺🔻🔼🔽🕉🕊🕋🕌🕍🕎🕐🕑🕒🕓🕔🕕🕖🕗🕘🕙🕚🕛🕜🕝🕞🕟🕠🕡🕢🕣🕤🕥🕦🕧🕯🕰🕳🕴🕵🕶🕷🕸🕹🕺🖇🖊🖋🖌🖍🖐🖕🖖🖤🖥🖨🖱🖲🖼🗂🗃🗄🗑🗒🗓🗜🗝🗞🗡🗣🗨🗯🗳🗺🗻🗼🗽🗾🗿😀😁😂😃😄😅😆😇😈😉😊😋😌😍😎😏😐😑😒😓😔😕😖😗😘😙😚😛😜😝😞😟😠😡😢😣😤😥😦😧😨😩😪😫😬😭😮😯😰😱😲😳😴😵😶😷😸😹😺😻😼😽😾😿🙀🙁🙂🙃🙄🙅🙆🙇🙈🙉🙊🙋🙌🙍🙎🙏🚀🚁🚂🚃🚄🚅🚆🚇🚈🚉🚊🚋🚌🚍🚎🚏🚐🚑🚒🚓🚔🚕🚖🚗🚘🚙🚚🚛🚜🚝🚞🚟🚠🚡🚢🚣🚤🚥🚦🚧🚨🚩🚪🚫🚬🚭🚮🚯🚰🚱🚲🚳🚴🚵🚶🚷🚸🚹🚺🚻🚼🚽🚾🚿🛀🛁🛂🛃🛄🛅🛋🛌🛍🛎🛏🛐🛑🛒🛠🛡🛢🛣🛤🛥🛩🛫🛬🛰🛳🛴🛵🛶🛷🛸🛹🤐🤑🤒🤓🤔🤕🤖🤗🤘🤙🤚🤛🤜🤝🤞🤟🤠🤡🤢🤣🤤🤥🤦🤧🤨🤩🤪🤫🤬🤭🤮🤯🤰🤱🤲🤳🤴🤵🤶🤷🤸🤹🤺🤼🤽🤾🥀🥁🥂🥃🥄🥅🥇🥈🥉🥊🥋🥌🥍🥎🥏🥐🥑🥒🥓🥔🥕🥖🥗🥘🥙🥚🥛🥜🥝🥞🥟🥠🥡🥢🥣🥤🥥🥦🥧🥨🥩🥪🥫🥬🥭🥮🥯🥰🥳🥴🥵🥶🥺🥼🥽🥾🥿🦀🦁🦂🦃🦄🦅🦆🦇🦈🦉🦊🦋🦌🦍🦎🦏🦐🦑🦒🦓🦔🦕🦖🦗🦘🦙🦚🦛🦜🦝🦞🦟🦠🦡🦢🦰🦱🦲🦳🦴🦵🦶🦷🦸🦹🧀🧁🧂🧐🧑🧒🧓🧔🧕🧖🧗🧘🧙🧚🧛🧜🧝🧞🧟🧠🧡🧢🧣🧤🧥🧦🧧🧨🧩🧪🧫🧬🧭🧮🧯🧰🧱🧲🧳🧴🧵🧶🧷🧸🧹🧺🧻🧼🧽🧾🧿󠁢󠁣󠁥󠁧󠁬󠁮󠁳󠁴󠁷󠁿' + + +# Amount of time in seconds to spend generating random short URLs until timeout +# Default: 5 +random_gen_timeout: 5 + +# String: Name shown on tab while on site and on page header +# Default: 'liteshort' +site_name: 'I.C.T.S.V. Inter-/Actief/ URL Shortener' + +# String: Domain where the shortlinks will be served from. Useful if using the web interface on a subdomain. +# If not set, it is automatically taken from the URL the shorten request is sent to. +# If you don't know, leave unset +# Default: unset +site_domain: + +# String: Subdomain to host the web interface on. +# Useful if you want the shorturls on the short domain but the web interface on a subdomain. +# If you don't know, leave unset +# Default: unset +subdomain: + +# String: URL which takes you to the most recent short URL's destination +# Short URLs cannot be created with this string if set +# Default: l +latest: 'l' + +# Boolean: Show link to project repository on GitHub at bottom right corner of page +# Default: true +show_github_link: false + +# Integer: How many items to show per page in the admin interface +# Default: 20 +admin_links_per_page: 20 diff --git a/config.yml b/config.yml deleted file mode 100644 index bc83ccd..0000000 --- a/config.yml +++ /dev/null @@ -1,65 +0,0 @@ -# String: Username to make admin API requests -# Default: 'admin' -admin_username: 'admin' - -# String: Plaintext password to make admin API requests -# Safe to remove if admin_hashed_password is set -# Default: unset -#admin_password: - -# String: Hashed password (bcrypt) to make admin API requests - Preferred over plaintext, use securepass.sh to generate -# Please note that authentication takes noticeably longer than using plaintext password -# Don't include the : segment, just the hash -# Default: unset (required to start application) -admin_hashed_password: - -# Boolean: Disables API. If set to true, admin_password/admin_hashed_password do not need to be set. -# Default: false -disable_api: false - -# String: Secret key used for cookies (used for storage of messages) -# This should be a 12-16 character randomized string with letters, numbers, and symbols -# Default: unset (required to start application) -secret_key: - -# String: Filename of the URL database without extension -# Default: 'urls' -database_name: 'urls' - -# Integer: Length of random short URLs by default -# Default: 4 -random_length: 4 - -# String: Allowed URL characters -# Default: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_ -allowed_chars: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_' - -# Amount of time in seconds to spend generating random short URLs until timeout -# Default: 5 -random_gen_timeout: 5 - -# String: Name shown on tab while on site and on page header -# Default: 'liteshort' -site_name: 'liteshort' - -# String: Domain where the shortlinks will be served from. Useful if using the web interface on a subdomain. -# If not set, it is automatically taken from the URL the shorten request is sent to. -# If you don't know, leave unset -# Default: unset -site_domain: - -# String: Subdomain to host the web interface on. -# Useful if you want the shorturls on the short domain but the web interface on a subdomain. -# If you don't know, leave unset -# Default: unset -subdomain: - -# String: URL which takes you to the most recent short URL's destination -# Short URLs cannot be created with this string if set -# Default: l -latest: 'l' - -# Boolean: Show link to project repository on GitHub at bottom right corner of page -# Default: true -show_github_link: true - diff --git a/liteshort.py b/liteshort.py index c57d590..833350d 100644 --- a/liteshort.py +++ b/liteshort.py @@ -1,9 +1,10 @@ -# Copyright (c) 2019 Steven Spangler <132@ikl.sh> +# Copyright (c) 2019 Steven Spangler <132@ikl.sh>, Kevin Alberts # This file is part of liteshort by 132ikl # This software is license under the MIT license. It should be included in your copy of this software. # A copy of the MIT license can be obtained at https://mit-license.org/ -from flask import Flask, current_app, flash, g, jsonify, make_response, redirect, render_template, request, send_from_directory, url_for +from flask import Flask, current_app, flash, g, jsonify, make_response, redirect, render_template, request, \ + send_from_directory, url_for, session import bcrypt import os import random @@ -22,13 +23,14 @@ def load_config(): req_options = {'admin_username': 'admin', 'database_name': "urls", 'random_length': 4, 'allowed_chars': 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_', 'random_gen_timeout': 5, 'site_name': 'liteshort', 'site_domain': None, 'show_github_link': True, - 'secret_key': None, 'disable_api': False, 'subdomain': '', 'latest': 'l' + 'secret_key': None, 'disable_api': False, 'subdomain': '', 'latest': 'l', 'admin_links_per_page': 20 } config_types = {'admin_username': str, 'database_name': str, 'random_length': int, 'allowed_chars': str, 'random_gen_timeout': int, 'site_name': str, 'site_domain': (str, type(None)), 'show_github_link': bool, 'secret_key': str, - 'disable_api': bool, 'subdomain': (str, type(None)), 'latest': (str, type(None)) + 'disable_api': bool, 'subdomain': (str, type(None)), 'latest': (str, type(None)), + 'admin_links_per_page': int } for option in req_options.keys(): @@ -128,6 +130,26 @@ def list_shortlinks(): return result +def list_shortlinks_page(page, limit=50): + assert page >= 1 + assert type(page) == int + assert type(limit) == int + start_index = (page - 1) * limit + result = query_db('SELECT * FROM urls ORDER BY short LIMIT ? OFFSET ?', (limit, start_index), False, None) + result = nested_list_to_dict(result) + return result + + +def get_num_pages(limit=50): + assert type(limit) == int + result = query_db('SELECT COUNT(*) FROM urls', (), False, None) + items = result[0][0] + page_count = result[0][0] // limit + if page_count * limit < items: + page_count += 1 + return page_count, items + + def nested_list_to_dict(l): d = {} for nl in l: @@ -300,5 +322,58 @@ def main_post(): return response(request, None, 'Long URL required') +@app.route('/login', methods=['POST']) +def login(): + if 'admin_hashed_password' not in app.config or app.config['admin_hashed_password'] is None: + raise AssertionError("Login is disabled.") + + if authenticate(request.form['username'], request.form['password']): + session['logged_in'] = True + else: + flash('Wrong password!', 'error') + return make_response(redirect(url_for('admin'))) + + +@app.route('/logout') +def logout(): + if 'logged_in' in session and session['logged_in']: + session['logged_in'] = False + return make_response(redirect(url_for('admin'))) + + +@app.route('/delete/') +def delete(short): + if 'logged_in' in session and session['logged_in']: + success = delete_url(short) + if success: + flash("Link '/{}' deleted.".format(short), "success") + else: + flash("Failed to delete URL.".format(short), "error") + page = request.args.get('page', '1') + return make_response(redirect(url_for('admin')+"?page="+page)) + else: + return make_response(redirect(url_for('admin'))) + + +@app.route('/admin') +def admin(): + if 'logged_in' in session and session['logged_in']: + page_count, num_items = get_num_pages(app.config['admin_links_per_page']) + page = request.args.get('page', '1') + try: + page = int(page) + if page > page_count: + return make_response(redirect(url_for('admin')+"?page="+str(page_count))) + if page < 1: + return make_response(redirect(url_for('admin')+"?page=1")) + except ValueError: + page = 1 + + urls = list_shortlinks_page(page, app.config['admin_links_per_page']) + return render_template('admin.html', urls=urls, page=page, page_count=page_count, num_items=num_items) + else: + return render_template('login.html') + + if __name__ == '__main__': app.run() diff --git a/static/admin.css b/static/admin.css new file mode 100644 index 0000000..7330810 --- /dev/null +++ b/static/admin.css @@ -0,0 +1,74 @@ +/* +Copyright (c) 2019 Kevin Alberts +This software is license under the MIT license. It should be included in your copy of this software. +A copy of the MIT license can be obtained at https://mit-license.org/ +*/ +.paginator-link { + display: inline-block; + padding: 8px; + margin: 2px; + background: #1D428A; + border: 1px solid #1D428A; + color: white; +} + +.paginator-link { + text-decoration: none; +} + +.paginator-link:hover { + border: 1px solid #1D428A; + color: white; + background: #2450A8; +} + +.paginator-current { + display: inline-block; + padding: 8px; + margin: 2px; + border: 1px solid #1D428A; + background: white; + color: #1D428A; +} + +#box-title .small { + font-size: 2rem +} + +#content_admin { + width: 1200px; +} + +#link-table { + border-collapse: separate; + border-spacing: 0; + margin-top: 1rem; + margin-bottom: 2rem; +} + +#link-table .link-table-header { + background-color: #1D428A; + color: white; +} + +#link-table .link-table-center { + text-align: center; +} + +#link-table tr:hover { + background-color: rgba(0, 0, 0, 0.1); +} +#link-table tr.link-table-header:hover { + background-color: #1D428A; + color: white; +} + +#link-table .link-table-header th { + text-align: left; + font-size: 1.1rem; + padding: 0.5rem 1rem; +} + +#link-table th, #link-table td { + padding: 0.2rem 1rem; +} diff --git a/static/ia.css b/static/ia.css index cfa200e..188e5ef 100644 --- a/static/ia.css +++ b/static/ia.css @@ -1,3 +1,8 @@ +/* +Copyright (c) 2019 Kevin Alberts +This software is license under the MIT license. It should be included in your copy of this software. +A copy of the MIT license can be obtained at https://mit-license.org/ +*/ body{ margin: 0 0 2rem; background-color: #f4f4f4; @@ -34,6 +39,11 @@ body{ line-height: 2.75rem; } +#title a { + color: white; + text-decoration: none; +} + #middle { width: 100%; display: flex; @@ -103,6 +113,21 @@ body{ border-bottom: 1px solid rgba(0, 0, 0, 0.5); } +#username, #password { + /*width: 1%;*/ + min-width: 40px; + flex: 1 1 auto; + font-size: 1.75rem; + color: black; + position: relative; + padding: 0.5rem; + + margin-top: 1rem; + margin-bottom: 1rem; + + border: 1px solid rgba(0, 0, 0, 0.5); +} + #submit { background-color: #077821; color: white; @@ -194,3 +219,13 @@ body{ color: #2450A8; text-decoration: underline; } + +#copyright { + margin-top: 2rem; + font-size: 0.9rem; +} + +#copyright span { + text-align: center; + display: block; +} diff --git a/static/styles.css b/static/styles.css index 25ebdef..498bfe2 100644 --- a/static/styles.css +++ b/static/styles.css @@ -1,5 +1,5 @@ /* -Copyright (c) 2019 Steven Spangler <132@ikl.sh> +Copyright (c) 2019 Steven Spangler <132@ikl.sh>, Kevin Alberts This file is part of liteshort by 132ikl This software is license under the MIT license. It should be included in your copy of this software. A copy of the MIT license can be obtained at https://mit-license.org/ @@ -67,4 +67,4 @@ div.github { div.github:hover { opacity: 1; -} \ No newline at end of file +} diff --git a/templates/admin.html b/templates/admin.html new file mode 100644 index 0000000..b223f97 --- /dev/null +++ b/templates/admin.html @@ -0,0 +1,100 @@ + + + + + + URL Admin - {{ config.site_name }} + + + + + + + +
+
+
+
+

URL Admin (Logout)

+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + {% if category == 'success' %} +
+ ✓ {{ message }} +
+ {% elif category == 'error' %} +
+ ✖ {{ message }} +
+ {% endif %} + {% endfor %} + {% endif %} + {% endwith %} + +

There are {{ num_items }} links in the database.

+ +
+ {% for i in range(1, page_count + 1) %} + {% if i == page %} + {{ i }} + {% else %} + {{ i }} + {% endif %} + {% endfor %} +
+ + + + + + + + {% for long, short in urls.items() %} + + + + + + {% else %} + + + + {% endfor %} + + +
+ {% for i in range(1, page_count + 1) %} + {% if i == page %} + {{ i }} + {% else %} + {{ i }} + {% endif %} + {% endfor %} +
+
+
+
+ + diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..ef75138 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,67 @@ + + + + + + Admin Login - {{ config.site_name }} + + + + + + +
+
+
+
+
+

Admin Login

+
+
+ +
+
+ +
+ +
+
+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + {% if category == 'success' %} +
+ ✓ {{ message }} +
+ {% elif category == 'error' %} +
+ ✖ {{ message }} +
+ {% endif %} + {% endfor %} + {% endif %} + {% endwith %} +
+
+
+ + diff --git a/templates/main.html b/templates/main.html index 08daf1a..210ccce 100644 --- a/templates/main.html +++ b/templates/main.html @@ -1,5 +1,5 @@