Use uni-token

💄 A lot of optimization
This commit is contained in:
2024-02-21 22:58:51 +08:00
parent 4101043d65
commit 1e04f2029f
50 changed files with 319 additions and 490 deletions

9
go.mod

@ -12,7 +12,7 @@ require (
github.com/rs/zerolog v1.31.0 github.com/rs/zerolog v1.31.0
github.com/samber/lo v1.39.0 github.com/samber/lo v1.39.0
github.com/spf13/viper v1.18.2 github.com/spf13/viper v1.18.2
golang.org/x/crypto v0.18.0 golang.org/x/crypto v0.19.0
golang.org/x/oauth2 v0.16.0 golang.org/x/oauth2 v0.16.0
gorm.io/datatypes v1.2.0 gorm.io/datatypes v1.2.0
gorm.io/driver/postgres v1.5.4 gorm.io/driver/postgres v1.5.4
@ -20,6 +20,7 @@ require (
) )
require ( require (
code.smartsheep.studio/hydrogen/identity v0.0.0-20240221130517-c169ffdacda8 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect github.com/andybalholm/brotli v1.0.5 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect
@ -59,11 +60,13 @@ require (
github.com/valyala/tcplisten v1.0.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 // indirect golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 // indirect
golang.org/x/net v0.20.0 // indirect golang.org/x/net v0.21.0 // indirect
golang.org/x/sync v0.5.0 // indirect golang.org/x/sync v0.5.0 // indirect
golang.org/x/sys v0.16.0 // indirect golang.org/x/sys v0.17.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/text v0.14.0 // indirect
google.golang.org/appengine v1.6.8 // indirect google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c // indirect
google.golang.org/grpc v1.61.1 // indirect
google.golang.org/protobuf v1.32.0 // indirect google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect

22
go.sum

@ -1,3 +1,9 @@
code.smartsheep.studio/hydrogen/identity v0.0.0-20240220134615-3b0cbbb6c9ed h1:/3rRncEKlN1GYWjUSJF8bUkwnCkTFon2opa+tGUTwEQ=
code.smartsheep.studio/hydrogen/identity v0.0.0-20240220134615-3b0cbbb6c9ed/go.mod h1:db+/Y/fLPSOu1JlsCoXEYPD26644S0S3Bg/1XNLtlHQ=
code.smartsheep.studio/hydrogen/identity v0.0.0-20240221124039-3393f751a072 h1:T3pP/cWpfHoxA6VrhFPq0EcrDVnUVXtfwQSzM3jFRfo=
code.smartsheep.studio/hydrogen/identity v0.0.0-20240221124039-3393f751a072/go.mod h1:db+/Y/fLPSOu1JlsCoXEYPD26644S0S3Bg/1XNLtlHQ=
code.smartsheep.studio/hydrogen/identity v0.0.0-20240221130517-c169ffdacda8 h1:WBi14r+jomgixVDFa8pPecrQshhlyJKBT51VZNs+PBY=
code.smartsheep.studio/hydrogen/identity v0.0.0-20240221130517-c169ffdacda8/go.mod h1:db+/Y/fLPSOu1JlsCoXEYPD26644S0S3Bg/1XNLtlHQ=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 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/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
@ -146,6 +152,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/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.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= 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/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 h1:+iq7lrkxmFNBM7xx+Rae2W6uyPfhPeDWD+n+JgppptE= 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/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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@ -156,6 +164,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= 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/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= 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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -174,6 +184,8 @@ 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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= 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/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.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-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.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/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
@ -192,6 +204,16 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/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 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ=
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c h1:NUsgEN92SQQqzfA+YtqYNqYmB3DMMYLlIwUZAQFVFbo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY=
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
google.golang.org/grpc v1.61.1 h1:kLAiWrZs7YeDM6MumDe7m3y4aM6wacLzM1Y/wiLP9XY=
google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 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.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=

@ -1,6 +1,7 @@
package main package main
import ( import (
"code.smartsheep.studio/hydrogen/interactive/pkg/grpc"
"code.smartsheep.studio/hydrogen/interactive/pkg/server" "code.smartsheep.studio/hydrogen/interactive/pkg/server"
"os" "os"
"os/signal" "os/signal"
@ -37,6 +38,13 @@ func main() {
log.Fatal().Err(err).Msg("An error occurred when running database auto migration.") log.Fatal().Err(err).Msg("An error occurred when running database auto migration.")
} }
// Connect other services
go func() {
if err := grpc.ConnectPassport(); err != nil {
log.Fatal().Err(err).Msg("An error occurred when connecting to identity grpc endpoint...")
}
}()
// Server // Server
server.NewServer() server.NewServer()
go server.Listen() go server.Listen()

24
pkg/grpc/client.go Normal file

@ -0,0 +1,24 @@
package grpc
import (
pwpb "code.smartsheep.studio/hydrogen/identity/pkg/grpc/proto"
"google.golang.org/grpc/credentials/insecure"
"github.com/spf13/viper"
"google.golang.org/grpc"
)
var Notify pwpb.NotifyClient
var Auth pwpb.AuthClient
func ConnectPassport() error {
addr := viper.GetString("identity.grpc_endpoint")
if conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials())); err != nil {
return err
} else {
Notify = pwpb.NewNotifyClient(conn)
Auth = pwpb.NewAuthClient(conn)
}
return nil
}

@ -2,6 +2,7 @@ package security
import ( import (
"fmt" "fmt"
"github.com/gofiber/fiber/v2"
"time" "time"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
@ -19,6 +20,11 @@ const (
JwtRefreshType = "refresh" JwtRefreshType = "refresh"
) )
const (
CookieAccessKey = "identity_auth_key"
CookieRefreshKey = "identity_refresh_key"
)
func EncodeJwt(id string, typ, sub string, aud []string, exp time.Time) (string, error) { func EncodeJwt(id string, typ, sub string, aud []string, exp time.Time) (string, error) {
tk := jwt.NewWithClaims(jwt.SigningMethodHS512, PayloadClaims{ tk := jwt.NewWithClaims(jwt.SigningMethodHS512, PayloadClaims{
jwt.RegisteredClaims{ jwt.RegisteredClaims{
@ -54,3 +60,22 @@ func DecodeJwt(str string) (PayloadClaims, error) {
return claims, fmt.Errorf("unexpected token payload: not payload claims type") return claims, fmt.Errorf("unexpected token payload: not payload claims type")
} }
} }
func SetJwtCookieSet(c *fiber.Ctx, access, refresh string) {
c.Cookie(&fiber.Cookie{
Name: CookieAccessKey,
Value: access,
Domain: viper.GetString("security.cookie_domain"),
SameSite: viper.GetString("security.cookie_samesite"),
Expires: time.Now().Add(60 * time.Minute),
Path: "/",
})
c.Cookie(&fiber.Cookie{
Name: CookieRefreshKey,
Value: refresh,
Domain: viper.GetString("security.cookie_domain"),
SameSite: viper.GetString("security.cookie_samesite"),
Expires: time.Now().Add(24 * 30 * time.Hour),
Path: "/",
})
}

@ -1,35 +1,51 @@
package server package server
import ( import (
"code.smartsheep.studio/hydrogen/interactive/pkg/database"
"code.smartsheep.studio/hydrogen/interactive/pkg/models"
"code.smartsheep.studio/hydrogen/interactive/pkg/security" "code.smartsheep.studio/hydrogen/interactive/pkg/security"
"code.smartsheep.studio/hydrogen/interactive/pkg/services"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/keyauth" "strings"
"strconv"
) )
var auth = keyauth.New(keyauth.Config{ func authMiddleware(c *fiber.Ctx) error {
KeyLookup: "header:Authorization", var token string
AuthScheme: "Bearer", if cookie := c.Cookies(security.CookieAccessKey); len(cookie) > 0 {
Validator: func(c *fiber.Ctx, token string) (bool, error) { token = cookie
claims, err := security.DecodeJwt(token) }
if err != nil { if header := c.Get(fiber.HeaderAuthorization); len(header) > 0 {
return false, err tk := strings.Replace(header, "Bearer", "", 1)
token = strings.TrimSpace(tk)
} }
id, _ := strconv.Atoi(claims.Subject) c.Locals("token", token)
var user models.Account if err := authFunc(c); err != nil {
if err := database.C.Where(&models.Account{ return err
BaseModel: models.BaseModel{ID: uint(id)},
}).First(&user).Error; err != nil {
return false, err
} }
return c.Next()
}
func authFunc(c *fiber.Ctx, overrides ...string) error {
var token string
if len(overrides) > 0 {
token = overrides[0]
} else {
if tk, ok := c.Locals("token").(string); !ok {
return fiber.NewError(fiber.StatusUnauthorized)
} else {
token = tk
}
}
rtk := c.Cookies(security.CookieRefreshKey)
if user, atk, rtk, err := services.Authenticate(token, rtk); err == nil {
if atk != token {
security.SetJwtCookieSet(c, atk, rtk)
}
c.Locals("principal", user) c.Locals("principal", user)
return nil
return true, nil } else {
}, return err
ContextKey: "token", }
}) }

@ -1,93 +0,0 @@
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
func buildOauth2Config() {
cfg = oauth2.Config{
RedirectURL: fmt.Sprintf("https://%s/auth/callback", viper.GetString("domain")),
ClientID: viper.GetString("identity.client_id"),
ClientSecret: viper.GetString("identity.client_secret"),
Scopes: []string{"openid"},
Endpoint: oauth2.Endpoint{
AuthURL: fmt.Sprintf("%s/auth/o/connect", viper.GetString("identity.endpoint")),
TokenURL: fmt.Sprintf("%s/api/auth/token", viper.GetString("identity.endpoint")),
AuthStyle: oauth2.AuthStyleInParams,
},
}
}
func doLogin(c *fiber.Ctx) error {
buildOauth2Config()
url := cfg.AuthCodeURL(uuid.NewString())
return c.JSON(fiber.Map{
"target": url,
})
}
func postLogin(c *fiber.Ctx) error {
buildOauth2Config()
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("identity.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.IdentityUserinfo
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,
})
}

@ -58,45 +58,41 @@ func NewServer() {
api := A.Group("/api").Name("API") api := A.Group("/api").Name("API")
{ {
api.Get("/auth", doLogin) api.Get("/users/me", authMiddleware, getUserinfo)
api.Get("/auth/callback", postLogin)
api.Post("/auth/refresh", doRefreshToken)
api.Get("/users/me", auth, getUserinfo)
api.Get("/users/:accountId", getOthersInfo) api.Get("/users/:accountId", getOthersInfo)
api.Get("/users/:accountId/follow", auth, getAccountFollowed) api.Get("/users/:accountId/follow", authMiddleware, getAccountFollowed)
api.Post("/users/:accountId/follow", auth, doFollowAccount) api.Post("/users/:accountId/follow", authMiddleware, doFollowAccount)
api.Get("/attachments/o/:fileId", cache.New(cache.Config{ api.Get("/attachments/o/:fileId", cache.New(cache.Config{
Expiration: 365 * 24 * time.Hour, Expiration: 365 * 24 * time.Hour,
CacheControl: true, CacheControl: true,
}), openAttachment) }), openAttachment)
api.Post("/attachments", auth, uploadAttachment) api.Post("/attachments", authMiddleware, uploadAttachment)
api.Get("/posts", listPost) api.Get("/posts", listPost)
api.Get("/posts/:postId", getPost) api.Get("/posts/:postId", getPost)
api.Post("/posts", auth, createPost) api.Post("/posts", authMiddleware, createPost)
api.Post("/posts/:postId/react/:reactType", auth, reactPost) api.Post("/posts/:postId/react/:reactType", authMiddleware, reactPost)
api.Put("/posts/:postId", auth, editPost) api.Put("/posts/:postId", authMiddleware, editPost)
api.Delete("/posts/:postId", auth, deletePost) api.Delete("/posts/:postId", authMiddleware, deletePost)
api.Get("/categories", listCategroies) api.Get("/categories", listCategroies)
api.Post("/categories", auth, newCategory) api.Post("/categories", authMiddleware, newCategory)
api.Put("/categories/:categoryId", auth, editCategory) api.Put("/categories/:categoryId", authMiddleware, editCategory)
api.Delete("/categories/:categoryId", auth, deleteCategory) api.Delete("/categories/:categoryId", authMiddleware, deleteCategory)
api.Get("/creators/posts", auth, listOwnPost) api.Get("/creators/posts", authMiddleware, listOwnPost)
api.Get("/creators/posts/:postId", auth, getOwnPost) api.Get("/creators/posts/:postId", authMiddleware, getOwnPost)
api.Get("/realms", listRealm) api.Get("/realms", listRealm)
api.Get("/realms/me", auth, listOwnedRealm) api.Get("/realms/me", authMiddleware, listOwnedRealm)
api.Get("/realms/me/available", auth, listAvailableRealm) api.Get("/realms/me/available", authMiddleware, listAvailableRealm)
api.Get("/realms/:realmId", getRealm) api.Get("/realms/:realmId", getRealm)
api.Post("/realms", auth, createRealm) api.Post("/realms", authMiddleware, createRealm)
api.Post("/realms/:realmId/invite", auth, inviteRealm) api.Post("/realms/:realmId/invite", authMiddleware, inviteRealm)
api.Post("/realms/:realmId/kick", auth, kickRealm) api.Post("/realms/:realmId/kick", authMiddleware, kickRealm)
api.Put("/realms/:realmId", auth, editRealm) api.Put("/realms/:realmId", authMiddleware, editRealm)
api.Delete("/realms/:realmId", auth, deleteRealm) api.Delete("/realms/:realmId", authMiddleware, deleteRealm)
} }
A.Use("/", cache.New(cache.Config{ A.Use("/", cache.New(cache.Config{

@ -9,5 +9,8 @@ func getMetadata(c *fiber.Ctx) error {
return c.JSON(fiber.Map{ return c.JSON(fiber.Map{
"name": viper.GetString("name"), "name": viper.GetString("name"),
"domain": viper.GetString("domain"), "domain": viper.GetString("domain"),
"components": fiber.Map{
"identity": viper.GetString("identity.endpoint"),
},
}) })
} }

@ -1,11 +1,13 @@
package services package services
import ( import (
"code.smartsheep.studio/hydrogen/identity/pkg/grpc/proto"
"code.smartsheep.studio/hydrogen/interactive/pkg/database" "code.smartsheep.studio/hydrogen/interactive/pkg/database"
"code.smartsheep.studio/hydrogen/interactive/pkg/grpc"
"code.smartsheep.studio/hydrogen/interactive/pkg/models" "code.smartsheep.studio/hydrogen/interactive/pkg/models"
"fmt" "context"
"github.com/gofiber/fiber/v2"
"github.com/spf13/viper" "github.com/spf13/viper"
"time"
) )
func FollowAccount(followerId, followingId uint) error { func FollowAccount(followerId, followingId uint) error {
@ -32,22 +34,19 @@ func GetAccountFollowed(user models.Account, target models.Account) (models.Acco
return relationship, err == nil return relationship, err == nil
} }
func NotifyAccount(user models.Account, subject, content string, links ...fiber.Map) error { func NotifyAccount(user models.Account, subject, content string, links ...*proto.NotifyLink) error {
agent := fiber.Post(viper.GetString("identity.endpoint") + "/api/dev/notify") ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
agent.JSON(fiber.Map{ defer cancel()
"client_id": viper.GetString("identity.client_id"),
"client_secret": viper.GetString("identity.client_secret"), _, err := grpc.Notify.NotifyUser(ctx, &proto.NotifyRequest{
"subject": subject, ClientId: viper.GetString("identity.client_id"),
"content": content, ClientSecret: viper.GetString("identity.client_secret"),
"links": links, Subject: subject,
"user_id": user.ExternalID, Content: content,
Links: links,
RecipientId: uint64(user.ID),
IsImportant: false,
}) })
if status, body, errs := agent.Bytes(); len(errs) > 0 { return err
return errs[0]
} else if status != 200 {
return fmt.Errorf(string(body))
}
return nil
} }

@ -1,40 +1,29 @@
package services package services
import ( import (
"code.smartsheep.studio/hydrogen/identity/pkg/grpc/proto"
"code.smartsheep.studio/hydrogen/interactive/pkg/database" "code.smartsheep.studio/hydrogen/interactive/pkg/database"
"code.smartsheep.studio/hydrogen/interactive/pkg/grpc"
"code.smartsheep.studio/hydrogen/interactive/pkg/models" "code.smartsheep.studio/hydrogen/interactive/pkg/models"
"code.smartsheep.studio/hydrogen/interactive/pkg/security" "context"
"errors" "errors"
"fmt"
"github.com/google/uuid"
"gorm.io/gorm" "gorm.io/gorm"
"strconv"
"time" "time"
) )
type IdentityUserinfo struct { func LinkAccount(userinfo *proto.Userinfo) (models.Account, error) {
Sub string `json:"sub"`
Name string `json:"name"`
Email string `json:"email"`
Picture string `json:"picture"`
PreferredUsername string `json:"preferred_username"`
}
func LinkAccount(userinfo IdentityUserinfo) (models.Account, error) {
id, _ := strconv.Atoi(userinfo.Sub)
var account models.Account var account models.Account
if err := database.C.Where(&models.Account{ if err := database.C.Where(&models.Account{
ExternalID: uint(id), ExternalID: uint(userinfo.Id),
}).First(&account).Error; err != nil { }).First(&account).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
account = models.Account{ account = models.Account{
Name: userinfo.Name, Name: userinfo.Name,
Nick: userinfo.PreferredUsername, Nick: userinfo.Nick,
Avatar: userinfo.Picture, Avatar: userinfo.Avatar,
EmailAddress: userinfo.Email, EmailAddress: userinfo.Email,
PowerLevel: 0, PowerLevel: 0,
ExternalID: uint(id), ExternalID: uint(userinfo.Id),
} }
return account, database.C.Save(&account).Error return account, database.C.Save(&account).Error
} }
@ -42,8 +31,8 @@ func LinkAccount(userinfo IdentityUserinfo) (models.Account, error) {
} }
account.Name = userinfo.Name account.Name = userinfo.Name
account.Nick = userinfo.PreferredUsername account.Nick = userinfo.Nick
account.Avatar = userinfo.Picture account.Avatar = userinfo.Avatar
account.EmailAddress = userinfo.Email account.EmailAddress = userinfo.Email
err := database.C.Save(&account).Error err := database.C.Save(&account).Error
@ -51,51 +40,21 @@ func LinkAccount(userinfo IdentityUserinfo) (models.Account, error) {
return account, err return account, err
} }
func GetToken(account models.Account) (string, string, error) { func Authenticate(atk, rtk string) (models.Account, string, string, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
var err error var err error
var refresh, access string var user models.Account
reply, err := grpc.Auth.Authenticate(ctx, &proto.AuthRequest{
sub := strconv.Itoa(int(account.ID)) AccessToken: atk,
access, err = security.EncodeJwt( RefreshToken: &rtk,
uuid.NewString(), })
security.JwtAccessType,
sub,
[]string{"interactive"},
time.Now().Add(30*time.Minute),
)
if err != nil { if err != nil {
return refresh, access, err return user, reply.GetAccessToken(), reply.GetRefreshToken(), 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 user, err = LinkAccount(reply.Userinfo)
}
func RefreshToken(token string) (string, string, error) { return user, reply.GetAccessToken(), reply.GetRefreshToken(), err
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)
} }

@ -1,11 +1,11 @@
package services package services
import ( import (
"code.smartsheep.studio/hydrogen/identity/pkg/grpc/proto"
"errors" "errors"
"fmt" "fmt"
"time" "time"
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"code.smartsheep.studio/hydrogen/interactive/pkg/database" "code.smartsheep.studio/hydrogen/interactive/pkg/database"
@ -230,7 +230,7 @@ func NewPost(
op.Author, op.Author,
fmt.Sprintf("%s replied you", user.Name), fmt.Sprintf("%s replied you", user.Name),
fmt.Sprintf("%s replied your post. Check it out!", user.Name), fmt.Sprintf("%s replied your post. Check it out!", user.Name),
fiber.Map{"label": "Related post", "url": postUrl}, &proto.NotifyLink{Label: "Related post", Url: postUrl},
) )
if err != nil { if err != nil {
log.Error().Err(err).Msg("An error occurred when notifying user...") log.Error().Err(err).Msg("An error occurred when notifying user...")
@ -257,7 +257,7 @@ func NewPost(
account, account,
fmt.Sprintf("%s just posted a post", user.Name), fmt.Sprintf("%s just posted a post", user.Name),
"Account you followed post a brand new post. Check it out!", "Account you followed post a brand new post. Check it out!",
fiber.Map{"label": "Related post", "url": postUrl}, &proto.NotifyLink{Label: "Related post", Url: postUrl},
) )
if err != nil { if err != nil {
log.Error().Err(err).Msg("An error occurred when notifying user...") log.Error().Err(err).Msg("An error occurred when notifying user...")

@ -1,5 +1,5 @@
:root { :root {
--bs-body-font-family: "IBM Plex Serif", "Noto Serif SC", sans-serif !important; --bs-body-font-family: "IBM Plex Sans", "Noto Serif SC", sans-serif !important;
} }
html, html,
@ -7,130 +7,117 @@ body {
font-family: var(--bs-body-font-family); font-family: var(--bs-body-font-family);
} }
/* ibm-plex-serif-100 - latin */ /* ibm-plex-sans-100 - latin */
@font-face { @font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ 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-family: 'IBM Plex Sans';
font-style: normal; font-style: normal;
font-weight: 100; font-weight: 100;
src: url("./ibm-plex-serif-v19-latin-100.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ src: url('./ibm-plex-sans-v19-latin-100.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
} }
/* ibm-plex-sans-100italic - latin */
/* ibm-plex-serif-100italic - latin */
@font-face { @font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ 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-family: 'IBM Plex Sans';
font-style: italic; font-style: italic;
font-weight: 100; font-weight: 100;
src: url("./ibm-plex-serif-v19-latin-100italic.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ src: url('./ibm-plex-sans-v19-latin-100italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
} }
/* ibm-plex-sans-200 - latin */
/* ibm-plex-serif-200 - latin */
@font-face { @font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ 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-family: 'IBM Plex Sans';
font-style: normal; font-style: normal;
font-weight: 200; font-weight: 200;
src: url("./ibm-plex-serif-v19-latin-200.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ src: url('./ibm-plex-sans-v19-latin-200.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
} }
/* ibm-plex-sans-200italic - latin */
/* ibm-plex-serif-200italic - latin */
@font-face { @font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ 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-family: 'IBM Plex Sans';
font-style: italic; font-style: italic;
font-weight: 200; font-weight: 200;
src: url("./ibm-plex-serif-v19-latin-200italic.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ src: url('./ibm-plex-sans-v19-latin-200italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
} }
/* ibm-plex-sans-300 - latin */
/* ibm-plex-serif-300 - latin */
@font-face { @font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ 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-family: 'IBM Plex Sans';
font-style: normal; font-style: normal;
font-weight: 300; font-weight: 300;
src: url("./ibm-plex-serif-v19-latin-300.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ src: url('./ibm-plex-sans-v19-latin-300.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
} }
/* ibm-plex-sans-300italic - latin */
/* ibm-plex-serif-300italic - latin */
@font-face { @font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ 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-family: 'IBM Plex Sans';
font-style: italic; font-style: italic;
font-weight: 300; font-weight: 300;
src: url("./ibm-plex-serif-v19-latin-300italic.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ src: url('./ibm-plex-sans-v19-latin-300italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
} }
/* ibm-plex-sans-regular - latin */
/* ibm-plex-serif-regular - latin */
@font-face { @font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ 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-family: 'IBM Plex Sans';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: url("./ibm-plex-serif-v19-latin-regular.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ src: url('./ibm-plex-sans-v19-latin-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
} }
/* ibm-plex-sans-italic - latin */
/* ibm-plex-serif-italic - latin */
@font-face { @font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ 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-family: 'IBM Plex Sans';
font-style: italic; font-style: italic;
font-weight: 400; font-weight: 400;
src: url("./ibm-plex-serif-v19-latin-italic.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ src: url('./ibm-plex-sans-v19-latin-italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
} }
/* ibm-plex-sans-500 - latin */
/* ibm-plex-serif-500 - latin */
@font-face { @font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ 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-family: 'IBM Plex Sans';
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
src: url("./ibm-plex-serif-v19-latin-500.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ src: url('./ibm-plex-sans-v19-latin-500.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
} }
/* ibm-plex-sans-500italic - latin */
/* ibm-plex-serif-500italic - latin */
@font-face { @font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ 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-family: 'IBM Plex Sans';
font-style: italic; font-style: italic;
font-weight: 500; font-weight: 500;
src: url("./ibm-plex-serif-v19-latin-500italic.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ src: url('./ibm-plex-sans-v19-latin-500italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
} }
/* ibm-plex-sans-600 - latin */
/* ibm-plex-serif-600 - latin */
@font-face { @font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ 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-family: 'IBM Plex Sans';
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
src: url("./ibm-plex-serif-v19-latin-600.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ src: url('./ibm-plex-sans-v19-latin-600.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
} }
/* ibm-plex-sans-600italic - latin */
/* ibm-plex-serif-600italic - latin */
@font-face { @font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ 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-family: 'IBM Plex Sans';
font-style: italic; font-style: italic;
font-weight: 600; font-weight: 600;
src: url("./ibm-plex-serif-v19-latin-600italic.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ src: url('./ibm-plex-sans-v19-latin-600italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
} }
/* ibm-plex-sans-700 - latin */
/* ibm-plex-serif-700 - latin */
@font-face { @font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ 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-family: 'IBM Plex Sans';
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
src: url("./ibm-plex-serif-v19-latin-700.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ src: url('./ibm-plex-sans-v19-latin-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
} }
/* ibm-plex-sans-700italic - latin */
/* ibm-plex-serif-700italic - latin */
@font-face { @font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ 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-family: 'IBM Plex Sans';
font-style: italic; font-style: italic;
font-weight: 700; font-weight: 700;
src: url("./ibm-plex-serif-v19-latin-700italic.woff2") format("woff2"); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ src: url('./ibm-plex-sans-v19-latin-700italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
} }
/* noto-serif-sc-200 - chinese-simplified */ /* noto-serif-sc-200 - chinese-simplified */

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -0,0 +1,22 @@
import { Show } from "solid-js";
export default function Avatar(props: { user: any }) {
return (
<Show
when={props.user?.avatar}
fallback={
<div class="avatar placeholder">
<div class="w-12 h-12 bg-neutral text-neutral-content">
<span class="text-xl uppercase">{props.user?.name?.substring(0, 1)}</span>
</div>
</div>
}
>
<div class="avatar">
<div class="w-12">
<img alt="avatar" src={props.user?.avatar} />
</div>
</div>
</Show>
);
}

@ -4,6 +4,7 @@ import { request } from "../../scripts/request.ts";
import PostAttachments from "./PostAttachments.tsx"; import PostAttachments from "./PostAttachments.tsx";
import * as marked from "marked"; import * as marked from "marked";
import DOMPurify from "dompurify"; import DOMPurify from "dompurify";
import Avatar from "../Avatar.tsx";
export default function PostItem(props: { export default function PostItem(props: {
post: any; post: any;
@ -28,7 +29,7 @@ export default function PostItem(props: {
setReacting(true); setReacting(true);
const res = await request(`/api/posts/${item.id}/react/${type}`, { const res = await request(`/api/posts/${item.id}/react/${type}`, {
method: "POST", method: "POST",
headers: { Authorization: `Bearer ${getAtk()}` }, headers: { Authorization: `Bearer ${getAtk()}` }
}); });
if (res.status !== 201 && res.status !== 204) { if (res.status !== 201 && res.status !== 204) {
props.onError(await res.text()); props.onError(await res.text());
@ -46,15 +47,8 @@ export default function PostItem(props: {
<Show when={!props.noAuthor}> <Show when={!props.noAuthor}>
<a href={`/accounts/${props.post.author.name}`}> <a href={`/accounts/${props.post.author.name}`}>
<div class="flex bg-base-200"> <div class="flex bg-base-200">
<div class="avatar pl-[20px]"> <div class="pl-[20px]">
<div class="w-12"> <Avatar user={props.post.author} />
<Show
when={props.post.author.avatar}
fallback={<span class="text-3xl">{props.post.author.name.substring(0, 1)}</span>}
>
<img alt="avatar" src={props.post.author.avatar} />
</Show>
</div>
</div> </div>
<div class="flex items-center px-5"> <div class="flex items-center px-5">
<div> <div>
@ -122,7 +116,8 @@ export default function PostItem(props: {
<Show when={!props.noControl}> <Show when={!props.noControl}>
<div class="relative"> <div class="relative">
<Show when={!userinfo?.isLoggedIn}> <Show when={!userinfo?.isLoggedIn}>
<div class="px-7 py-2.5 h-12 w-full opacity-0 transition-opacity hover:opacity-100 bg-base-100 border-t border-base-200 z-[1] absolute top-0 left-0"> <div
class="px-7 py-2.5 h-12 w-full opacity-0 transition-opacity hover:opacity-100 bg-base-100 border-t border-base-200 z-[1] absolute top-0 left-0">
<b>Login!</b> To access entire platform. <b>Login!</b> To access entire platform.
</div> </div>
</Show> </Show>

@ -4,6 +4,7 @@ import { request } from "../../scripts/request.ts";
import styles from "./PostPublish.module.css"; import styles from "./PostPublish.module.css";
import PostEditActions from "./PostEditActions.tsx"; import PostEditActions from "./PostEditActions.tsx";
import Avatar from "../Avatar.tsx";
export default function PostPublish(props: { export default function PostPublish(props: {
replying?: any, replying?: any,
@ -100,7 +101,7 @@ export default function PostPublish(props: {
categories: categories(), categories: categories(),
tags: tags(), tags: tags(),
realm_id: props.realmId, realm_id: props.realmId,
published_at: publishedAt() ? new Date(publishedAt()) : new Date(), published_at: publishedAt() ? new Date(publishedAt()) : new Date()
}) })
}); });
if (res.status !== 200) { if (res.status !== 200) {
@ -124,13 +125,8 @@ export default function PostPublish(props: {
<> <>
<form id="publish" onSubmit={(evt) => (props.editing ? doEdit : doPost)(evt)} onReset={() => resetForm()}> <form id="publish" onSubmit={(evt) => (props.editing ? doEdit : doPost)(evt)} onReset={() => resetForm()}>
<div id="publish-identity" class="flex border-y border-base-200"> <div id="publish-identity" class="flex border-y border-base-200">
<div class="avatar pl-[20px]"> <div class="pl-[20px]">
<div class="w-12"> <Avatar user={userinfo?.profiles} />
<Show when={userinfo?.profiles?.avatar}
fallback={<span class="text-3xl">{userinfo?.displayName.substring(0, 1)}</span>}>
<img alt="avatar" src={userinfo?.profiles?.avatar} />
</Show>
</div>
</div> </div>
<div class="flex flex-grow"> <div class="flex flex-grow">
<input name="title" value={props.editing?.title ?? ""} <input name="title" value={props.editing?.title ?? ""}

@ -37,8 +37,6 @@ const router = (basename?: string) => (
<Route path="/publish" component={lazy(() => import("./pages/creators/publish.tsx"))} /> <Route path="/publish" component={lazy(() => import("./pages/creators/publish.tsx"))} />
<Route path="/edit/:postId" component={lazy(() => import("./pages/creators/edit.tsx"))} /> <Route path="/edit/:postId" component={lazy(() => import("./pages/creators/edit.tsx"))} />
</Route> </Route>
<Route path="/auth" component={lazy(() => import("./pages/auth/callout.tsx"))} />
<Route path="/auth/callback" component={lazy(() => import("./pages/auth/callback.tsx"))} />
</Router> </Router>
</UserinfoProvider> </UserinfoProvider>
</WellKnownProvider> </WellKnownProvider>

@ -1,4 +1,4 @@
import { For, Match, Switch } from "solid-js"; import { createMemo, For, Match, Switch } from "solid-js";
import { clearUserinfo, useUserinfo } from "../../stores/userinfo.tsx"; import { clearUserinfo, useUserinfo } from "../../stores/userinfo.tsx";
import { useNavigate } from "@solidjs/router"; import { useNavigate } from "@solidjs/router";
import { useWellKnown } from "../../stores/wellKnown.tsx"; import { useWellKnown } from "../../stores/wellKnown.tsx";
@ -20,9 +20,11 @@ export default function Navigator() {
const userinfo = useUserinfo(); const userinfo = useUserinfo();
const navigate = useNavigate(); const navigate = useNavigate();
const endpoint = createMemo(() => wellKnown?.components?.identity)
function logout() { function logout() {
clearUserinfo(); clearUserinfo();
navigate("/auth/login"); navigate("/");
} }
return ( return (
@ -54,7 +56,7 @@ export default function Navigator() {
</button> </button>
</Match> </Match>
<Match when={!userinfo?.isLoggedIn}> <Match when={!userinfo?.isLoggedIn}>
<a href="/auth" class="btn btn-sm btn-primary"> <a href={`${endpoint()}/auth/login?redirect_uri=${window.location}`} class="btn btn-sm btn-primary">
Login Login
</a> </a>
</Match> </Match>

@ -1,65 +0,0 @@
import { createSignal, Show } from "solid-js";
import { readProfiles } from "../../stores/userinfo.tsx";
import { useNavigate } from "@solidjs/router";
import Cookie from "universal-cookie";
import { request } from "../../scripts/request.ts";
export default function AuthCallback() {
const [error, setError] = createSignal<string | null>(null);
const [status, setStatus] = createSignal("Communicating with Goatpass...");
const navigate = useNavigate();
async function callback() {
const res = await request(`/api/auth/callback${location.search}`);
if (res.status !== 200) {
setError(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 });
setStatus("Pulling your personal data...");
await readProfiles();
setStatus("Redirecting...")
setTimeout(() => navigate("/"), 1850)
}
}
callback();
return (
<div class="w-full h-full flex justify-center items-center">
<div class="card w-[480px] max-w-screen shadow-xl">
<div class="card-body">
<div id="header" class="text-center mb-5">
<h1 class="text-xl font-bold">Authenticate</h1>
<p>Via your Goatpass account</p>
</div>
<div class="pt-16 text-center">
<div class="text-center">
<div>
<span class="loading loading-lg loading-bars"></span>
</div>
<span>{status()}</span>
</div>
</div>
<Show when={error()} fallback={<div class="mt-16"></div>}>
<div id="alerts" class="mt-16">
<div role="alert" class="alert alert-error">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="capitalize">{error()}</span>
</div>
</div>
</Show>
</div>
</div>
</div>
);
}

@ -1,56 +0,0 @@
import { createSignal, Show } from "solid-js";
import { request } from "../../scripts/request.ts";
export default function AuthCallout() {
const [error, setError] = createSignal<string | null>(null);
const [status, setStatus] = createSignal("Communicating with Goatpass...");
async function communicate() {
const res = await request(`/api/auth${location.search}`);
if (res.status !== 200) {
setError(await res.text());
} else {
const data = await res.json();
setStatus("Got you! Now redirecting...");
window.open(data["target"], "_self");
}
}
communicate();
return (
<div class="w-full h-full flex justify-center items-center">
<div class="card w-[480px] max-w-screen shadow-xl">
<div class="card-body">
<div id="header" class="text-center mb-5">
<h1 class="text-xl font-bold">Authenticate</h1>
<p>Via your Goatpass account</p>
</div>
<div class="pt-16 text-center">
<div class="text-center">
<div>
<span class="loading loading-lg loading-bars"></span>
</div>
<span>{status()}</span>
</div>
</div>
<Show when={error()} fallback={<div class="mt-16"></div>}>
<div id="alerts" class="mt-16">
<div role="alert" class="alert alert-error">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="capitalize">{error()}</span>
</div>
</div>
</Show>
</div>
</div>
</div>
);
}

@ -14,39 +14,20 @@ const UserinfoContext = createContext<Userinfo>();
const defaultUserinfo: Userinfo = { const defaultUserinfo: Userinfo = {
isLoggedIn: false, isLoggedIn: false,
displayName: "Citizen", displayName: "Citizen",
profiles: null, profiles: null
}; };
const [userinfo, setUserinfo] = createStore<Userinfo>(structuredClone(defaultUserinfo)); const [userinfo, setUserinfo] = createStore<Userinfo>(structuredClone(defaultUserinfo));
export function getAtk(): string { export function getAtk(): string {
return new Cookie().get("access_token"); return new Cookie().get("identity_auth_key");
}
export async function refreshAtk() {
const rtk = new Cookie().get("refresh_token");
const res = await request("/api/auth/refresh", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
refresh_token: rtk,
})
});
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 { function checkLoggedIn(): boolean {
return new Cookie().get("access_token"); return new Cookie().get("identity_auth_key");
} }
export async function readProfiles(recovering = true) { export async function readProfiles() {
if (!checkLoggedIn()) return; if (!checkLoggedIn()) return;
const res = await request("/api/users/me", { const res = await request("/api/users/me", {
@ -54,28 +35,28 @@ export async function readProfiles(recovering = true) {
}); });
if (res.status !== 200) { if (res.status !== 200) {
if (recovering) {
// Auto retry after refresh access token
await refreshAtk();
return await readProfiles(false);
} else {
clearUserinfo(); clearUserinfo();
window.location.reload(); window.location.reload();
} }
}
const data = await res.json(); const data = await res.json();
setUserinfo({ setUserinfo({
isLoggedIn: true, isLoggedIn: true,
displayName: data["name"], displayName: data["name"],
profiles: data, profiles: data
}); });
} }
export function clearUserinfo() { export function clearUserinfo() {
new Cookie().remove("access_token", {path: "/", maxAge: undefined}); const cookies = document.cookie.split(";");
new Cookie().remove("refresh_token", {path: "/", maxAge: undefined}); for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i];
const eqPos = cookie.indexOf("=");
const name = eqPos > -1 ? cookie.substring(0, eqPos) : cookie;
document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT";
}
setUserinfo(defaultUserinfo); setUserinfo(defaultUserinfo);
} }

@ -12,7 +12,8 @@ content = "uploads"
[identity] [identity]
client_id = "goatplaza" client_id = "goatplaza"
client_secret = "Z9k9AFTj^p" client_secret = "Z9k9AFTj^p"
endpoint = "https://id.smartsheep.studio" endpoint = "http://localhost:8444"
grpc_endpoint = "127.0.0.1:7444"
[mailer] [mailer]
name = "Alphabot <alphabot@smartsheep.studio>" name = "Alphabot <alphabot@smartsheep.studio>"
@ -21,6 +22,12 @@ smtp_port = 465
username = "alphabot@smartsheep.studio" username = "alphabot@smartsheep.studio"
password = "gz937Zxxzfcd9SeH" password = "gz937Zxxzfcd9SeH"
[security]
cookie_domain = "localhost"
cookie_samesite = "Lax"
access_token_duration = 300
refresh_token_duration = 2592000
[database] [database]
dsn = "host=localhost dbname=hy_interactive port=5432 sslmode=disable" dsn = "host=localhost dbname=hy_interactive port=5432 sslmode=disable"
prefix = "interactive_" prefix = "interactive_"