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

View File

@ -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
View 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
}

View File

@ -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: "/",
})
}

View File

@ -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
}
}

View File

@ -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,
})
}

View File

@ -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{

View File

@ -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"),
},
})
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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...")

View File

@ -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 */

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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>
);
}

View File

@ -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>

View File

@ -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 ?? ""}

View File

@ -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>

View File

@ -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>

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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);
}