commit 7babade475dea45e22941afa4f10e7cd0ea64934 Author: Nix13 Date: Sun May 2 16:04:45 2021 +0300 initial commit on github diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e8c5bae --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.idea/ +build/ +cdn/ +.DS_Store +private.config.json \ No newline at end of file diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..d99f164 --- /dev/null +++ b/build.bat @@ -0,0 +1,4 @@ +set GOOS=linux +set GOARCH=amd64 +go build -o ../build/backend . +pause \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..7ad0bf8 --- /dev/null +++ b/build.sh @@ -0,0 +1 @@ +GOOS=linux GOARCH=amd64 go build -o build/backend . \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 0000000..23ee1ae --- /dev/null +++ b/config.json @@ -0,0 +1,11 @@ +{ + "address": "", + "port": "8080", + "dev_frontend_url": "", + "frontend_url": "http://localhost:3000", + "cdn_path": "cdn", + "cdn_url": "https://cdn.unmei.space", + "version": "1.2", + "build": 123, + "debug": true +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..29a882d --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module backend + +go 1.14 + +require ( + github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/getsentry/sentry-go v0.10.0 + github.com/gorilla/mux v1.8.0 + github.com/jmoiron/sqlx v1.3.3 + github.com/lib/pq v1.10.1 + golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b + golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba + google.golang.org/appengine v1.6.6 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d250a48 --- /dev/null +++ b/go.sum @@ -0,0 +1,211 @@ +github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= +github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo= +github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= +github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= +github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= +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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= +github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= +github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= +github.com/getsentry/sentry-go v0.10.0 h1:6gwY+66NHKqyZrdi6O2jGdo7wGdo9b3B69E01NFgT5g= +github.com/getsentry/sentry-go v0.10.0/go.mod h1:kELm/9iCblqUYh+ZRML7PNdCvEuw24wBvJPYyi86cws= +github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= +github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= +github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= +github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= +github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI= +github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= +github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk= +github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g= +github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= +github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= +github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= +github.com/jmoiron/sqlx v1.3.3 h1:j82X0bf7oQ27XeqxicSZsTU5suPwKElg3oyxNn43iTk= +github.com/jmoiron/sqlx v1.3.3/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= +github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8= +github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE= +github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE= +github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro= +github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8= +github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g= +github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= +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/lib/pq v1.7.0 h1:h93mCPfUSkaul3Ka/VG8uZdmW1uMHDGxzu0NWHuJmHY= +github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.1 h1:6VXZrLU0jHBYyAqrSPa+MgPfnSvTPuMgK+k0o5kVFWo= +github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4= +github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= +github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= +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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +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/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +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.5/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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= +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-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM= +golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +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/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI= +golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= +gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/mail_templates/activate_account.html b/mail_templates/activate_account.html new file mode 100644 index 0000000..66aefdf --- /dev/null +++ b/mail_templates/activate_account.html @@ -0,0 +1,34 @@ + + + + + + + +

Привет, {{.Name}}!

+ Нажми тут для подтверждения аккаунта! +

Зачем нужно подтверждение аккаунта?

+

Без подтверждения аккаунта будет невозможно восстановить пароль, а так же комментировать на сайте!

+

Данное письмо было сгенерировано автоматически, не нужно на него отвечать.

+ + \ No newline at end of file diff --git a/mail_templates/restore_password.html b/mail_templates/restore_password.html new file mode 100755 index 0000000..eda65f1 --- /dev/null +++ b/mail_templates/restore_password.html @@ -0,0 +1,32 @@ + + + + + + + +

Привет, {{.Name}}!

+ Нажми тут для восстановления пароля! +

Данное письмо было сгенерировано автоматически, не нужно на него отвечать.

+ + diff --git a/main.go b/main.go new file mode 100644 index 0000000..f138a1d --- /dev/null +++ b/main.go @@ -0,0 +1,55 @@ +package main + +import ( + v1 "backend/v1" + "fmt" + "log" + "net/http" + "time" + + "github.com/getsentry/sentry-go" + "github.com/gorilla/mux" +) + +var router *mux.Router +var config *v1.Config + +func init() { + v1.LoadPrivateConfig() + log.Println("Private config was loaded") + + config = v1.LoadConfig() + log.Println("Config was loaded") + log.Println(fmt.Sprintf("Initizalized CDN path at %v", config.CDNPath)) + log.Println(fmt.Sprintf("Initizalized CDN URL at %v", config.CDNUrl)) + + err := sentry.Init(sentry.ClientOptions{ + Dsn: "https://b4bdbdbf01b344178b6fd69920ab898c@sentry.io/1817643", + }) + + if err != nil { + fmt.Printf("Sentry initialization failed: %v\n", err) + } + + router = mux.NewRouter().StrictSlash(true) + v1.RegisterRouters(router) + + v1.ConnectToDB() +} + +func main() { + server := http.Server{ + Addr: fmt.Sprintf("%v:%v", config.Address, config.Port), + Handler: router, + IdleTimeout: 120 * time.Second, + ReadTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, + } + + log.Println(fmt.Sprintf("Staring on %v:%v", config.Address, config.Port)) + err := server.ListenAndServe() + if err != nil { + log.Println(err) + sentry.CaptureException(err) + } +} diff --git a/v1/auth_routes.go b/v1/auth_routes.go new file mode 100644 index 0000000..f56fc2c --- /dev/null +++ b/v1/auth_routes.go @@ -0,0 +1,666 @@ +package v1 + +import ( + "bytes" + "database/sql" + "encoding/base64" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "strconv" + "strings" + "time" + + "github.com/dgrijalva/jwt-go" + "github.com/getsentry/sentry-go" + "github.com/gorilla/mux" + "golang.org/x/crypto/bcrypt" +) + +type Login struct { + Login string `json:"login"` + Password string `json:"password"` + Recaptcha string `json:"recaptcha"` +} + +type Register struct { + Login string `json:"login"` + Email string `json:"email"` + Password string `json:"password"` + Recaptcha string `json:"recaptcha"` +} + +type Activate struct { + Token string `json:"token"` +} + +func RegisterAuthRoutes(r *mux.Router) { + r.HandleFunc("/auth/login", resolveGenerateNewToken).Methods("OPTIONS", "POST") + r.HandleFunc("/auth/logout", resolveDeleteToken).Methods("OPTIONS", "POST") + r.HandleFunc("/auth/register", resolveRegisterUser).Methods("OPTIONS", "POST") + r.HandleFunc("/auth/register/vk", resolveRegisterUserVK).Methods("OPTIONS", "POST") + + r.HandleFunc("/auth/activate", resolveActivateUser).Methods("OPTIONS", "POST") + r.HandleFunc("/auth/activateToken", resolveGenerateActivateToken).Methods("OPTIONS", "POST") + + r.HandleFunc("/auth/restore", resolveRestorePassword).Methods("OPTIONS", "POST") + r.HandleFunc("/auth/restoreToken", resolveGenerateRestorePassword).Methods("OPTIONS", "POST") +} + +var ( + loginAttempts = make(map[string][]time.Time) + restoreGenerations = make(map[int64]time.Time) + activationGenerations = make(map[int]time.Time) +) + +func resolveRegisterUserVK(w http.ResponseWriter, r *http.Request) { + type Body struct { + Code string `json:"code"` + } + body := new(Body) + reqBody, err := ReadBody(r) + if err != nil { + Error(w, err) + return + } + err = json.Unmarshal(reqBody, body) + if err != nil { + Error(w, err) + return + } + + req, err := http.Get("https://oauth.vk.com/access_token?client_id=7836090&client_secret=HqlMicDyJyq4E3SvsPba&redirect_uri=http://localhost:3000/vk_auth&code="+body.Code) + if err != nil { + Error(w, err) + return + } + + b, err := ioutil.ReadAll(req.Body) + if err != nil { + Error(w, err) + return + } + log.Println(string(b)) +} + +func resolveRestorePassword(w http.ResponseWriter, r *http.Request) { + type Restore struct { + Token string `json:"token"` + NewPassword string `json:"new_password"` + } + + restore := new(Restore) + body, err := ReadBody(r) + if err != nil { + w.WriteHeader(400) + Error(w, err) + } + err = json.Unmarshal(body, restore) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + tokenString, err := base64.StdEncoding.DecodeString(restore.Token) + if err != nil { + Error(w, err) + return + } + + token, err := jwt.Parse(string(tokenString), func(token *jwt.Token) (i interface{}, e error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(privateConfig.ActivateTokenSecret), nil + }) + if err != nil { + Error(w, err) + return + } + if !token.Valid { + RawError(w, "token not valid") + return + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + RawError(w, "token not valid") + return + } + + t, ok := claims["time"].(float64) + if !ok { + RawError(w, "token not valid") + return + } + + id, ok := claims["id"].(float64) + if !ok { + RawError(w, "token not valid") + return + } + + genTime := time.Unix(int64(t), 0) + if time.Since(genTime) > time.Hour*1 { + TokenExpired.Ferror(w) + return + } + + user, err := FetchUser(int(id)) + if err != nil { + Error(w, err) + return + } + samePassword := comparePasswords(restore.NewPassword, user.Password) + if samePassword { + SamePassword.Ferror(w) + return + } + + hashedPassword := hashAndSaltPassword([]byte(restore.NewPassword)) + + err = UpdatePassword(int(id), hashedPassword) + Response(w, err, "ok") +} + +func resolveGenerateRestorePassword(w http.ResponseWriter, r *http.Request) { + type Restore struct { + Email string `json:"email"` + Recaptcha string `json:"recaptcha"` + } + + var restore = &Restore{} + body, err := ReadBody(r) + if err != nil { + w.WriteHeader(400) + Error(w, err) + } + err = json.Unmarshal(body, restore) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + valid, err := validateRecaptcha(restore.Recaptcha, r.RemoteAddr) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + Error(w, err) + return + } + if !valid { + RecaptchaFailed.Ferror(w) + return + } + + user, err := FetchUserByLogin(restore.Email) + if err == sql.ErrNoRows { + NotFound.Ferror(w) + return + } + if err != nil { + Error(w, err) + return + } + + t, ok := restoreGenerations[user.ID] + if !ok { + restoreGenerations[user.ID] = time.Now() + } else { + if t.Add(time.Minute * 5).After(time.Now()) { + TooOftenGeneration.Ferror(w) + return + } + } + + restoreToken := createActivateToken(user) + var mailData = struct { + Name string + URL string + }{Name: user.Username, URL: fmt.Sprintf("%s/restore/%s", config.FrontendUrl, restoreToken)} + SendMailTemplate(user.Email, "Восстановление пароля", "restore_password.html", mailData) + Response(w, nil, "ok") +} + +func resolveActivateUser(w http.ResponseWriter, r *http.Request) { + var activate Activate + body, err := ReadBody(r) + if err != nil { + w.WriteHeader(400) + Error(w, err) + } + err = json.Unmarshal(body, &activate) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + tokenString, err := base64.StdEncoding.DecodeString(activate.Token) + if err != nil { + Error(w, err) + return + } + + token, err := jwt.Parse(string(tokenString), func(token *jwt.Token) (i interface{}, e error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(privateConfig.ActivateTokenSecret), nil + }) + if err != nil { + Error(w, err) + return + } + + if !token.Valid { + RawError(w, "token not valid") + return + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + RawError(w, "token not valid") + return + } + + t, ok := claims["time"].(float64) + if !ok { + RawError(w, "token not valid") + return + } + + id, ok := claims["id"].(float64) + if !ok { + RawError(w, "token not valid") + return + } + + genTime := time.Unix(int64(t), 0) + if time.Since(genTime) > time.Hour*1 { + TokenExpired.Ferror(w) + return + } + + user, err := FetchUser(int(id)) + if err == sql.ErrNoRows { + NotFound.Ferror(w) + return + } + if err != nil { + Error(w, err) + return + } + if user.IsActivated { + AlreadyActivated.Ferror(w) + return + } + user, err = ActivateUser(int(id)) + Response(w, err, user) +} + +func resolveRegisterUser(w http.ResponseWriter, r *http.Request) { + var reg Register + body, err := ReadBody(r) + if err != nil { + w.WriteHeader(400) + Error(w, err) + } + _ = json.Unmarshal(body, ®) + + if reg.Login == "" || reg.Password == "" || reg.Email == "" || reg.Recaptcha == "" { + w.WriteHeader(http.StatusForbidden) + RawError(w, "no") + return + } + + _, err = FetchUserByLogin(reg.Login) + if err != sql.ErrNoRows { + RawError(w, "User already register!") + return + } + + valid, err := validateRecaptcha(reg.Recaptcha, r.RemoteAddr) + if err != nil || !valid { + RecaptchaFailed.Ferror(w) + return + } + + user, err := CreateUser(reg.Login, hashAndSaltPassword([]byte(reg.Password)), reg.Email) + Response(w, err, user) + + activateToken := createActivateToken(user) + var mailData = struct { + Name string + URL string + }{Name: user.Username, URL: fmt.Sprintf("%s/activate/%s", config.FrontendUrl, activateToken)} + SendMailTemplate(user.Email, "Активируйте аккаунт", "activate_account.html", mailData) + + embeds := make([]Embed, 1) + embeds = append(embeds, Embed{ + Title: "Nix13", + Type: "rich", + Description: "Был зарегестрирован новый пользователь!", + Timestamp: time.Now(), + URL: fmt.Sprintf("https://unmei.space/user/%d", 1), + }) + err = ExecuteWebhook( + "https://discord.com/api/webhooks/790593203862241320/vGyrLPzbmtKXHLaOpVrwNwn15tBK58CrsL3rQdAiUQSbi_jBvclpfUT1SoM75j98hBKY", + WebhookContent{Embeds: embeds}, + ) + if err != nil { + sentry.CaptureException(err) + log.Println(err) + } +} + +func resolveDeleteToken(w http.ResponseWriter, _ *http.Request) { + cookie := &http.Cookie{ + Name: "token", + Value: "", + Path: "/", + SameSite: http.SameSiteLaxMode, + Expires: time.Time{}, + } + http.SetCookie(w, cookie) +} + +func resolveGenerateNewToken(w http.ResponseWriter, r *http.Request) { + var login Login + body, err := ReadBody(r) + if err != nil { + Error(w, err) + return + } + err = json.Unmarshal(body, &login) + if err != nil { + Error(w, err) + return + } + + userAttempts := append(loginAttempts[r.RemoteAddr], time.Now()) + + for index, t := range userAttempts { + if t.Add(time.Hour).Before(time.Now()) { + userAttempts = append(userAttempts[:index], userAttempts[index+1:]...) + } + } + + if len(loginAttempts[r.RemoteAddr])+1 >= 5 { + if len(login.Recaptcha) == 0 { + RecaptchaNeeded.Ferror(w) + return + } + + valid, err := validateRecaptcha(login.Recaptcha, r.RemoteAddr) + if err != nil { + Error(w, err) + return + } + if !valid { + RecaptchaFailed.Ferror(w) + return + } + } + + if len(login.Password) == 0 || len(login.Login) == 0 { + w.WriteHeader(http.StatusBadRequest) + RawError(w, "No login or/and password") + return + } + + user, err := FetchUserByLogin(login.Login) + log.Println(login.Login) + if err == sql.ErrNoRows || user == nil { + WrongPassword.Ferror(w) + return + } + + if !comparePasswords(login.Password, user.Password) { + WrongPassword.Ferror(w) + return + } + + claims := jwt.MapClaims{ + "id": user.ID, + "is_superuser": user.IsSuperuser, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString([]byte(privateConfig.AuthTokenSecret)) + if err != nil { + Error(w, err) + return + } + expires := time.Now().Add(365 * 24 * time.Hour) + cookie := &http.Cookie{ + Name: "token", + Value: tokenString, + Path: "/", + Expires: expires, + Secure: false, + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + } + http.SetCookie(w, cookie) + + params := r.URL.Query() + authType := params.Get("auth_type") + + switch authType { + case "token": + Response(w, err, tokenString) + default: + Response(w, err, user) + } +} + +func resolveGenerateActivateToken(w http.ResponseWriter, r *http.Request) { + tokenCookie, err := r.Cookie("token") + if err == http.ErrNoCookie { + w.WriteHeader(http.StatusUnauthorized) + return + } + if err != nil { + Error(w, err) + return + } + + token, err := jwt.Parse(tokenCookie.Value, func(token *jwt.Token) (i interface{}, e error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(privateConfig.AuthTokenSecret), nil + }) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + Error(w, err) + return + } + + if !token.Valid { + w.WriteHeader(http.StatusInternalServerError) + RawError(w, "Unhandled exception!") + return + } + + claims := token.Claims.(jwt.MapClaims) + id := int(claims["id"].(float64)) + + t, ok := activationGenerations[id] + if !ok { + activationGenerations[id] = time.Now() + } else { + if t.Add(time.Minute * 5).After(time.Now()) { + TooOftenGeneration.Ferror(w) + return + } + } + + user, err := FetchUser(id) + if err != nil { + Error(w, err) + return + } + if user.IsActivated { + AlreadyActivated.Ferror(w) + return + } + activateToken := createActivateToken(user) + var mailData = struct { + Name string + URL string + }{Name: user.Username, URL: fmt.Sprintf("%s/activate/%s", config.FrontendUrl, activateToken)} + SendMailTemplate(user.Email, "Активируйте аккаунт", "activate_account.html", mailData) + Response(w, nil, "ok") +} + +func hashAndSaltPassword(password []byte) string { + hash, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost) + if err != nil { + log.Println(err) + sentry.CaptureException(err) + } + return string(hash) +} + +func comparePasswords(password, hashedPassword string) bool { + return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) == nil +} + +func createActivateToken(user *User) string { + claims := jwt.MapClaims{ + "id": user.ID, + "time": time.Now().Unix(), + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, _ := token.SignedString([]byte(privateConfig.ActivateTokenSecret)) + hashed := base64.StdEncoding.EncodeToString([]byte(tokenString)) + return hashed +} + +func hasPermission(user *User, permission string) bool { + if user == nil { + return false + } + permissions := strings.Split(user.Group.Permissions, ",") + if user.IsSuperuser || user.Group.IsSuperuser { + return true + } + for _, perm := range permissions { + if perm == permission { + return true + } + } + return false +} + +func validateRecaptcha(token string, ip string) (bool, error) { + url := fmt.Sprintf("https://www.google.com/recaptcha/api/siteverify?secret=%s&response=%s&remoteip=%s", privateConfig.RecaptchaSecret, token, ip) + + resp, err := http.Post(url, "application/json", bytes.NewBuffer([]byte(""))) + if err != nil { + return false, err + } + + validateBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return false, err + } + err = resp.Body.Close() + if err != nil { + return false, err + } + var validate map[string]interface{} + err = json.Unmarshal(validateBody, &validate) + if err != nil { + return false, err + } + if !validate["success"].(bool) { + return false, nil + } + return true, nil +} + +func JWTMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet || r.Method == http.MethodOptions || strings.HasPrefix(r.URL.Path, "/v1/auth") { + next.ServeHTTP(w, r) + return + } + + tokenCookie, err := r.Cookie("token") + if err == http.ErrNoCookie { + w.WriteHeader(http.StatusUnauthorized) + return + } + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + Error(w, err) + return + } + tokenString := tokenCookie.Value + + if len(tokenString) == 0 { + w.WriteHeader(http.StatusUnauthorized) + return + } + + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (i interface{}, e error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(privateConfig.AuthTokenSecret), nil + }) + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + Error(w, err) + return + } + + if !token.Valid { + w.WriteHeader(http.StatusInternalServerError) + RawError(w, "Unhandled exception!") + return + } + + if strings.HasSuffix(r.URL.Path, "comments") { + next.ServeHTTP(w, r) + return + } + + claims := token.Claims.(jwt.MapClaims) + + user, err := FetchCurrentUser(r) + if err != nil { + Error(w, err) + return + } + + if strings.HasPrefix(r.URL.Path, "/v1/users") { + vars := mux.Vars(r) + id, _ := strconv.Atoi(vars["userId"]) + + if !strings.HasPrefix(r.URL.Path, "/v1/users/me") && id != int(claims["id"].(float64)) && !hasPermission(user, "users") { + NoAccess.Ferror(w) + return + } + } else { + path := strings.Split(r.URL.Path, "/") + if len(path) < 3 { + RawError(w, "something wrong") + return + } + category := path[2] + + if !hasPermission(user, category) { + NoAccess.Ferror(w) + return + } + } + + next.ServeHTTP(w, r) + }) +} diff --git a/v1/characters_routes.go b/v1/characters_routes.go new file mode 100755 index 0000000..f7a29b1 --- /dev/null +++ b/v1/characters_routes.go @@ -0,0 +1,88 @@ +package v1 + +import ( + "database/sql" + "encoding/json" + "net/http" + "strconv" + + "github.com/gorilla/mux" +) + +func RegisterCharactersRoutes(r *mux.Router) { + r.HandleFunc("/characters", resolveCharacters).Methods("GET", "OPTIONS", "POST") + r.HandleFunc("/characters/{id:[0-9]+}", resolveCharacter).Methods("GET", "OPTIONS", "PUT", "DELETE") + r.HandleFunc("/characters/{id:[0-9]+}/novels", resolveCharacterNovels).Methods("GET", "OPTIONS") +} + +func resolveCharacters(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + characters, err := FetchCharacters() + Response(w, err, characters) + } else if r.Method == http.MethodPost { + Response(w, nil, "todo") + } +} + +func resolveCharacter(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, _ := strconv.Atoi(vars["id"]) + + if r.Method == http.MethodGet { + char, err := FetchCharacter(id) + Response(w, err, char) + } else if r.Method == http.MethodPut { + user, err := FetchCurrentUser(r) + if err != nil { + NoAccess.Ferror(w) + return + } + + hasPerm := hasPermission(user, "characters") + if !hasPerm { + NoAccess.Ferror(w) + return + } + + body, err := ReadBody(r) + if err != nil { + Error(w, err) + return + } + character := new(Character) + err = json.Unmarshal(body, character) + if err != nil { + Error(w, err) + return + } + + character, err = UpdateCharacter(character) + Response(w, err, character) + } else if r.Method == http.MethodDelete { + user, err := FetchCurrentUser(r) + if err != nil { + NoAccess.Ferror(w) + return + } + + hasPerm := hasPermission(user, "characters") + if !hasPerm { + NoAccess.Ferror(w) + return + } + + err = DeleteCharacter(id) + if err == sql.ErrNoRows { + NotFound.Ferror(w) + return + } + Response(w, err, "ok") + } +} + +func resolveCharacterNovels(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, _ := strconv.Atoi(vars["id"]) + novels, err := FetchCharacterNovels(id) + Response(w, err, novels) +} diff --git a/v1/characters_sql.go b/v1/characters_sql.go new file mode 100755 index 0000000..e404441 --- /dev/null +++ b/v1/characters_sql.go @@ -0,0 +1,44 @@ +package v1 + +type Character struct { + ID int32 `json:"id"` + OriginalName string `db:"original_name" json:"original_name"` + LocalizedName string `db:"localized_name" json:"localized_name"` + Description string `json:"description"` + Image string `json:"image"` + Main bool `json:"main"` +} + +func FetchCharacters() ([]*Character, error) { + var characters = make([]*Character, 0) + err := database.Select(&characters, `select * from characters;`) + return characters, err +} + +func FetchCharacter(id int) (*Character, error) { + var char = &Character{} + err := database.Get(char, `select * from characters where id=$1;`, id) + return char, err +} +func UpdateCharacter(character *Character) (*Character, error) { + rows, err := database.NamedQuery("update characters set original_name=:original_name, localized_name=:localized_name, description=:description, image=:image where id=:id returning *;", character) + if err != nil { + return nil, err + } + + char := new(Character) + if rows.Next() { + err = rows.StructScan(char) + } + return char, err +} +func DeleteCharacter(id int) error { + _, err := database.Exec("delete from characters where id=$1;", id) + return err +} + +func FetchCharacterNovels(id int) ([]Novel, error) { + var novels = make([]Novel, 0) + err := database.Select(&novels, `select id, original_name, localized_name, coalesce(n.image, '') as image from character_novel join novels n on (n.id=character_novel.novel_id) where character_id=$1;`, id) + return novels, err +} diff --git a/v1/club_routes.go b/v1/club_routes.go new file mode 100644 index 0000000..3957d45 --- /dev/null +++ b/v1/club_routes.go @@ -0,0 +1,40 @@ +package v1 + +import ( + "github.com/gorilla/mux" + "net/http" + "strconv" +) + +func RegisterClubRoutes(r *mux.Router) { + r.HandleFunc("/clubs", resolveClubs).Methods("GET", "OPTIONS", "POST") + r.HandleFunc("/clubs/{id:[0-9]+}", resolveClub).Methods("GET", "OPTIONS") +} + +func resolveClubs(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + userId, err := GetCurrentUserID(r) + if err != nil { + Error(w, err) + return + } + clubs, err := GetClubs(userId) + Response(w, err, clubs) + } +} + +func resolveClub(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + rawId, _ := strconv.Atoi(vars["id"]) + id := uint16(rawId) + + if r.Method == http.MethodGet { + userId, err := GetCurrentUserID(r) + if err != nil { + Error(w, err) + return + } + club, err := GetClub(id, userId) + Response(w, err, club) + } +} diff --git a/v1/club_sql.go b/v1/club_sql.go new file mode 100644 index 0000000..2cbd06a --- /dev/null +++ b/v1/club_sql.go @@ -0,0 +1,21 @@ +package v1 + +type Club struct { + ID uint16 `json:"id"` + Name string `json:"name"` + Avatar string `json:"avatar"` + Owner int `json:"-"` + IsPrivate bool `json:"is_private" db:"is_private"` +} + +func GetClubs(userId uint64) ([]*Club, error) { + clubs := make([]*Club, 0) + err := database.Select(&clubs, "select clubs.* from clubs join club_member cm on clubs.id = cm.club_id where not is_private or cm.user_id=$1;", userId) + return clubs, err +} + +func GetClub(id uint16, userId uint64) (*Club, error) { + club := new(Club) + err := database.Get(club, "select clubs.* from clubs join club_member cm on clubs.id = cm.club_id where club_id=$1 and (not is_private or cm.user_id=$2);", id, userId) + return club, err +} diff --git a/v1/config.go b/v1/config.go new file mode 100644 index 0000000..aea6470 --- /dev/null +++ b/v1/config.go @@ -0,0 +1,68 @@ +package v1 + +import ( + "encoding/json" + "io/ioutil" + "log" + "net/smtp" +) + +type Config struct { + Address string `json:"address"` + Port string `json:"port"` + DevFrontendUrl string `json:"dev_frontend_url"` + FrontendUrl string `json:"frontend_url"` + CDNPath string `json:"cdn_path"` + CDNUrl string `json:"cdn_url"` + Version string `json:"version"` + Build uint32 `json:"build"` + Debug bool `json:"debug"` +} + +type PrivateConfig struct { + AuthTokenSecret string `json:"auth_token"` + ActivateTokenSecret string `json:"activate_token"` + RecaptchaSecret string `json:"recaptcha"` + Database struct { + User string `json:"user"` + Password string `json:"password"` + URL string `json:"url"` + Name string `json:"name"` + } `json:"database"` + Email struct { + Username string `json:"username"` + Password string `json:"password"` + Host string `json:"host"` + } `json:"email"` +} + +var ( + config *Config + privateConfig *PrivateConfig + mailAuth smtp.Auth +) + +func LoadConfig() *Config { + data, err := ioutil.ReadFile("config.json") + if err != nil { + log.Fatalln(err) + } + err = json.Unmarshal(data, &config) + if err != nil { + log.Fatalln(err) + } + return config +} + +func LoadPrivateConfig() { + data, err := ioutil.ReadFile("private.config.json") + if err != nil { + log.Fatalln(err) + } + err = json.Unmarshal(data, &privateConfig) + if err != nil { + log.Fatalln(err) + } + + mailAuth = smtp.PlainAuth("", privateConfig.Email.Username, privateConfig.Email.Password, privateConfig.Email.Host) +} diff --git a/v1/discord.go b/v1/discord.go new file mode 100644 index 0000000..d995d99 --- /dev/null +++ b/v1/discord.go @@ -0,0 +1,31 @@ +package v1 + +import ( + "bytes" + "encoding/json" + "net/http" + "time" +) + +type Embed struct { + Title string `json:"title"` + Type string `json:"type"` + Description string `json:"description"` + URL string `json:"url"` + Timestamp time.Time `json:"timestamp"` + Color uint32 `json:"color"` +} + +type WebhookContent struct { + Content string `json:"content"` + Embeds []Embed `json:"embeds"` +} + +func ExecuteWebhook(url string, content WebhookContent) error { + body, err := json.Marshal(content) + if err != nil { + return err + } + _, err = http.Post(url, "application/json", bytes.NewReader(body)) + return err +} diff --git a/v1/errors.go b/v1/errors.go new file mode 100755 index 0000000..ebfeed6 --- /dev/null +++ b/v1/errors.go @@ -0,0 +1,107 @@ +package v1 + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/getsentry/sentry-go" +) + +type ApiError struct { + Code uint8 `json:"code"` + Text string `json:"text"` + HTTPCode uint16 `json:"-"` +} + +var ( + WrongPassword = ApiError{ + Code: 1, + Text: "Wrong login and/or password!", + HTTPCode: http.StatusBadRequest, + } + RecaptchaFailed = ApiError{ + Code: 2, + Text: "Recaptcha check failed!", + HTTPCode: http.StatusBadRequest, + } + NoToken = ApiError{ + Code: 3, + Text: "No authorization token passed!", + HTTPCode: http.StatusUnauthorized, + } + NoAccess = ApiError{ + Code: 4, + Text: "No Access!", + HTTPCode: http.StatusForbidden, + } + AlreadyActivated = ApiError{ + Code: 5, + Text: "Account already activated!", + HTTPCode: http.StatusBadRequest, + } + TokenExpired = ApiError{ + Code: 6, + Text: "Activate token has expired", + HTTPCode: 400, + } + TooOftenGeneration = ApiError{ + Code: 7, + Text: "Too often generating links", + HTTPCode: 400, + } + SamePassword = ApiError{ + Code: 8, + Text: "Same password", + HTTPCode: 400, + } + RecaptchaNeeded = ApiError{ + Code: 9, + Text: "Recaptcha needed", + HTTPCode: 0, + } + NotFound = ApiError{ + Code: 100, + Text: "Not found!", + HTTPCode: http.StatusNotFound, + } + TooManyRequests = ApiError{ + Code: 101, + Text: "Too many requests", + HTTPCode: 429, + } + Unauthorized = ApiError{ + Code: 102, + Text: "Unauthorized", + HTTPCode: http.StatusUnauthorized, + } +) + +func (e ApiError) Marshall() string { + data, err := json.Marshal(e) + if err != nil { + fmt.Println(err) + sentry.CaptureException(err) + return "" + } + return string(data) +} + +func (e ApiError) Ferror(w http.ResponseWriter) { + const baseError = `{"error":true,"error_data":%v}` + if e.HTTPCode > 0 { + w.WriteHeader(int(e.HTTPCode)) + } + + data, err := json.Marshal(e) + if err != nil { + fmt.Println(err) + sentry.CaptureException(err) + } + + _, err = fmt.Fprintf(w, baseError, string(data)) + if err != nil { + fmt.Println(err) + sentry.CaptureException(err) + } +} diff --git a/v1/games_routes.go b/v1/games_routes.go new file mode 100644 index 0000000..e1f7b29 --- /dev/null +++ b/v1/games_routes.go @@ -0,0 +1,19 @@ +package v1 + +import ( + "github.com/gorilla/mux" + "net/http" + "strconv" +) + +func RegisterGamesRoutes(r *mux.Router) { + r.HandleFunc("/games/{id:[0-9]+}", resolveGame) +} + +func resolveGame(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, _ := strconv.Atoi(vars["id"]) + game, err := FetchGame(int32(id)) + Response(w, err, game) +} + diff --git a/v1/games_sql.go b/v1/games_sql.go new file mode 100644 index 0000000..9ceaede --- /dev/null +++ b/v1/games_sql.go @@ -0,0 +1,18 @@ +package v1 + +import "time" + +type Game struct { + ID int32 `json:"id"` + OriginalName string `json:"original_name" db:"original_name"` + LocalizedName string `json:"localized_name" db:"localized_name"` + Description string `json:"description"` + Image string `json:"image"` + ReleaseDate time.Time `json:"release_date" db:"release_date"` +} + +func FetchGame(id int32) (Game, error) { + var game Game + err := database.Get(&game, "select * from games where id = $1 limit 1;", id) + return game, err +} \ No newline at end of file diff --git a/v1/general_routes.go b/v1/general_routes.go new file mode 100644 index 0000000..27a06b5 --- /dev/null +++ b/v1/general_routes.go @@ -0,0 +1,218 @@ +package v1 + +import ( + "crypto/md5" + "database/sql" + "encoding/hex" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "strconv" + "strings" + + "github.com/dgrijalva/jwt-go" + "github.com/getsentry/sentry-go" + "github.com/gorilla/mux" +) + +var lpServer *LongPollServer + +func RegisterRouters(r *mux.Router) { + sub := r.PathPrefix("/v1").Subrouter() + + lpServer = InitLongPollServer() + + sub.Use(apiMiddleware) + sub.Use(LimitMiddleware) + go CleanupVisitors() + sub.Use(JWTMiddleware) + + sub.HandleFunc("/version", func(w http.ResponseWriter, _ *http.Request) { + Response(w, nil, map[string]string{ + "version": config.Version, + "build": strconv.Itoa(int(config.Build)), + }) + }) + + RegisterAuthRoutes(sub) + RegisterUsersRoutes(sub) + RegisterNovelsRoutes(sub) + RegisterCharactersRoutes(sub) + RegisterGenresRoutes(sub) + RegisterGamesRoutes(sub) + RegisterClubRoutes(sub) + RegisterGroupsRoutes(sub) + RegisterNewsRoutes(sub) + RegisterTokensRouter(sub) + RegisterLongPollRoutes(sub) + + if config.Debug { + err := sub.Walk(func(route *mux.Route, _ *mux.Router, _ []*mux.Route) error { + path, err := route.GetPathTemplate() + if err != nil { + return err + } + log.Printf("loading path %s\n", path) + return nil + }) + + if err != nil { + log.Fatalln(err) + } + } +} + +func Response(w http.ResponseWriter, err error, data interface{}) { + const baseResponse = `{"error":false,"data":%v}` + + if err != nil { + if err == sql.ErrNoRows { + NotFound.Ferror(w) + } else { + w.WriteHeader(http.StatusInternalServerError) + Error(w, err) + } + } else { + j, err := json.Marshal(&data) + if err != nil { + fmt.Println(err) + sentry.CaptureException(err) + return + } + + _, err = fmt.Fprintf(w, baseResponse, string(j)) + if err != nil { + fmt.Println(err) + sentry.CaptureException(err) + } + } +} + +func ResponsePagination(w http.ResponseWriter, err error, data interface{}, pagination interface{}) { + const baseResponse = `{"error":false,"pagination":%v,"data":%v}` + + if err != nil { + if err == sql.ErrNoRows { + NotFound.Ferror(w) + } else { + w.WriteHeader(http.StatusInternalServerError) + Error(w, err) + } + } else { + jsonData, err := json.Marshal(&data) + if err != nil { + fmt.Println(err) + sentry.CaptureException(err) + return + } + + jsonPagination, err := json.Marshal(&pagination) + if err != nil { + fmt.Println(err) + sentry.CaptureException(err) + return + } + + _, err = fmt.Fprintf(w, baseResponse, string(jsonPagination), string(jsonData)) + if err != nil { + fmt.Println(err) + sentry.CaptureException(err) + } + } +} + +func RawError(w http.ResponseWriter, e string) { + const baseError = `{"error":true,"error_data":{"code":-1,"text":"%v"}}` + + _, err := fmt.Fprintf(w, baseError, strings.ReplaceAll(e, `"`, `\"`)) + if err != nil { + fmt.Println(err) + sentry.CaptureException(err) + } +} + +func Error(w http.ResponseWriter, e error) { + const baseError = `{"error":true,"error_data":{"code":-1,"text":"%v"}}` + + _, err := fmt.Fprintf(w, baseError, strings.ReplaceAll(e.Error(), `"`, `\"`)) + if err != nil { + fmt.Println(err) + sentry.CaptureException(err) + } +} + +func JSON(w http.ResponseWriter, data string) { + _, err := fmt.Fprint(w, data) + if err != nil { + fmt.Println(err) + sentry.CaptureException(err) + } +} + +func ReadBody(r *http.Request) ([]byte, error) { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, err + } + + err = r.Body.Close() + return body, err +} + +func MD5(s string) string { + hash := md5.New() + hash.Write([]byte(s)) + return hex.EncodeToString(hash.Sum(nil)) +} + +func GetCurrentUserID(r *http.Request) (uint64, error) { + tokenCookie, err := r.Cookie("token") + if err != nil { + return 0, err + } + token, err := jwt.Parse(tokenCookie.Value, func(token *jwt.Token) (interface{}, error) { + if token.Method != jwt.SigningMethodHS256 { + return nil, fmt.Errorf("invalid singning method: %s", token.Method) + } + return []byte(privateConfig.AuthTokenSecret), nil + }) + if err != nil { + return 0, err + } + claims := token.Claims.(jwt.MapClaims) + id := int(claims["id"].(float64)) + return uint64(id), nil +} + +func apiMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Dev") == "1" { + w.Header().Add("Access-Control-Allow-Origin", config.DevFrontendUrl) + } else { + w.Header().Add("Access-Control-Allow-Origin", config.FrontendUrl) + } + w.Header().Add("Access-Control-Allow-Credentials", "true") + w.Header().Add("Content-Type", "application/json; charset=utf-8") + //w.Header().Add("Cache-Control", "max-age=60") + + if r.Method == http.MethodOptions { + w.Header().Add("Access-Control-Allow-Headers", "Content-Type, Date, Content-Length, Location, dev") + w.Header().Add("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, OPTIONS") + return + } + + if database == nil { + log.Println("db not initialized") + return + } + err := database.Ping() + if err != nil && strings.Contains(err.Error(), "broken pipe") { + ConnectToDB() + log.Println("connection to db reopened") + } + + next.ServeHTTP(w, r) + }) +} diff --git a/v1/general_sql.go b/v1/general_sql.go new file mode 100644 index 0000000..29e16d6 --- /dev/null +++ b/v1/general_sql.go @@ -0,0 +1,23 @@ +package v1 + +import ( + "fmt" + "github.com/getsentry/sentry-go" + "github.com/jmoiron/sqlx" + _ "github.com/lib/pq" + "log" +) + +var database *sqlx.DB + +func ConnectToDB() { + var err error + conf := privateConfig.Database + url := fmt.Sprintf("host=%s port=5432 user=%s password=%s dbname=%s", conf.URL, conf.User, conf.Password, conf.Name) + database, err = sqlx.Open("postgres", url) + if err != nil { + sentry.CaptureException(err) + log.Fatalln(err) + } + log.Printf("Connected to DB with name %v as %v on %v\n", conf.Name, conf.User, conf.URL) +} diff --git a/v1/genres_routes.go b/v1/genres_routes.go new file mode 100755 index 0000000..836cc0f --- /dev/null +++ b/v1/genres_routes.go @@ -0,0 +1,38 @@ +package v1 + +import ( + "github.com/gorilla/mux" + "net/http" + "strconv" +) + +func RegisterGenresRoutes(r *mux.Router) { + r.HandleFunc("/genres", resolveGenres).Methods("GET", "OPTIONS", "POST") // GET, POST + r.HandleFunc("/genres/{id:[0-9]+}", resolveGenre).Methods("GET", "OPTIONS", "PUT", "DELETE") // GET, PUT, DELETE + r.HandleFunc("/genres/{id:[0-9]+}/novels", resolveGenreNovels).Methods("GET", "OPTIONS") // GET +} + +func resolveGenres(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + genres, err := FetchGenres() + Response(w, err, genres) + } else if r.Method == http.MethodPost { + RawError(w, "method not implemented") + } +} + +func resolveGenre(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, _ := strconv.Atoi(vars["id"]) + + genre, err := FetchGenre(id) + Response(w, err, genre) +} + +func resolveGenreNovels(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, _ := strconv.Atoi(vars["id"]) + + novels, err := FetchGenreNovels(id) + Response(w, err, novels) +} diff --git a/v1/genres_sql.go b/v1/genres_sql.go new file mode 100644 index 0000000..3d17879 --- /dev/null +++ b/v1/genres_sql.go @@ -0,0 +1,26 @@ +package v1 + +// TODO +type Genre struct { + ID int16 `json:"id"` + Name string `json:"name"` + LocalizedName string `json:"localized_name" db:"localized_name"` +} + +func FetchGenres() ([]*Genre, error) { + var genres = make([]*Genre, 0) + err := database.Select(&genres, "select * from genres;") + return genres, err +} + +func FetchGenre(id int) (*Genre, error) { + var genre = &Genre{} + err := database.Get(genre, "select * from genres where id = $1;", id) + return genre, err +} + +func FetchGenreNovels(id int) ([]*Novel, error) { + var novels = make([]*Novel, 0) + err := database.Select(&novels, "select novels.* from novel_genre join novels on (novel_genre.novel_id = novels.id) where genre_id = $1;", id) + return novels, err +} diff --git a/v1/groups_routes.go b/v1/groups_routes.go new file mode 100644 index 0000000..ce7a11b --- /dev/null +++ b/v1/groups_routes.go @@ -0,0 +1,31 @@ +package v1 + +import ( + "github.com/gorilla/mux" + "net/http" + "strconv" +) + +func RegisterGroupsRoutes(r *mux.Router) { + r.HandleFunc("/groups", resolveGroups).Methods("GET", "OPTIONS", "POST") + r.HandleFunc("/groups/{id:[0-9]+}", resolveGroup).Methods("GET", "OPTIONS", "PUT", "DELETE") +} + +func resolveGroups(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + groups, err := GetGroups() + Response(w, err, groups) + } else { + w.WriteHeader(http.StatusMethodNotAllowed) + } +} + +func resolveGroup(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, _ := strconv.Atoi(vars["id"]) + + if r.Method == http.MethodGet { + group, err := GetGroup(id) + Response(w, err, group) + } +} \ No newline at end of file diff --git a/v1/groups_sql.go b/v1/groups_sql.go new file mode 100644 index 0000000..6a7e741 --- /dev/null +++ b/v1/groups_sql.go @@ -0,0 +1,23 @@ +package v1 + +type Group struct { + ID uint8 `json:"id"` + Name string `json:"name"` + Color string `json:"color"` + MaxAvatarSize int `json:"max_avatar_size" db:"max_ava_size"` + MaxAvaWeight int64 `json:"max_avatar_weight" db:"max_ava_weight"` + IsSuperuser bool `json:"is_superuser" db:"is_superuser"` + Permissions string `json:"permissions"` +} + +func GetGroups() ([]*Group, error) { + var groups = make([]*Group, 0) + err := database.Select(&groups, "select * from groups;") + return groups, err +} + +func GetGroup(id int) (*Group, error) { + var group = &Group{} + err := database.Get(group, "select * from groups where id=$1;", id) + return group, err +} \ No newline at end of file diff --git a/v1/lp.go b/v1/lp.go new file mode 100755 index 0000000..be4aaac --- /dev/null +++ b/v1/lp.go @@ -0,0 +1,89 @@ +package v1 + +import ( + "fmt" + "net/http" + "time" + + "github.com/gorilla/mux" +) + +func RegisterLongPollRoutes(r *mux.Router) { + r.HandleFunc("/lp", resolveLongPoll).Methods("GET", "OPTIONS", "POST") +} + +func resolveLongPoll(w http.ResponseWriter, r *http.Request) { + userId, err := GetCurrentUserID(r) + if err == ErrNotAuthorized { + NoToken.Ferror(w) + return + } + if err == http.ErrNoCookie { + NoToken.Ferror(w) + return + } + events, err := lpServer.GetEvents(userId) + Response(w, err, events) +} + +var NoEventsErr = fmt.Errorf("no events") + +type LongPollServer struct { + clients map[uint64]*LongPollClient +} + +type LongPollClient struct { + events []*LongPollEvent +} + +type LongPollEvent struct { + Action string `json:"action"` + TS int64 `json:"ts"` + Payload interface{} `json:"payload"` +} + +func InitLongPollServer() *LongPollServer { + return &LongPollServer{ + clients: make(map[uint64]*LongPollClient), + } +} + +func (lpServer *LongPollServer) PushEvent(payload interface{}, userId uint64, action string) { + lpClient, ok := lpServer.clients[userId] + if !ok { + lpClient = new(LongPollClient) + lpServer.clients[userId] = lpClient + } + + lpClient.events = append(lpClient.events, &LongPollEvent{ + action, + time.Now().Unix(), + payload, + }) +} + +func (lpServer *LongPollServer) PushEventToAll(payload interface{}, action string) { + for _, lpClient := range lpServer.clients { + lpClient.events = append(lpClient.events, &LongPollEvent{ + action, + time.Now().Unix(), + payload, + }) + } +} + +func (lpServer *LongPollServer) GetEvents(userId uint64) ([]*LongPollEvent, error) { + lpClient, ok := lpServer.clients[userId] + if !ok { + lpServer.clients[userId] = new(LongPollClient) + return nil, NoEventsErr + } + + if len(lpClient.events) == 0 { + return nil, NoEventsErr + } + + events := lpClient.events + lpClient.events = make([]*LongPollEvent, 0) + return events, nil +} diff --git a/v1/mail.go b/v1/mail.go new file mode 100644 index 0000000..d36730c --- /dev/null +++ b/v1/mail.go @@ -0,0 +1,44 @@ +package v1 + +import ( + "bytes" + "fmt" + "html/template" + "log" + "net/smtp" + "os" +) + +const ( + format = "From: %v\r\n" + + "To: %v\r\n" + + "Subject: %v\r\n" + + "Content-Type: text/html; charset=UTF-8\r\n" + + "MIME-version: 1.0;\r\n\r\n" + + "%v\r\n" +) + +func SendMail(to string, subject string, body string) { + msg := []byte(fmt.Sprintf(format, privateConfig.Email.Username, to, subject, body)) + err := smtp.SendMail(privateConfig.Email.Host+":587", mailAuth, privateConfig.Email.Username, []string{to}, msg) + if err != nil { + log.Println(err) + } +} + +func SendMailTemplate(to string, subject string, name string, data interface{}) { + wd, err := os.Getwd() + if err != nil { + log.Println(err) + } + t, err := template.ParseFiles(wd + `/mail_templates/` + name) + if err != nil { + log.Println(err) + } + buf := new(bytes.Buffer) + if err := t.Execute(buf, data); err != nil { + log.Println(err) + } + body := buf.String() + SendMail(to, subject, body) +} diff --git a/v1/news_routes.go b/v1/news_routes.go new file mode 100755 index 0000000..5a729bb --- /dev/null +++ b/v1/news_routes.go @@ -0,0 +1,107 @@ +package v1 + +import ( + "encoding/json" + "github.com/gorilla/mux" + "net/http" + "strconv" +) + +func RegisterNewsRoutes(r *mux.Router) { + r.HandleFunc("/news", resolveNews).Methods("GET", "OPTIONS", "POST") + r.HandleFunc("/news/{id:[0-9]+}", resolvePost).Methods("GET", "OPTIONS", "PUT", "DELETE") +} + +func resolveNews(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + q := r.URL.Query() + + offset, _ := strconv.Atoi(q.Get("offset")) + limit, _ := strconv.Atoi(q.Get("limit")) + if limit == 0 { + limit = 20 + } + + total, err := FetchTotalNews() + if err != nil { + Error(w, err) + return + } + + pagination := map[string]interface{}{ + "offset": offset, + "limit": limit, + "total": total, + } + + news, err := FetchNews(offset, limit) + ResponsePagination(w, err, news, pagination) + } else if r.Method == http.MethodPost { + body, err := ReadBody(r) + if err != nil { + Error(w, err) + return + } + + post := new(Post) + err = json.Unmarshal(body, post) + if err != nil { + Error(w, err) + return + } + + uid, err := GetCurrentUserID(r) + if err != nil { + Error(w, err) + return + } + + post.AuthorID = uid + + createdPost, err := CreatePost(post) + Response(w, err, createdPost) + } +} + +func resolvePost(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, _ := strconv.Atoi(vars["id"]) + + if r.Method == http.MethodGet { + post, err := FetchPost(id) + Response(w, err, post) + } else if r.Method == http.MethodPut { + body, err := ReadBody(r) + if err != nil { + Error(w, err) + return + } + + post, err := FetchPost(id) + if err != nil { + Error(w, err) + return + } + + err = json.Unmarshal(body, post) + if err != nil { + Error(w, err) + return + } + + err = UpdatePost(post) + if err != nil { + Error(w, err) + return + } + + w.WriteHeader(http.StatusNoContent) + } else if r.Method == http.MethodDelete { + err := DeletePost(id) + if err != nil { + Error(w, err) + return + } + w.WriteHeader(http.StatusNoContent) + } +} diff --git a/v1/news_sql.go b/v1/news_sql.go new file mode 100755 index 0000000..9534e97 --- /dev/null +++ b/v1/news_sql.go @@ -0,0 +1,51 @@ +package v1 + +import ( + "time" +) + +type Post struct { + ID uint16 `json:"id"` + Title string `json:"title"` + ShortPost string `json:"short_post" db:"short_post"` + FullPost string `json:"full_post" db:"full_post"` + Date time.Time `json:"date"` + AuthorID uint64 `json:"author_id" db:"author_id" sql:"REFERENCES users(id)"` + Author string `json:"author"` +} + +func FetchTotalNews() (int, error) { + count := 0 + err := database.Get(&count, `select count("news") from news;`) + return count, err +} +func FetchNews(offset, limit int) ([]Post, error) { + news := make([]Post, 0) + err := database.Select(&news, "select news.*, u.username as author from news inner join users u on news.author_id = u.id offset $1 limit $2;", offset, limit) + return news, err +} +func CreatePost(post *Post) (*Post, error) { + destPost := new(Post) + r, err := database.NamedQuery("insert into news (title, short_post, full_post, author_id) values (:title, :short_post, :full_post, :author_id) returning *;", post) + if err != nil { + return nil, err + } + if r.Next() { + err = r.StructScan(destPost) + } + return destPost, err +} + +func FetchPost(id int) (*Post, error) { + post := &Post{} + err := database.Get(post, "select news.*, u.username as author from news inner join users u on news.author_id = u.id where news.id = $1;", id) + return post, err +} +func UpdatePost(post *Post) error { + _, err := database.NamedExec("update news set title=:title, short_post=:short_post, full_post=:full_post where id=:id;", post) + return err +} +func DeletePost(id int) error { + _, err := database.Exec("delete from news where id=$1;", id) + return err +} diff --git a/v1/novels_routes.go b/v1/novels_routes.go new file mode 100644 index 0000000..cc61481 --- /dev/null +++ b/v1/novels_routes.go @@ -0,0 +1,239 @@ +package v1 + +import ( + "database/sql" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "path/filepath" + "strconv" + "strings" + + "github.com/gorilla/mux" +) + +func RegisterNovelsRoutes(r *mux.Router) { + r.HandleFunc("/novels", resolveNovels).Methods("GET", "OPTIONS", "POST") + r.HandleFunc("/novels/{novelId:[0-9]+}", resolveNovel).Methods("GET", "OPTIONS", "PUT", "DELETE") + r.HandleFunc("/novels/{novelId:[0-9]+}/cover", uploadNovelCover).Methods("POST", "OPTIONS") + r.HandleFunc("/novels/{novelId:[0-9]+}/comments", resolveNovelComments).Methods("GET", "OPTIONS", "POST") + r.HandleFunc("/novels/{novelId:[0-9]+}/characters", resolveNovelCharacters).Methods("GET", "OPTIONS") + r.HandleFunc("/novels/{novelId:[0-9]+}/genres", resolveNovelGenres).Methods("GET", "OPTIONS") +} + +func resolveNovels(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + sortBy, ok := r.URL.Query()["sort"] + + if !ok || len(sortBy) <= 0 { + sortBy = append(sortBy, "rating") + } + + query, ok := r.URL.Query()["q"] + if ok { + novels, err := SearchNovels(query[0]) + Response(w, err, novels) + return + } + + novels, err := FetchNovels(sortBy[0]) + Response(w, err, &novels) + } else if r.Method == http.MethodPost { + body, err := ReadBody(r) + if err != nil { + Error(w, err) + return + } + + var novel Novel + err = json.Unmarshal(body, &novel) + if err != nil { + Error(w, err) + return + } + + novel, err = CreateNovel(&novel) + if err == nil { + w.WriteHeader(http.StatusCreated) + w.Header().Add("Location", fmt.Sprintf("https://%v%v/%v", r.Host, r.URL, novel.ID)) + } + Response(w, err, &novel) + } +} + +func resolveNovelGenres(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, _ := strconv.Atoi(vars["novelId"]) + + genres, err := FetchNovelGenres(int32(id)) + Response(w, err, genres) +} + +func resolveNovelCharacters(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + novelId, _ := strconv.Atoi(vars["novelId"]) + + characters, err := FetchNovelCharacters(novelId) + Response(w, err, characters) +} + +func resolveNovel(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + novelId, _ := strconv.Atoi(vars["novelId"]) + + if r.Method == http.MethodGet { + novel, err := FetchNovel(novelId) + Response(w, err, novel) + } else if r.Method == http.MethodPut { + body, err := ReadBody(r) + if err != nil { + Error(w, err) + return + } + + novel, err := FetchNovel(novelId) + if err != nil { + Response(w, err, nil) + return + } + err = json.Unmarshal(body, novel) + if err != nil { + Error(w, err) + return + } + + novel, err = UpdateNovel(novel) + Response(w, err, novel) + } else if r.Method == http.MethodDelete { + _, err := FetchNovel(novelId) + if err == sql.ErrNoRows { + NotFound.Ferror(w) + return + } + + err = DeleteNovel(novelId) + if err != nil { + w.WriteHeader(500) + Error(w, err) + return + } + w.WriteHeader(http.StatusNoContent) + JSON(w, "") + } +} + +func uploadNovelCover(w http.ResponseWriter, r *http.Request) { + user, err := FetchCurrentUser(r) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + Error(w, err) + return + } + + if !hasPermission(user, "novel") { + NoAccess.Ferror(w) + return + } + + vars := mux.Vars(r) + novelId, _ := strconv.Atoi(vars["novelId"]) + + err = r.ParseMultipartForm(1024 * 1024 * 2) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + Error(w, err) + return + } + + file, handler, err := r.FormFile("cover") + if err != nil { + w.WriteHeader(http.StatusBadRequest) + Error(w, err) + return + } + + if !strings.HasPrefix(handler.Header["Content-Type"][0], "image") { + w.WriteHeader(http.StatusBadRequest) + RawError(w, "Wrong document type!") + return + } + + fileBytes, err := ioutil.ReadAll(file) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + Error(w, err) + return + } + err = file.Close() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + Error(w, err) + return + } + + ext := strings.Split(handler.Header["Content-Type"][0], "/")[1] + path := filepath.Join(config.CDNPath, "novels", "logo", fmt.Sprintf("%v.%v", novelId, ext)) + + err = ioutil.WriteFile(path, fileBytes, 0644) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + Error(w, err) + return + } + coverPath := fmt.Sprintf("%v/novels/logo/%v.%v", config.CDNUrl, novelId, ext) + err = UpdateNovelCover(novelId, coverPath) + Response(w, err, coverPath) +} + +func resolveNovelComments(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + novelId, _ := strconv.Atoi(vars["novelId"]) + + if r.Method == http.MethodGet { + offsetParam, ok := r.URL.Query()["offset"] + if !ok || len(offsetParam) <= 0 { + offsetParam = append(offsetParam, "0") + } + + countParam, ok := r.URL.Query()["count"] + if !ok || len(countParam) <= 0 { + countParam = append(countParam, "5") + } + + offset, _ := strconv.Atoi(offsetParam[0]) + count, _ := strconv.Atoi(countParam[0]) + + var response = make(map[string]interface{}, 2) + comments, err := FetchNovelComments(novelId) + response["count"] = len(comments) + + if offset+count > len(comments) { + comments = comments[offset:] + } else { + comments = comments[offset : offset+count] + } + response["comments"] = comments + Response(w, err, response) + } else if r.Method == http.MethodPost { + user, err := FetchCurrentUser(r) + if err != nil { + Error(w, err) + return + } + body, err := ReadBody(r) + if err != nil { + Error(w, err) + return + } + commentData := CommentNovel{NovelID: int16(novelId), UserID: user.ID} + err = json.Unmarshal(body, &commentData) + if err != nil { + Error(w, err) + return + } + + comment, err := AddNovelComment(commentData) + Response(w, err, comment) + } +} diff --git a/v1/novels_sql.go b/v1/novels_sql.go new file mode 100644 index 0000000..de9d67a --- /dev/null +++ b/v1/novels_sql.go @@ -0,0 +1,225 @@ +package v1 + +import ( + "fmt" + "strings" + "time" +) + +type Novel struct { + ID int32 `json:"id"` + OriginalName string `db:"original_name" json:"original_name"` + LocalizedName string `db:"localized_name" json:"localized_name"` + Description string `json:"description"` + Image string `json:"image"` + Rating float32 `json:"rating"` + ReleaseDate time.Time `json:"release_date" db:"release_date"` + Duration uint64 `json:"duration"` + ExitStatus string `json:"exit_status" db:"exit_status"` + IsDemo bool `json:"is_demo" db:"is_demo"` + Platforms string `json:"platforms"` + Genres []Genre `json:"genres" ignore:"1"` + Links []NovelLink `json:"links" ignore:"1"` + Walkthrough string `json:"walkthrough"` + RatingAll []RatingAll `json:"rating_all" ignore:"1"` + StatusAll []StatusAll `json:"status_all" ignore:"1"` +} + +type ShortNovel struct { + ID int32 `json:"id"` + OriginalName string `db:"original_name" json:"original_name"` + LocalizedName string `db:"localized_name" json:"localized_name"` + Description string `json:"-"` + Image string `json:"image"` + Rating float32 `json:"rating"` + ReleaseDate time.Time `json:"release_date" db:"release_date"` + Duration uint64 `json:"-"` + ExitStatus string `json:"exit_status" db:"exit_status"` + IsDemo bool `json:"is_demo" db:"is_demo"` + Platforms string `json:"-"` + Genres []Genre `json:"genres" ignore:"1"` + Links []NovelLink `json:"-" ignore:"1"` + Walkthrough string `json:"-"` + RatingAll []RatingAll `json:"rating_all" ignore:"1"` +} + +type CommentNovel struct { + ID int32 `json:"id" db:"id"` + UserID int64 `json:"user_id" db:"user_id" sql:"REFERENCES users(id)"` + NovelID int16 `json:"novel_id" db:"novel_id" sql:"REFERENCES novels(id)"` + Text string `json:"text" db:"text"` +} + +type NovelLink struct { + NovelID int32 `json:"novel_id" db:"novel_id"` + Name string `json:"name"` + Link string `json:"link"` + Type string `json:"type"` +} + +type RatingAll struct { + Mark int `json:"mark"` + Count int `json:"count"` +} + +type StatusAll struct { + Status string `json:"status"` + Count int `json:"count"` +} + +func FetchNovels(orderBy string) ([]*ShortNovel, error) { + var novels = make([]*ShortNovel, 0) + err := database.Select(&novels, fmt.Sprintf("select id, original_name, localized_name, image, rating, release_date from novels order by %v desc;", orderBy)) + if err != nil { + return nil, err + } + + tx := database.MustBegin() + for _, novel := range novels { + novel.Genres = make([]Genre, 0) + err = tx.Select(&novel.Genres, "select g.* from novel_genre join genres g on g.id = novel_genre.genre_id where novel_id=$1;", novel.ID) + if err != nil { + return nil, err + } + } + err = tx.Commit() + return novels, err +} + +func FetchNovel(id int) (*Novel, error) { + novel := new(Novel) + err := database.Get(novel, `select * from novels where id = $1;`, id) + if err != nil { + return novel, err + } + + novel.Genres = make([]Genre, 0) + err = database.Select(&novel.Genres, `select genres.* from novel_genre join genres on (novel_genre.genre_id = genres.id) where novel_id = $1`, id) + if err != nil { + return novel, err + } + + novel.RatingAll = make([]RatingAll, 0) + err = database.Select(&novel.RatingAll, `select mark, count(mark) from user_novel where novel_id=$1 and mark > 0 group by mark;`, id) + if err != nil { + return novel, err + } + + novel.StatusAll = make([]StatusAll, 0) + err = database.Select(&novel.StatusAll, `select status, count(status) from user_novel where novel_id=$1 group by status;`, id) + if err != nil { + return novel, err + } + + novel.Links = make([]NovelLink, 0) + err = database.Select(&novel.Links, "select * from novel_link where novel_id=$1;", novel.ID) + return novel, err +} +func UpdateNovel(novel *Novel) (*Novel, error) { + var newNovel = new(Novel) + setPart := "" + setPartList := make([]string, 0) + for _, i := range GetStructAndNamedForDatabase(newNovel) { + setPartList = append(setPartList, i[0]+"="+i[1]) + } + setPart = strings.Join(setPartList, ",") + rows, err := database.NamedQuery(fmt.Sprintf(`update novels set %s where id=:id returning *;`, setPart), novel) + if err != nil { + return newNovel, err + } + err = UpdateNovelGenres(novel.ID, novel.Genres) + if err != nil { + return newNovel, err + } + rows.Next() + err = rows.StructScan(&newNovel) + if err != nil { + return newNovel, err + } + genres, err := FetchNovelGenres(novel.ID) + newNovel.Genres = genres + + return newNovel, err +} +func UpdateNovelGenres(novelId int32, genres []Genre) error { + _, err := database.Exec("delete from novel_genre where novel_id=$1;", novelId) + if err != nil { + return err + } + + tx, err := database.Begin() + if err != nil { + return err + } + for _, genre := range genres { + _, err = tx.Exec("insert into novel_genre values ($1, $2);", genre.ID, novelId) + if err != nil { + return err + } + } + err = tx.Commit() + if err != nil { + return err + } + return nil +} +func UpdateNovelCover(novelId int, url string) error { + _, err := database.Exec("update novels set image=$1 where id=$2;", url, novelId) + return err +} + +func CreateNovel(novelData *Novel) (Novel, error) { + var novel Novel + rows, err := database.NamedQuery(`insert into novels (original_name, localized_name, image, description, is_demo) values (:original_name, :localized_name, :image, :description, :is_demo) returning *;`, novelData) + if rows != nil && rows.Next() { + _ = rows.StructScan(&novel) + } + return novel, err +} +func DeleteNovel(id int) error { + _, err := database.Exec("delete from novels where id = $1;", id) + return err +} +func SearchNovels(query string) ([]ShortNovel, error) { + novels := make([]ShortNovel, 0) + err := database.Select(&novels, "select * from search_novel($1) order by rating desc;", query) + if err != nil { + return novels, err + } + tx := database.MustBegin() + for _, novel := range novels { + novel.Genres = make([]Genre, 0) + err = tx.Select(&novel.Genres, "select g.* from novel_genre join genres g on g.id = novel_genre.genre_id where novel_id=$1;", novel.ID) + if err != nil { + return nil, err + } + } + err = tx.Commit() + return novels, err +} + +func FetchNovelComments(id int) ([]CommentNovel, error) { + var comments = make([]CommentNovel, 0) + err := database.Select(&comments, `select * from novel_comment where novel_id = $1;`, id) + return comments, err +} +func AddNovelComment(commentData CommentNovel) (CommentNovel, error) { + var comment CommentNovel + rows, err := database.NamedQuery(`insert into novel_comment(user_id, novel_id, text) values (:user_id, :novel_id, :text) returning *;`, commentData) + if rows != nil && rows.Next() { + _ = rows.StructScan(&comment) + } + return comment, err +} + +func FetchNovelCharacters(id int) ([]Character, error) { + var characters = make([]Character, 0) + err := database.Select(&characters, `select characters.*, character_novel.main from character_novel join characters on (characters.id=character_novel.character_id) where novel_id=$1;`, id) + return characters, err +} + +func FetchNovelGenres(id int32) ([]Genre, error) { + var genres = make([]Genre, 0) + err := database.Select(&genres, `select genres.* from novel_genre join genres on (novel_genre.genre_id = genres.id) where novel_id = $1`, id) + return genres, err +} diff --git a/v1/ratelimiter.go b/v1/ratelimiter.go new file mode 100644 index 0000000..d751992 --- /dev/null +++ b/v1/ratelimiter.go @@ -0,0 +1,63 @@ +package v1 + +import ( + "net" + "net/http" + "sync" + "time" + + "golang.org/x/time/rate" +) + +type visitor struct { + limiter *rate.Limiter + lastSeen time.Time +} + +var visitors = make(map[string]*visitor) +var mu sync.Mutex + +func CleanupVisitors() { + for { + time.Sleep(time.Minute) + + mu.Lock() + for ip, v := range visitors { + if time.Since(v.lastSeen) > time.Minute*3 { + delete(visitors, ip) + } + } + mu.Unlock() + } +} + +func getVisitor(ip string) *rate.Limiter { + mu.Lock() + defer mu.Unlock() + + v, exists := visitors[ip] + if !exists { + limiter := rate.NewLimiter(1, 20) + visitors[ip] = &visitor{ limiter, time.Now() } + return limiter + } + + v.lastSeen = time.Now() + return v.limiter +} + +func LimitMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ip, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + return + } + + limiter := getVisitor(ip) + if !limiter.Allow() { + TooManyRequests.Ferror(w) + return + } + next.ServeHTTP(w, r) + }) +} diff --git a/v1/tokens_routes.go b/v1/tokens_routes.go new file mode 100644 index 0000000..56b5185 --- /dev/null +++ b/v1/tokens_routes.go @@ -0,0 +1,38 @@ +package v1 + +import ( + "github.com/gorilla/mux" + "net/http" +) + +func RegisterTokensRouter(r *mux.Router) { + sub := r.PathPrefix("/tokens").Subrouter() + + sub.HandleFunc("/auth", auth) + sub.HandleFunc("/apps", resolveApps) + sub.HandleFunc("/apps/{id:[0-9]+}", auth) +} + +func auth(w http.ResponseWriter, r *http.Request) { + tokenCookie, err := r.Cookie("token") + if err == http.ErrNoCookie || len(tokenCookie.Value) == 0 { + Unauthorized.Ferror(w) + return + } + + +} + +func resolveApps(w http.ResponseWriter, r *http.Request) { + user, err := FetchCurrentUser(r) + if err != nil { + Error(w, err) + return + } + if !hasPermission(user, "apps") { + NoAccess.Ferror(w) + return + } + apps, err := FetchApps() + Response(w, err, apps) +} diff --git a/v1/tokens_sql.go b/v1/tokens_sql.go new file mode 100644 index 0000000..47d72b5 --- /dev/null +++ b/v1/tokens_sql.go @@ -0,0 +1,20 @@ +package v1 + +type App struct { + ID int `json:"id"` + Name string `json:"name"` + Secret string `json:"-"` + IsVerified bool `json:"is_verified" db:"is_verified"` +} + +func FetchApp(id int) (*App, error) { + var app = &App{} + err := database.Get(app, "select * from apps where id=$1;", id) + return app, err +} + +func FetchApps() ([]*App, error) { + var apps = make([]*App, 0) + err := database.Select(&apps, "select * from apps;") + return apps, err +} \ No newline at end of file diff --git a/v1/users_routes.go b/v1/users_routes.go new file mode 100755 index 0000000..c81afc3 --- /dev/null +++ b/v1/users_routes.go @@ -0,0 +1,300 @@ +package v1 + +import ( + "bytes" + "database/sql" + "encoding/json" + "fmt" + "image" + _ "image/gif" + _ "image/jpeg" + _ "image/png" + "io/ioutil" + "log" + "net/http" + "path/filepath" + "strconv" + "strings" + + "github.com/gorilla/mux" +) + +func RegisterUsersRoutes(r *mux.Router) { + r.HandleFunc("/users", resolveUsers).Methods("GET", "OPTIONS", "POST") + r.HandleFunc("/users/me", resolveCurrentUser).Methods("GET", "OPTIONS") + r.HandleFunc("/users/me/avatar", uploadAvatar).Methods("OPTIONS", "POST") + r.HandleFunc("/users/me/settings", resolveSettings).Methods("GET", "OPTIONS", "POST") + r.HandleFunc("/users/{userId:[0-9]+}", resolveUser).Methods("GET", "OPTIONS", "PUT") + r.HandleFunc("/users/{userId:[0-9]+}/novels", resolveUserNovels).Methods("GET", "OPTIONS", "POST") + r.HandleFunc("/users/{userId:[0-9]+}/novels/{novelId:[0-9]+}", resolveUserNovel).Methods("GET", "OPTIONS", "PUT", "DELETE") + r.HandleFunc("/users/{userId:[0-9]+}/novels/comments", resolveUserNovelsComments).Methods("GET", "OPTIONS", "POST") +} + +type Settings struct { + UseGravatar bool `json:"use_gravatar" db:"use_gravatar"` + Avatar string `json:"avatar"` + Theme string `json:"theme"` +} + +func resolveSettings(w http.ResponseWriter, r *http.Request) { + id, err := GetCurrentUserID(r) + if err != nil { + Error(w, err) + return + } + + if r.Method == "GET" { + settings, err := FetchUserSettings(id) + Response(w, err, settings) + } else if r.Method == "POST" { + settings, err := FetchUserSettings(id) + if err != nil { + Error(w, err) + return + } + + body, err := ReadBody(r) + if err != nil { + Error(w, err) + return + } + + err = json.Unmarshal(body, settings) + if err != nil { + Error(w, err) + return + } + + user, err := UpdateUserSettings(id, settings) + Response(w, err, user) + } +} + +func resolveUsers(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + users, err := FetchUsers() + Response(w, err, users) + } else { + RawError(w, "Work in progress...") + } +} + +func resolveCurrentUser(w http.ResponseWriter, r *http.Request) { + user, err := FetchCurrentUser(r) + if err == ErrNotAuthorized { + NoToken.Ferror(w) + return + } + if err == http.ErrNoCookie { + NoToken.Ferror(w) + return + } + Response(w, err, user) +} + +func resolveUser(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, _ := strconv.Atoi(vars["userId"]) + + if r.Method == http.MethodGet { + user, err := FetchUser(id) + Response(w, err, user) + return + } + + currentUser, err := FetchCurrentUser(r) + if err != nil && err != ErrNotAuthorized { + Error(w, err) + return + } else if err == ErrNotAuthorized || currentUser == nil { + currentUser = &User{ID: -1} + } + + if r.Method == http.MethodPut { + if currentUser.ID != int64(id) && !hasPermission(currentUser, "users") { + NoAccess.Ferror(w) + return + } + _, err := FetchUser(id) + if err != nil { + Error(w, err) + return + } + + body, err := ReadBody(r) + if err != nil { + Error(w, err) + return + } + + var u *User + err = json.Unmarshal(body, u) + if err != nil { + Error(w, err) + return + } + user, err := UpdateUser(u) + Response(w, err, user) + } +} + +func uploadAvatar(w http.ResponseWriter, r *http.Request) { + user, err := FetchCurrentUser(r) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + Error(w, err) + return + } + + err = r.ParseMultipartForm(user.Group.MaxAvaWeight) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + Error(w, err) + return + } + + file, handler, err := r.FormFile("avatar") + if err != nil { + w.WriteHeader(http.StatusBadRequest) + Error(w, err) + return + } + + if !strings.HasPrefix(handler.Header["Content-Type"][0], "image") { + w.WriteHeader(http.StatusBadRequest) + RawError(w, "Wrong document type!") + return + } + + fileBytes, err := ioutil.ReadAll(file) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + Error(w, err) + return + } + err = file.Close() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + Error(w, err) + return + } + + img, _, err := image.DecodeConfig(bytes.NewReader(fileBytes)) + if err != nil { + log.Println(err) + } + width, height := img.Width, img.Width + if width > user.Group.MaxAvatarSize && height > user.Group.MaxAvatarSize { + w.WriteHeader(http.StatusBadRequest) + RawError(w, "Image too big!") + return + } + + ext := strings.Split(handler.Header["Content-Type"][0], "/")[1] + path := filepath.Join(config.CDNPath, "users", "avatars", fmt.Sprintf("%v.%v", user.ID, ext)) + + err = ioutil.WriteFile(path, fileBytes, 0644) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + Error(w, err) + return + } + avatarPath := fmt.Sprintf("%v/users/avatars/%v.%v", config.CDNUrl, user.ID, ext) + Response(w, nil, avatarPath) +} + +func resolveUserNovels(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + userId, _ := strconv.Atoi(vars["userId"]) + + if r.Method == http.MethodGet { + novels, err := FetchUserNovels(userId) + Response(w, err, novels) + } else if r.Method == http.MethodPost { + body, err := ReadBody(r) + if err != nil { + Error(w, err) + return + } + var novel = &UserNovels{} + err = json.Unmarshal(body, novel) + if err != nil { + Error(w, err) + return + } + + novel.UserID = int64(userId) + + novel, err = CreateUserNovel(novel) + if err == nil { + w.WriteHeader(http.StatusCreated) + w.Header().Add("Location", fmt.Sprintf("https://%v%v/%v", r.Host, r.URL, novel.NovelID)) + } + Response(w, err, novel) + } +} + +func resolveUserNovel(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + userId, _ := strconv.Atoi(vars["userId"]) + novelId, _ := strconv.Atoi(vars["novelId"]) + + if r.Method == http.MethodGet { + novel, err := FetchUserNovel(userId, novelId) + Response(w, err, novel) + } else if r.Method == http.MethodPut { + _, err := FetchNovel(novelId) + if err == sql.ErrNoRows { + NotFound.Ferror(w) + return + } + + _, err = FetchUser(userId) + if err == sql.ErrNoRows { + NotFound.Ferror(w) + return + } + + novel, _ := FetchUserNovel(userId, novelId) + body, err := ReadBody(r) + if err != nil { + Error(w, err) + return + } + + if novel == nil { + NotFound.Ferror(w) + return + } + + err = json.Unmarshal(body, &novel) + if err != nil { + Error(w, err) + return + } + + updatedNovel, err := UpdateUserNovel(novel) + lpServer.PushEvent(updatedNovel, uint64(userId), "update-user-novel") + Response(w, err, updatedNovel) + } else if r.Method == http.MethodDelete { + _, err := FetchUserNovel(userId, novelId) + if err == sql.ErrNoRows { + NotFound.Ferror(w) + return + } + + err = DeleteUserNovel(userId, novelId) + if err != nil { + Error(w, err) + } + w.WriteHeader(http.StatusNoContent) + } +} + +func resolveUserNovelsComments(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, _ := strconv.Atoi(vars["userId"]) + + comments, err := FetchUserNovelsComments(id) + Response(w, err, comments) +} diff --git a/v1/users_sql.go b/v1/users_sql.go new file mode 100755 index 0000000..f00959f --- /dev/null +++ b/v1/users_sql.go @@ -0,0 +1,202 @@ +package v1 + +import ( + "fmt" + "net/http" + "time" + + "github.com/dgrijalva/jwt-go" +) + +type User struct { + ID int64 `json:"id"` + Username string `json:"username"` + Password string `json:"-"` + Email string `json:"-"` + RegistrationDate time.Time `json:"reg_date" db:"reg_date"` + LastSeen time.Time `json:"last_seen" db:"last_seen"` + Avatar string `json:"avatar"` + IsSuperuser bool `json:"is_superuser" db:"is_superuser"` + IsActivated bool `json:"is_activated" db:"is_activated"` + GroupID int8 `json:"-" db:"group_id"` + Group Group `json:"group"` + Cover string `json:"cover"` + UseGravatar bool `json:"use_gravatar" db:"use_gravatar"` + Theme string `json:"theme"` + IsBanned bool `json:"is_banned" db:"is_banned"` +} + +type UserNovels struct { + UserID int64 `json:"user_id" db:"user_id" sql:"REFERENCES users(id)"` + NovelID int16 `json:"novel_id" db:"novel_id" sql:"REFERENCES novels(id)"` + Mark int8 `json:"mark"` + Status string `json:"status"` +} + +type UserNovelWithInfo struct { + UserNovels + Novel +} + +var ( + ErrNotAuthorized = fmt.Errorf("%s", "not authorized") +) + +func FetchUsers() ([]*User, error) { + var users = make([]*User, 0) + err := database.Select(&users, "select * from users limit 100;") + for _, user := range users { + if user.UseGravatar { + user.Avatar = GetUserGravatar(user.Email) + } + } + return users, err +} + +func FetchUser(id int) (*User, error) { + var user = &User{} + err := database.Get(user, `select users.* from users where users.id = $1;`, id) + if err == nil { + if user.UseGravatar { + user.Avatar = GetUserGravatar(user.Email) + } + err = database.Get(&user.Group, `select * from groups where id = $1;`, user.GroupID) + } + return user, err +} +func UpdateUser(user *User) (*User, error) { + var u = &User{} + rows, err := database.NamedQuery(`update users set avatar=:avatar, username=:username, is_activated=:is_activated, use_gravatar=:use_gravatar where id=:id returning *;`, user) + if rows != nil && rows.Next() { + _ = rows.StructScan(u) + if u.UseGravatar { + u.Avatar = GetUserGravatar(u.Email) + } + } + return u, err +} +func UpdatePassword(id int, password string) error { + _, err := database.Exec("update users set password=$1 where id=$2;", password, id) + return err +} + +func FetchCurrentUser(r *http.Request) (*User, error) { + cookie, err := r.Cookie("token") + if err == http.ErrNoCookie { + return nil, ErrNotAuthorized + } + if err != nil { + return nil, err + } + if len(cookie.Value) == 0 { + return nil, ErrNotAuthorized + } + return FetchUserByToken(cookie.Value) +} + +func FetchUserByToken(tokenString string) (*User, error) { + var user = &User{} + + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (i interface{}, e error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(privateConfig.AuthTokenSecret), nil + }) + if err != nil { + return user, err + } + if !token.Valid { + return user, fmt.Errorf("token not valid") + } + if claims, ok := token.Claims.(jwt.MapClaims); ok { + err = database.Get(user, `select users.* from users where users.id = $1;`, claims["id"]) + if err == nil { + if user.UseGravatar { + user.Avatar = GetUserGravatar(user.Email) + } + err = database.Get(&user.Group, `select * from groups where id = $1;`, user.GroupID) + } + } + return user, err +} + +func FetchUserByLogin(login string) (*User, error) { + user := new(User) + err := database.Get(user, `select * from users where username = $1 or email = $1;`, login) + if err == nil { + if user.UseGravatar { + user.Avatar = GetUserGravatar(user.Email) + } + err = database.Get(&user.Group, `select * from groups where id = $1;`, user.GroupID) + } + return user, err +} + +func FetchUserNovels(id int) ([]*UserNovelWithInfo, error) { + var novels = make([]*UserNovelWithInfo, 0) + err := database.Select(&novels, `select novels.*, user_novel.* from user_novel inner join novels on (novels.id = user_novel.novel_id) where user_id = $1;`, id) + return novels, err +} + +func CreateUserNovel(novelData *UserNovels) (*UserNovels, error) { + var novel = &UserNovels{} + rows, err := database.NamedQuery(`insert into user_novel(user_id, novel_id, mark, status) VALUES (:user_id, :novel_id, :mark, :status) returning *;`, novelData) + if rows != nil && rows.Next() { + _ = rows.StructScan(novel) + } + return novel, err +} +func FetchUserNovel(userId int, novelId int) (*UserNovels, error) { + var novel = &UserNovels{} + err := database.Get(novel, `select user_novel.* from user_novel where user_id = $1 and novel_id = $2;`, userId, novelId) + return novel, err +} +func UpdateUserNovel(novel *UserNovels) (*UserNovels, error) { + var updatedNovel = &UserNovels{} + rows, err := database.NamedQuery(`update user_novel set mark=:mark, status=:status where user_id=:user_id and novel_id=:novel_id returning *;`, novel) + _, _ = database.Exec(`update novels set rating=avg_novel($1) where id=$1;`, novel.NovelID) + if rows != nil && rows.Next() { + _ = rows.StructScan(updatedNovel) + } + return updatedNovel, err +} +func DeleteUserNovel(userId int, novelId int) error { + _, err := database.Exec("delete from user_novel where user_id = $1 and novel_id = $2;", userId, novelId) + return err +} + +func FetchUserNovelsComments(id int) ([]CommentNovel, error) { + var comments = make([]CommentNovel, 0) + err := database.Select(&comments, `select * from novel_comment where user_id = $1;`, id) + return comments, err +} + +func CreateUser(username string, password string, email string) (*User, error) { + var user = &User{} + var currentTime = time.Now() + err := database.Get(user, "insert into users(username, password, email, reg_date, last_seen) VALUES ($1, $2, $3, $4, $4) returning *;", username, password, email, currentTime) + return user, err +} +func ActivateUser(id int) (*User, error) { + var user = &User{} + err := database.Get(user, `update users set is_activated=true where id=$1 returning *;`, id) + return user, err +} + +func GetUserGravatar(email string) string { + return fmt.Sprintf("https://www.gravatar.com/avatar/%s", MD5(email)) +} + +// Settings +func FetchUserSettings(id uint64) (*Settings, error) { + settings := new(Settings) + err := database.Get(settings, "select use_gravatar, avatar, theme from users where id=$1;", id) + return settings, err +} + +func UpdateUserSettings(id uint64, settings *Settings) (*User, error) { + user := new(User) + err := database.Get(user, "update users set use_gravatar=$2, avatar=$3, theme=$4 where id=$1 returning *;", id, settings.UseGravatar, settings.Avatar, settings.Theme) + return user, err +} diff --git a/v1/utils.go b/v1/utils.go new file mode 100644 index 0000000..5351301 --- /dev/null +++ b/v1/utils.go @@ -0,0 +1,65 @@ +package v1 + +import ( + "reflect" + "strings" +) + +func StringOrDefault(a, b string) string { + if len(a) == 0 { + return b + } + return a +} + +func GetStructForDatabase(s interface{}) []string { + out := make([]string, 0) + el := reflect.ValueOf(s).Elem() + for i := 0; i < el.NumField(); i++ { + t := el.Type().Field(i) + tag := t.Tag + db := tag.Get("db") + if len(db) == 0 { + out = append(out, strings.ToLower(t.Name)) + } else { + out = append(out, db) + } + } + return out +} + +func GetNamedForDatabase(s interface{}) []string { + out := make([]string, 0) + el := reflect.ValueOf(s).Elem() + for i := 0; i < el.NumField(); i++ { + t := el.Type().Field(i) + tag := t.Tag + db := tag.Get("db") + if len(db) == 0 { + out = append(out, ":"+strings.ToLower(t.Name)) + } else { + out = append(out, ":"+db) + } + } + return out +} + +func GetStructAndNamedForDatabase(s interface{}) [][]string { + out := make([][]string, 0) + el := reflect.ValueOf(s).Elem() + for i := 0; i < el.NumField(); i++ { + t := el.Type().Field(i) + tag := t.Tag + ignore := tag.Get("ignore") + if ignore != "1" { + db := tag.Get("db") + if len(db) == 0 { + out = append(out, []string{strings.ToLower(t.Name), ":" + strings.ToLower(t.Name)}) + } else { + out = append(out, []string{db, ":" + db}) + } + } + } + return out + +}