✨ Use uni-token
💄 A lot of optimization
This commit is contained in:
parent
4101043d65
commit
1e04f2029f
9
go.mod
9
go.mod
@ -12,7 +12,7 @@ require (
|
||||
github.com/rs/zerolog v1.31.0
|
||||
github.com/samber/lo v1.39.0
|
||||
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
|
||||
gorm.io/datatypes v1.2.0
|
||||
gorm.io/driver/postgres v1.5.4
|
||||
@ -20,6 +20,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
code.smartsheep.studio/hydrogen/identity v0.0.0-20240221130517-c169ffdacda8 // indirect
|
||||
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
|
||||
@ -59,11 +60,13 @@ require (
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // 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/sys v0.16.0 // indirect
|
||||
golang.org/x/sys v0.17.0 // indirect
|
||||
golang.org/x/text v0.14.0 // 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
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
22
go.sum
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/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
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.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||
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/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
|
||||
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.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
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/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
|
||||
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.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
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=
|
||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||
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/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||
|
@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"code.smartsheep.studio/hydrogen/interactive/pkg/grpc"
|
||||
"code.smartsheep.studio/hydrogen/interactive/pkg/server"
|
||||
"os"
|
||||
"os/signal"
|
||||
@ -37,6 +38,13 @@ func main() {
|
||||
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.NewServer()
|
||||
go server.Listen()
|
||||
|
24
pkg/grpc/client.go
Normal file
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 (
|
||||
"fmt"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
@ -19,6 +20,11 @@ const (
|
||||
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) {
|
||||
tk := jwt.NewWithClaims(jwt.SigningMethodHS512, PayloadClaims{
|
||||
jwt.RegisteredClaims{
|
||||
@ -54,3 +60,22 @@ func DecodeJwt(str string) (PayloadClaims, error) {
|
||||
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
|
||||
|
||||
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/services"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/keyauth"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
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
|
||||
func authMiddleware(c *fiber.Ctx) error {
|
||||
var token string
|
||||
if cookie := c.Cookies(security.CookieAccessKey); len(cookie) > 0 {
|
||||
token = cookie
|
||||
}
|
||||
if header := c.Get(fiber.HeaderAuthorization); len(header) > 0 {
|
||||
tk := strings.Replace(header, "Bearer", "", 1)
|
||||
token = strings.TrimSpace(tk)
|
||||
}
|
||||
|
||||
c.Locals("token", token)
|
||||
|
||||
if err := authFunc(c); err != nil {
|
||||
return 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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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)
|
||||
|
||||
return true, nil
|
||||
},
|
||||
ContextKey: "token",
|
||||
})
|
||||
return nil
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -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.Get("/auth", doLogin)
|
||||
api.Get("/auth/callback", postLogin)
|
||||
api.Post("/auth/refresh", doRefreshToken)
|
||||
|
||||
api.Get("/users/me", auth, getUserinfo)
|
||||
api.Get("/users/me", authMiddleware, getUserinfo)
|
||||
api.Get("/users/:accountId", getOthersInfo)
|
||||
api.Get("/users/:accountId/follow", auth, getAccountFollowed)
|
||||
api.Post("/users/:accountId/follow", auth, doFollowAccount)
|
||||
api.Get("/users/:accountId/follow", authMiddleware, getAccountFollowed)
|
||||
api.Post("/users/:accountId/follow", authMiddleware, doFollowAccount)
|
||||
|
||||
api.Get("/attachments/o/:fileId", cache.New(cache.Config{
|
||||
Expiration: 365 * 24 * time.Hour,
|
||||
CacheControl: true,
|
||||
}), openAttachment)
|
||||
api.Post("/attachments", auth, uploadAttachment)
|
||||
api.Post("/attachments", authMiddleware, uploadAttachment)
|
||||
|
||||
api.Get("/posts", listPost)
|
||||
api.Get("/posts/:postId", getPost)
|
||||
api.Post("/posts", auth, createPost)
|
||||
api.Post("/posts/:postId/react/:reactType", auth, reactPost)
|
||||
api.Put("/posts/:postId", auth, editPost)
|
||||
api.Delete("/posts/:postId", auth, deletePost)
|
||||
api.Post("/posts", authMiddleware, createPost)
|
||||
api.Post("/posts/:postId/react/:reactType", authMiddleware, reactPost)
|
||||
api.Put("/posts/:postId", authMiddleware, editPost)
|
||||
api.Delete("/posts/:postId", authMiddleware, deletePost)
|
||||
|
||||
api.Get("/categories", listCategroies)
|
||||
api.Post("/categories", auth, newCategory)
|
||||
api.Put("/categories/:categoryId", auth, editCategory)
|
||||
api.Delete("/categories/:categoryId", auth, deleteCategory)
|
||||
api.Post("/categories", authMiddleware, newCategory)
|
||||
api.Put("/categories/:categoryId", authMiddleware, editCategory)
|
||||
api.Delete("/categories/:categoryId", authMiddleware, deleteCategory)
|
||||
|
||||
api.Get("/creators/posts", auth, listOwnPost)
|
||||
api.Get("/creators/posts/:postId", auth, getOwnPost)
|
||||
api.Get("/creators/posts", authMiddleware, listOwnPost)
|
||||
api.Get("/creators/posts/:postId", authMiddleware, getOwnPost)
|
||||
|
||||
api.Get("/realms", listRealm)
|
||||
api.Get("/realms/me", auth, listOwnedRealm)
|
||||
api.Get("/realms/me/available", auth, listAvailableRealm)
|
||||
api.Get("/realms/me", authMiddleware, listOwnedRealm)
|
||||
api.Get("/realms/me/available", authMiddleware, listAvailableRealm)
|
||||
api.Get("/realms/:realmId", getRealm)
|
||||
api.Post("/realms", auth, createRealm)
|
||||
api.Post("/realms/:realmId/invite", auth, inviteRealm)
|
||||
api.Post("/realms/:realmId/kick", auth, kickRealm)
|
||||
api.Put("/realms/:realmId", auth, editRealm)
|
||||
api.Delete("/realms/:realmId", auth, deleteRealm)
|
||||
api.Post("/realms", authMiddleware, createRealm)
|
||||
api.Post("/realms/:realmId/invite", authMiddleware, inviteRealm)
|
||||
api.Post("/realms/:realmId/kick", authMiddleware, kickRealm)
|
||||
api.Put("/realms/:realmId", authMiddleware, editRealm)
|
||||
api.Delete("/realms/:realmId", authMiddleware, deleteRealm)
|
||||
}
|
||||
|
||||
A.Use("/", cache.New(cache.Config{
|
||||
|
@ -9,5 +9,8 @@ func getMetadata(c *fiber.Ctx) error {
|
||||
return c.JSON(fiber.Map{
|
||||
"name": viper.GetString("name"),
|
||||
"domain": viper.GetString("domain"),
|
||||
"components": fiber.Map{
|
||||
"identity": viper.GetString("identity.endpoint"),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -1,11 +1,13 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"code.smartsheep.studio/hydrogen/identity/pkg/grpc/proto"
|
||||
"code.smartsheep.studio/hydrogen/interactive/pkg/database"
|
||||
"code.smartsheep.studio/hydrogen/interactive/pkg/grpc"
|
||||
"code.smartsheep.studio/hydrogen/interactive/pkg/models"
|
||||
"fmt"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"context"
|
||||
"github.com/spf13/viper"
|
||||
"time"
|
||||
)
|
||||
|
||||
func FollowAccount(followerId, followingId uint) error {
|
||||
@ -32,22 +34,19 @@ func GetAccountFollowed(user models.Account, target models.Account) (models.Acco
|
||||
return relationship, err == nil
|
||||
}
|
||||
|
||||
func NotifyAccount(user models.Account, subject, content string, links ...fiber.Map) error {
|
||||
agent := fiber.Post(viper.GetString("identity.endpoint") + "/api/dev/notify")
|
||||
agent.JSON(fiber.Map{
|
||||
"client_id": viper.GetString("identity.client_id"),
|
||||
"client_secret": viper.GetString("identity.client_secret"),
|
||||
"subject": subject,
|
||||
"content": content,
|
||||
"links": links,
|
||||
"user_id": user.ExternalID,
|
||||
func NotifyAccount(user models.Account, subject, content string, links ...*proto.NotifyLink) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
|
||||
_, err := grpc.Notify.NotifyUser(ctx, &proto.NotifyRequest{
|
||||
ClientId: viper.GetString("identity.client_id"),
|
||||
ClientSecret: viper.GetString("identity.client_secret"),
|
||||
Subject: subject,
|
||||
Content: content,
|
||||
Links: links,
|
||||
RecipientId: uint64(user.ID),
|
||||
IsImportant: false,
|
||||
})
|
||||
|
||||
if status, body, errs := agent.Bytes(); len(errs) > 0 {
|
||||
return errs[0]
|
||||
} else if status != 200 {
|
||||
return fmt.Errorf(string(body))
|
||||
}
|
||||
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
@ -1,40 +1,29 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"code.smartsheep.studio/hydrogen/identity/pkg/grpc/proto"
|
||||
"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/security"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type IdentityUserinfo struct {
|
||||
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)
|
||||
|
||||
func LinkAccount(userinfo *proto.Userinfo) (models.Account, error) {
|
||||
var account models.Account
|
||||
if err := database.C.Where(&models.Account{
|
||||
ExternalID: uint(id),
|
||||
ExternalID: uint(userinfo.Id),
|
||||
}).First(&account).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
account = models.Account{
|
||||
Name: userinfo.Name,
|
||||
Nick: userinfo.PreferredUsername,
|
||||
Avatar: userinfo.Picture,
|
||||
Nick: userinfo.Nick,
|
||||
Avatar: userinfo.Avatar,
|
||||
EmailAddress: userinfo.Email,
|
||||
PowerLevel: 0,
|
||||
ExternalID: uint(id),
|
||||
ExternalID: uint(userinfo.Id),
|
||||
}
|
||||
return account, database.C.Save(&account).Error
|
||||
}
|
||||
@ -42,8 +31,8 @@ func LinkAccount(userinfo IdentityUserinfo) (models.Account, error) {
|
||||
}
|
||||
|
||||
account.Name = userinfo.Name
|
||||
account.Nick = userinfo.PreferredUsername
|
||||
account.Avatar = userinfo.Picture
|
||||
account.Nick = userinfo.Nick
|
||||
account.Avatar = userinfo.Avatar
|
||||
account.EmailAddress = userinfo.Email
|
||||
|
||||
err := database.C.Save(&account).Error
|
||||
@ -51,51 +40,21 @@ func LinkAccount(userinfo IdentityUserinfo) (models.Account, error) {
|
||||
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 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),
|
||||
)
|
||||
var user models.Account
|
||||
reply, err := grpc.Auth.Authenticate(ctx, &proto.AuthRequest{
|
||||
AccessToken: atk,
|
||||
RefreshToken: &rtk,
|
||||
})
|
||||
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 user, reply.GetAccessToken(), reply.GetRefreshToken(), 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)
|
||||
user, err = LinkAccount(reply.Userinfo)
|
||||
|
||||
return user, reply.GetAccessToken(), reply.GetRefreshToken(), err
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"code.smartsheep.studio/hydrogen/identity/pkg/grpc/proto"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"code.smartsheep.studio/hydrogen/interactive/pkg/database"
|
||||
@ -230,7 +230,7 @@ func NewPost(
|
||||
op.Author,
|
||||
fmt.Sprintf("%s replied you", 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 {
|
||||
log.Error().Err(err).Msg("An error occurred when notifying user...")
|
||||
@ -257,7 +257,7 @@ func NewPost(
|
||||
account,
|
||||
fmt.Sprintf("%s just posted a post", user.Name),
|
||||
"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 {
|
||||
log.Error().Err(err).Msg("An error occurred when notifying user...")
|
||||
|
@ -1,5 +1,5 @@
|
||||
: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,
|
||||
@ -7,130 +7,117 @@ body {
|
||||
font-family: var(--bs-body-font-family);
|
||||
}
|
||||
|
||||
/* ibm-plex-serif-100 - latin */
|
||||
/* ibm-plex-sans-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-family: 'IBM Plex Sans';
|
||||
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+ */
|
||||
src: url('./ibm-plex-sans-v19-latin-100.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* ibm-plex-serif-100italic - latin */
|
||||
/* ibm-plex-sans-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-family: 'IBM Plex Sans';
|
||||
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+ */
|
||||
src: url('./ibm-plex-sans-v19-latin-100italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* ibm-plex-serif-200 - latin */
|
||||
/* ibm-plex-sans-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-family: 'IBM Plex Sans';
|
||||
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+ */
|
||||
src: url('./ibm-plex-sans-v19-latin-200.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* ibm-plex-serif-200italic - latin */
|
||||
/* ibm-plex-sans-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-family: 'IBM Plex Sans';
|
||||
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+ */
|
||||
src: url('./ibm-plex-sans-v19-latin-200italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* ibm-plex-serif-300 - latin */
|
||||
/* ibm-plex-sans-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-family: 'IBM Plex Sans';
|
||||
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+ */
|
||||
src: url('./ibm-plex-sans-v19-latin-300.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* ibm-plex-serif-300italic - latin */
|
||||
/* ibm-plex-sans-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-family: 'IBM Plex Sans';
|
||||
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+ */
|
||||
src: url('./ibm-plex-sans-v19-latin-300italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* ibm-plex-serif-regular - latin */
|
||||
/* ibm-plex-sans-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-family: 'IBM Plex Sans';
|
||||
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+ */
|
||||
src: url('./ibm-plex-sans-v19-latin-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* ibm-plex-serif-italic - latin */
|
||||
/* ibm-plex-sans-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-family: 'IBM Plex Sans';
|
||||
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+ */
|
||||
src: url('./ibm-plex-sans-v19-latin-italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* ibm-plex-serif-500 - latin */
|
||||
/* ibm-plex-sans-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-family: 'IBM Plex Sans';
|
||||
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+ */
|
||||
src: url('./ibm-plex-sans-v19-latin-500.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* ibm-plex-serif-500italic - latin */
|
||||
/* ibm-plex-sans-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-family: 'IBM Plex Sans';
|
||||
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+ */
|
||||
src: url('./ibm-plex-sans-v19-latin-500italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* ibm-plex-serif-600 - latin */
|
||||
/* ibm-plex-sans-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-family: 'IBM Plex Sans';
|
||||
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+ */
|
||||
src: url('./ibm-plex-sans-v19-latin-600.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* ibm-plex-serif-600italic - latin */
|
||||
/* ibm-plex-sans-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-family: 'IBM Plex Sans';
|
||||
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+ */
|
||||
src: url('./ibm-plex-sans-v19-latin-600italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* ibm-plex-serif-700 - latin */
|
||||
/* ibm-plex-sans-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-family: 'IBM Plex Sans';
|
||||
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+ */
|
||||
src: url('./ibm-plex-sans-v19-latin-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* ibm-plex-serif-700italic - latin */
|
||||
/* ibm-plex-sans-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-family: 'IBM Plex Sans';
|
||||
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+ */
|
||||
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 */
|
||||
|
BIN
pkg/view/src/assets/fonts/ibm-plex-sans-v19-latin-100.woff2
Executable file
BIN
pkg/view/src/assets/fonts/ibm-plex-sans-v19-latin-100.woff2
Executable file
Binary file not shown.
BIN
pkg/view/src/assets/fonts/ibm-plex-sans-v19-latin-100italic.woff2
Executable file
BIN
pkg/view/src/assets/fonts/ibm-plex-sans-v19-latin-100italic.woff2
Executable file
Binary file not shown.
BIN
pkg/view/src/assets/fonts/ibm-plex-sans-v19-latin-200.woff2
Executable file
BIN
pkg/view/src/assets/fonts/ibm-plex-sans-v19-latin-200.woff2
Executable file
Binary file not shown.
BIN
pkg/view/src/assets/fonts/ibm-plex-sans-v19-latin-200italic.woff2
Executable file
BIN
pkg/view/src/assets/fonts/ibm-plex-sans-v19-latin-200italic.woff2
Executable file
Binary file not shown.
BIN
pkg/view/src/assets/fonts/ibm-plex-sans-v19-latin-300.woff2
Executable file
BIN
pkg/view/src/assets/fonts/ibm-plex-sans-v19-latin-300.woff2
Executable file
Binary file not shown.
BIN
pkg/view/src/assets/fonts/ibm-plex-sans-v19-latin-300italic.woff2
Executable file
BIN
pkg/view/src/assets/fonts/ibm-plex-sans-v19-latin-300italic.woff2
Executable file
Binary file not shown.
BIN
pkg/view/src/assets/fonts/ibm-plex-sans-v19-latin-500.woff2
Executable file
BIN
pkg/view/src/assets/fonts/ibm-plex-sans-v19-latin-500.woff2
Executable file
Binary file not shown.
BIN
pkg/view/src/assets/fonts/ibm-plex-sans-v19-latin-500italic.woff2
Executable file
BIN
pkg/view/src/assets/fonts/ibm-plex-sans-v19-latin-500italic.woff2
Executable file
Binary file not shown.
BIN
pkg/view/src/assets/fonts/ibm-plex-sans-v19-latin-600.woff2
Executable file
BIN
pkg/view/src/assets/fonts/ibm-plex-sans-v19-latin-600.woff2
Executable file
Binary file not shown.
BIN
pkg/view/src/assets/fonts/ibm-plex-sans-v19-latin-600italic.woff2
Executable file
BIN
pkg/view/src/assets/fonts/ibm-plex-sans-v19-latin-600italic.woff2
Executable file
Binary file not shown.
BIN
pkg/view/src/assets/fonts/ibm-plex-sans-v19-latin-700.woff2
Executable file
BIN
pkg/view/src/assets/fonts/ibm-plex-sans-v19-latin-700.woff2
Executable file
Binary file not shown.
BIN
pkg/view/src/assets/fonts/ibm-plex-sans-v19-latin-700italic.woff2
Executable file
BIN
pkg/view/src/assets/fonts/ibm-plex-sans-v19-latin-700italic.woff2
Executable file
Binary file not shown.
BIN
pkg/view/src/assets/fonts/ibm-plex-sans-v19-latin-italic.woff2
Executable file
BIN
pkg/view/src/assets/fonts/ibm-plex-sans-v19-latin-italic.woff2
Executable file
Binary file not shown.
BIN
pkg/view/src/assets/fonts/ibm-plex-sans-v19-latin-regular.woff2
Executable file
BIN
pkg/view/src/assets/fonts/ibm-plex-sans-v19-latin-regular.woff2
Executable file
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.
Binary file not shown.
22
pkg/view/src/components/Avatar.tsx
Normal file
22
pkg/view/src/components/Avatar.tsx
Normal file
@ -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 * as marked from "marked";
|
||||
import DOMPurify from "dompurify";
|
||||
import Avatar from "../Avatar.tsx";
|
||||
|
||||
export default function PostItem(props: {
|
||||
post: any;
|
||||
@ -28,7 +29,7 @@ export default function PostItem(props: {
|
||||
setReacting(true);
|
||||
const res = await request(`/api/posts/${item.id}/react/${type}`, {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${getAtk()}` },
|
||||
headers: { Authorization: `Bearer ${getAtk()}` }
|
||||
});
|
||||
if (res.status !== 201 && res.status !== 204) {
|
||||
props.onError(await res.text());
|
||||
@ -46,15 +47,8 @@ export default function PostItem(props: {
|
||||
<Show when={!props.noAuthor}>
|
||||
<a href={`/accounts/${props.post.author.name}`}>
|
||||
<div class="flex bg-base-200">
|
||||
<div class="avatar pl-[20px]">
|
||||
<div class="w-12">
|
||||
<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 class="pl-[20px]">
|
||||
<Avatar user={props.post.author} />
|
||||
</div>
|
||||
<div class="flex items-center px-5">
|
||||
<div>
|
||||
@ -122,7 +116,8 @@ export default function PostItem(props: {
|
||||
<Show when={!props.noControl}>
|
||||
<div class="relative">
|
||||
<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.
|
||||
</div>
|
||||
</Show>
|
||||
|
@ -4,6 +4,7 @@ import { request } from "../../scripts/request.ts";
|
||||
|
||||
import styles from "./PostPublish.module.css";
|
||||
import PostEditActions from "./PostEditActions.tsx";
|
||||
import Avatar from "../Avatar.tsx";
|
||||
|
||||
export default function PostPublish(props: {
|
||||
replying?: any,
|
||||
@ -100,7 +101,7 @@ export default function PostPublish(props: {
|
||||
categories: categories(),
|
||||
tags: tags(),
|
||||
realm_id: props.realmId,
|
||||
published_at: publishedAt() ? new Date(publishedAt()) : new Date(),
|
||||
published_at: publishedAt() ? new Date(publishedAt()) : new Date()
|
||||
})
|
||||
});
|
||||
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()}>
|
||||
<div id="publish-identity" class="flex border-y border-base-200">
|
||||
<div class="avatar pl-[20px]">
|
||||
<div class="w-12">
|
||||
<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 class="pl-[20px]">
|
||||
<Avatar user={userinfo?.profiles} />
|
||||
</div>
|
||||
<div class="flex flex-grow">
|
||||
<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="/edit/:postId" component={lazy(() => import("./pages/creators/edit.tsx"))} />
|
||||
</Route>
|
||||
<Route path="/auth" component={lazy(() => import("./pages/auth/callout.tsx"))} />
|
||||
<Route path="/auth/callback" component={lazy(() => import("./pages/auth/callback.tsx"))} />
|
||||
</Router>
|
||||
</UserinfoProvider>
|
||||
</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 { useNavigate } from "@solidjs/router";
|
||||
import { useWellKnown } from "../../stores/wellKnown.tsx";
|
||||
@ -20,9 +20,11 @@ export default function Navigator() {
|
||||
const userinfo = useUserinfo();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const endpoint = createMemo(() => wellKnown?.components?.identity)
|
||||
|
||||
function logout() {
|
||||
clearUserinfo();
|
||||
navigate("/auth/login");
|
||||
navigate("/");
|
||||
}
|
||||
|
||||
return (
|
||||
@ -54,7 +56,7 @@ export default function Navigator() {
|
||||
</button>
|
||||
</Match>
|
||||
<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
|
||||
</a>
|
||||
</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>
|
||||
);
|
||||
}
|
@ -1,92 +1,73 @@
|
||||
import Cookie from "universal-cookie";
|
||||
import {createContext, useContext} from "solid-js";
|
||||
import {createStore} from "solid-js/store";
|
||||
import { createContext, useContext } from "solid-js";
|
||||
import { createStore } from "solid-js/store";
|
||||
import { request } from "../scripts/request.ts";
|
||||
|
||||
export interface Userinfo {
|
||||
isLoggedIn: boolean,
|
||||
displayName: string,
|
||||
profiles: any,
|
||||
isLoggedIn: boolean,
|
||||
displayName: string,
|
||||
profiles: any,
|
||||
}
|
||||
|
||||
const UserinfoContext = createContext<Userinfo>();
|
||||
|
||||
const defaultUserinfo: Userinfo = {
|
||||
isLoggedIn: false,
|
||||
displayName: "Citizen",
|
||||
profiles: null,
|
||||
isLoggedIn: false,
|
||||
displayName: "Citizen",
|
||||
profiles: null
|
||||
};
|
||||
|
||||
const [userinfo, setUserinfo] = createStore<Userinfo>(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 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});
|
||||
}
|
||||
return new Cookie().get("identity_auth_key");
|
||||
}
|
||||
|
||||
function checkLoggedIn(): boolean {
|
||||
return new Cookie().get("access_token");
|
||||
return new Cookie().get("identity_auth_key");
|
||||
}
|
||||
|
||||
export async function readProfiles(recovering = true) {
|
||||
if (!checkLoggedIn()) return;
|
||||
export async function readProfiles() {
|
||||
if (!checkLoggedIn()) return;
|
||||
|
||||
const res = await request("/api/users/me", {
|
||||
headers: {"Authorization": `Bearer ${getAtk()}`}
|
||||
});
|
||||
const res = await request("/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();
|
||||
}
|
||||
}
|
||||
if (res.status !== 200) {
|
||||
clearUserinfo();
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const data = await res.json();
|
||||
|
||||
setUserinfo({
|
||||
isLoggedIn: true,
|
||||
displayName: data["name"],
|
||||
profiles: data,
|
||||
});
|
||||
setUserinfo({
|
||||
isLoggedIn: true,
|
||||
displayName: data["name"],
|
||||
profiles: data
|
||||
});
|
||||
}
|
||||
|
||||
export function clearUserinfo() {
|
||||
new Cookie().remove("access_token", {path: "/", maxAge: undefined});
|
||||
new Cookie().remove("refresh_token", {path: "/", maxAge: undefined});
|
||||
setUserinfo(defaultUserinfo);
|
||||
const cookies = document.cookie.split(";");
|
||||
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);
|
||||
}
|
||||
|
||||
export function UserinfoProvider(props: any) {
|
||||
return (
|
||||
<UserinfoContext.Provider value={userinfo}>
|
||||
{props.children}
|
||||
</UserinfoContext.Provider>
|
||||
);
|
||||
return (
|
||||
<UserinfoContext.Provider value={userinfo}>
|
||||
{props.children}
|
||||
</UserinfoContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useUserinfo() {
|
||||
return useContext(UserinfoContext);
|
||||
return useContext(UserinfoContext);
|
||||
}
|
@ -12,7 +12,8 @@ content = "uploads"
|
||||
[identity]
|
||||
client_id = "goatplaza"
|
||||
client_secret = "Z9k9AFTj^p"
|
||||
endpoint = "https://id.smartsheep.studio"
|
||||
endpoint = "http://localhost:8444"
|
||||
grpc_endpoint = "127.0.0.1:7444"
|
||||
|
||||
[mailer]
|
||||
name = "Alphabot <alphabot@smartsheep.studio>"
|
||||
@ -21,6 +22,12 @@ smtp_port = 465
|
||||
username = "alphabot@smartsheep.studio"
|
||||
password = "gz937Zxxzfcd9SeH"
|
||||
|
||||
[security]
|
||||
cookie_domain = "localhost"
|
||||
cookie_samesite = "Lax"
|
||||
access_token_duration = 300
|
||||
refresh_token_duration = 2592000
|
||||
|
||||
[database]
|
||||
dsn = "host=localhost dbname=hy_interactive port=5432 sslmode=disable"
|
||||
prefix = "interactive_"
|
||||
|
Loading…
Reference in New Issue
Block a user