commit 434773976f8284e130ee7c2d00ef8c839d35a57a Author: LittleSheep Date: Thu Feb 1 23:26:17 2024 +0800 :tada: Init project from scratch diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/Interactive.iml b/.idea/Interactive.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/Interactive.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..79e159c --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2263f11 --- /dev/null +++ b/go.mod @@ -0,0 +1,72 @@ +module code.smartsheep.studio/hydrogen/interactive + +go 1.21.6 + +require ( + code.smartsheep.studio/hydrogen/passport v0.0.0-20240201075828-dbc09bd7af8a + github.com/go-playground/validator/v10 v10.17.0 + github.com/gofiber/fiber/v2 v2.52.0 + github.com/json-iterator/go v1.1.12 + github.com/rs/zerolog v1.31.0 + github.com/samber/lo v1.39.0 + github.com/spf13/viper v1.18.2 + gorm.io/driver/postgres v1.5.4 + gorm.io/gorm v1.25.6 +) + +require ( + github.com/andybalholm/brotli v1.0.5 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-sql-driver/mysql v1.7.1 // indirect + github.com/golang-jwt/jwt/v5 v5.2.0 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/uuid v1.5.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect + github.com/jackc/pgx/v5 v5.5.1 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible // indirect + github.com/klauspost/compress v1.17.0 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.1.1 // indirect + github.com/philhofer/fwd v1.1.2 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/tinylib/msgp v1.1.8 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.51.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.18.0 // indirect + golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/oauth2 v0.16.0 // indirect + golang.org/x/sync v0.5.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/protobuf v1.32.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/datatypes v1.2.0 // indirect + gorm.io/driver/mysql v1.5.2 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4f00e02 --- /dev/null +++ b/go.sum @@ -0,0 +1,225 @@ +code.smartsheep.studio/hydrogen/passport v0.0.0-20240201075828-dbc09bd7af8a h1:66YEiBiB+S7QaUnNqQ/Q8zzgGNkQwcHlJdE2s/RdV6k= +code.smartsheep.studio/hydrogen/passport v0.0.0-20240201075828-dbc09bd7af8a/go.mod h1:LM5I1tdQLXY6n+hou3TPNV/v9hA/cD59zlYpspdzGks= +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ4E5T9gDA0AIH74= +github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= +github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofiber/fiber/v2 v2.52.0 h1:S+qXi7y+/Pgvqq4DrSmREGiFwtB7Bu6+QFLuIHYw/UE= +github.com/gofiber/fiber/v2 v2.52.0/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= +github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= +github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +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/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= +github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.1 h1:5I9etrGkLrN+2XPCsi6XLlV5DITbSL/xBZdmAxFcXPI= +github.com/jackc/pgx/v5 v5.5.1/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA= +github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= +github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE= +github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= +github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= +github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= +github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= +github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +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.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= +github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= +github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +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.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 h1:+iq7lrkxmFNBM7xx+Rae2W6uyPfhPeDWD+n+JgppptE= +golang.org/x/exp v0.0.0-20231219180239-dc181d75b848/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.7.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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= +golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= +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/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +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.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +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= +gorm.io/datatypes v1.2.0 h1:5YT+eokWdIxhJgWHdrb2zYUimyk0+TaFth+7a0ybzco= +gorm.io/datatypes v1.2.0/go.mod h1:o1dh0ZvjIjhH/bngTpypG6lVRJ5chTBxE09FH/71k04= +gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs= +gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8= +gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo= +gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0= +gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU= +gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= +gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0= +gorm.io/driver/sqlserver v1.4.1/go.mod h1:DJ4P+MeZbc5rvY58PnmN1Lnyvb5gw5NPzGshHDnJLig= +gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gorm.io/gorm v1.25.6 h1:V92+vVda1wEISSOMtodHVRcUIOPYa2tgQtyF+DfFx+A= +gorm.io/gorm v1.25.6/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= diff --git a/pkg/cmd/main.go b/pkg/cmd/main.go new file mode 100644 index 0000000..f573670 --- /dev/null +++ b/pkg/cmd/main.go @@ -0,0 +1,52 @@ +package main + +import ( + "code.smartsheep.studio/hydrogen/interactive/pkg/server" + "os" + "os/signal" + "syscall" + + interactive "code.smartsheep.studio/hydrogen/interactive/pkg" + "code.smartsheep.studio/hydrogen/interactive/pkg/database" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/spf13/viper" +) + +func init() { + zerolog.TimeFieldFormat = zerolog.TimeFormatUnix + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout}) +} + +func main() { + // Configure settings + viper.AddConfigPath(".") + viper.AddConfigPath("..") + viper.SetConfigName("settings") + viper.SetConfigType("toml") + + // Load settings + if err := viper.ReadInConfig(); err != nil { + log.Panic().Err(err).Msg("An error occurred when loading settings.") + } + + // Connect to database + if err := database.NewSource(); err != nil { + log.Fatal().Err(err).Msg("An error occurred when connect to database.") + } else if err := database.RunMigration(database.C); err != nil { + log.Fatal().Err(err).Msg("An error occurred when running database auto migration.") + } + + // Server + server.NewServer() + go server.Listen() + + // Messages + log.Info().Msgf("Passport v%s is started...", interactive.AppVersion) + + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + log.Info().Msgf("Passport v%s is quitting...", interactive.AppVersion) +} diff --git a/pkg/database/migrator.go b/pkg/database/migrator.go new file mode 100644 index 0000000..ed17371 --- /dev/null +++ b/pkg/database/migrator.go @@ -0,0 +1,16 @@ +package database + +import ( + "code.smartsheep.studio/hydrogen/interactive/pkg/models" + "gorm.io/gorm" +) + +func RunMigration(source *gorm.DB) error { + if err := source.AutoMigrate( + &models.Account{}, + ); err != nil { + return err + } + + return nil +} diff --git a/pkg/database/source.go b/pkg/database/source.go new file mode 100644 index 0000000..5fdb79d --- /dev/null +++ b/pkg/database/source.go @@ -0,0 +1,28 @@ +package database + +import ( + "github.com/rs/zerolog/log" + "github.com/samber/lo" + "github.com/spf13/viper" + "gorm.io/driver/postgres" + "gorm.io/gorm" + "gorm.io/gorm/logger" + "gorm.io/gorm/schema" +) + +var C *gorm.DB + +func NewSource() error { + var err error + + dialector := postgres.Open(viper.GetString("database.dsn")) + C, err = gorm.Open(dialector, &gorm.Config{NamingStrategy: schema.NamingStrategy{ + TablePrefix: viper.GetString("database.prefix"), + }, Logger: logger.New(&log.Logger, logger.Config{ + Colorful: true, + IgnoreRecordNotFoundError: true, + LogLevel: lo.Ternary(viper.GetBool("debug"), logger.Info, logger.Silent), + })}) + + return err +} diff --git a/pkg/meta.go b/pkg/meta.go new file mode 100644 index 0000000..e0251b4 --- /dev/null +++ b/pkg/meta.go @@ -0,0 +1,5 @@ +package pkg + +const ( + AppVersion = "1.0.0" +) diff --git a/pkg/models/accounts.go b/pkg/models/accounts.go new file mode 100644 index 0000000..a72340c --- /dev/null +++ b/pkg/models/accounts.go @@ -0,0 +1,14 @@ +package models + +// Account profiles basically fetched from Hydrogen.Passport +// But cache at here for better usage +// At the same time this model can make relations between local models +type Account struct { + BaseModel + + Name string `json:"name"` + Avatar string `json:"avatar"` + EmailAddress string `json:"email_address"` + PowerLevel int `json:"power_level"` + ExternalID uint `json:"external_id"` +} diff --git a/pkg/models/base.go b/pkg/models/base.go new file mode 100644 index 0000000..0052acd --- /dev/null +++ b/pkg/models/base.go @@ -0,0 +1,17 @@ +package models + +import ( + "time" + + "gorm.io/datatypes" + "gorm.io/gorm" +) + +type JSONMap = datatypes.JSONType[map[string]any] + +type BaseModel struct { + ID uint `json:"id" gorm:"primaryKey"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"` +} diff --git a/pkg/security/encryptor.go b/pkg/security/encryptor.go new file mode 100644 index 0000000..6cebde0 --- /dev/null +++ b/pkg/security/encryptor.go @@ -0,0 +1,12 @@ +package security + +import "golang.org/x/crypto/bcrypt" + +func HashPassword(raw string) string { + data, _ := bcrypt.GenerateFromPassword([]byte(raw), 12) + return string(data) +} + +func VerifyPassword(text string, password string) bool { + return bcrypt.CompareHashAndPassword([]byte(password), []byte(text)) == nil +} diff --git a/pkg/security/jwt.go b/pkg/security/jwt.go new file mode 100644 index 0000000..898243f --- /dev/null +++ b/pkg/security/jwt.go @@ -0,0 +1,56 @@ +package security + +import ( + "fmt" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/spf13/viper" +) + +type PayloadClaims struct { + jwt.RegisteredClaims + + Type string `json:"typ"` +} + +const ( + JwtAccessType = "access" + JwtRefreshType = "refresh" +) + +func EncodeJwt(id string, typ, sub string, aud []string, exp time.Time) (string, error) { + tk := jwt.NewWithClaims(jwt.SigningMethodHS512, PayloadClaims{ + jwt.RegisteredClaims{ + Subject: sub, + Audience: aud, + Issuer: fmt.Sprintf("https://%s", viper.GetString("domain")), + ExpiresAt: jwt.NewNumericDate(exp), + NotBefore: jwt.NewNumericDate(time.Now()), + IssuedAt: jwt.NewNumericDate(time.Now()), + ID: id, + }, + typ, + }) + + return tk.SignedString([]byte(viper.GetString("secret"))) +} + +func DecodeJwt(str string) (PayloadClaims, error) { + var claims PayloadClaims + tk, err := jwt.ParseWithClaims(str, &claims, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(viper.GetString("secret")), nil + }) + if err != nil { + return claims, err + } + + if data, ok := tk.Claims.(*PayloadClaims); ok { + return *data, nil + } else { + return claims, fmt.Errorf("unexpected token payload: not payload claims type") + } +} diff --git a/pkg/server/auth.go b/pkg/server/auth.go new file mode 100644 index 0000000..07ea9ee --- /dev/null +++ b/pkg/server/auth.go @@ -0,0 +1,35 @@ +package server + +import ( + "code.smartsheep.studio/hydrogen/interactive/pkg/database" + "code.smartsheep.studio/hydrogen/interactive/pkg/models" + "code.smartsheep.studio/hydrogen/passport/pkg/security" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/keyauth" + "strconv" +) + +var auth = keyauth.New(keyauth.Config{ + KeyLookup: "header:Authorization", + AuthScheme: "Bearer", + Validator: func(c *fiber.Ctx, token string) (bool, error) { + claims, err := security.DecodeJwt(token) + if err != nil { + return false, err + } + + id, _ := strconv.Atoi(claims.Subject) + + var user models.Account + if err := database.C.Where(&models.Account{ + BaseModel: models.BaseModel{ID: uint(id)}, + }).First(&user).Error; err != nil { + return false, err + } + + c.Locals("principal", user) + + return true, nil + }, + ContextKey: "token", +}) diff --git a/pkg/server/auth_api.go b/pkg/server/auth_api.go new file mode 100644 index 0000000..5e604cd --- /dev/null +++ b/pkg/server/auth_api.go @@ -0,0 +1,87 @@ +package server + +import ( + "code.smartsheep.studio/hydrogen/interactive/pkg/services" + "context" + "encoding/json" + "fmt" + "github.com/gofiber/fiber/v2" + "github.com/google/uuid" + "github.com/spf13/viper" + "golang.org/x/oauth2" +) + +var cfg = oauth2.Config{ + RedirectURL: fmt.Sprintf("https://%s/api/auth/callback", viper.GetString("domain")), + ClientID: viper.GetString("passport.client_id"), + ClientSecret: viper.GetString("passport.client_secret"), + Scopes: []string{"openid"}, + Endpoint: oauth2.Endpoint{ + AuthURL: fmt.Sprintf("%s/auth/o/connect", viper.GetString("passport.endpoint")), + TokenURL: fmt.Sprintf("%s/api/auth/token", viper.GetString("passport.endpoint")), + AuthStyle: oauth2.AuthStyleInParams, + }, +} + +func doLogin(c *fiber.Ctx) error { + url := cfg.AuthCodeURL(uuid.NewString()) + + return c.JSON(fiber.Map{ + "target": url, + }) +} + +func doPostLogin(c *fiber.Ctx) error { + code := c.Query("code") + + token, err := cfg.Exchange(context.Background(), code) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("failed to exchange token: %q", err)) + } + + agent := fiber. + Get(fmt.Sprintf("%s/api/users/me", viper.GetString("passport.endpoint"))). + Set(fiber.HeaderAuthorization, fmt.Sprintf("Bearer %s", token.AccessToken)) + + _, body, errs := agent.Bytes() + if len(errs) > 0 { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("failed to get userinfo: %q", errs)) + } + + var userinfo services.PassportUserinfo + err = json.Unmarshal(body, &userinfo) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("failed to parse userinfo: %q", err)) + } + + account, err := services.LinkAccount(userinfo) + access, refresh, err := services.GetToken(account) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("failed to get token: %q", err)) + } + + return c.JSON(fiber.Map{ + "access_token": access, + "refresh_token": refresh, + }) +} + +func doRefreshToken(c *fiber.Ctx) error { + var data struct { + RefreshToken string `json:"refresh_token" validate:"required"` + } + + if err := BindAndValidate(c, &data); err != nil { + return err + } + + access, refresh, err := services.RefreshToken(data.RefreshToken) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("failed to get token: %q", err)) + } + + return c.JSON(fiber.Map{ + "access_token": access, + "refresh_token": refresh, + }) +} diff --git a/pkg/server/startup.go b/pkg/server/startup.go new file mode 100644 index 0000000..5e3d690 --- /dev/null +++ b/pkg/server/startup.go @@ -0,0 +1,79 @@ +package server + +import ( + "code.smartsheep.studio/hydrogen/interactive/pkg/view" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cache" + "github.com/gofiber/fiber/v2/middleware/cors" + "github.com/gofiber/fiber/v2/middleware/filesystem" + "github.com/gofiber/fiber/v2/middleware/idempotency" + "github.com/gofiber/fiber/v2/middleware/logger" + jsoniter "github.com/json-iterator/go" + "github.com/rs/zerolog/log" + "github.com/spf13/viper" + "net/http" + "strings" + "time" +) + +var A *fiber.App + +func NewServer() { + A = fiber.New(fiber.Config{ + DisableStartupMessage: true, + EnableIPValidation: true, + ServerHeader: "Hydrogen.Interactive", + AppName: "Hydrogen.Interactive", + ProxyHeader: fiber.HeaderXForwardedFor, + JSONEncoder: jsoniter.ConfigCompatibleWithStandardLibrary.Marshal, + JSONDecoder: jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal, + EnablePrintRoutes: viper.GetBool("debug"), + }) + + A.Use(idempotency.New()) + A.Use(cors.New(cors.Config{ + AllowCredentials: true, + AllowMethods: strings.Join([]string{ + fiber.MethodGet, + fiber.MethodPost, + fiber.MethodHead, + fiber.MethodOptions, + fiber.MethodPut, + fiber.MethodDelete, + fiber.MethodPatch, + }, ","), + AllowOriginsFunc: func(origin string) bool { + return true + }, + })) + + A.Use(logger.New(logger.Config{ + Format: "${status} | ${latency} | ${method} ${path}\n", + Output: log.Logger, + })) + + A.Get("/.well-known", getMetadata) + + api := A.Group("/api").Name("API") + { + api.Get("/auth", doLogin) + api.Get("/auth/callback", doPostLogin) + api.Post("/auth/refresh", doRefreshToken) + } + + A.Use("/", cache.New(cache.Config{ + Expiration: 24 * time.Hour, + CacheControl: true, + }), filesystem.New(filesystem.Config{ + Root: http.FS(view.FS), + PathPrefix: "dist", + Index: "index.html", + NotFoundFile: "dist/index.html", + })) +} + +func Listen() { + if err := A.Listen(viper.GetString("bind")); err != nil { + log.Fatal().Err(err).Msg("An error occurred when starting server...") + } +} diff --git a/pkg/server/utils.go b/pkg/server/utils.go new file mode 100644 index 0000000..8502c7f --- /dev/null +++ b/pkg/server/utils.go @@ -0,0 +1,18 @@ +package server + +import ( + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" +) + +var validation = validator.New(validator.WithRequiredStructEnabled()) + +func BindAndValidate(c *fiber.Ctx, out any) error { + if err := c.BodyParser(out); err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } else if err := validation.Struct(out); err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + return nil +} diff --git a/pkg/server/well_known_api.go b/pkg/server/well_known_api.go new file mode 100644 index 0000000..147123d --- /dev/null +++ b/pkg/server/well_known_api.go @@ -0,0 +1,13 @@ +package server + +import ( + "github.com/gofiber/fiber/v2" + "github.com/spf13/viper" +) + +func getMetadata(c *fiber.Ctx) error { + return c.JSON(fiber.Map{ + "name": viper.GetString("name"), + "domain": viper.GetString("domain"), + }) +} diff --git a/pkg/services/auth.go b/pkg/services/auth.go new file mode 100644 index 0000000..4db7c0d --- /dev/null +++ b/pkg/services/auth.go @@ -0,0 +1,90 @@ +package services + +import ( + "code.smartsheep.studio/hydrogen/interactive/pkg/database" + "code.smartsheep.studio/hydrogen/interactive/pkg/models" + "code.smartsheep.studio/hydrogen/interactive/pkg/security" + "errors" + "fmt" + "github.com/google/uuid" + "gorm.io/gorm" + "strconv" + "time" +) + +type PassportUserinfo struct { + Sub uint `json:"sub"` + Email string `json:"email"` + Picture string `json:"picture"` + PreferredUsernames string `json:"preferred_usernames"` +} + +func LinkAccount(userinfo PassportUserinfo) (models.Account, error) { + var account models.Account + if err := database.C.Where(&models.Account{ + ExternalID: userinfo.Sub, + }).First(&account).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + account = models.Account{ + Name: userinfo.PreferredUsernames, + Avatar: userinfo.Picture, + EmailAddress: userinfo.Email, + PowerLevel: 0, + ExternalID: userinfo.Sub, + } + return account, database.C.Save(&account).Error + } + return account, err + } + + return account, nil +} + +func GetToken(account models.Account) (string, string, error) { + var err error + var refresh, access string + + sub := strconv.Itoa(int(account.ID)) + access, err = security.EncodeJwt( + uuid.NewString(), + security.JwtAccessType, + sub, + []string{"interactive"}, + time.Now().Add(30*time.Minute), + ) + if err != nil { + return refresh, access, err + } + refresh, err = security.EncodeJwt( + uuid.NewString(), + security.JwtRefreshType, + sub, + []string{"interactive"}, + time.Now().Add(30*24*time.Hour), + ) + if err != nil { + return refresh, access, err + } + + return access, refresh, nil +} + +func RefreshToken(token string) (string, string, error) { + parseInt := func(str string) int { + val, _ := strconv.Atoi(str) + return val + } + + var account models.Account + if claims, err := security.DecodeJwt(token); err != nil { + return "404", "403", err + } else if claims.Type != security.JwtRefreshType { + return "404", "403", fmt.Errorf("invalid token type, expected refresh token") + } else if err := database.C.Where(models.Account{ + BaseModel: models.BaseModel{ID: uint(parseInt(claims.Subject))}, + }).First(&account).Error; err != nil { + return "404", "403", err + } + + return GetToken(account) +} diff --git a/pkg/services/mailer.go b/pkg/services/mailer.go new file mode 100644 index 0000000..74301fe --- /dev/null +++ b/pkg/services/mailer.go @@ -0,0 +1,51 @@ +package services + +import ( + "crypto/tls" + "fmt" + "net/smtp" + "net/textproto" + + "github.com/jordan-wright/email" + "github.com/spf13/viper" +) + +func SendMail(target string, subject string, content string) error { + mail := &email.Email{ + To: []string{target}, + From: viper.GetString("mailer.name"), + Subject: subject, + Text: []byte(content), + Headers: textproto.MIMEHeader{}, + } + return mail.SendWithTLS( + fmt.Sprintf("%s:%d", viper.GetString("mailer.smtp_host"), viper.GetInt("mailer.smtp_port")), + smtp.PlainAuth( + "", + viper.GetString("mailer.username"), + viper.GetString("mailer.password"), + viper.GetString("mailer.smtp_host"), + ), + &tls.Config{ServerName: viper.GetString("mailer.smtp_host")}, + ) +} + +func SendMailHTML(target string, subject string, content string) error { + mail := &email.Email{ + To: []string{target}, + From: viper.GetString("mailer.name"), + Subject: subject, + HTML: []byte(content), + Headers: textproto.MIMEHeader{}, + } + return mail.SendWithTLS( + fmt.Sprintf("%s:%d", viper.GetString("mailer.smtp_host"), viper.GetInt("mailer.smtp_port")), + smtp.PlainAuth( + "", + viper.GetString("mailer.username"), + viper.GetString("mailer.password"), + viper.GetString("mailer.smtp_host"), + ), + &tls.Config{ServerName: viper.GetString("mailer.smtp_host")}, + ) +} diff --git a/pkg/view/.gitignore b/pkg/view/.gitignore new file mode 100644 index 0000000..01566b9 --- /dev/null +++ b/pkg/view/.gitignore @@ -0,0 +1,5 @@ +/dist +/node_modules + +package-lock.json +yarn.lock \ No newline at end of file diff --git a/pkg/view/README.md b/pkg/view/README.md new file mode 100644 index 0000000..99613fc --- /dev/null +++ b/pkg/view/README.md @@ -0,0 +1,28 @@ +## Usage + +```bash +$ npm install # or pnpm install or yarn install +``` + +### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs) + +## Available Scripts + +In the project directory, you can run: + +### `npm run dev` + +Runs the app in the development mode.
+Open [http://localhost:5173](http://localhost:5173) to view it in the browser. + +### `npm run build` + +Builds the app for production to the `dist` folder.
+It correctly bundles Solid in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.
+Your app is ready to be deployed! + +## Deployment + +Learn more about deploying your application with the [documentations](https://vitejs.dev/guide/static-deploy.html) diff --git a/pkg/view/embed.go b/pkg/view/embed.go new file mode 100644 index 0000000..ec34587 --- /dev/null +++ b/pkg/view/embed.go @@ -0,0 +1,6 @@ +package view + +import "embed" + +//go:embed all:dist +var FS embed.FS diff --git a/pkg/view/index.html b/pkg/view/index.html new file mode 100644 index 0000000..3c940c2 --- /dev/null +++ b/pkg/view/index.html @@ -0,0 +1,13 @@ + + + + + + + Goatpass + + +
+ + + diff --git a/pkg/view/package.json b/pkg/view/package.json new file mode 100644 index 0000000..e5a0dfa --- /dev/null +++ b/pkg/view/package.json @@ -0,0 +1,26 @@ +{ + "name": "@hydrogen/interactive-web", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@solidjs/router": "^0.10.10", + "solid-js": "^1.8.7", + "universal-cookie": "^7.0.2" + }, + "devDependencies": { + "autoprefixer": "^10.4.17", + "daisyui": "^4.6.0", + "postcss": "^8.4.33", + "solid-devtools": "^0.29.3", + "tailwindcss": "^3.4.1", + "typescript": "^5.2.2", + "vite": "^5.0.8", + "vite-plugin-solid": "^2.8.0" + } +} diff --git a/pkg/view/postcss.config.js b/pkg/view/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/pkg/view/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/pkg/view/public/favicon.svg b/pkg/view/public/favicon.svg new file mode 100644 index 0000000..3edea9a --- /dev/null +++ b/pkg/view/public/favicon.svg @@ -0,0 +1,21 @@ + + Logo + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pkg/view/src/.prettierrc b/pkg/view/src/.prettierrc new file mode 100644 index 0000000..78bd232 --- /dev/null +++ b/pkg/view/src/.prettierrc @@ -0,0 +1,5 @@ +{ + "printWidth": 120, + "tabWidth": 2, + "singleQuote": false +} \ No newline at end of file diff --git a/pkg/view/src/assets/fonts/fonts.css b/pkg/view/src/assets/fonts/fonts.css new file mode 100644 index 0000000..e4f139c --- /dev/null +++ b/pkg/view/src/assets/fonts/fonts.css @@ -0,0 +1,197 @@ +:root { + --bs-body-font-family: "IBM Plex Serif", "Noto Serif SC", sans-serif !important; +} + +html, +body { + font-family: var(--bs-body-font-family); +} + +/* ibm-plex-serif-100 - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: "IBM Plex Serif"; + font-style: normal; + font-weight: 100; + src: url("./ibm-plex-serif-v19-latin-100.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* ibm-plex-serif-100italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: "IBM Plex Serif"; + font-style: italic; + font-weight: 100; + src: url("./ibm-plex-serif-v19-latin-100italic.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* ibm-plex-serif-200 - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: "IBM Plex Serif"; + font-style: normal; + font-weight: 200; + src: url("./ibm-plex-serif-v19-latin-200.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* ibm-plex-serif-200italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: "IBM Plex Serif"; + font-style: italic; + font-weight: 200; + src: url("./ibm-plex-serif-v19-latin-200italic.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* ibm-plex-serif-300 - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: "IBM Plex Serif"; + font-style: normal; + font-weight: 300; + src: url("./ibm-plex-serif-v19-latin-300.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* ibm-plex-serif-300italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: "IBM Plex Serif"; + font-style: italic; + font-weight: 300; + src: url("./ibm-plex-serif-v19-latin-300italic.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* ibm-plex-serif-regular - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: "IBM Plex Serif"; + font-style: normal; + font-weight: 400; + src: url("./ibm-plex-serif-v19-latin-regular.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* ibm-plex-serif-italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: "IBM Plex Serif"; + font-style: italic; + font-weight: 400; + src: url("./ibm-plex-serif-v19-latin-italic.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* ibm-plex-serif-500 - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: "IBM Plex Serif"; + font-style: normal; + font-weight: 500; + src: url("./ibm-plex-serif-v19-latin-500.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* ibm-plex-serif-500italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: "IBM Plex Serif"; + font-style: italic; + font-weight: 500; + src: url("./ibm-plex-serif-v19-latin-500italic.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* ibm-plex-serif-600 - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: "IBM Plex Serif"; + font-style: normal; + font-weight: 600; + src: url("./ibm-plex-serif-v19-latin-600.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* ibm-plex-serif-600italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: "IBM Plex Serif"; + font-style: italic; + font-weight: 600; + src: url("./ibm-plex-serif-v19-latin-600italic.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* ibm-plex-serif-700 - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: "IBM Plex Serif"; + font-style: normal; + font-weight: 700; + src: url("./ibm-plex-serif-v19-latin-700.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* ibm-plex-serif-700italic - latin */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: "IBM Plex Serif"; + font-style: italic; + font-weight: 700; + src: url("./ibm-plex-serif-v19-latin-700italic.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* noto-serif-sc-200 - chinese-simplified */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: "Noto Serif SC"; + font-style: normal; + font-weight: 200; + src: url("./noto-serif-sc-v22-chinese-simplified-200.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* noto-serif-sc-300 - chinese-simplified */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: "Noto Serif SC"; + font-style: normal; + font-weight: 300; + src: url("./noto-serif-sc-v22-chinese-simplified-300.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* noto-serif-sc-regular - chinese-simplified */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: "Noto Serif SC"; + font-style: normal; + font-weight: 400; + src: url("./noto-serif-sc-v22-chinese-simplified-regular.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* noto-serif-sc-500 - chinese-simplified */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: "Noto Serif SC"; + font-style: normal; + font-weight: 500; + src: url("./noto-serif-sc-v22-chinese-simplified-500.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* noto-serif-sc-600 - chinese-simplified */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: "Noto Serif SC"; + font-style: normal; + font-weight: 600; + src: url("./noto-serif-sc-v22-chinese-simplified-600.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* noto-serif-sc-700 - chinese-simplified */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: "Noto Serif SC"; + font-style: normal; + font-weight: 700; + src: url("./noto-serif-sc-v22-chinese-simplified-700.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* noto-serif-sc-900 - chinese-simplified */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: "Noto Serif SC"; + font-style: normal; + font-weight: 900; + src: url("./noto-serif-sc-v22-chinese-simplified-900.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} diff --git a/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-100.woff2 b/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-100.woff2 new file mode 100755 index 0000000..066ac60 Binary files /dev/null and b/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-100.woff2 differ diff --git a/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-100italic.woff2 b/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-100italic.woff2 new file mode 100755 index 0000000..d0852f8 Binary files /dev/null and b/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-100italic.woff2 differ diff --git a/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-200.woff2 b/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-200.woff2 new file mode 100755 index 0000000..b6fed8e Binary files /dev/null and b/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-200.woff2 differ diff --git a/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-200italic.woff2 b/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-200italic.woff2 new file mode 100755 index 0000000..5119f1f Binary files /dev/null and b/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-200italic.woff2 differ diff --git a/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-300.woff2 b/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-300.woff2 new file mode 100755 index 0000000..aaab334 Binary files /dev/null and b/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-300.woff2 differ diff --git a/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-300italic.woff2 b/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-300italic.woff2 new file mode 100755 index 0000000..2fbe4c4 Binary files /dev/null and b/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-300italic.woff2 differ diff --git a/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-500.woff2 b/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-500.woff2 new file mode 100755 index 0000000..f71d315 Binary files /dev/null and b/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-500.woff2 differ diff --git a/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-500italic.woff2 b/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-500italic.woff2 new file mode 100755 index 0000000..b4cd639 Binary files /dev/null and b/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-500italic.woff2 differ diff --git a/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-600.woff2 b/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-600.woff2 new file mode 100755 index 0000000..5e04a71 Binary files /dev/null and b/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-600.woff2 differ diff --git a/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-600italic.woff2 b/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-600italic.woff2 new file mode 100755 index 0000000..6661fd1 Binary files /dev/null and b/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-600italic.woff2 differ diff --git a/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-700.woff2 b/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-700.woff2 new file mode 100755 index 0000000..316d5b5 Binary files /dev/null and b/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-700.woff2 differ diff --git a/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-700italic.woff2 b/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-700italic.woff2 new file mode 100755 index 0000000..1d7b71b Binary files /dev/null and b/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-700italic.woff2 differ diff --git a/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-italic.woff2 b/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-italic.woff2 new file mode 100755 index 0000000..5645b4d Binary files /dev/null and b/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-italic.woff2 differ diff --git a/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-regular.woff2 b/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-regular.woff2 new file mode 100755 index 0000000..4a563ca Binary files /dev/null and b/pkg/view/src/assets/fonts/ibm-plex-serif-v19-latin-regular.woff2 differ diff --git a/pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-200.woff2 b/pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-200.woff2 new file mode 100755 index 0000000..d90173a Binary files /dev/null and b/pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-200.woff2 differ diff --git a/pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-300.woff2 b/pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-300.woff2 new file mode 100755 index 0000000..385e4f8 Binary files /dev/null and b/pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-300.woff2 differ diff --git a/pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-500.woff2 b/pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-500.woff2 new file mode 100755 index 0000000..9699234 Binary files /dev/null and b/pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-500.woff2 differ diff --git a/pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-600.woff2 b/pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-600.woff2 new file mode 100755 index 0000000..ee418fd Binary files /dev/null and b/pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-600.woff2 differ diff --git a/pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-700.woff2 b/pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-700.woff2 new file mode 100755 index 0000000..03494a8 Binary files /dev/null and b/pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-700.woff2 differ diff --git a/pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-900.woff2 b/pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-900.woff2 new file mode 100755 index 0000000..149b461 Binary files /dev/null and b/pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-900.woff2 differ diff --git a/pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-regular.woff2 b/pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-regular.woff2 new file mode 100755 index 0000000..6c638af Binary files /dev/null and b/pkg/view/src/assets/fonts/noto-serif-sc-v22-chinese-simplified-regular.woff2 differ diff --git a/pkg/view/src/index.css b/pkg/view/src/index.css new file mode 100644 index 0000000..55b7c9b --- /dev/null +++ b/pkg/view/src/index.css @@ -0,0 +1,8 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html, body { + padding: 0; + margin: 0; +} \ No newline at end of file diff --git a/pkg/view/src/index.tsx b/pkg/view/src/index.tsx new file mode 100644 index 0000000..39f65ad --- /dev/null +++ b/pkg/view/src/index.tsx @@ -0,0 +1,32 @@ +import "solid-devtools"; + +/* @refresh reload */ +import { render } from "solid-js/web"; + +import "./index.css"; +import "./assets/fonts/fonts.css"; +import { lazy } from "solid-js"; +import { Route, Router } from "@solidjs/router"; + +import RootLayout from "./layouts/RootLayout.tsx"; +import { UserinfoProvider } from "./stores/userinfo.tsx"; +import { WellKnownProvider } from "./stores/wellKnown.tsx"; + +const root = document.getElementById("root"); + +render(() => ( + + + + import("./pages/dashboard.tsx"))} /> + import("./pages/security.tsx"))} /> + import("./pages/personalise.tsx"))} /> + import("./pages/auth/login.tsx"))} /> + import("./pages/auth/register.tsx"))} /> + import("./pages/auth/connect.tsx"))} /> + import("./pages/auth/callback.tsx"))} /> + import("./pages/users/confirm.tsx"))} /> + + + +), root!); diff --git a/pkg/view/src/layouts/RootLayout.tsx b/pkg/view/src/layouts/RootLayout.tsx new file mode 100644 index 0000000..475c644 --- /dev/null +++ b/pkg/view/src/layouts/RootLayout.tsx @@ -0,0 +1,46 @@ +import Navbar from "./shared/Navbar.tsx"; +import { readProfiles, useUserinfo } from "../stores/userinfo.tsx"; +import { createEffect, createSignal, Show } from "solid-js"; +import { readWellKnown } from "../stores/wellKnown.tsx"; +import { BeforeLeaveEventArgs, useBeforeLeave, useLocation, useNavigate } from "@solidjs/router"; + +export default function RootLayout(props: any) { + const [ready, setReady] = createSignal(false); + + Promise.all([readWellKnown(), readProfiles()]).then(() => setReady(true)); + + const navigate = useNavigate(); + const userinfo = useUserinfo(); + + const location = useLocation(); + + createEffect(() => { + if (ready()) { + keepGate(location.pathname); + } + }, [ready, userinfo]); + + function keepGate(path: string, e?: BeforeLeaveEventArgs) { + const whitelist = ["/auth/login", "/auth/register", "/users/me/confirm"]; + + if (!userinfo?.isLoggedIn && !whitelist.includes(path)) { + if (!e?.defaultPrevented) e?.preventDefault(); + navigate(`/auth/login?redirect_uri=${path}`); + } + } + + useBeforeLeave((e: BeforeLeaveEventArgs) => keepGate(e.to.toString().split("?")[0], e)); + + return ( + +
+ +
+ + }> + +
{props.children}
+
+ ); +} \ No newline at end of file diff --git a/pkg/view/src/layouts/shared/Navbar.tsx b/pkg/view/src/layouts/shared/Navbar.tsx new file mode 100644 index 0000000..686a46e --- /dev/null +++ b/pkg/view/src/layouts/shared/Navbar.tsx @@ -0,0 +1,118 @@ +import { For, Match, Show, Switch } from "solid-js"; +import { clearUserinfo, useUserinfo } from "../../stores/userinfo.tsx"; +import { useNavigate } from "@solidjs/router"; +import { useWellKnown } from "../../stores/wellKnown.tsx"; + +interface MenuItem { + label: string; + href?: string; + children?: MenuItem[]; +} + +export default function Navbar() { + const nav: MenuItem[] = [ + { + label: "You", children: [ + { label: "Dashboard", href: "/" }, + { label: "Security", href: "/security" }, + { label: "Personalise", href: "/personalise" } + ] + } + ]; + + const wellKnown = useWellKnown(); + const userinfo = useUserinfo(); + const navigate = useNavigate(); + + function logout() { + clearUserinfo(); + navigate("/auth/login"); + } + + return ( + + ); +} diff --git a/pkg/view/src/pages/dashboard.tsx b/pkg/view/src/pages/dashboard.tsx new file mode 100644 index 0000000..de7cab7 --- /dev/null +++ b/pkg/view/src/pages/dashboard.tsx @@ -0,0 +1,108 @@ +import { getAtk, readProfiles, useUserinfo } from "../stores/userinfo.tsx"; +import { createSignal, For, Show } from "solid-js"; + +export default function DashboardPage() { + const userinfo = useUserinfo(); + + const [error, setError] = createSignal(null); + + function getGreeting() { + const currentHour = new Date().getHours(); + + if (currentHour >= 0 && currentHour < 12) { + return "Good morning! Wishing you a day filled with joy and success. ☀️"; + } else if (currentHour >= 12 && currentHour < 18) { + return "Afternoon! Hope you have a productive and joyful afternoon! ☀️"; + } else { + return "Good evening! Wishing you a relaxing and pleasant evening. 🌙"; + } + } + + async function readNotification(item: any) { + const res = await fetch(`/api/notifications/${item.id}/read`, { + method: "PUT", + headers: { Authorization: `Bearer ${getAtk()}` } + }); + if (res.status !== 200) { + setError(await res.text()); + } else { + await readProfiles(); + setError(null); + } + } + + return ( +
+
+

{userinfo?.displayName}

+

{getGreeting()}

+
+ +
+ + + + + + +
+ +
+
+

Notifications

+
+ + + + + + + +
You're done! There are no notifications unread for you.
+
+ 0}> + + + + {item => + + + + } + + +
+

{item.subject}

+

{item.content}

+ +
+
+
+
+
+ +
+ ); +} \ No newline at end of file diff --git a/pkg/view/src/stores/userinfo.tsx b/pkg/view/src/stores/userinfo.tsx new file mode 100644 index 0000000..fd4d10f --- /dev/null +++ b/pkg/view/src/stores/userinfo.tsx @@ -0,0 +1,95 @@ +import Cookie from "universal-cookie"; +import { createContext, useContext } from "solid-js"; +import { createStore } from "solid-js/store"; + +export interface Userinfo { + isLoggedIn: boolean, + displayName: string, + profiles: any, + meta: any +} + +const UserinfoContext = createContext(); + +const defaultUserinfo: Userinfo = { + isLoggedIn: false, + displayName: "Citizen", + profiles: null, + meta: null +}; + +const [userinfo, setUserinfo] = createStore(structuredClone(defaultUserinfo)); + +export function getAtk(): string { + return new Cookie().get("access_token"); +} + +export async function refreshAtk() { + const rtk = new Cookie().get("refresh_token"); + + const res = await fetch("/api/auth/token", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + refresh_token: rtk, + grant_type: "refresh_token" + }) + }); + if (res.status !== 200) { + console.error(await res.text()) + } else { + const data = await res.json(); + new Cookie().set("access_token", data["access_token"], { path: "/", maxAge: undefined }); + new Cookie().set("refresh_token", data["refresh_token"], { path: "/", maxAge: undefined }); + } +} + +function checkLoggedIn(): boolean { + return new Cookie().get("access_token"); +} + +export async function readProfiles(recovering = true) { + if (!checkLoggedIn()) return; + + const res = await fetch("/api/users/me", { + headers: { "Authorization": `Bearer ${getAtk()}` } + }); + + if (res.status !== 200) { + if (recovering) { + // Auto retry after refresh access token + await refreshAtk(); + return await readProfiles(false); + } else { + clearUserinfo(); + window.location.reload(); + } + } + + const data = await res.json(); + + setUserinfo({ + isLoggedIn: true, + displayName: data["nick"], + profiles: null, + meta: data + }); +} + +export function clearUserinfo() { + new Cookie().remove("access_token", { path: "/", maxAge: undefined }); + new Cookie().remove("refresh_token", { path: "/", maxAge: undefined }); + setUserinfo(defaultUserinfo); +} + +export function UserinfoProvider(props: any) { + return ( + + {props.children} + + ); +} + +export function useUserinfo() { + return useContext(UserinfoContext); +} \ No newline at end of file diff --git a/pkg/view/src/stores/wellKnown.tsx b/pkg/view/src/stores/wellKnown.tsx new file mode 100644 index 0000000..5c5e821 --- /dev/null +++ b/pkg/view/src/stores/wellKnown.tsx @@ -0,0 +1,23 @@ +import { createContext, useContext } from "solid-js"; +import { createStore } from "solid-js/store"; + +const WellKnownContext = createContext(); + +const [wellKnown, setWellKnown] = createStore(null); + +export async function readWellKnown() { + const res = await fetch("/.well-known") + setWellKnown(await res.json()) +} + +export function WellKnownProvider(props: any) { + return ( + + {props.children} + + ); +} + +export function useWellKnown() { + return useContext(WellKnownContext); +} \ No newline at end of file diff --git a/pkg/view/src/vite-env.d.ts b/pkg/view/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/pkg/view/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/pkg/view/tailwind.config.js b/pkg/view/tailwind.config.js new file mode 100644 index 0000000..ef0f6ee --- /dev/null +++ b/pkg/view/tailwind.config.js @@ -0,0 +1,44 @@ +/** @type {import("tailwindcss").Config} */ +export default { + content: [ + "./src/**/*.{js,jsx,ts,tsx}" + ], + daisyui: { + themes: [ + { + light: { + ...require("daisyui/src/theming/themes")["light"], + primary: "#4750a3", + secondary: "#93c5fd", + accent: "#0f766e", + info: "#67e8f9", + success: "#15803d", + warning: "#f97316", + error: "#dc2626", + "--rounded-box": "0", + "--rounded-btn": "0", + "--rounded-badge": "0", + "--tab-radius": "0" + } + }, + { + dark: { + ...require("daisyui/src/theming/themes")["dark"], + primary: "#4750a3", + secondary: "#93c5fd", + accent: "#0f766e", + info: "#67e8f9", + success: "#15803d", + warning: "#f97316", + error: "#dc2626", + "--rounded-box": "0", + "--rounded-btn": "0", + "--rounded-badge": "0", + "--tab-radius": "0" + } + } + ] + }, + plugins: [require("daisyui")] +}; + diff --git a/pkg/view/tsconfig.json b/pkg/view/tsconfig.json new file mode 100644 index 0000000..b5ebb60 --- /dev/null +++ b/pkg/view/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "ES2015", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/pkg/view/tsconfig.node.json b/pkg/view/tsconfig.node.json new file mode 100644 index 0000000..42872c5 --- /dev/null +++ b/pkg/view/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/pkg/view/vite.config.ts b/pkg/view/vite.config.ts new file mode 100644 index 0000000..e8abad0 --- /dev/null +++ b/pkg/view/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from "vite"; +import solid from "vite-plugin-solid"; +import devtools from "solid-devtools/vite"; + +export default defineConfig({ + plugins: [devtools({ autoname: true }), solid()], + server: { + proxy: { + "/api": "http://localhost:8444", + "/.well-known": "http://localhost:8444" + } + } +}); diff --git a/settings.toml b/settings.toml new file mode 100644 index 0000000..9795e14 --- /dev/null +++ b/settings.toml @@ -0,0 +1,28 @@ +debug = true + +name = "Goatplaza" +maintainer = "SmartSheep Studio" + +bind = "0.0.0.0:8445" +domain = "feed.smartsheep.studio" +secret = "LtTjzAGFLshwXhN4ZD4nG5KlMv1MWcsvfv03TSZYnT1VhiAnLIZFTnHUwR0XhGgi" + +content = "uploads" + +everyone_postable = false + +[passport] +client_id = "goatplaza" +client_secret = "Z9k9AFTj^p" +endpoint = "https://id.smartsheep.studio" + +[mailer] +name = "Alphabot " +smtp_host = "smtp.exmail.qq.com" +smtp_port = 465 +username = "alphabot@smartsheep.studio" +password = "gz937Zxxzfcd9SeH" + +[database] +dsn = "host=localhost dbname=hy_interactive port=5432 sslmode=disable" +prefix = "interactive_"