Add the minimal golang web application to login users
Most of the code comes from Numerus, except that i replaced the router with a simpler chain of handlers that i already used with some success in other projects that feels a little cleaner.
This commit is contained in:
parent
6d0a282307
commit
20c7b7ad5f
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||||
|
<data-source source="LOCAL" name="tipus@localhost" uuid="f4f01e13-e150-41a5-ac76-faf86346280d">
|
||||||
|
<driver-ref>postgresql</driver-ref>
|
||||||
|
<synchronize>true</synchronize>
|
||||||
|
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
|
||||||
|
<jdbc-url>jdbc:postgresql://localhost:5432/tipus</jdbc-url>
|
||||||
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
|
</data-source>
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="SqlDialectMappings">
|
||||||
|
<file url="PROJECT" dialect="PostgreSQL" />
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -1,8 +1,11 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<module type="JAVA_MODULE" version="4">
|
<module type="JAVA_MODULE" version="4">
|
||||||
|
<component name="Go" enabled="true" />
|
||||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
<exclude-output />
|
<exclude-output />
|
||||||
<content url="file://$MODULE_DIR$" />
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/web" type="java-resource" />
|
||||||
|
</content>
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
</component>
|
</component>
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
/**
|
||||||
|
* SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
*/
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
tipus "dev.tandem.ws/tandem/tipus/pkg"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
db, err := tipus.NewDatabase(context.Background(), os.Getenv("TIPUS_DATABASE_URL"))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: ":8080",
|
||||||
|
Handler: tipus.NewApplication(db),
|
||||||
|
ReadTimeout: 5 * time.Second,
|
||||||
|
WriteTimeout: 10 * time.Second,
|
||||||
|
IdleTimeout: 2 * time.Minute,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
log.Printf("INFO — listening on %s\n", srv.Addr)
|
||||||
|
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
|
||||||
|
log.Fatalf("ERROR — http server: %s\n", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
sigCh := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-sigCh
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
log.Print("INFO - stopping server")
|
||||||
|
if err := srv.Shutdown(ctx); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
module dev.tandem.ws/tandem/tipus
|
||||||
|
|
||||||
|
go 1.16
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/jackc/pgx/v4 v4.18.1
|
||||||
|
github.com/leonelquinteros/gotext v1.5.2
|
||||||
|
golang.org/x/text v0.7.0
|
||||||
|
)
|
|
@ -0,0 +1,204 @@
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||||
|
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
||||||
|
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||||
|
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
|
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
|
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||||
|
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/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||||
|
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||||
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
|
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
|
||||||
|
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||||
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
|
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
|
||||||
|
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
||||||
|
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||||
|
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
|
||||||
|
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||||
|
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
|
||||||
|
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
|
||||||
|
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
|
||||||
|
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
|
||||||
|
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
|
||||||
|
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
|
||||||
|
github.com/jackc/pgconn v1.14.0 h1:vrbA9Ud87g6JdFWkHTJXppVce58qPIdP7N8y0Ml/A7Q=
|
||||||
|
github.com/jackc/pgconn v1.14.0/go.mod h1:9mBNlny0UvkgJdCDvdVHYSjI+8tD2rnKK69Wz8ti++E=
|
||||||
|
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
|
||||||
|
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
||||||
|
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
|
||||||
|
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
|
||||||
|
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
|
||||||
|
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
|
||||||
|
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.3.2 h1:7eY55bdBeCz1F2fTzSz69QC+pG46jYq9/jtSPiJ5nn0=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.3.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
|
||||||
|
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
|
||||||
|
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
|
||||||
|
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
|
||||||
|
github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw=
|
||||||
|
github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
|
||||||
|
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
|
||||||
|
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
|
||||||
|
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
|
||||||
|
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
|
||||||
|
github.com/jackc/pgx/v4 v4.18.1 h1:YP7G1KABtKpB5IHrO9vYwSrCOhs7p3uqhvhhQBptya0=
|
||||||
|
github.com/jackc/pgx/v4 v4.18.1/go.mod h1:FydWkUyadDmdNH/mHnGob881GawxeEm7TcMCzkb+qQE=
|
||||||
|
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
|
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
|
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
|
github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0=
|
||||||
|
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
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/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/leonelquinteros/gotext v1.5.2 h1:T2y6ebHli+rMBCjcJlHTXyUrgXqsKBhl/ormgvt7lPo=
|
||||||
|
github.com/leonelquinteros/gotext v1.5.2/go.mod h1:AT4NpQrOmyj1L/+hLja6aR0lk81yYYL4ePnj2kp7d6M=
|
||||||
|
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/lib/pq v1.1.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.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
|
||||||
|
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||||
|
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
|
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
|
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||||
|
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||||
|
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||||
|
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||||
|
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||||
|
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
|
||||||
|
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||||
|
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||||
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
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/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||||
|
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
|
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
|
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||||
|
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||||
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
|
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||||
|
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||||
|
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||||
|
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
|
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
|
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||||
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||||
|
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
|
||||||
|
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||||
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||||
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/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-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/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/sys v0.0.0-20180905080454-ebe1bf3edb33/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-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
|
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/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/text v0.3.4/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.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
|
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
|
@ -0,0 +1,64 @@
|
||||||
|
package pkg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ApplicationHandler struct {
|
||||||
|
db *Db
|
||||||
|
fileHandler http.Handler
|
||||||
|
loginHandler *LoginHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewApplication(db *Db) http.Handler {
|
||||||
|
var handler http.Handler = &ApplicationHandler{
|
||||||
|
db: db,
|
||||||
|
fileHandler: http.FileServer(http.Dir("web/static")),
|
||||||
|
loginHandler: &LoginHandler{},
|
||||||
|
}
|
||||||
|
handler = LocaleSetter(db, handler)
|
||||||
|
handler = LoginChecker(db, handler)
|
||||||
|
handler = Recoverer(handler)
|
||||||
|
handler = Logger(handler)
|
||||||
|
return handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ApplicationHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var head string
|
||||||
|
head, r.URL.Path = shiftPath(r.URL.Path)
|
||||||
|
switch head {
|
||||||
|
case "static":
|
||||||
|
h.fileHandler.ServeHTTP(w, r)
|
||||||
|
case "login":
|
||||||
|
h.loginHandler.ServeHTTP(w, r)
|
||||||
|
case "":
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
h.serveHomePage(w, r)
|
||||||
|
default:
|
||||||
|
methodNotAllowed(w, r, http.MethodGet)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ApplicationHandler) serveHomePage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mustRenderWebTemplate(w, r, "home.gohtml", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func shiftPath(p string) (head, tail string) {
|
||||||
|
p = path.Clean("/" + p)
|
||||||
|
if i := strings.IndexByte(p[1:], '/') + 1; i <= 0 {
|
||||||
|
return p[1:], "/"
|
||||||
|
} else {
|
||||||
|
return p[1:i], p[i:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func methodNotAllowed(w http.ResponseWriter, _ *http.Request, allowed ...string) {
|
||||||
|
w.Header().Set("Allow", strings.Join(allowed, ", "))
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
|
@ -0,0 +1,162 @@
|
||||||
|
package pkg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v4"
|
||||||
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Db struct {
|
||||||
|
*pgxpool.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDatabase(ctx context.Context, connString string) (*Db, error) {
|
||||||
|
config, err := pgxpool.ParseConfig(connString)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error {
|
||||||
|
if _, err := conn.Exec(ctx, "SET search_path TO tipus, public"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
config.BeforeAcquire = func(ctx context.Context, conn *pgx.Conn) bool {
|
||||||
|
cookie := ""
|
||||||
|
if value, ok := ctx.Value(ContextCookieKey).(string); ok {
|
||||||
|
cookie = value
|
||||||
|
}
|
||||||
|
if _, err := conn.Exec(ctx, "select set_cookie($1)", cookie); err != nil {
|
||||||
|
log.Printf("ERROR - Failed to set role: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
config.AfterRelease = func(conn *pgx.Conn) bool {
|
||||||
|
if _, err := conn.Exec(context.Background(), "RESET ROLE"); err != nil {
|
||||||
|
log.Printf("ERROR - Failed to reset role: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
pool, err := pgxpool.ConnectConfig(ctx, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Db{pool}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func notFoundErrorOrPanic(err error) bool {
|
||||||
|
if err == pgx.ErrNoRows {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Db) Acquire(ctx context.Context) (*Conn, error) {
|
||||||
|
conn, err := db.Pool.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Conn{conn}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Db) MustAcquire(ctx context.Context) *Conn {
|
||||||
|
conn, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return conn
|
||||||
|
}
|
||||||
|
|
||||||
|
type Conn struct {
|
||||||
|
*pgxpool.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) MustBegin(ctx context.Context) *Tx {
|
||||||
|
tx, err := c.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return &Tx{tx}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) MustGetText(ctx context.Context, def string, sql string, args ...interface{}) string {
|
||||||
|
var result string
|
||||||
|
if notFoundErrorOrPanic(c.Conn.QueryRow(ctx, sql, args...).Scan(&result)) {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) MustGetBool(ctx context.Context, sql string, args ...interface{}) bool {
|
||||||
|
var result bool
|
||||||
|
if err := c.Conn.QueryRow(ctx, sql, args...).Scan(&result); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) MustExec(ctx context.Context, sql string, args ...interface{}) {
|
||||||
|
if _, err := c.Conn.Exec(ctx, sql, args...); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) MustQuery(ctx context.Context, sql string, args ...interface{}) pgx.Rows {
|
||||||
|
rows, err := c.Conn.Query(ctx, sql, args...)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tx struct {
|
||||||
|
pgx.Tx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tx *Tx) MustCommit(ctx context.Context) {
|
||||||
|
if err := tx.Commit(ctx); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tx *Tx) MustRollback(ctx context.Context) {
|
||||||
|
if err := tx.Rollback(ctx); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tx *Tx) MustGetInteger(ctx context.Context, sql string, args ...interface{}) int {
|
||||||
|
var result int
|
||||||
|
if err := tx.QueryRow(ctx, sql, args...).Scan(&result); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tx *Tx) MustGetIntegerOrDefault(ctx context.Context, def int, sql string, args ...interface{}) int {
|
||||||
|
var result int
|
||||||
|
if notFoundErrorOrPanic(tx.QueryRow(ctx, sql, args...).Scan(&result)) {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tx *Tx) MustCopyFrom(ctx context.Context, tableName string, columns []string, rows [][]interface{}) int64 {
|
||||||
|
copied, err := tx.CopyFrom(ctx, pgx.Identifier{tableName}, columns, pgx.CopyFromRows(rows))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return copied
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
package form
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Attribute struct {
|
||||||
|
Key, Val string
|
||||||
|
}
|
||||||
|
|
||||||
|
type InputField struct {
|
||||||
|
Name string
|
||||||
|
Label string
|
||||||
|
Type string
|
||||||
|
Val string
|
||||||
|
Is string
|
||||||
|
Required bool
|
||||||
|
Attributes []template.HTMLAttr
|
||||||
|
Errors []error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (field *InputField) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
field.Val = ""
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch v := value.(type) {
|
||||||
|
case time.Time:
|
||||||
|
if field.Type == "date" {
|
||||||
|
field.Val = v.Format("2006-01-02")
|
||||||
|
} else if field.Type == "time" {
|
||||||
|
field.Val = v.Format("15:04")
|
||||||
|
} else {
|
||||||
|
field.Val = v.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
field.Val = fmt.Sprintf("%v", v)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (field *InputField) Value() (driver.Value, error) {
|
||||||
|
return field.Val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (field *InputField) FillValue(r *http.Request) {
|
||||||
|
field.Val = strings.TrimSpace(r.FormValue(field.Name))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (field *InputField) Integer() int {
|
||||||
|
value, err := strconv.Atoi(field.Val)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (field *InputField) IntegerOrNil() interface{} {
|
||||||
|
if field.Val != "" {
|
||||||
|
if i := field.Integer(); i > 0 {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (field *InputField) Float64() float64 {
|
||||||
|
value, err := strconv.ParseFloat(field.Val, 64)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (field *InputField) String() string {
|
||||||
|
return field.Val
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
package form
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/mail"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Validator struct {
|
||||||
|
Valid bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewValidator() *Validator {
|
||||||
|
return &Validator{true}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Validator) AllOK() bool {
|
||||||
|
return v.Valid
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Validator) CheckRequiredInput(field *InputField, message string) bool {
|
||||||
|
return v.checkInput(field, field.Val != "", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Validator) CheckInputMinLength(field *InputField, min int, message string) bool {
|
||||||
|
return v.checkInput(field, len(field.Val) >= min, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Validator) CheckValidEmailInput(field *InputField, message string) bool {
|
||||||
|
_, err := mail.ParseAddress(field.Val)
|
||||||
|
return v.checkInput(field, err == nil, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Validator) CheckPasswordConfirmation(password *InputField, confirm *InputField, message string) bool {
|
||||||
|
return v.checkInput(confirm, password.Val == confirm.Val, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Validator) CheckValidURL(field *InputField, message string) bool {
|
||||||
|
_, err := url.ParseRequestURI(field.Val)
|
||||||
|
return v.checkInput(field, err == nil, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Validator) CheckValidDate(field *InputField, message string) bool {
|
||||||
|
_, err := time.Parse("2006-01-02", field.Val)
|
||||||
|
return v.checkInput(field, err == nil, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Validator) CheckValidInteger(field *InputField, min int, max int, message string) bool {
|
||||||
|
value, err := strconv.Atoi(field.Val)
|
||||||
|
return v.checkInput(field, err == nil && value >= min && value <= max, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Validator) CheckValidDecimal(field *InputField, min float64, max float64, message string) bool {
|
||||||
|
value, err := strconv.ParseFloat(field.Val, 64)
|
||||||
|
return v.checkInput(field, err == nil && value >= min && value <= max, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Validator) checkInput(field *InputField, ok bool, message string) bool {
|
||||||
|
if !ok {
|
||||||
|
field.Errors = append(field.Errors, errors.New(message))
|
||||||
|
v.Valid = false
|
||||||
|
}
|
||||||
|
return ok
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
package pkg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/leonelquinteros/gotext"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
)
|
||||||
|
|
||||||
|
const contextLocaleKey = "tipus-locale"
|
||||||
|
|
||||||
|
type Locale struct {
|
||||||
|
*gotext.Locale
|
||||||
|
CurrencyPattern string
|
||||||
|
Language language.Tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLocale(lang availableLanguage) *Locale {
|
||||||
|
return &Locale{
|
||||||
|
gotext.NewLocale("locales", lang.tag.String()),
|
||||||
|
lang.currencyPattern,
|
||||||
|
lang.tag,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func LocaleSetter(db *Db, next http.Handler) http.Handler {
|
||||||
|
availableLanguages := mustGetAvailableLanguages(db)
|
||||||
|
|
||||||
|
locales := map[language.Tag]*Locale{}
|
||||||
|
var tags []language.Tag
|
||||||
|
for _, lang := range availableLanguages {
|
||||||
|
locale := NewLocale(lang)
|
||||||
|
locale.AddDomain("tipus")
|
||||||
|
locales[lang.tag] = locale
|
||||||
|
tags = append(tags, lang.tag)
|
||||||
|
}
|
||||||
|
defaultLocale := locales[language.Catalan]
|
||||||
|
var matcher = language.NewMatcher(tags)
|
||||||
|
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var locale *Locale
|
||||||
|
user := getUser(r)
|
||||||
|
locale = locales[user.Language]
|
||||||
|
if locale == nil {
|
||||||
|
t, _, err := language.ParseAcceptLanguage(r.Header.Get("Accept-Language"))
|
||||||
|
if err == nil {
|
||||||
|
tag, _, _ := matcher.Match(t...)
|
||||||
|
var ok bool
|
||||||
|
locale, ok = locales[tag]
|
||||||
|
for !ok && !tag.IsRoot() {
|
||||||
|
tag = tag.Parent()
|
||||||
|
locale, ok = locales[tag]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if locale == nil {
|
||||||
|
locale = defaultLocale
|
||||||
|
}
|
||||||
|
ctx := context.WithValue(r.Context(), contextLocaleKey, locale)
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLocale(r *http.Request) *Locale {
|
||||||
|
return r.Context().Value(contextLocaleKey).(*Locale)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pgettext(context string, str string, locale *Locale) string {
|
||||||
|
return locale.GetC(str, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
func gettext(str string, locale *Locale) string {
|
||||||
|
return locale.Get(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
type availableLanguage struct {
|
||||||
|
tag language.Tag
|
||||||
|
currencyPattern string
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustGetAvailableLanguages(db *Db) []availableLanguage {
|
||||||
|
rows, err := db.Query(context.Background(), "select lang_tag, currency_pattern from language where selectable")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var langs []availableLanguage
|
||||||
|
for rows.Next() {
|
||||||
|
var langTag string
|
||||||
|
var currencyPattern string
|
||||||
|
err = rows.Scan(&langTag, ¤cyPattern)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
langs = append(langs, availableLanguage{language.MustParse(langTag), currencyPattern})
|
||||||
|
}
|
||||||
|
if rows.Err() != nil {
|
||||||
|
panic(rows.Err())
|
||||||
|
}
|
||||||
|
|
||||||
|
return langs
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
package pkg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type loggerResponseWriter struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
statusCode int
|
||||||
|
responseSize int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *loggerResponseWriter) WriteHeader(statusCode int) {
|
||||||
|
w.statusCode = statusCode
|
||||||
|
w.ResponseWriter.WriteHeader(statusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *loggerResponseWriter) Write(b []byte) (int, error) {
|
||||||
|
w.responseSize += len(b)
|
||||||
|
return w.ResponseWriter.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *loggerResponseWriter) Flush() {
|
||||||
|
if f, ok := w.ResponseWriter.(http.Flusher); ok {
|
||||||
|
f.Flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Logger(handler http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
t := time.Now()
|
||||||
|
logger := loggerResponseWriter{w, 0, 0}
|
||||||
|
|
||||||
|
handler.ServeHTTP(&logger, r)
|
||||||
|
|
||||||
|
statusCode := logger.statusCode
|
||||||
|
if statusCode == 0 {
|
||||||
|
statusCode = http.StatusOK
|
||||||
|
}
|
||||||
|
referer := r.Referer()
|
||||||
|
if referer == "" {
|
||||||
|
referer = "-"
|
||||||
|
}
|
||||||
|
log.Printf("HTTP - %s - - [%s] \"%s %s %s\" %d %d \"%s\" \"%s\" %s\n",
|
||||||
|
r.RemoteAddr,
|
||||||
|
t.Format("02/Jan/2006:15:04:05 -0700"),
|
||||||
|
r.Method,
|
||||||
|
r.URL.Path,
|
||||||
|
r.Proto,
|
||||||
|
statusCode,
|
||||||
|
logger.responseSize,
|
||||||
|
referer,
|
||||||
|
r.UserAgent(),
|
||||||
|
time.Since(t),
|
||||||
|
)
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,207 @@
|
||||||
|
package pkg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"html/template"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"dev.tandem.ws/tandem/tipus/pkg/form"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ContextUserKey = "tipus-user"
|
||||||
|
ContextCookieKey = "tipus-cookie"
|
||||||
|
ContextConnKey = "tipus-database"
|
||||||
|
sessionCookie = "tipus-session"
|
||||||
|
defaultRole = "guest"
|
||||||
|
csrfTokenField = "csfrToken"
|
||||||
|
)
|
||||||
|
|
||||||
|
type loginForm struct {
|
||||||
|
locale *Locale
|
||||||
|
Errors []error
|
||||||
|
Email *form.InputField
|
||||||
|
Password *form.InputField
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLoginForm(locale *Locale) *loginForm {
|
||||||
|
return &loginForm{
|
||||||
|
locale: locale,
|
||||||
|
Email: &form.InputField{
|
||||||
|
Name: "email",
|
||||||
|
Label: pgettext("input", "Email", locale),
|
||||||
|
Type: "email",
|
||||||
|
Required: true,
|
||||||
|
Attributes: []template.HTMLAttr{
|
||||||
|
`autofocus="autofocus"`,
|
||||||
|
`autocomplete="username"`,
|
||||||
|
`autocapitalize="none"`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Password: &form.InputField{
|
||||||
|
Name: "password",
|
||||||
|
Label: pgettext("input", "Password", locale),
|
||||||
|
Type: "password",
|
||||||
|
Required: true,
|
||||||
|
Attributes: []template.HTMLAttr{
|
||||||
|
`autocomplete="current-password"`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *loginForm) Parse(r *http.Request) error {
|
||||||
|
err := r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f.Email.FillValue(r)
|
||||||
|
f.Password.FillValue(r)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *loginForm) Validate() bool {
|
||||||
|
validator := form.NewValidator()
|
||||||
|
if validator.CheckRequiredInput(f.Email, gettext("Email can not be empty.", f.locale)) {
|
||||||
|
validator.CheckValidEmailInput(f.Email, gettext("This value is not a valid email. It should be like name@domain.com.", f.locale))
|
||||||
|
}
|
||||||
|
validator.CheckRequiredInput(f.Password, gettext("Password can not be empty.", f.locale))
|
||||||
|
return validator.AllOK()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *loginForm) mustRender(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mustRenderWebTemplate(w, r, "login.gohtml", f)
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginHandler struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LoginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var head string
|
||||||
|
head, r.URL.Path = shiftPath(r.URL.Path)
|
||||||
|
switch head {
|
||||||
|
case "":
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
h.serveLoginForm(w, r)
|
||||||
|
case http.MethodPost:
|
||||||
|
h.handleLogin(w, r)
|
||||||
|
default:
|
||||||
|
methodNotAllowed(w, r, http.MethodGet, http.MethodPost)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LoginHandler) serveLoginForm(w http.ResponseWriter, r *http.Request) {
|
||||||
|
user := getUser(r)
|
||||||
|
if user.LoggedIn {
|
||||||
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
locale := getLocale(r)
|
||||||
|
login := newLoginForm(locale)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
login.mustRender(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LoginHandler) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
|
user := getUser(r)
|
||||||
|
if user.LoggedIn {
|
||||||
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
locale := getLocale(r)
|
||||||
|
login := newLoginForm(locale)
|
||||||
|
if err := login.Parse(r); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if login.Validate() {
|
||||||
|
conn := getConn(r)
|
||||||
|
cookie := conn.MustGetText(r.Context(), "", "select login($1, $2, $3)", login.Email, login.Password, remoteAddr(r))
|
||||||
|
if cookie != "" {
|
||||||
|
setSessionCookie(w, cookie)
|
||||||
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
login.Errors = append(login.Errors, errors.New(gettext("Invalid user or password.", locale)))
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||||
|
}
|
||||||
|
login.mustRender(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func remoteAddr(r *http.Request) string {
|
||||||
|
address := r.Header.Get("X-Forwarded-For")
|
||||||
|
if address == "" {
|
||||||
|
address, _, _ = net.SplitHostPort(r.RemoteAddr)
|
||||||
|
}
|
||||||
|
return address
|
||||||
|
}
|
||||||
|
|
||||||
|
func setSessionCookie(w http.ResponseWriter, cookie string) {
|
||||||
|
http.SetCookie(w, createSessionCookie(cookie, 8766*24*time.Hour))
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSessionCookie(value string, duration time.Duration) *http.Cookie {
|
||||||
|
return &http.Cookie{
|
||||||
|
Name: sessionCookie,
|
||||||
|
Value: value,
|
||||||
|
Path: "/",
|
||||||
|
Expires: time.Now().Add(duration),
|
||||||
|
HttpOnly: true,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppUser struct {
|
||||||
|
Email string
|
||||||
|
LoggedIn bool
|
||||||
|
Role string
|
||||||
|
Language language.Tag
|
||||||
|
CsrfToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoginChecker(db *Db, next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var ctx = r.Context()
|
||||||
|
if cookie, err := r.Cookie(sessionCookie); err == nil {
|
||||||
|
ctx = context.WithValue(ctx, ContextCookieKey, cookie.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn := db.MustAcquire(ctx)
|
||||||
|
defer conn.Release()
|
||||||
|
ctx = context.WithValue(ctx, ContextConnKey, conn)
|
||||||
|
|
||||||
|
user := &AppUser{
|
||||||
|
Email: "",
|
||||||
|
LoggedIn: false,
|
||||||
|
Role: defaultRole,
|
||||||
|
}
|
||||||
|
row := conn.QueryRow(ctx, "select coalesce(email, ''), role, lang_tag, csrf_token from user_profile")
|
||||||
|
var langTag string
|
||||||
|
if err := row.Scan(&user.Email, &user.Role, &langTag, &user.CsrfToken); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
user.LoggedIn = user.Email != ""
|
||||||
|
user.Language, _ = language.Parse(langTag)
|
||||||
|
ctx = context.WithValue(ctx, ContextUserKey, user)
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUser(r *http.Request) *AppUser {
|
||||||
|
return r.Context().Value(ContextUserKey).(*AppUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getConn(r *http.Request) *Conn {
|
||||||
|
return r.Context().Value(ContextConnKey).(*Conn)
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package pkg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Recoverer(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
if r == http.ErrAbortHandler {
|
||||||
|
panic(r)
|
||||||
|
}
|
||||||
|
err, ok := r.(error)
|
||||||
|
if !ok {
|
||||||
|
err = fmt.Errorf("%v", r)
|
||||||
|
}
|
||||||
|
stack := make([]byte, 4<<10)
|
||||||
|
length := runtime.Stack(stack, true)
|
||||||
|
log.Printf("PANIC - %v %s", err, stack[:length])
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package pkg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func templateFile(name string) string {
|
||||||
|
return "web/template/" + name
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustRenderTemplate(wr io.Writer, r *http.Request, layout string, filename string, data interface{}) {
|
||||||
|
locale := getLocale(r)
|
||||||
|
user := getUser(r)
|
||||||
|
t := template.New(filename)
|
||||||
|
t.Funcs(template.FuncMap{
|
||||||
|
"gettext": locale.Get,
|
||||||
|
"pgettext": locale.GetC,
|
||||||
|
"currentLocale": func() string {
|
||||||
|
return locale.Language.String()
|
||||||
|
},
|
||||||
|
"csrfToken": func() template.HTML {
|
||||||
|
return template.HTML(fmt.Sprintf(`<input type="hidden" name="%s" value="%s">`, csrfTokenField, user.CsrfToken))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if _, err := t.ParseFiles(templateFile(filename), templateFile(layout), templateFile("form.gohtml")); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if w, ok := wr.(http.ResponseWriter); ok {
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
}
|
||||||
|
if err := t.ExecuteTemplate(wr, layout, data); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustRenderWebTemplate(w io.Writer, r *http.Request, filename string, data interface{}) {
|
||||||
|
mustRenderTemplate(w, r, "web.gohtml", filename, data)
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
*, *::before, *::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--tipus--color--black: #3f3b37;
|
||||||
|
--tipus--color--dark-gray: #8a8885;
|
||||||
|
--tipus--color--light-gray: #e1dbd6;
|
||||||
|
--tipus--color--white: #ffffff;
|
||||||
|
--tipus--color--yellow: #ffd200;
|
||||||
|
--tipus--color--red: #ff7a53;
|
||||||
|
--tipus--color--rosy: #ffbaa6;
|
||||||
|
--tipus--color--green: #5ae487;
|
||||||
|
--tipus--color--light-green: #9fefb9;
|
||||||
|
--tipus--color--blue: #55bfff;
|
||||||
|
--tipus--color--light-blue: #cbebff;
|
||||||
|
--tipus--color--hay: #ffe673;
|
||||||
|
|
||||||
|
--tipus--text-color: var(--tipus--color--black);
|
||||||
|
--tipus--background-color: var(--tipus--color--white);
|
||||||
|
--tipus--font-family: 'JetBrains Mono';
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-family: var(--tipus--font-family), monospace;
|
||||||
|
font-size: 62.5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
body, dialog {
|
||||||
|
background-color: var(--tipus--background-color);
|
||||||
|
color: var(--tipus--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
img, picture, video, canvas, svg {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, button, textarea, select {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
p, h1, h2, h3, h4, h5, h6 {
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="submit"], button, .button {
|
||||||
|
min-width: 34rem;
|
||||||
|
background-color: var(--tipus--color--white);
|
||||||
|
border: 2px solid var(--tipus--color--black);
|
||||||
|
text-transform: uppercase;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="submit"]:hover, button:hover {
|
||||||
|
background-color: var(--tipus--color--light-gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="submit"]:active, button:active {
|
||||||
|
background-color: var(--tipus--color--black);
|
||||||
|
border-color: var(--tipus--color--white);
|
||||||
|
color: var(--tipus--color--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
#login {
|
||||||
|
background-color: var(--tipus--color--hay);
|
||||||
|
padding: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login h2 {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
div[role="alert"].error {
|
||||||
|
padding: 1.25em;
|
||||||
|
background-color: var(--tipus--color--red);
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
{{ define "input-field" -}}
|
||||||
|
{{- /*gotype: dev.tandem.ws/tandem/tipus/pkg/form.InputField*/ -}}
|
||||||
|
<div class="input{{ if .Errors }} has-errors{{ end }}"{{ if .Is }} is="{{ .Is }}"{{ end }}>
|
||||||
|
{{ if eq .Type "textarea" }}
|
||||||
|
<textarea name="{{ .Name }}" id="{{ .Name }}-field"
|
||||||
|
{{- range $attribute := .Attributes }} {{$attribute}} {{ end }}
|
||||||
|
{{ if .Required }}required="required"{{ end }} placeholder="{{ .Label }}"
|
||||||
|
>{{ .Val }}</textarea>
|
||||||
|
{{ else }}
|
||||||
|
<input type="{{ .Type }}" name="{{ .Name }}" id="{{ .Name }}-field"
|
||||||
|
{{- range $attribute := .Attributes }} {{$attribute}} {{ end }}
|
||||||
|
{{ if .Required }}required="required"{{ end }} value="{{ .Val }}" placeholder="{{ .Label }}">
|
||||||
|
{{ end }}
|
||||||
|
<label for="{{ .Name }}-field">{{ .Label }}</label>
|
||||||
|
{{- if .Errors }}
|
||||||
|
<ul>
|
||||||
|
{{- range $error := .Errors }}
|
||||||
|
<li>{{ . }}</li>
|
||||||
|
{{- end }}
|
||||||
|
</ul>
|
||||||
|
{{- end }}
|
||||||
|
</div>
|
||||||
|
{{- end }}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{{ define "title" -}}
|
||||||
|
{{( pgettext "Home" "title" )}}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{ define "content" -}}
|
||||||
|
{{- end }}
|
|
@ -0,0 +1,22 @@
|
||||||
|
{{ define "title" -}}
|
||||||
|
{{( pgettext "Login" "title" )}}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{ define "content" -}}
|
||||||
|
{{- /*gotype: dev.tandem.ws/tandem/tipus/pkg.loginForm*/ -}}
|
||||||
|
{{ if .Errors -}}
|
||||||
|
<div class="error" role="alert">
|
||||||
|
{{ range .Errors -}}
|
||||||
|
<p>{{ . }}</p>
|
||||||
|
{{- end }}
|
||||||
|
</div>
|
||||||
|
{{- end }}
|
||||||
|
<section id="login">
|
||||||
|
<h2>{{( pgettext "Login" "title" )}}</h2>
|
||||||
|
<form method="POST" action="/login">
|
||||||
|
{{ template "input-field" .Email }}
|
||||||
|
{{ template "input-field" .Password }}
|
||||||
|
<button type="submit">{{( pgettext "Login" "action" )}}</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
{{- end }}
|
|
@ -0,0 +1,12 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="{{ currentLocale }}">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>{{ template "title" . }} — Tipus</title>
|
||||||
|
<link rel="stylesheet" type="text/css" media="screen" href="/static/tipus.css">
|
||||||
|
</head>
|
||||||
|
<body class="web">
|
||||||
|
{{- template "content" . }}
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue