From 7653470a7c87f6d82c2dcb8606558c39333e694c Mon Sep 17 00:00:00 2001 From: Wim Brand Date: Tue, 17 Dec 2019 19:28:30 +0100 Subject: [PATCH] add user config page and admin menu --- go.mod | 20 +++ go.sum | 115 +++++++++++++++ pixelapi/admin.go | 30 ++++ pixelapi/pixelapi.go | 11 +- pixelapi/user.go | 70 ++++++--- res/template/account/email_confirm.html | 37 +++++ res/template/account/user_home.html | 7 +- res/template/account/user_settings.html | 11 +- res/template/admin.html | 13 +- res/template/fragments/form.html | 76 +++++----- webcontroller/admin_panel.go | 97 ++++++++++++ webcontroller/forms/form.go | 2 + webcontroller/template_data.go | 2 + webcontroller/user_account.go | 61 ++++---- webcontroller/user_settings.go | 188 ++++++++++++++++++++++++ webcontroller/web_controller.go | 15 +- 16 files changed, 648 insertions(+), 107 deletions(-) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 res/template/account/email_confirm.html create mode 100644 webcontroller/admin_panel.go create mode 100644 webcontroller/user_settings.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..48d6531 --- /dev/null +++ b/go.mod @@ -0,0 +1,20 @@ +module fornaxian.com/pixeldrain-web + +go 1.13 + +require ( + fornaxian.com/pixeldrain-api v0.0.0-20191216095319-0533f903c681 + github.com/Fornaxian/config v0.0.0-20180915150834-ac41cf746a70 + github.com/Fornaxian/log v0.0.0-20190617093801-1c7ce9a7c9b3 + github.com/google/uuid v1.1.1 + github.com/julienschmidt/httprouter v1.3.0 + github.com/k0kubun/pp v3.0.1+incompatible // indirect + github.com/mattn/go-colorable v0.1.4 // indirect + github.com/mattn/go-isatty v0.0.11 // indirect + github.com/microcosm-cc/bluemonday v1.0.2 + github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca // indirect + github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect + github.com/timakin/gonvert v0.0.0-20170112000238-5dce59dbd0d8 + golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 // indirect + gopkg.in/russross/blackfriday.v2 v2.0.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..cef0515 --- /dev/null +++ b/go.sum @@ -0,0 +1,115 @@ +fornaxian.com/pixeldrain-api v0.0.0-20191213180911-5823f7064801/go.mod h1:bhJgwXdFTFrZUdQ+vBXzUQiAhvuFnEgZgxeXBhWddLU= +fornaxian.com/pixeldrain-api v0.0.0-20191216095319-0533f903c681 h1:tLIj6RhZHsJW1riDUZj7c4UQ3Gojl8Sh6CHj9cC4nzQ= +fornaxian.com/pixeldrain-api v0.0.0-20191216095319-0533f903c681/go.mod h1:HooOANQfnTseH1XR/kJ2R8sKpPM68GdzHQyHiUlaZqI= +fornaxian.com/pixeldrain-web v0.0.0-20191211101305-cc3c5d58e432/go.mod h1:UwobVC5YVBOtfTc6eBSBjD2tdLQ+xYYp7+web6nCjD0= +fornaxian.com/pixelstore v0.0.0-20191212224440-0453456df082/go.mod h1:saC4IwAOpIip/M7fURYMTcUo5fhwp/m5GmvoQucSwLQ= +fornaxian.com/pixelstore v0.0.0-20191213205107-7cc26c05cb69/go.mod h1:6cdebY9AhneUTQZQ+dyXnIZmMYxCAVgJ5lCeUTWPR8M= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Fornaxian/config v0.0.0-20180915150834-ac41cf746a70 h1:yRkXab8h+BAWEphLE0qexJVxIOdPgw+3T9VSLuyPXus= +github.com/Fornaxian/config v0.0.0-20180915150834-ac41cf746a70/go.mod h1:Ig5am30IOP/eqsjogI1TuSlOTIeTPHoMOpYYM1bisww= +github.com/Fornaxian/log v0.0.0-20190617093801-1c7ce9a7c9b3 h1:PfKr7anK3z4kLG9V6BbbKOVFhVaGEAJi4HxXCAa+QeU= +github.com/Fornaxian/log v0.0.0-20190617093801-1c7ce9a7c9b3/go.mod h1:jdnyerqAlXJJpQmpyrdmSYMitRaRZ8RejEXuXz6n5QY= +github.com/Fornaxian/unifilter v0.0.0-20180623154047-e65e144d5942/go.mod h1:ofV5syadd2nI4gOc/rP1yPnXkARgm+E1D/U38mbUj44= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/threefish v0.0.0-20120919164726-3ecf4c494abf/go.mod h1:bXVurdTuvOiJu7NHALemFe0JMvC2UmwYHW+7fcZaZ2M= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/k0kubun/pp v3.0.1+incompatible h1:3tqvf7QgUnZ5tXO6pNAZlrvHgl6DvifjDrd9g2S9Z40= +github.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= +github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/reedsolomon v1.9.2/go.mod h1:CwCi+NUr9pqSVktrkN+Ondf06rkhYZ/pcNv7fu+8Un4= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s= +github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI= +github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.4/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/timakin/gonvert v0.0.0-20170112000238-5dce59dbd0d8 h1:gw/M1/pCu7oELGHZ6rvktNmMbdWhf9kHc7WYrbLeKdo= +github.com/timakin/gonvert v0.0.0-20170112000238-5dce59dbd0d8/go.mod h1:oqLl90kSlp4+8wMQKql9ZdQGa4/5pVCxOOpTVWkoyV0= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/xtaci/smux v1.3.3/go.mod h1:f+nYm6SpuHMy/SH0zpbvAFHT1QoMcgLOsWcFip5KfPw= +gitlab.com/NebulousLabs/Sia v1.4.1/go.mod h1:pmBBguXJl2nxajST2OtRv0FOIMSggtn5evGpE9Pju3Y= +gitlab.com/NebulousLabs/demotemutex v0.0.0-20151003192217-235395f71c40/go.mod h1:HfnnxM8isYA7FUlqS5h34XTeiBhPtcuCquVujKsn9aw= +gitlab.com/NebulousLabs/entropy-mnemonics v0.0.0-20181018051301-7532f67e3500/go.mod h1:4koft3fRXTETovKPTeX/Aggj+ajCGWCcuuBBc598Pcs= +gitlab.com/NebulousLabs/errors v0.0.0-20171229012116-7ead97ef90b8/go.mod h1:ZkMZ0dpQyWwlENaeZVBiQRjhMEZvk6VTXquzl3FOFP8= +gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40/go.mod h1:rOnSnoRyxMI3fe/7KIbVcsHRGxe30OONv8dEgo+vCfA= +gitlab.com/NebulousLabs/go-upnp v0.0.0-20181011194642-3a71999ed0d3/go.mod h1:sleOmkovWsDEQVYXmOJhx69qheoMTmCuPYyiCFCihlg= +gitlab.com/NebulousLabs/merkletree v0.0.0-20190207030457-bc4a11e31a0d/go.mod h1:xItahGeKIkh9BQfxDEX6O3eWxOxbLBPX738sXm0uVaQ= +gitlab.com/NebulousLabs/ratelimit v0.0.0-20180716154200-1308156c2eaf/go.mod h1:vowDA1cdvtWW678ugB7L/yKT2pCN37aH6zYp9NF5Isc= +gitlab.com/NebulousLabs/threadgroup v0.0.0-20180716154133-88a11db9e46c/go.mod h1:w05nvlkvHlk3Vfc7mcU29Toic1X0BcYUnKoTHS0ea2Y= +gitlab.com/NebulousLabs/writeaheadlog v0.0.0-20190703190009-cb822c37bc94/go.mod h1:Lhpa9AcbWcYKcc4amZsOHqJdQglnkWrGuUI68XC7U2Q= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20191206065243-da761ea9ff43/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 h1:gSbV7h1NRL2G1xTg/owz62CST1oJBmxy4QpMMregXVQ= +golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/russross/blackfriday.v2 v2.0.0 h1:+FlnIV8DSQnT7NZ43hcVKcdJdzZoeCmJj4Ql8gq5keA= +gopkg.in/russross/blackfriday.v2 v2.0.0/go.mod h1:6sSBNz/GtOm/pJTuh5UmBK2ZHfmnxGbl2NZg1UliSOI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/pixelapi/admin.go b/pixelapi/admin.go index 5367d52..2471be0 100644 --- a/pixelapi/admin.go +++ b/pixelapi/admin.go @@ -1,5 +1,7 @@ package pixelapi +import "net/url" + // IsAdmin is the response to the /admin/is_admin API type IsAdmin struct { Success bool `json:"success"` @@ -14,3 +16,31 @@ func (p *PixelAPI) UserIsAdmin() (resp IsAdmin, err error) { } return resp, nil } + +// AdminGlobal is a global setting in pixeldrain's back-end +type AdminGlobal struct { + Key string `json:"key"` + Value string `json:"value"` +} + +// AdminGlobals is an array of globals +type AdminGlobals struct { + Success bool `json:"success"` + Globals []AdminGlobal `json:"globals"` +} + +// AdminGetGlobals returns if the logged in user is an admin user +func (p *PixelAPI) AdminGetGlobals() (resp AdminGlobals, err error) { + if err = p.jsonRequest("GET", p.apiEndpoint+"/admin/globals", &resp); err != nil { + return resp, err + } + return resp, nil +} + +// AdminSetGlobals returns if the logged in user is an admin user +func (p *PixelAPI) AdminSetGlobals(key, value string) (resp SuccessResponse, err error) { + var form = url.Values{} + form.Add("key", key) + form.Add("value", value) + return resp, p.form("POST", p.apiEndpoint+"/admin/globals", form, &resp, true) +} diff --git a/pixelapi/pixelapi.go b/pixelapi/pixelapi.go index 5691bfe..f7cdd3f 100644 --- a/pixelapi/pixelapi.go +++ b/pixelapi/pixelapi.go @@ -164,7 +164,6 @@ func (p *PixelAPI) form( } func parseJSONResponse(resp *http.Response, target interface{}, catchErrors bool) error { - var jdec = json.NewDecoder(resp.Body) var err error // Test for client side and server side errors @@ -172,8 +171,7 @@ func parseJSONResponse(resp *http.Response, target interface{}, catchErrors bool var errResp = Error{ ReqError: false, } - err = jdec.Decode(&errResp) - if err != nil { + if err = json.NewDecoder(resp.Body).Decode(&errResp); err != nil { log.Error("Can't decode this: %v", err) return Error{ ReqError: true, @@ -185,8 +183,11 @@ func parseJSONResponse(resp *http.Response, target interface{}, catchErrors bool return errResp } - err = jdec.Decode(target) - if err != nil { + if target == nil { + return nil + } + + if err = json.NewDecoder(resp.Body).Decode(target); err != nil { r, _ := ioutil.ReadAll(resp.Body) log.Error("Can't decode this: %v. %s", err, r) return Error{ diff --git a/pixelapi/user.go b/pixelapi/user.go index 7df6bd4..0d63bcd 100644 --- a/pixelapi/user.go +++ b/pixelapi/user.go @@ -3,6 +3,7 @@ package pixelapi import ( "fmt" "net/url" + "strconv" ) // Registration is the response to the UserRegister API. The register API can @@ -66,6 +67,7 @@ func (p *PixelAPI) UserLogin(username, password string, saveKey bool) (resp *Log type UserInfo struct { Success bool `json:"success"` Username string `json:"username"` + Email string `json:"email"` } // UserInfo returns information about the logged in user. Requires an API key @@ -80,13 +82,8 @@ func (p *PixelAPI) UserInfo() (resp *UserInfo, err error) { // UserSessionDestroy destroys an API key so it can no longer be used to perform // actions -func (p *PixelAPI) UserSessionDestroy(key string) (resp *SuccessResponse, err error) { - resp = &SuccessResponse{} - err = p.jsonRequest("DELETE", p.apiEndpoint+"/user/session", resp) - if err != nil { - return nil, err - } - return resp, nil +func (p *PixelAPI) UserSessionDestroy(key string) (err error) { + return p.jsonRequest("DELETE", p.apiEndpoint+"/user/session", nil) } type UserFiles struct { @@ -114,25 +111,64 @@ type UserLists struct { func (p *PixelAPI) UserLists(page, limit int) (resp *UserLists, err error) { resp = &UserLists{Lists: make([]List, 0)} - err = p.jsonRequest( + if err = p.jsonRequest( "GET", fmt.Sprintf("%s/user/lists?page=%d&limit=%d", p.apiEndpoint, page, limit), resp, - ) - if err != nil { + ); err != nil { return nil, err } return resp, nil } -func (p *PixelAPI) UserPasswordSet(oldPW, newPW string) (resp *SuccessResponse, err error) { - resp = &SuccessResponse{} +func (p *PixelAPI) UserPasswordSet(oldPW, newPW string) (err error) { var form = url.Values{} form.Add("old_password", oldPW) form.Add("new_password", newPW) - err = p.form("PUT", p.apiEndpoint+"/user/password", form, resp, true) - if err != nil { - return nil, err - } - return resp, nil + return p.form("PUT", p.apiEndpoint+"/user/password", form, nil, true) +} + +// UserEmailReset starts the e-mail change process. An email will be sent to the +// new address to verify that it's real. Once the link in the e-mail is clicked +// the key it contains can be sent to the API with UserEmailResetConfirm and the +// change will be applied +func (p *PixelAPI) UserEmailReset(email string, delete bool) (err error) { + var form = url.Values{} + form.Add("new_email", email) + form.Add("delete", strconv.FormatBool(delete)) + return p.form("PUT", p.apiEndpoint+"/user/email_reset", form, nil, true) +} + +// UserEmailResetConfirm finishes process of changing a user's e-mail address +func (p *PixelAPI) UserEmailResetConfirm(key string) (err error) { + var form = url.Values{} + form.Add("key", key) + return p.form("PUT", p.apiEndpoint+"/user/email_reset_cofirm", form, nil, true) +} + +// UserPasswordReset starts the password reset process. An email will be sent +// the user to verify that it really wanted to reset the password. Once the link +// in the e-mail is clicked the key it contains can be sent to the API with +// UserPasswordResetConfirm and a new password can be set +func (p *PixelAPI) UserPasswordReset(email string, recaptchaResponse string) (err error) { + var form = url.Values{} + form.Add("email", email) + form.Add("recaptcha_response", recaptchaResponse) + return p.form("PUT", p.apiEndpoint+"/user/password_reset", form, nil, true) +} + +// UserPasswordResetConfirm finishes process of resetting a user's password. If +// the key is valid the new_password parameter will be saved as the new password +func (p *PixelAPI) UserPasswordResetConfirm(key string, newPassword string) (err error) { + var form = url.Values{} + form.Add("key", key) + form.Add("new_password", newPassword) + return p.form("PUT", p.apiEndpoint+"/user/password_reset_confirm", form, nil, true) +} + +// UserSetUsername changes the user's username. +func (p *PixelAPI) UserSetUsername(username string) (err error) { + var form = url.Values{} + form.Add("new_username", username) + return p.form("PUT", p.apiEndpoint+"/user/username", form, nil, true) } diff --git a/res/template/account/email_confirm.html b/res/template/account/email_confirm.html new file mode 100644 index 0000000..2b9a77c --- /dev/null +++ b/res/template/account/email_confirm.html @@ -0,0 +1,37 @@ +{{define "email_confirm"}} + + + {{template "meta_tags" "E-mail verification"}} + {{template "user_style" .}} + + + {{template "page_top" .}} + +
+
+ {{if eq .Other "success"}} +

Success!

+

+ Your account's e-mail address has been updated. +

+ {{else if eq .Other "not_found"}} +

E-mail change failed

+

+ This e-mail change request does not exist or has expired. + Please try again if you still want to change your e-mail + address. +

+ {{else}} +

Error

+

+ Something went wrong while processing this request. Please + try again later. +

+ {{end}} +
+
+ {{template "page_bottom" .}} + {{template "analytics"}} + + +{{end}} diff --git a/res/template/account/user_home.html b/res/template/account/user_home.html index 326acb0..a7036c9 100644 --- a/res/template/account/user_home.html +++ b/res/template/account/user_home.html @@ -12,11 +12,12 @@

Welcome home, {{.Username}}!

-

Actions

+

Account information

+ Update account settings

Your most recently uploaded files:

diff --git a/res/template/account/user_settings.html b/res/template/account/user_settings.html index f4af7ef..b2059ce 100644 --- a/res/template/account/user_settings.html +++ b/res/template/account/user_settings.html @@ -8,14 +8,11 @@ {{template "page_top" .}} -

User configuration

+

{{.Title}}

- -

What would you like to do?

- - + {{template "form" .Other.PasswordForm}} + {{template "form" .Other.EmailForm}} + {{template "form" .Other.UsernameForm}}
{{template "page_bottom" .}} {{template "analytics"}} diff --git a/res/template/admin.html b/res/template/admin.html index 72de30c..50b7a73 100644 --- a/res/template/admin.html +++ b/res/template/admin.html @@ -77,7 +77,14 @@ }, ticks: { callback: function(value, index, values) { - return Math.round((value*8/1e6)/(interval*60)) + " Mbps"; + if (value > 1e12) { + return Math.round(value/1e9)/1e3 + " TB"; + } else if (value > 1e9) { + return Math.round(value/1e6)/1e3 + " GB"; + } else if (value > 1e6) { + return Math.round(value/1e3)/1e3 + " MB"; + } + return value/1e3 + " kB"; } }, gridLines: { @@ -127,6 +134,10 @@ setData(); +
+ {{else}}

;)

{{end}} diff --git a/res/template/fragments/form.html b/res/template/fragments/form.html index ab5fbde..dadca98 100644 --- a/res/template/fragments/form.html +++ b/res/template/fragments/form.html @@ -1,24 +1,23 @@ {{define "form"}}

{{.Title}}

{{.PreFormHTML}} - {{if eq .Submitted true}} - {{if eq .SubmitSuccess true}} -
- {{index .SubmitMessages 0}} -
- {{else}} -
- Something went wrong, please correct these errors before continuing:
-
    - {{range $msg := .SubmitMessages}} -
  • {{$msg}}
  • - {{end}} -
-
- {{end}} - {{end}} -
+ {{if eq .Submitted true}} + {{if eq .SubmitSuccess true}} +
+ {{index .SubmitMessages 0}} +
+ {{else}} +
+ Something went wrong, please correct these errors before continuing:
+
    + {{range $msg := .SubmitMessages}} +
  • {{$msg}}
  • + {{end}} +
+
+ {{end}} + {{end}} {{if ne .Username ""}} @@ -27,23 +26,32 @@ {{range $index, $field := .Fields}} - - + {{if eq $field.Type "textarea"}} + + {{else}} + + + {{end}} {{if or (ne $field.Description "") (eq $field.Separator true)}}
{{$field.Label}} - {{if eq $field.Type "text"}} - - {{else if eq $field.Type "username"}} - - {{else if eq $field.Type "email"}} - - {{else if eq $field.Type "current-password"}} - - {{else if eq $field.Type "new-password"}} - - {{else if eq $field.Type "captcha"}} - -
- {{end}} -
+ {{$field.Label}}
+ +
{{$field.Label}} + {{if eq $field.Type "text"}} + + {{else if eq $field.Type "number"}} + + {{else if eq $field.Type "username"}} + + {{else if eq $field.Type "email"}} + + {{else if eq $field.Type "current-password"}} + + {{else if eq $field.Type "new-password"}} + + {{else if eq $field.Type "captcha"}} + +
+ {{end}} +
diff --git a/webcontroller/admin_panel.go b/webcontroller/admin_panel.go new file mode 100644 index 0000000..357d2b6 --- /dev/null +++ b/webcontroller/admin_panel.go @@ -0,0 +1,97 @@ +package webcontroller + +import ( + "fmt" + "html/template" + "net/http" + + "fornaxian.com/pixeldrain-web/pixelapi" + "fornaxian.com/pixeldrain-web/webcontroller/forms" + "github.com/Fornaxian/log" +) + +func (wc *WebController) adminGlobalsForm(td *TemplateData, r *http.Request) (f forms.Form) { + if isAdmin, err := td.PixelAPI.UserIsAdmin(); err != nil { + td.Title = err.Error() + return forms.Form{Title: td.Title} + } else if !isAdmin.IsAdmin { + td.Title = ";)" + return forms.Form{Title: td.Title} + } + + td.Title = "Pixeldrain global configuration" + f = forms.Form{ + Name: "admin_globals", + Title: td.Title, + PreFormHTML: template.HTML("

Careful! The slightest typing error could bring the whole website down

"), + BackLink: "/admin", + SubmitLabel: "Submit", + } + + globals, err := td.PixelAPI.AdminGetGlobals() + if err != nil { + f.SubmitMessages = []template.HTML{template.HTML(err.Error())} + return f + } + var globalsMap = make(map[string]string) + for _, v := range globals.Globals { + f.Fields = append(f.Fields, forms.Field{ + Name: v.Key, + DefaultValue: v.Value, + Label: v.Key, + Type: func() forms.FieldType { + switch v.Key { + case + "email_address_change_body", + "email_password_reset_body": + return forms.FieldTypeTextarea + case + "api_ratelimit_limit", + "api_ratelimit_rate", + "cron_interval_seconds", + "file_inactive_expiry_days", + "max_file_size", + "pixelstore_min_redundancy": + return forms.FieldTypeNumber + default: + return forms.FieldTypeText + } + }(), + }) + globalsMap[v.Key] = v.Value + } + + if f.ReadInput(r) { + var successfulUpdates = 0 + for k, v := range f.Fields { + if v.EnteredValue == globalsMap[v.Name] { + continue // Change changes, no need to update + } + + // Value changed, try to update global setting + if _, err = td.PixelAPI.AdminSetGlobals(v.Name, v.EnteredValue); err != nil { + if apiErr, ok := err.(pixelapi.Error); ok { + f.SubmitMessages = append(f.SubmitMessages, template.HTML(apiErr.Message)) + } else { + log.Error("%s", err) + f.SubmitMessages = append(f.SubmitMessages, template.HTML( + fmt.Sprintf("Failed to set '%s': %s", v.Name, err), + )) + return f + } + } else { + f.Fields[k].DefaultValue = v.EnteredValue + successfulUpdates++ + } + + } + if len(f.SubmitMessages) == 0 { + // Request was a success + f.SubmitSuccess = true + f.SubmitMessages = []template.HTML{template.HTML( + fmt.Sprintf("Success! %d values updated", successfulUpdates), + )} + } + } + return f +} diff --git a/webcontroller/forms/form.go b/webcontroller/forms/form.go index 3f23e41..2a3dafb 100644 --- a/webcontroller/forms/form.go +++ b/webcontroller/forms/form.go @@ -79,6 +79,8 @@ type FieldType string // Fields which can be in a form const ( FieldTypeText FieldType = "text" + FieldTypeTextarea FieldType = "textarea" + FieldTypeNumber FieldType = "number" FieldTypeUsername FieldType = "username" FieldTypeEmail FieldType = "email" FieldTypeCurrentPassword FieldType = "current-password" diff --git a/webcontroller/template_data.go b/webcontroller/template_data.go index ec8ab9a..1199a96 100644 --- a/webcontroller/template_data.go +++ b/webcontroller/template_data.go @@ -16,6 +16,7 @@ import ( type TemplateData struct { Authenticated bool Username string + Email string UserAgent string UserStyle template.CSS APIEndpoint template.URL @@ -67,6 +68,7 @@ func (wc *WebController) newTemplateData(w http.ResponseWriter, r *http.Request) // Authentication succeeded t.Authenticated = true t.Username = uinf.Username + t.Email = uinf.Email } else { t.PixelAPI = pixelapi.New(wc.conf.APIURLInternal, "") } diff --git a/webcontroller/user_account.go b/webcontroller/user_account.go index 62eb36f..6153280 100644 --- a/webcontroller/user_account.go +++ b/webcontroller/user_account.go @@ -18,9 +18,8 @@ func (wc *WebController) serveLogout( ) { if key, err := wc.getAPIKey(r); err == nil { var api = pixelapi.New(wc.conf.APIURLInternal, key) - _, err1 := api.UserSessionDestroy(key) - if err1 != nil { - log.Warn("logout failed for session '%s': %s", key, err1) + if err = api.UserSessionDestroy(key); err != nil { + log.Warn("logout failed for session '%s': %s", key, err) } } @@ -151,9 +150,12 @@ func (wc *WebController) loginForm(td *TemplateData, r *http.Request) (f forms.F BackLink: "/", SubmitLabel: "Login", PostFormHTML: template.HTML( - `
If you don't have a pixeldrain account yet, you can ` + + `

If you don't have a pixeldrain account yet, you can ` + `register here. No e-mail address is ` + - `required.
`, + `required.

` + + `

Forgot your password? If your account has a valid e-mail ` + + `address you can request a new ` + + `password here.

`, ), } @@ -184,45 +186,35 @@ func (wc *WebController) loginForm(td *TemplateData, r *http.Request) (f forms.F return f } -func (wc *WebController) passwordForm(td *TemplateData, r *http.Request) (f forms.Form) { - td.Title = "Change Password" +func (wc *WebController) passwordResetForm(td *TemplateData, r *http.Request) (f forms.Form) { + td.Title = "Recover lost password" f = forms.Form{ - Name: "password_change", + Name: "password_reset", Title: td.Title, Fields: []forms.Field{ { - Name: "old_password", - Label: "Old Password", - Type: forms.FieldTypeCurrentPassword, + Name: "email", + Label: "E-mail address", + Description: "we will send a password reset link to this " + + "e-mail address", + Separator: true, + Type: forms.FieldTypeEmail, }, { - Name: "new_password1", - Label: "New Password", - Type: forms.FieldTypeNewPassword, - }, { - Name: "new_password2", - Label: "New Password verification", - Type: forms.FieldTypeCurrentPassword, + Name: "recaptcha_response", + Label: "Turing test (click the white box)", + Description: "the reCaptcha turing test verifies that you " + + "are not an evil robot that is trying hijack accounts", + Separator: true, + Type: forms.FieldTypeCaptcha, + CaptchaSiteKey: wc.captchaKey(), }, }, - BackLink: "/user", + BackLink: "/login", SubmitLabel: "Submit", } if f.ReadInput(r) { - if f.FieldVal("new_password1") != f.FieldVal("new_password2") { - f.SubmitMessages = []template.HTML{ - "Password verification failed. Please enter the same " + - "password in both new password fields"} - return f - } - - // Passwords match, send the request and fill in the response in the - // form - _, err := td.PixelAPI.UserPasswordSet( - f.FieldVal("old_password"), - f.FieldVal("new_password1"), - ) - if err != nil { + if err := td.PixelAPI.UserPasswordReset(f.FieldVal("email"), f.FieldVal("recaptcha_response")); err != nil { if apiErr, ok := err.(pixelapi.Error); ok { f.SubmitMessages = []template.HTML{template.HTML(apiErr.Message)} } else { @@ -230,9 +222,8 @@ func (wc *WebController) passwordForm(td *TemplateData, r *http.Request) (f form f.SubmitMessages = []template.HTML{"Internal Server Error"} } } else { - // Request was a success f.SubmitSuccess = true - f.SubmitMessages = []template.HTML{"Success! Your password has been updated"} + f.SubmitMessages = []template.HTML{"Success! E-mail sent"} } } return f diff --git a/webcontroller/user_settings.go b/webcontroller/user_settings.go new file mode 100644 index 0000000..a02babd --- /dev/null +++ b/webcontroller/user_settings.go @@ -0,0 +1,188 @@ +package webcontroller + +import ( + "html/template" + "net/http" + + "fornaxian.com/pixeldrain-web/pixelapi" + "fornaxian.com/pixeldrain-web/webcontroller/forms" + "github.com/Fornaxian/log" + "github.com/julienschmidt/httprouter" +) + +func (wc *WebController) serveUserSettings( + w http.ResponseWriter, + r *http.Request, + p httprouter.Params, +) { + td := wc.newTemplateData(w, r) + + if !td.Authenticated { + http.Redirect(w, r, "/login", http.StatusSeeOther) + return + } + + td.Title = "Account settings" + td.Other = struct { + PasswordForm forms.Form + EmailForm forms.Form + UsernameForm forms.Form + }{ + PasswordForm: wc.passwordForm(td, r), + EmailForm: wc.emailForm(td, r), + UsernameForm: wc.usernameForm(td, r), + } + wc.templates.Get().ExecuteTemplate(w, "user_settings", td) +} + +func (wc *WebController) passwordForm(td *TemplateData, r *http.Request) (f forms.Form) { + f = forms.Form{ + Name: "password_change", + Title: "Change password", + Fields: []forms.Field{ + { + Name: "old_password", + Label: "Old Password", + Type: forms.FieldTypeCurrentPassword, + }, { + Name: "new_password1", + Label: "New Password", + Type: forms.FieldTypeNewPassword, + }, { + Name: "new_password2", + Label: "New Password again", + Description: "we need you to repeat your password so you " + + "won't be locked out of your account if you make a " + + "typing error", + Type: forms.FieldTypeNewPassword, + }, + }, + SubmitLabel: "Submit", + } + + if f.ReadInput(r) { + if f.FieldVal("new_password1") != f.FieldVal("new_password2") { + f.SubmitMessages = []template.HTML{ + "Password verification failed. Please enter the same " + + "password in both new password fields"} + return f + } + + // Passwords match, send the request and fill in the response in the + // form + if err := td.PixelAPI.UserPasswordSet( + f.FieldVal("old_password"), + f.FieldVal("new_password1"), + ); err != nil { + if apiErr, ok := err.(pixelapi.Error); ok { + f.SubmitMessages = []template.HTML{template.HTML(apiErr.Message)} + } else { + log.Error("%s", err) + f.SubmitMessages = []template.HTML{"Internal Server Error"} + } + } else { + // Request was a success + f.SubmitSuccess = true + f.SubmitMessages = []template.HTML{"Success! Your password has been updated"} + } + } + return f +} + +func (wc *WebController) emailForm(td *TemplateData, r *http.Request) (f forms.Form) { + f = forms.Form{ + Name: "email_change", + Title: "Change e-mail address", + Fields: []forms.Field{ + { + Name: "new_email", + Label: "New e-mail address", + Description: "we will send an e-mail to the new address to " + + "verify that it's real. The address will be saved once " + + "the link in the message is clicked. If the e-mail " + + "doesn't arrive right away please check your spam box too", + Type: forms.FieldTypeEmail, + }, + }, + SubmitLabel: "Submit", + } + + if f.ReadInput(r) { + if err := td.PixelAPI.UserEmailReset( + f.FieldVal("new_email"), + false, + ); err != nil { + if apiErr, ok := err.(pixelapi.Error); ok { + f.SubmitMessages = []template.HTML{template.HTML(apiErr.Message)} + } else { + log.Error("%s", err) + f.SubmitMessages = []template.HTML{"Internal Server Error"} + } + } else { + // Request was a success + f.SubmitSuccess = true + f.SubmitMessages = []template.HTML{"Success! E-mail sent"} + } + } + return f +} + +func (wc *WebController) serveEmailConfirm( + w http.ResponseWriter, + r *http.Request, + p httprouter.Params, +) { + var status string + if key, err := wc.getAPIKey(r); err == nil { + err = pixelapi.New(wc.conf.APIURLInternal, key).UserEmailResetConfirm(r.FormValue("key")) + if err != nil && err.Error() == "not_found" { + status = "not_found" + } else if err != nil { + status = "internal_error" + } else { + status = "success" + } + } + + td := wc.newTemplateData(w, r) + td.Other = status + + wc.templates.Get().ExecuteTemplate(w, "email_confirm", td) +} + +func (wc *WebController) usernameForm(td *TemplateData, r *http.Request) (f forms.Form) { + f = forms.Form{ + Name: "username_change", + Title: "Change username", + Fields: []forms.Field{ + { + Name: "new_username", + Label: "New username", + Description: "changing your username also changes the name " + + "used to log in. If you forget your username you can " + + "still log in using your e-mail address if you have one " + + "configured", + Type: forms.FieldTypeUsername, + }, + }, + SubmitLabel: "Submit", + } + + if f.ReadInput(r) { + if err := td.PixelAPI.UserSetUsername(f.FieldVal("new_username")); err != nil { + if apiErr, ok := err.(pixelapi.Error); ok { + f.SubmitMessages = []template.HTML{template.HTML(apiErr.Message)} + } else { + log.Error("%s", err) + f.SubmitMessages = []template.HTML{"Internal Server Error"} + } + } else { + // Request was a success + f.SubmitSuccess = true + f.SubmitMessages = []template.HTML{template.HTML( + "Success! You are now " + f.FieldVal("new_username"), + )} + } + } + return f +} diff --git a/webcontroller/web_controller.go b/webcontroller/web_controller.go index 0456de9..dbc5461 100644 --- a/webcontroller/web_controller.go +++ b/webcontroller/web_controller.go @@ -78,7 +78,9 @@ func New(r *httprouter.Router, prefix string, conf *conf.PixelWebConfig) *WebCon r.GET(p+"/register" /* */, wc.serveForm(wc.registerForm, false)) r.POST(p+"/register" /* */, wc.serveForm(wc.registerForm, false)) r.GET(p+"/login" /* */, wc.serveForm(wc.loginForm, false)) - r.POST(p+"/login" /* */, wc.serveForm(wc.loginForm, false)) + r.POST(p+"/login" /* */, wc.serveForm(wc.loginForm, false)) + r.GET(p+"/password_reset" /* */, wc.serveForm(wc.passwordResetForm, false)) + r.POST(p+"/password_reset" /* */, wc.serveForm(wc.passwordResetForm, false)) r.GET(p+"/logout" /* */, wc.serveTemplate("logout", true)) r.POST(p+"/logout" /* */, wc.serveLogout) r.GET(p+"/user" /* */, wc.serveTemplate("user_home", true)) @@ -87,11 +89,14 @@ func New(r *httprouter.Router, prefix string, conf *conf.PixelWebConfig) *WebCon r.GET(p+"/user/filemanager" /**/, wc.serveTemplate("file_manager", true)) // User account settings - r.GET(p+"/user/settings" /* */, wc.serveTemplate("user_settings", true)) - r.GET(p+"/user/change_password" /* */, wc.serveForm(wc.passwordForm, true)) - r.POST(p+"/user/change_password" /**/, wc.serveForm(wc.passwordForm, true)) + r.GET(p+"/user/settings" /* */, wc.serveUserSettings) + r.POST(p+"/user/settings" /* */, wc.serveUserSettings) + r.GET(p+"/user/confirm_email" /**/, wc.serveEmailConfirm) - r.GET(p+"/admin", wc.serveTemplate("admin_panel", true)) + // Admin settings + r.GET(p+"/admin" /* */, wc.serveTemplate("admin_panel", true)) + r.GET(p+"/admin/globals" /* */, wc.serveForm(wc.adminGlobalsForm, true)) + r.POST(p+"/admin/globals" /**/, wc.serveForm(wc.adminGlobalsForm, true)) r.NotFound = http.HandlerFunc(wc.serveNotFound)