Author | Jakob Wakeling <[email protected]> |
Date | 2023-07-17 09:54:54 |
Commit | ae5fc19f6ebb7260278962f9099a6f7aa4b6d577 |
Implement user password authentication
Diffstat
A | .gitignore | | | 2 | ++ |
A | README.md | | | 8 | ++++++++ |
A | go.mod | | | 31 | +++++++++++++++++++++++++++++++ |
A | go.sum | | | 130 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | main.go | | | 76 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | res/admin_user_index.html | | | 31 | +++++++++++++++++++++++++++++++ |
A | res/repo_create.html | | | 20 | ++++++++++++++++++++ |
A | res/repo_index.html | | | 31 | +++++++++++++++++++++++++++++++ |
A | res/res.go | | | 21 | +++++++++++++++++++++ |
A | res/style.css | | | 8 | ++++++++ |
A | res/user_create.html | | | 22 | ++++++++++++++++++++++ |
A | res/user_login.html | | | 18 | ++++++++++++++++++ |
A | src/admin.go | | | 114 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | src/auth.go | | | 105 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | src/goit.go | | | 77 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | src/repo.go | | | 118 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | src/user.go | | | 121 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | src/util.go | | | 34 | ++++++++++++++++++++++++++++++++++ |
18 files changed, 967 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2cc5ddb --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.vscode/ +/bin/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..f334d0d --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# Goit + +A simple and lightweight Git web server. + +## Meta + +Copyright (C) 2023, Jakob Wakeling +All rights reserved. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7974e86 --- /dev/null +++ b/go.mod @@ -0,0 +1,31 @@ +module github.com/Jamozed/Goit + +go 1.20 + +require ( + github.com/go-git/go-git/v5 v5.7.0 + github.com/gorilla/mux v1.8.0 + github.com/mattn/go-sqlite3 v1.14.17 + golang.org/x/crypto v0.9.0 +) + +require ( + github.com/Microsoft/go-winio v0.5.2 // indirect + github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 // indirect + github.com/acomagu/bufpipe v1.0.4 // indirect + github.com/cloudflare/circl v1.3.3 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.4.1 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/imdario/mergo v0.3.15 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/sergi/go-diff v1.1.0 // indirect + github.com/skeema/knownhosts v1.1.1 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sys v0.8.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2b14a0d --- /dev/null +++ b/go.sum @@ -0,0 +1,130 @@ +github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 h1:ZK3C5DtzV2nVAQTx5S5jQvMeDqWtD1By5mOoyY/xJek= +github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE= +github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= +github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= +github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4= +github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8= +github.com/go-git/go-git/v5 v5.7.0 h1:t9AudWVLmqzlo+4bqdf7GY+46SUuRsx59SboFxkq2aE= +github.com/go-git/go-git/v5 v5.7.0/go.mod h1:coJHKEOk5kUClpsNlXrUvPrDxY3w3gjHvhcZd8Fodw8= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= +github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +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/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= +github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= +github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.1.1 h1:MTk78x9FPgDFVFkDLTrsnnfCJl7g1C/nnKvePgrIngE= +github.com/skeema/knownhosts v1.1.1/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo= +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.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..6110bde --- /dev/null +++ b/main.go @@ -0,0 +1,76 @@ +// main.go +// Copyright (C) 2023, Jakob Wakeling +// All rights reserved. + +package main + +import ( + "log" + "net/http" + "time" + + "github.com/Jamozed/Goit/res" + goit "github.com/Jamozed/Goit/src" + "github.com/gorilla/mux" +) + +func main() { + g, err := goit.InitGoit() + if err != nil { + log.Fatalln("[InitGoit]", err.Error()) + } else { + defer g.Close() + } + + mx := mux.NewRouter() + mx.StrictSlash(true) + + mx.Path("/").HandlerFunc(g.HandleIndex) + mx.Path("/user/login").Methods("GET", "POST").HandlerFunc(g.HandleUserLogin) + mx.Path("/user/logout").Methods("GET", "POST").HandlerFunc(g.HandleUserLogout) + // mx.Path("/user/settings").Methods("GET").HandlerFunc() + mx.Path("/repo/create").Methods("GET", "POST").HandlerFunc(g.HandleRepoCreate) + // mx.Path("/repo/delete").Methods("POST").HandlerFunc() + // mx.Path("/admin/settings").Methods("GET").HandlerFunc() + mx.Path("/admin/user").Methods("GET").HandlerFunc(g.HandleAdminUserIndex) + // mx.Path("/admin/repos").Methods("GET").HandlerFunc() + mx.Path("/admin/user/create").Methods("GET", "POST").HandlerFunc(g.HandleAdminUserCreate) + // mx.Path("/admin/user/edit").Methods("GET", "POST").HandlerFunc() + + rm := mx.Path("/{repo}/").Subrouter() + // rm.Path("/").Methods("GET").HandlerFunc() + // rm.Path("/log").Methods("GET").HandlerFunc() + // rm.Path("/tree").Methods("GET").HandlerFunc() + // rm.Path("/refs").Methods("GET").HandlerFunc() + + mx.Path("/static/style.css").Methods(http.MethodGet).HandlerFunc(handleStyle) + + mx.PathPrefix("/").HandlerFunc(http.NotFound) + rm.PathPrefix("/").HandlerFunc(http.NotFound) + + /* Create a ticker to periodically cleanup expired sessions */ + tick := time.NewTicker(1 * time.Hour) + go func() { + for range tick.C { + goit.CleanupSessions() + } + }() + + if err := http.ListenAndServe(":8080", logHttp(mx)); err != nil { + log.Fatalln("[HTTP]", err) + } +} + +func logHttp(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Println("[HTTP]", r.RemoteAddr, r.Method, r.URL) + handler.ServeHTTP(w, r) + }) +} + +func handleStyle(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "text/css") + if _, err := w.Write([]byte(res.Style)); err != nil { + log.Println("[handleStyle]", err.Error()) + } +} diff --git a/res/admin_user_index.html b/res/admin_user_index.html new file mode 100644 index 0000000..ed84ecd --- /dev/null +++ b/res/admin_user_index.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<head> + <meta charset="UTF-8"> + <title>Users</title> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <link rel="stylesheet" type="text/css" href="/static/style.css"> +</head> +<body> + <table> + <thead> + <tr> + <td><b>ID</b></td> + <td><b>Username</b></td> + <td><b>Full Name</b></td> + <td><b>Is Admin</b></td> + <td></td> + </tr> + </thead> + <tbody> + {{range .Users}} + <tr> + <td>{{.Id}}</td> + <td>{{.Name}}</td> + <td>{{.FullName}}</td> + <td>{{.IsAdmin}}</td> + <td><a>Edit</a></td> + </tr> + {{end}} + </tbody> + </table> +</body> diff --git a/res/repo_create.html b/res/repo_create.html new file mode 100644 index 0000000..dd04bad --- /dev/null +++ b/res/repo_create.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<head> + <meta charset="UTF-8"> + <title>Create Repository</title> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <link rel="stylesheet" href="/static/style.css"> +</head> +<body> + <form action="/repo/create" method="post"> + <label for="reponame">Name:</label> + <input type="text" name="reponame"><br> + <label for="visibility">Visibility:</label> + <select name="visibility"> + <option value="public">Public</option> + <option value="private">Private</option> + </select><br> + <input type="submit" value="Create"> + </form> + <p>{{.Msg}}</p> +</body> diff --git a/res/repo_index.html b/res/repo_index.html new file mode 100644 index 0000000..450097b --- /dev/null +++ b/res/repo_index.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<head> + <meta charset="UTF-8"> + <title>Repositories</title> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <link rel="stylesheet" type="text/css" href="/static/style.css"> +</head> +<body> + <table> + <thead> + <tr> + <td><b>Name</b></td> + <td><b>Description</b></td> + <td><b>Owner</b></td> + <td><b>Visibility</b></td> + <td><b>Last Commit</b></td> + </tr> + </thead> + <tbody> + {{range .Repos}} + <tr> + <td><a href="/{{.Name}}/">{{.Name}}</a></td> + <td>{{.Description}}</td> + <td>{{.Owner}}</td> + <td>{{.Visibility}}</td> + <td>{{.LastCommit}}</td> + </tr> + {{end}} + </tbody> + </table> +</body> diff --git a/res/res.go b/res/res.go new file mode 100644 index 0000000..8819dec --- /dev/null +++ b/res/res.go @@ -0,0 +1,21 @@ +package res + +import _ "embed" + +//go:embed repo_index.html +var RepoIndex string + +//go:embed user_login.html +var UserLogin string + +//go:embed repo_create.html +var RepoCreate string + +//go:embed user_create.html +var UserCreate string + +//go:embed admin_user_index.html +var AdminUserIndex string + +//go:embed style.css +var Style string diff --git a/res/style.css b/res/style.css new file mode 100644 index 0000000..2f78065 --- /dev/null +++ b/res/style.css @@ -0,0 +1,8 @@ +html { background-color: #111111; color: #888888; height: 100%; } +body { font-family: monospace; margin: 0; width: 100%; } +a { color: #FF7E00; text-decoration: none; } +a:hover { text-decoration: underline; } + +table { margin: 1em; } +table td { padding: 0 0.4em; } +table tr:hover td { background-color: #222222; } diff --git a/res/user_create.html b/res/user_create.html new file mode 100644 index 0000000..b04c0c3 --- /dev/null +++ b/res/user_create.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<head> + <meta charset="UTF-8"> + <title>Create User</title> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <link rel="stylesheet" type="text/css" href="/static/style.css"> +</head> +<body> + <h1>Create User</h1> + <form action="/admin/user/create" method="post"> + <label for="username">Username:</label> + <input type="text" name="username"><br> + <label for="fullname">Full Name:</label> + <input type="text" name="fullname"><br> + <label for="password">Password:</label> + <input type="password" name="password"><br> + <label for="admin">Admin:</label> + <input type="checkbox" name="admin" value="true"><br> + <input type="submit" value="Create"> + </form> + <p>{{.Msg}}</p> +</body> diff --git a/res/user_login.html b/res/user_login.html new file mode 100644 index 0000000..1ade9a1 --- /dev/null +++ b/res/user_login.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<head> + <meta charset="UTF-8"> + <title>Login</title> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <link rel="stylesheet" href="/static/style.css"> +</head> +<body> + <h1>Login</h1> + <form action="/user/login" method="post"> + <label for="username">Username:</label> + <input type="text" name="username"><br> + <label for="password">Password:</label> + <input type="password" name="password"><br> + <input type="submit" value="Login"> + </form> + <p>{{.Msg}}</p> +</body> diff --git a/src/admin.go b/src/admin.go new file mode 100644 index 0000000..6f5a75e --- /dev/null +++ b/src/admin.go @@ -0,0 +1,114 @@ +// admin.go +// Copyright (C) 2023, Jakob Wakeling +// All rights reserved. + +package goit + +import ( + "fmt" + "html/template" + "log" + "net/http" + "strings" + + "github.com/Jamozed/Goit/res" +) + +var ( + adminUserIndex *template.Template +) + +func init() { + adminUserIndex = template.Must(template.New("admin_user_index").Parse(res.AdminUserIndex)) +} + +func (g *Goit) HandleAdminUserIndex(w http.ResponseWriter, r *http.Request) { + if ok, uid := AuthHttp(r); !ok { + http.NotFound(w, r) + return + } else if user, err := g.GetUser(uid); err != nil { + log.Println("[Admin:User:Create:Auth]", err.Error()) + http.NotFound(w, r) + return + } else if !user.IsAdmin { + http.NotFound(w, r) + return + } + + if rows, err := g.db.Query("SELECT id, name, name_full, is_admin FROM users"); err != nil { + log.Println("[Admin:User:Index:SELECT]", err.Error()) + http.Error(w, "500 internal server error", http.StatusInternalServerError) + } else { + defer rows.Close() + + type row struct{ Id, Name, FullName, IsAdmin string } + users := []row{} + + for rows.Next() { + u := User{} + + if err := rows.Scan(&u.Id, &u.Name, &u.NameFull, &u.IsAdmin); err != nil { + log.Println("[Admin:User:Index:SELECT:Scan]", err.Error()) + } else { + users = append(users, row{fmt.Sprint(u.Id), u.Name, u.NameFull, If(u.IsAdmin, "true", "false")}) + } + } + + if err := rows.Err(); err != nil { + log.Println("[Admin:User:Index:SELECT:Err]", err.Error()) + http.Error(w, "500 internal server error", http.StatusInternalServerError) + } else { + adminUserIndex.Execute(w, struct{ Users []row }{users}) + } + } +} + +func (g *Goit) HandleAdminUserCreate(w http.ResponseWriter, r *http.Request) { + if ok, uid := AuthHttp(r); !ok { + http.NotFound(w, r) + return + } else if user, err := g.GetUser(uid); err != nil { + log.Println("[Admin:User:Create:Auth]", err.Error()) + http.NotFound(w, r) + return + } else if !user.IsAdmin { + http.NotFound(w, r) + return + } + + data := struct{ Msg string }{""} + + if r.Method == http.MethodPost { + username := strings.ToLower(r.FormValue("username")) + fullname := r.FormValue("fullname") + password := r.FormValue("password") + admin := r.FormValue("admin") == "true" + + if username == "" { + data.Msg = "Username cannot be empty" + } else if SliceContains(reserved, username) { + data.Msg = "Username \"" + username + "\" is reserved" + } else if exists, err := g.UserExists(username); err != nil { + log.Println("[Admin:User:Create:Exists]", err.Error()) + http.Error(w, "500 internal server error", http.StatusInternalServerError) + return + } else if exists { + data.Msg = "Username \"" + username + "\" is taken" + } else if salt, err := Salt(); err != nil { + log.Println("[Admin:User:Create:Salt]", err.Error()) + http.Error(w, "500 internal server error", http.StatusInternalServerError) + return + } else if _, err := g.db.Exec( + "INSERT INTO users (name, name_full, pass, pass_algo, salt, is_admin) VALUES (?, ?, ?, ?, ?, ?)", + username, fullname, Hash(password, salt), "argon2", salt, admin, + ); err != nil { + log.Println("[Admin:User:Create:INSERT]", err.Error()) + http.Error(w, "500 internal server error", http.StatusInternalServerError) + return + } else { + data.Msg = "User \"" + username + "\" created successfully" + } + } + + userCreate.Execute(w, data) +} diff --git a/src/auth.go b/src/auth.go new file mode 100644 index 0000000..15ace19 --- /dev/null +++ b/src/auth.go @@ -0,0 +1,105 @@ +// auth.go +// Copyright (C) 2023, Jakob Wakeling +// All rights reserved. + +package goit + +import ( + "crypto/rand" + "encoding/base64" + "fmt" + "log" + "math" + "net/http" + "time" + + "golang.org/x/crypto/argon2" +) + +type session struct { + id uint64 + expiry time.Time +} + +var sessions = map[string]session{} + +/* Hash a password with a salt using Argon2. */ +func Hash(pass string, salt []byte) []byte { + return argon2.IDKey([]byte(pass), salt, 3, 64*1024, 4, 32) +} + +/* Generate a random Base64 salt. */ +func Salt() ([]byte, error) { + b := make([]byte, 16) + if _, err := rand.Read(b); err != nil { + return nil, err + } + + return b, nil +} + +func NewSession(id uint64, expiry time.Time) (string, error) { + b := make([]byte, 24) + if _, err := rand.Read(b); err != nil { + return "", err + } + + s := base64.StdEncoding.EncodeToString(b) + sessions[s] = session{id, expiry} + return s, nil +} + +func EndSession(s string) { + delete(sessions, s) +} + +func Auth(s string) (bool, uint64) { + if v, ok := sessions[s]; ok { + if v.expiry.After(time.Now()) { + return true, v.id + } else { + delete(sessions, s) + } + } + + return false, math.MaxUint64 +} + +func AuthHttp(r *http.Request) (bool, uint64) { + if c := Cookie(r, "session"); c != nil { + return Auth(c.Value) + } + + return false, math.MaxUint64 +} + +func SessionCookie(r *http.Request) string { + if c := Cookie(r, "session"); c != nil { + return c.Value + } + + return "" +} + +func GetSessions() (s string) { + for k, v := range sessions { + s += fmt.Sprint(k, v.id, v.expiry) + } + + return s +} + +func CleanupSessions() { + n := 0 + + for k, v := range sessions { + if v.expiry.Before(time.Now()) { + delete(sessions, k) + n += 1 + } + } + + if n > 0 { + log.Println("[Sessions] Cleaned up", n, "expired sessions") + } +} diff --git a/src/goit.go b/src/goit.go new file mode 100644 index 0000000..17c535a --- /dev/null +++ b/src/goit.go @@ -0,0 +1,77 @@ +// goit.go +// Copyright (C) 2023, Jakob Wakeling +// All rights reserved. + +package goit + +import ( + "database/sql" + "fmt" + "log" + + _ "github.com/mattn/go-sqlite3" +) + +type Goit struct { + db *sql.DB +} + +/* Initialise Goit. */ +func InitGoit() (g *Goit, err error) { + g = &Goit{} + + if g.db, err = sql.Open("sqlite3", "./goit.db"); err != nil { + return nil, fmt.Errorf("[SQL:open] %w", err) + } + + if _, err = g.db.Exec( + `CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE NOT NULL, + name_full TEXT UNIQUE NOT NULL, + pass BLOB NOT NULL, + pass_algo TEXT NOT NULL, + salt BLOB NOT NULL, + is_admin BOOLEAN NOT NULL + )`, + ); err != nil { + return nil, fmt.Errorf("[CREATE:users] %w", err) + } + + if _, err = g.db.Exec( + `CREATE TABLE IF NOT EXISTS repos ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + owner_id INTEGER NOT NULL, + name TEXT UNIQUE NOT NULL, + name_lower TEXT UNIQUE NOT NULL, + description TEXT NOT NULL, + default_branch TEXT NOT NULL, + is_private BOOLEAN NOT NULL + )`, + ); err != nil { + return nil, fmt.Errorf("[CREATE:repos] %w", err) + } + + /* Create an admin user if one does not exist */ + if exists, err := g.UserExists("admin"); err != nil { + log.Println("[admin:Exists]", err.Error()) + err = nil /* ignored */ + } else if !exists { + if salt, err := Salt(); err != nil { + log.Println("[admin:Salt]", err.Error()) + err = nil /* ignored */ + } else if _, err = g.db.Exec( + "INSERT INTO users (id, name, name_full, pass, pass_algo, salt, is_admin) VALUES (?, ?, ?, ?, ?, ?, ?)", + 0, "admin", "Administrator", Hash("admin", salt), "argon2", salt, true, + ); err != nil { + log.Println("[admin:INSERT]", err.Error()) + err = nil /* ignored */ + } + } + + return g, nil +} + +func (g *Goit) Close() error { + return g.db.Close() +} diff --git a/src/repo.go b/src/repo.go new file mode 100644 index 0000000..04db773 --- /dev/null +++ b/src/repo.go @@ -0,0 +1,118 @@ +// repo.go +// Copyright (C) 2023, Jakob Wakeling +// All rights reserved. + +package goit + +import ( + "database/sql" + "errors" + "html/template" + "log" + "net/http" + "strings" + + "github.com/Jamozed/Goit/res" +) + +type Repo struct { + Id uint64 + OwnerId uint64 + Name string + NameLower string + Description string + DefaultBranch string + IsPrivate bool +} + +var ( + repoIndex *template.Template + repoCreate *template.Template +) + +func init() { + repoIndex = template.Must(template.New("repo_index").Parse(res.RepoIndex)) + repoCreate = template.Must(template.New("repo_create").Parse(res.RepoCreate)) +} + +func (g *Goit) HandleIndex(w http.ResponseWriter, r *http.Request) { + authOk, uid := AuthHttp(r) + + if rows, err := g.db.Query("SELECT id, owner_id, name, description, is_private FROM repos"); err != nil { + log.Println("[Index:SELECT]", err.Error()) + http.Error(w, "500 internal server error", http.StatusInternalServerError) + } else { + defer rows.Close() + + type row struct{ Name, Description, Owner, Visibility, LastCommit string } + repos := []row{} + + for rows.Next() { + r := Repo{} + + if err := rows.Scan(&r.Id, &r.OwnerId, &r.Name, &r.Description, &r.IsPrivate); err != nil { + log.Println("[Index:SELECT:Scan]", err.Error()) + } else if !r.IsPrivate || (authOk && uid == r.OwnerId) { + owner, err := g.GetUser(r.OwnerId) + if err != nil { + log.Println("[Index:SELECT:UserName]", err.Error()) + } + + repos = append(repos, row{r.Name, "", owner.Name, If(r.IsPrivate, "private", "public"), ""}) + } + } + + if err := rows.Err(); err != nil { + log.Println("[Index:SELECT:Err]", err.Error()) + http.Error(w, "500 internal server error", http.StatusInternalServerError) + } else { + repoIndex.Execute(w, struct{ Repos []row }{repos}) + } + } +} + +func (g *Goit) HandleRepoCreate(w http.ResponseWriter, r *http.Request) { + if ok, uid := AuthHttp(r); !ok { + http.Error(w, "401 unauthorized", http.StatusUnauthorized) + } else if r.Method == http.MethodPost { + name := r.FormValue("reponame") + private := r.FormValue("visibility") == "private" + + if taken, err := RepoExists(g.db, name); err != nil { + log.Println("[RepoCreate:RepoExists]", err.Error()) + http.Error(w, "500 internal server error", http.StatusInternalServerError) + } else if taken { + repoCreate.Execute(w, struct{ Msg string }{"Reponame is taken"}) + } else if SliceContains[string](reserved, name) { + repoCreate.Execute(w, struct{ Msg string }{"Reponame is reserved"}) + } else { + if _, err := g.db.Exec( + `INSERT INTO repos ( + owner_id, name, name_lower, description, default_branch, is_private + ) VALUES (?, ?, ?, ?, ?, ?)`, + uid, name, strings.ToLower(name), "", "master", private, + ); err != nil { + log.Println("[RepoCreate:INSERT]", err.Error()) + http.Error(w, "500 internal server error", http.StatusInternalServerError) + } else { + http.Redirect(w, r, "/"+name+"/", http.StatusFound) + } + } + } else /* GET */ { + repoCreate.Execute(w, nil) + } +} + +func RepoExists(db *sql.DB, name string) (bool, error) { + if err := db.QueryRow( + "SELECT name FROM repos WHERE name_lower = ?", strings.ToLower(name), + ).Scan(&name); err != nil { + if !errors.Is(err, sql.ErrNoRows) { + return false, err + } else { + return false, nil + } + } else { + return true, nil + } +} diff --git a/src/user.go b/src/user.go new file mode 100644 index 0000000..53e7653 --- /dev/null +++ b/src/user.go @@ -0,0 +1,121 @@ +// user.go +// Copyright (C) 2023, Jakob Wakeling +// All rights reserved. + +package goit + +import ( + "bytes" + "database/sql" + "errors" + "fmt" + "html/template" + "log" + "net/http" + "strings" + "time" + + "github.com/Jamozed/Goit/res" +) + +type User struct { + Id uint64 + Name string + NameFull string + Pass []byte + PassAlgo string + Salt []byte + IsAdmin bool +} + +var ( + reserved []string = []string{"admin", "repo", "static", "user"} + + userLogin *template.Template + userCreate *template.Template +) + +func init() { + userLogin = template.Must(template.New("user_login").Parse(res.UserLogin)) + userCreate = template.Must(template.New("user_create").Parse(res.UserCreate)) +} + +func (g *Goit) HandleUserLogin(w http.ResponseWriter, r *http.Request) { + if ok, _ := AuthHttp(r); ok { + http.Redirect(w, r, "/", http.StatusFound) + return + } + + data := struct{ Msg string }{""} + + if r.Method == http.MethodPost { + u := User{} + username := strings.ToLower(r.FormValue("username")) + password := r.FormValue("password") + + if username == "" { + data.Msg = "Username cannot be empty" + } else if exists, err := g.UserExists(username); err != nil { + log.Println("[User:Login:Exists]", err.Error()) + http.Error(w, "500 internal server error", http.StatusInternalServerError) + return + } else if !exists { + data.Msg = "Invalid credentials" + } else if err := g.db.QueryRow( + "SELECT id, name, pass, pass_algo, salt FROM users WHERE name = ?", username, + ).Scan(&u.Id, &u.Name, &u.Pass, &u.PassAlgo, &u.Salt); err != nil { + log.Println("[User:Login:SELECT]", err.Error()) + http.Error(w, "500 internal server error", http.StatusInternalServerError) + return + } else if !bytes.Equal(Hash(password, u.Salt), u.Pass) { + data.Msg = "Invalid credentials" + } else { + expiry := time.Now().Add(15 * time.Minute) + if s, err := NewSession(u.Id, expiry); err != nil { + log.Println("[User:Login:Session]", err.Error()) + http.Error(w, "500 internal server error", http.StatusInternalServerError) + return + } else { + http.SetCookie(w, &http.Cookie{Name: "session", Value: s, Path: "/", Expires: expiry}) + http.Redirect(w, r, "/", http.StatusFound) + return + } + } + } + + userLogin.Execute(w, data) +} + +func (g *Goit) HandleUserLogout(w http.ResponseWriter, r *http.Request) { + EndSession(SessionCookie(r)) + http.SetCookie(w, &http.Cookie{Name: "session", Path: "/", MaxAge: -1}) + http.Redirect(w, r, "/", http.StatusFound) +} + +func (g *Goit) GetUser(id uint64) (*User, error) { + u := User{} + + if err := g.db.QueryRow( + "SELECT id, name, name_full, is_admin FROM users WHERE id = ?", id, + ).Scan(&u.Id, &u.Name, &u.NameFull, &u.IsAdmin); err != nil { + if !errors.Is(err, sql.ErrNoRows) { + return nil, fmt.Errorf("[SELECT:user] %w", err) + } else { + return nil, nil + } + } else { + return &u, nil + } +} + +func (g *Goit) UserExists(name string) (bool, error) { + if err := g.db.QueryRow("SELECT name FROM users WHERE name = ?", strings.ToLower(name)).Scan(&name); err != nil { + if !errors.Is(err, sql.ErrNoRows) { + return false, err + } else { + return false, nil + } + } else { + return true, nil + } +} diff --git a/src/util.go b/src/util.go new file mode 100644 index 0000000..40255f1 --- /dev/null +++ b/src/util.go @@ -0,0 +1,34 @@ +// util.go +// Copyright (C) 2023, Jakob Wakeling +// All rights reserved. + +package goit + +import "net/http" + +func If[T any](cond bool, a, b T) T { + if cond { + return a + } else { + return b + } +} + +func SliceContains[T comparable](s []T, e T) bool { + for _, v := range s { + if v == e { + return true + } + } + + return false +} + +/* Return the named cookie or nil if not found. */ +func Cookie(r *http.Request, name string) *http.Cookie { + if c, err := r.Cookie(name); err != nil { + return nil + } else { + return c + } +}