mirror of
https://github.com/AlistGo/alist.git
synced 2025-11-26 03:45:05 +08:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
280960ce3e | ||
|
|
74332e91fb | ||
|
|
540d6c7064 | ||
|
|
55b2bb6b80 | ||
|
|
d5df6fa4cf | ||
|
|
3353055482 | ||
|
|
4d7c2a09ce | ||
|
|
5b8c26510b | ||
|
|
91cc7529a0 | ||
|
|
f61d13d433 |
@@ -1,4 +1,4 @@
|
||||
FROM alpine:edge
|
||||
FROM alpine:3.20.7
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
ARG INSTALL_FFMPEG=false
|
||||
@@ -31,4 +31,4 @@ RUN /entrypoint.sh version
|
||||
ENV PUID=0 PGID=0 UMASK=022 RUN_ARIA2=${INSTALL_ARIA2}
|
||||
VOLUME /opt/alist/data/
|
||||
EXPOSE 5244 5245
|
||||
CMD [ "/entrypoint.sh" ]
|
||||
CMD [ "/entrypoint.sh" ]
|
||||
|
||||
@@ -101,6 +101,10 @@ English | [中文](./README_cn.md) | [日本語](./README_ja.md) | [Contributing
|
||||
|
||||
<https://alistgo.com/>
|
||||
|
||||
## API Documentation (via Apifox):
|
||||
|
||||
<https://alist-public.apifox.cn/>
|
||||
|
||||
## Demo
|
||||
|
||||
<https://al.nn.ci>
|
||||
|
||||
@@ -99,6 +99,10 @@
|
||||
|
||||
<https://alistgo.com/zh/>
|
||||
|
||||
## API 文档(通过 Apifox 提供)
|
||||
|
||||
<https://alist-public.apifox.cn/>
|
||||
|
||||
## Demo
|
||||
|
||||
<https://al.nn.ci>
|
||||
|
||||
@@ -100,6 +100,10 @@
|
||||
|
||||
<https://alistgo.com/>
|
||||
|
||||
## APIドキュメント(Apifox 提供)
|
||||
|
||||
<https://alist-public.apifox.cn/>
|
||||
|
||||
## デモ
|
||||
|
||||
<https://al.nn.ci>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/bootstrap/patch/v3_46_0"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@@ -16,6 +17,12 @@ func Init() {
|
||||
bootstrap.InitConfig()
|
||||
bootstrap.Log()
|
||||
bootstrap.InitDB()
|
||||
|
||||
if v3_46_0.IsLegacyRoleDetected() {
|
||||
utils.Log.Warnf("Detected legacy role format, executing ConvertLegacyRoles patch early...")
|
||||
v3_46_0.ConvertLegacyRoles()
|
||||
}
|
||||
|
||||
data.InitData()
|
||||
bootstrap.InitStreamLimit()
|
||||
bootstrap.InitIndex()
|
||||
|
||||
@@ -11,7 +11,7 @@ type Addition struct {
|
||||
RefreshToken string `json:"refresh_token" required:"true"`
|
||||
OrderBy string `json:"order_by" type:"select" options:"name,size,updated_at,created_at"`
|
||||
OrderDirection string `json:"order_direction" type:"select" options:"ASC,DESC"`
|
||||
OauthTokenURL string `json:"oauth_token_url" default:"https://api.nn.ci/alist/ali_open/token"`
|
||||
OauthTokenURL string `json:"oauth_token_url" default:"https://api.alistgo.com/alist/ali_open/token"`
|
||||
ClientID string `json:"client_id" required:"false" help:"Keep it empty if you don't have one"`
|
||||
ClientSecret string `json:"client_secret" required:"false" help:"Keep it empty if you don't have one"`
|
||||
RemoveWay string `json:"remove_way" required:"true" type:"select" options:"trash,delete"`
|
||||
|
||||
@@ -11,8 +11,8 @@ type Addition struct {
|
||||
OrderBy string `json:"order_by" type:"select" options:"name,time,size" default:"name"`
|
||||
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
||||
DownloadAPI string `json:"download_api" type:"select" options:"official,crack,crack_video" default:"official"`
|
||||
ClientID string `json:"client_id" required:"true" default:"iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v"`
|
||||
ClientSecret string `json:"client_secret" required:"true" default:"jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG"`
|
||||
ClientID string `json:"client_id" required:"true" default:"hq9yQ9w9kR4YHj1kyYafLygVocobh7Sf"`
|
||||
ClientSecret string `json:"client_secret" required:"true" default:"YH2VpZcFJHYNnV6vLfHQXDBhcE7ZChyE"`
|
||||
CustomCrackUA string `json:"custom_crack_ua" required:"true" default:"netdisk"`
|
||||
AccessToken string
|
||||
UploadThread string `json:"upload_thread" default:"3" help:"1<=thread<=32"`
|
||||
|
||||
@@ -103,6 +103,8 @@ func InitialSettings() []model.SettingItem {
|
||||
{Key: conf.AllowIndexed, Value: "false", Type: conf.TypeBool, Group: model.SITE},
|
||||
{Key: conf.AllowMounted, Value: "true", Type: conf.TypeBool, Group: model.SITE},
|
||||
{Key: conf.RobotsTxt, Value: "User-agent: *\nAllow: /", Type: conf.TypeText, Group: model.SITE},
|
||||
// newui settings
|
||||
{Key: conf.UseNewui, Value: "false", Type: conf.TypeBool, Group: model.SITE},
|
||||
// style settings
|
||||
{Key: conf.Logo, Value: "https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg", Type: conf.TypeText, Group: model.STYLE},
|
||||
{Key: conf.Favicon, Value: "https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg", Type: conf.TypeString, Group: model.STYLE},
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package v3_46_0
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
"github.com/alist-org/alist/v3/internal/db"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
@@ -83,47 +86,101 @@ func ConvertLegacyRoles() {
|
||||
}
|
||||
}
|
||||
|
||||
users, _, err := op.GetUsers(1, -1)
|
||||
rawDb := db.GetDb()
|
||||
table := conf.Conf.Database.TablePrefix + "users"
|
||||
rows, err := rawDb.Table(table).Select("id, username, role").Rows()
|
||||
if err != nil {
|
||||
utils.Log.Errorf("[convert roles] failed to get users: %v", err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for i := range users {
|
||||
user := users[i]
|
||||
if user.Role == nil {
|
||||
var updatedCount int
|
||||
for rows.Next() {
|
||||
var id uint
|
||||
var username string
|
||||
var rawRole []byte
|
||||
|
||||
if err := rows.Scan(&id, &username, &rawRole); err != nil {
|
||||
utils.Log.Warnf("[convert roles] skip user scan err: %v", err)
|
||||
continue
|
||||
}
|
||||
changed := false
|
||||
var roles model.Roles
|
||||
for _, r := range user.Role {
|
||||
|
||||
utils.Log.Debugf("[convert roles] user: %s raw role: %s", username, string(rawRole))
|
||||
|
||||
if len(rawRole) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var oldRoles []int
|
||||
wasSingleInt := false
|
||||
if err := json.Unmarshal(rawRole, &oldRoles); err != nil {
|
||||
var single int
|
||||
if err := json.Unmarshal(rawRole, &single); err != nil {
|
||||
utils.Log.Warnf("[convert roles] user %s has invalid role: %s", username, string(rawRole))
|
||||
continue
|
||||
}
|
||||
oldRoles = []int{single}
|
||||
wasSingleInt = true
|
||||
}
|
||||
|
||||
var newRoles model.Roles
|
||||
for _, r := range oldRoles {
|
||||
switch r {
|
||||
case model.ADMIN:
|
||||
roles = append(roles, int(adminRole.ID))
|
||||
if int(adminRole.ID) != r {
|
||||
changed = true
|
||||
}
|
||||
newRoles = append(newRoles, int(adminRole.ID))
|
||||
case model.GUEST:
|
||||
roles = append(roles, int(guestRole.ID))
|
||||
if int(guestRole.ID) != r {
|
||||
changed = true
|
||||
}
|
||||
newRoles = append(newRoles, int(guestRole.ID))
|
||||
case model.GENERAL:
|
||||
roles = append(roles, int(generalRole.ID))
|
||||
if int(generalRole.ID) != r {
|
||||
changed = true
|
||||
}
|
||||
newRoles = append(newRoles, int(generalRole.ID))
|
||||
default:
|
||||
roles = append(roles, r)
|
||||
newRoles = append(newRoles, r)
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
user.Role = roles
|
||||
if err := db.UpdateUser(&user); err != nil {
|
||||
utils.Log.Errorf("[convert roles] failed to update user %s: %v", user.Username, err)
|
||||
|
||||
if wasSingleInt {
|
||||
err := rawDb.Table(table).Where("id = ?", id).Update("role", newRoles).Error
|
||||
if err != nil {
|
||||
utils.Log.Errorf("[convert roles] failed to update user %s: %v", username, err)
|
||||
} else {
|
||||
updatedCount++
|
||||
utils.Log.Infof("[convert roles] updated user %s: %v → %v", username, oldRoles, newRoles)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
utils.Log.Infof("[convert roles] completed role conversion for %d users", len(users))
|
||||
utils.Log.Infof("[convert roles] completed role conversion for %d users", updatedCount)
|
||||
}
|
||||
|
||||
func IsLegacyRoleDetected() bool {
|
||||
rawDb := db.GetDb()
|
||||
table := conf.Conf.Database.TablePrefix + "users"
|
||||
rows, err := rawDb.Table(table).Select("role").Rows()
|
||||
if err != nil {
|
||||
utils.Log.Errorf("[role check] failed to scan user roles: %v", err)
|
||||
return false
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var raw sql.RawBytes
|
||||
if err := rows.Scan(&raw); err != nil {
|
||||
continue
|
||||
}
|
||||
if len(raw) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var roles []int
|
||||
if err := json.Unmarshal(raw, &roles); err == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var single int
|
||||
if err := json.Unmarshal(raw, &single); err == nil {
|
||||
utils.Log.Infof("[role check] detected legacy int role: %d", single)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ const (
|
||||
AllowIndexed = "allow_indexed"
|
||||
AllowMounted = "allow_mounted"
|
||||
RobotsTxt = "robots_txt"
|
||||
UseNewui = "use_newui"
|
||||
|
||||
Logo = "logo"
|
||||
Favicon = "favicon"
|
||||
|
||||
@@ -2,12 +2,14 @@ package db
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
"path"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -24,6 +26,20 @@ func GetUserByRole(role int) (*model.User, error) {
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
func GetUsersByRole(roleID int) ([]model.User, error) {
|
||||
var users []model.User
|
||||
if err := db.Find(&users).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result []model.User
|
||||
for _, u := range users {
|
||||
if slices.Contains(u.Role, roleID) {
|
||||
result = append(result, u)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func GetUserByName(username string) (*model.User, error) {
|
||||
user := model.User{Username: username}
|
||||
if err := db.Where(user).First(&user).Error; err != nil {
|
||||
@@ -108,25 +124,29 @@ func RemoveAuthn(u *model.User, id string) error {
|
||||
return UpdateAuthn(u.ID, string(res))
|
||||
}
|
||||
|
||||
func UpdateUserBasePathPrefix(oldPath, newPath string) ([]string, error) {
|
||||
func UpdateUserBasePathPrefix(oldPath, newPath string, usersOpt ...[]model.User) ([]string, error) {
|
||||
var users []model.User
|
||||
var modifiedUsernames []string
|
||||
|
||||
if err := db.Find(&users).Error; err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to load users")
|
||||
}
|
||||
|
||||
oldPathClean := path.Clean(oldPath)
|
||||
|
||||
if len(usersOpt) > 0 {
|
||||
users = usersOpt[0]
|
||||
} else {
|
||||
if err := db.Find(&users).Error; err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to load users")
|
||||
}
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
basePath := path.Clean(user.BasePath)
|
||||
updated := false
|
||||
|
||||
if basePath == oldPathClean {
|
||||
user.BasePath = newPath
|
||||
user.BasePath = path.Clean(newPath)
|
||||
updated = true
|
||||
} else if strings.HasPrefix(basePath, oldPathClean+"/") {
|
||||
user.BasePath = newPath + basePath[len(oldPathClean):]
|
||||
user.BasePath = path.Clean(newPath + basePath[len(oldPathClean):])
|
||||
updated = true
|
||||
}
|
||||
|
||||
@@ -140,3 +160,13 @@ func UpdateUserBasePathPrefix(oldPath, newPath string) ([]string, error) {
|
||||
|
||||
return modifiedUsernames, nil
|
||||
}
|
||||
|
||||
func CountUsersByRoleAndEnabledExclude(roleID uint, excludeUserID uint) (int64, error) {
|
||||
var count int64
|
||||
jsonValue := fmt.Sprintf("[%d]", roleID)
|
||||
err := db.Model(&model.User{}).
|
||||
Where("disabled = ? AND id != ?", false, excludeUserID).
|
||||
Where("JSON_CONTAINS(role, ?)", jsonValue).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package op
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
"time"
|
||||
|
||||
"github.com/Xhofe/go-cache"
|
||||
@@ -96,12 +97,35 @@ func UpdateRole(r *model.Role) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if old.Name == "admin" || old.Name == "guest" {
|
||||
switch old.Name {
|
||||
case "admin":
|
||||
return errs.ErrChangeDefaultRole
|
||||
case "guest":
|
||||
r.Name = "guest"
|
||||
}
|
||||
for i := range r.PermissionScopes {
|
||||
r.PermissionScopes[i].Path = utils.FixAndCleanPath(r.PermissionScopes[i].Path)
|
||||
}
|
||||
if len(old.PermissionScopes) > 0 && len(r.PermissionScopes) > 0 &&
|
||||
old.PermissionScopes[0].Path != r.PermissionScopes[0].Path {
|
||||
|
||||
oldPath := old.PermissionScopes[0].Path
|
||||
newPath := r.PermissionScopes[0].Path
|
||||
|
||||
users, err := db.GetUsersByRole(int(r.ID))
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed to get users by role")
|
||||
}
|
||||
|
||||
modifiedUsernames, err := db.UpdateUserBasePathPrefix(oldPath, newPath, users)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed to update user base path when role updated")
|
||||
}
|
||||
|
||||
for _, name := range modifiedUsernames {
|
||||
userCache.Del(name)
|
||||
}
|
||||
}
|
||||
roleCache.Del(fmt.Sprint(r.ID))
|
||||
roleCache.Del(r.Name)
|
||||
return db.UpdateRole(r)
|
||||
|
||||
@@ -46,6 +46,11 @@ func GetStorageByMountPath(mountPath string) (driver.Driver, error) {
|
||||
func CreateStorage(ctx context.Context, storage model.Storage) (uint, error) {
|
||||
storage.Modified = time.Now()
|
||||
storage.MountPath = utils.FixAndCleanPath(storage.MountPath)
|
||||
|
||||
if storage.MountPath == "/" {
|
||||
return 0, errors.New("Mount path cannot be '/'")
|
||||
}
|
||||
|
||||
var err error
|
||||
// check driver first
|
||||
driverName := storage.Driver
|
||||
@@ -205,6 +210,9 @@ func UpdateStorage(ctx context.Context, storage model.Storage) error {
|
||||
}
|
||||
storage.Modified = time.Now()
|
||||
storage.MountPath = utils.FixAndCleanPath(storage.MountPath)
|
||||
if storage.MountPath == "/" {
|
||||
return errors.New("Mount path cannot be '/'")
|
||||
}
|
||||
err = db.UpdateStorage(&storage)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed update storage in database")
|
||||
|
||||
@@ -78,7 +78,25 @@ func GetUsers(pageIndex, pageSize int) (users []model.User, count int64, err err
|
||||
|
||||
func CreateUser(u *model.User) error {
|
||||
u.BasePath = utils.FixAndCleanPath(u.BasePath)
|
||||
return db.CreateUser(u)
|
||||
|
||||
err := db.CreateUser(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
roles, err := GetRolesByUserID(u.ID)
|
||||
if err == nil {
|
||||
for _, role := range roles {
|
||||
if len(role.PermissionScopes) > 0 {
|
||||
u.BasePath = utils.FixAndCleanPath(role.PermissionScopes[0].Path)
|
||||
break
|
||||
}
|
||||
}
|
||||
_ = db.UpdateUser(u)
|
||||
userCache.Del(u.Username)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteUserById(id uint) error {
|
||||
@@ -106,6 +124,17 @@ func UpdateUser(u *model.User) error {
|
||||
}
|
||||
userCache.Del(old.Username)
|
||||
u.BasePath = utils.FixAndCleanPath(u.BasePath)
|
||||
if len(u.Role) > 0 {
|
||||
roles, err := GetRolesByUserID(u.ID)
|
||||
if err == nil {
|
||||
for _, role := range roles {
|
||||
if len(role.PermissionScopes) > 0 {
|
||||
u.BasePath = utils.FixAndCleanPath(role.PermissionScopes[0].Path)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return db.UpdateUser(u)
|
||||
}
|
||||
|
||||
@@ -136,3 +165,11 @@ func DelUserCache(username string) error {
|
||||
userCache.Del(username)
|
||||
return nil
|
||||
}
|
||||
|
||||
func CountEnabledAdminsExcluding(userID uint) (int64, error) {
|
||||
adminRole, err := GetRoleByName("admin")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return db.CountUsersByRoleAndEnabledExclude(adminRole.ID, userID)
|
||||
}
|
||||
|
||||
@@ -66,9 +66,13 @@ func UpdateRole(c *gin.Context) {
|
||||
common.ErrorResp(c, err, 500, true)
|
||||
return
|
||||
}
|
||||
if role.Name == "admin" || role.Name == "guest" {
|
||||
switch role.Name {
|
||||
case "admin":
|
||||
common.ErrorResp(c, errs.ErrChangeDefaultRole, 403)
|
||||
return
|
||||
|
||||
case "guest":
|
||||
req.Name = "guest"
|
||||
}
|
||||
if err := op.UpdateRole(&req); err != nil {
|
||||
common.ErrorResp(c, err, 500, true)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handles
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"strconv"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
@@ -60,10 +61,18 @@ func UpdateUser(c *gin.Context) {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
}
|
||||
//if !utils.SliceEqual(user.Role, req.Role) {
|
||||
// common.ErrorStrResp(c, "role can not be changed", 400)
|
||||
// return
|
||||
//}
|
||||
|
||||
if user.Username == "admin" {
|
||||
if !utils.SliceEqual(user.Role, req.Role) {
|
||||
common.ErrorStrResp(c, "cannot change role of admin user", 403)
|
||||
return
|
||||
}
|
||||
if user.Username != req.Username {
|
||||
common.ErrorStrResp(c, "cannot change username of admin user", 403)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if req.Password == "" {
|
||||
req.PwdHash = user.PwdHash
|
||||
req.Salt = user.Salt
|
||||
@@ -74,9 +83,16 @@ func UpdateUser(c *gin.Context) {
|
||||
if req.OtpSecret == "" {
|
||||
req.OtpSecret = user.OtpSecret
|
||||
}
|
||||
if req.Disabled && req.IsAdmin() {
|
||||
common.ErrorStrResp(c, "admin user can not be disabled", 400)
|
||||
return
|
||||
if req.Disabled && user.IsAdmin() {
|
||||
count, err := op.CountEnabledAdminsExcluding(user.ID)
|
||||
if err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
return
|
||||
}
|
||||
if count == 0 {
|
||||
common.ErrorStrResp(c, "at least one enabled admin must be kept", 400)
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := op.UpdateUser(&req); err != nil {
|
||||
common.ErrorResp(c, err, 500)
|
||||
|
||||
@@ -41,6 +41,15 @@ func Auth(c *gin.Context) {
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
if len(guest.Role) > 0 {
|
||||
roles, err := op.GetRolesByUserID(guest.ID)
|
||||
if err != nil {
|
||||
common.ErrorStrResp(c, fmt.Sprintf("Fail to load guest roles: %v", err), 500)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
guest.RolesDetail = roles
|
||||
}
|
||||
c.Set("user", guest)
|
||||
log.Debugf("use empty token: %+v", guest)
|
||||
c.Next()
|
||||
|
||||
Reference in New Issue
Block a user