mirror of https://github.com/synctv-org/synctv
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
377 lines
9.4 KiB
Go
377 lines
9.4 KiB
Go
package middlewares
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/synctv-org/synctv/internal/conf"
|
|
"github.com/synctv-org/synctv/internal/db"
|
|
dbModel "github.com/synctv-org/synctv/internal/model"
|
|
"github.com/synctv-org/synctv/internal/op"
|
|
"github.com/synctv-org/synctv/server/model"
|
|
"github.com/zijiren233/gencontainer/synccache"
|
|
"github.com/zijiren233/stream"
|
|
)
|
|
|
|
var (
|
|
ErrAuthFailed = errors.New("auth failed")
|
|
ErrAuthExpired = errors.New("auth expired")
|
|
)
|
|
|
|
type AuthClaims struct {
|
|
UserId string `json:"u"`
|
|
UserVersion uint32 `json:"uv"`
|
|
jwt.RegisteredClaims
|
|
}
|
|
|
|
type AuthRoomClaims struct {
|
|
AuthClaims
|
|
RoomId string `json:"r"`
|
|
RoomVersion uint32 `json:"rv"`
|
|
}
|
|
|
|
func authRoom(Authorization string) (*AuthRoomClaims, error) {
|
|
t, err := jwt.ParseWithClaims(strings.TrimPrefix(Authorization, `Bearer `), &AuthRoomClaims{}, func(token *jwt.Token) (any, error) {
|
|
return stream.StringToBytes(conf.Conf.Jwt.Secret), nil
|
|
})
|
|
if err != nil {
|
|
return nil, ErrAuthFailed
|
|
}
|
|
claims, ok := t.Claims.(*AuthRoomClaims)
|
|
if !ok || !t.Valid {
|
|
return nil, ErrAuthFailed
|
|
}
|
|
return claims, nil
|
|
}
|
|
|
|
func authUser(Authorization string) (*AuthClaims, error) {
|
|
t, err := jwt.ParseWithClaims(strings.TrimPrefix(Authorization, `Bearer `), &AuthClaims{}, func(token *jwt.Token) (any, error) {
|
|
return stream.StringToBytes(conf.Conf.Jwt.Secret), nil
|
|
})
|
|
if err != nil {
|
|
return nil, ErrAuthFailed
|
|
}
|
|
claims, ok := t.Claims.(*AuthClaims)
|
|
if !ok || !t.Valid {
|
|
return nil, ErrAuthFailed
|
|
}
|
|
return claims, nil
|
|
}
|
|
|
|
func AuthRoom(Authorization string) (*op.UserEntry, *op.RoomEntry, error) {
|
|
claims, err := authRoom(Authorization)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if len(claims.RoomId) != 32 {
|
|
return nil, nil, ErrAuthFailed
|
|
}
|
|
|
|
if len(claims.UserId) != 32 {
|
|
return nil, nil, ErrAuthFailed
|
|
}
|
|
|
|
userE, err := op.LoadOrInitUserByID(claims.UserId)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
user := userE.Value()
|
|
|
|
if !user.CheckVersion(claims.UserVersion) {
|
|
return nil, nil, ErrAuthExpired
|
|
}
|
|
|
|
roomE, err := op.LoadOrInitRoomByID(claims.RoomId)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
room := roomE.Value()
|
|
|
|
if !room.CheckVersion(claims.RoomVersion) {
|
|
return nil, nil, ErrAuthExpired
|
|
}
|
|
|
|
rus, err := room.LoadOrCreateMemberStatus(user.ID)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if !rus.IsActive() {
|
|
if rus.IsPending() {
|
|
return nil, nil, fmt.Errorf("user is pending, need admin to approve")
|
|
}
|
|
return nil, nil, fmt.Errorf("user is banned")
|
|
}
|
|
|
|
return userE, roomE, nil
|
|
}
|
|
|
|
func AuthUser(Authorization string) (*op.UserEntry, error) {
|
|
claims, err := authUser(Authorization)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(claims.UserId) != 32 {
|
|
return nil, ErrAuthFailed
|
|
}
|
|
|
|
userE, err := op.LoadOrInitUserByID(claims.UserId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
user := userE.Value()
|
|
|
|
if user.IsGuest() {
|
|
return nil, errors.New("user is guest, can not login")
|
|
}
|
|
|
|
if !user.CheckVersion(claims.UserVersion) {
|
|
return nil, ErrAuthExpired
|
|
}
|
|
|
|
return userE, nil
|
|
}
|
|
|
|
func NewAuthUserToken(user *op.User) (string, error) {
|
|
if user.IsBanned() {
|
|
return "", errors.New("user banned")
|
|
}
|
|
if user.IsPending() {
|
|
return "", errors.New("user is pending, need admin to approve")
|
|
}
|
|
if user.IsGuest() {
|
|
return "", errors.New("user is guest, can not login")
|
|
}
|
|
t, err := time.ParseDuration(conf.Conf.Jwt.Expire)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
claims := &AuthClaims{
|
|
UserId: user.ID,
|
|
UserVersion: user.Version(),
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
NotBefore: jwt.NewNumericDate(time.Now()),
|
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(t)),
|
|
},
|
|
}
|
|
return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(stream.StringToBytes(conf.Conf.Jwt.Secret))
|
|
}
|
|
|
|
func NewAuthRoomToken(user *op.User, room *op.Room) (string, error) {
|
|
if user.IsBanned() {
|
|
return "", errors.New("user banned")
|
|
}
|
|
if user.IsPending() {
|
|
return "", errors.New("user is pending, need admin to approve")
|
|
}
|
|
|
|
if room.IsBanned() {
|
|
return "", errors.New("room banned")
|
|
}
|
|
if room.IsPending() {
|
|
return "", errors.New("room is pending, need admin to approve")
|
|
}
|
|
|
|
member, err := room.LoadOrCreateRoomMember(user.ID)
|
|
if err != nil {
|
|
if errors.Is(err, db.ErrNotFound("")) {
|
|
return "", fmt.Errorf("this room was disabled join new user")
|
|
}
|
|
return "", fmt.Errorf("load room member failed: %w", err)
|
|
}
|
|
switch member.Status {
|
|
case dbModel.RoomMemberStatusBanned:
|
|
return "", fmt.Errorf("user is banned")
|
|
case dbModel.RoomMemberStatusPending:
|
|
return "", fmt.Errorf("user is pending, need admin to approve")
|
|
default:
|
|
if member.Status.IsNotActive() {
|
|
return "", fmt.Errorf("user is not active")
|
|
}
|
|
}
|
|
|
|
t, err := time.ParseDuration(conf.Conf.Jwt.Expire)
|
|
if err != nil {
|
|
return "", fmt.Errorf("parse jwt expire failed: %w", err)
|
|
}
|
|
claims := &AuthRoomClaims{
|
|
AuthClaims: AuthClaims{
|
|
UserId: user.ID,
|
|
UserVersion: user.Version(),
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
NotBefore: jwt.NewNumericDate(time.Now()),
|
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(t)),
|
|
},
|
|
},
|
|
RoomId: room.ID,
|
|
RoomVersion: room.Version(),
|
|
}
|
|
return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(stream.StringToBytes(conf.Conf.Jwt.Secret))
|
|
}
|
|
|
|
func AuthUserMiddleware(ctx *gin.Context) {
|
|
token, err := GetAuthorizationTokenFromContext(ctx)
|
|
if err != nil {
|
|
ctx.AbortWithStatusJSON(http.StatusUnauthorized, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
userE, err := AuthUser(token)
|
|
if err != nil {
|
|
ctx.AbortWithStatusJSON(http.StatusUnauthorized, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
user := userE.Value()
|
|
if user.IsBanned() {
|
|
ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorStringResp("user banned"))
|
|
return
|
|
}
|
|
if user.IsPending() {
|
|
ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorStringResp("user is pending, need admin to approve"))
|
|
return
|
|
}
|
|
|
|
ctx.Set("user", userE)
|
|
log := ctx.MustGet("log").(*logrus.Entry)
|
|
if log.Data == nil {
|
|
log.Data = make(logrus.Fields, 3)
|
|
}
|
|
log.Data["uid"] = user.ID
|
|
log.Data["unm"] = user.Username
|
|
log.Data["uro"] = user.Role.String()
|
|
}
|
|
|
|
func AuthRoomMiddleware(ctx *gin.Context) {
|
|
token, err := GetAuthorizationTokenFromContext(ctx)
|
|
if err != nil {
|
|
ctx.AbortWithStatusJSON(http.StatusUnauthorized, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
userE, roomE, err := AuthRoom(token)
|
|
if err != nil {
|
|
ctx.AbortWithStatusJSON(http.StatusUnauthorized, model.NewApiErrorResp(err))
|
|
return
|
|
}
|
|
|
|
user := userE.Value()
|
|
if user.IsBanned() {
|
|
ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorStringResp("user banned"))
|
|
return
|
|
}
|
|
if user.IsPending() {
|
|
ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorStringResp("user is pending, need admin to approve"))
|
|
return
|
|
}
|
|
|
|
room := roomE.Value()
|
|
if room.IsBanned() {
|
|
ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorStringResp("room banned"))
|
|
return
|
|
}
|
|
if room.IsPending() {
|
|
ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorStringResp("room is pending, need admin to approve"))
|
|
return
|
|
}
|
|
|
|
ctx.Set("user", userE)
|
|
ctx.Set("room", roomE)
|
|
log := ctx.MustGet("log").(*logrus.Entry)
|
|
if log.Data == nil {
|
|
log.Data = make(logrus.Fields, 5)
|
|
}
|
|
log.Data["rid"] = room.ID
|
|
log.Data["rnm"] = room.Name
|
|
log.Data["uid"] = user.ID
|
|
log.Data["unm"] = user.Username
|
|
log.Data["uro"] = user.Role.String()
|
|
}
|
|
|
|
func AuthRoomWithoutGuestMiddleware(ctx *gin.Context) {
|
|
AuthRoomMiddleware(ctx)
|
|
if ctx.IsAborted() {
|
|
return
|
|
}
|
|
|
|
user := ctx.MustGet("user").(*synccache.Entry[*op.User]).Value()
|
|
if user.IsGuest() {
|
|
ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorStringResp("guest is no permission"))
|
|
return
|
|
}
|
|
}
|
|
|
|
func AuthRoomAdminMiddleware(ctx *gin.Context) {
|
|
AuthRoomMiddleware(ctx)
|
|
if ctx.IsAborted() {
|
|
return
|
|
}
|
|
|
|
room := ctx.MustGet("room").(*synccache.Entry[*op.Room]).Value()
|
|
user := ctx.MustGet("user").(*synccache.Entry[*op.User]).Value()
|
|
|
|
if !user.IsRoomAdmin(room) {
|
|
ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorStringResp("user has no permission"))
|
|
return
|
|
}
|
|
}
|
|
|
|
func AuthRoomCreatorMiddleware(ctx *gin.Context) {
|
|
AuthRoomMiddleware(ctx)
|
|
if ctx.IsAborted() {
|
|
return
|
|
}
|
|
|
|
room := ctx.MustGet("room").(*synccache.Entry[*op.Room]).Value()
|
|
user := ctx.MustGet("user").(*synccache.Entry[*op.User]).Value()
|
|
|
|
if room.CreatorID != user.ID {
|
|
ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorStringResp("user is not creator"))
|
|
return
|
|
}
|
|
}
|
|
|
|
func AuthAdminMiddleware(ctx *gin.Context) {
|
|
AuthUserMiddleware(ctx)
|
|
if ctx.IsAborted() {
|
|
return
|
|
}
|
|
|
|
userE := ctx.MustGet("user").(*synccache.Entry[*op.User])
|
|
if !userE.Value().IsAdmin() {
|
|
ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorStringResp("user is not admin"))
|
|
return
|
|
}
|
|
}
|
|
|
|
func AuthRootMiddleware(ctx *gin.Context) {
|
|
AuthUserMiddleware(ctx)
|
|
if ctx.IsAborted() {
|
|
return
|
|
}
|
|
|
|
userE := ctx.MustGet("user").(*synccache.Entry[*op.User])
|
|
if !userE.Value().IsRoot() {
|
|
ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorStringResp("user is not root"))
|
|
return
|
|
}
|
|
}
|
|
|
|
func GetAuthorizationTokenFromContext(ctx *gin.Context) (string, error) {
|
|
Authorization := ctx.GetHeader("Authorization")
|
|
if Authorization != "" {
|
|
ctx.Set("token", Authorization)
|
|
return Authorization, nil
|
|
}
|
|
Authorization = ctx.Query("token")
|
|
if Authorization != "" {
|
|
ctx.Set("token", Authorization)
|
|
return Authorization, nil
|
|
}
|
|
return "", errors.New("token is empty")
|
|
}
|