Files
OpenList/internal/model/user.go
KirCute e4c902dd93 feat(share): support more secure file sharing (#991)
提供一种类似大多数网盘的文件分享操作,这种分享方式可以通过强制 Web 代理隐藏文件源路径,可以设置分享码、最大访问数和过期时间,并且不需要启用 guest 用户。

在全局设置中可以调整:
- 是否强制 Web 代理
- 是否允许预览
- 是否允许预览压缩文件
- 分享文件后,点击“复制链接”按钮复制的内容

前端部分:OpenListTeam/OpenList-Frontend#156
文档部分:OpenListTeam/OpenList-Docs#130

Close #183
Close #526
Close #860
Close #892
Close #1079


* feat(share): support more secure file sharing

* feat(share): add archive preview

* fix(share): fix some bugs

* feat(openlist_share): add openlist share driver

* fix(share): lack unwrap when get virtual path

* fix: use unwrapPath instead of path for virtual file name comparison

* fix(share): change request method of /api/share/list from GET to Any

* fix(share): path traversal vulnerability in sharing path check

* 修复分享alias驱动的文件 没开代理时无法获取URL

* fix(sharing): update error message for sharing root link extraction

---------

Co-authored-by: Suyunmeng <69945917+Suyunmeng@users.noreply.github.com>
Co-authored-by: j2rong4cn <j2rong@qq.com>
2025-08-19 15:10:02 +08:00

195 lines
4.6 KiB
Go

package model
import (
"encoding/binary"
"encoding/json"
"fmt"
"time"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/v4/pkg/utils/random"
"github.com/OpenListTeam/go-cache"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/pkg/errors"
)
const (
GENERAL = iota
GUEST // only one exists
ADMIN
)
const StaticHashSalt = "https://github.com/alist-org/alist"
var LoginCache = cache.NewMemCache[int]()
var (
DefaultLockDuration = time.Minute * 5
DefaultMaxAuthRetries = 5
)
type User struct {
ID uint `json:"id" gorm:"primaryKey"` // unique key
Username string `json:"username" gorm:"unique" binding:"required"` // username
PwdHash string `json:"-"` // password hash
PwdTS int64 `json:"-"` // password timestamp
Salt string `json:"-"` // unique salt
Password string `json:"password"` // password
BasePath string `json:"base_path"` // base path
Role int `json:"role"` // user's role
Disabled bool `json:"disabled"`
// Determine permissions by bit
// 0: can see hidden files
// 1: can access without password
// 2: can add offline download tasks
// 3: can mkdir and upload
// 4: can rename
// 5: can move
// 6: can copy
// 7: can remove
// 8: webdav read
// 9: webdav write
// 10: ftp/sftp login and read
// 11: ftp/sftp write
// 12: can read archives
// 13: can decompress archives
// 14: can share
Permission int32 `json:"permission"`
OtpSecret string `json:"-"`
SsoID string `json:"sso_id"` // unique by sso platform
Authn string `gorm:"type:text" json:"-"`
}
func (u *User) IsGuest() bool {
return u.Role == GUEST
}
func (u *User) IsAdmin() bool {
return u.Role == ADMIN
}
func (u *User) ValidateRawPassword(password string) error {
return u.ValidatePwdStaticHash(StaticHash(password))
}
func (u *User) ValidatePwdStaticHash(pwdStaticHash string) error {
if pwdStaticHash == "" {
return errors.WithStack(errs.EmptyPassword)
}
if u.PwdHash != HashPwd(pwdStaticHash, u.Salt) {
return errors.WithStack(errs.WrongPassword)
}
return nil
}
func (u *User) SetPassword(pwd string) *User {
u.Salt = random.String(16)
u.PwdHash = TwoHashPwd(pwd, u.Salt)
u.PwdTS = time.Now().Unix()
return u
}
func (u *User) CanSeeHides() bool {
return u.Permission&1 == 1
}
func (u *User) CanAccessWithoutPassword() bool {
return (u.Permission>>1)&1 == 1
}
func (u *User) CanAddOfflineDownloadTasks() bool {
return (u.Permission>>2)&1 == 1
}
func (u *User) CanWrite() bool {
return (u.Permission>>3)&1 == 1
}
func (u *User) CanRename() bool {
return (u.Permission>>4)&1 == 1
}
func (u *User) CanMove() bool {
return (u.Permission>>5)&1 == 1
}
func (u *User) CanCopy() bool {
return (u.Permission>>6)&1 == 1
}
func (u *User) CanRemove() bool {
return (u.Permission>>7)&1 == 1
}
func (u *User) CanWebdavRead() bool {
return (u.Permission>>8)&1 == 1
}
func (u *User) CanWebdavManage() bool {
return (u.Permission>>9)&1 == 1
}
func (u *User) CanFTPAccess() bool {
return (u.Permission>>10)&1 == 1
}
func (u *User) CanFTPManage() bool {
return (u.Permission>>11)&1 == 1
}
func (u *User) CanReadArchives() bool {
return (u.Permission>>12)&1 == 1
}
func (u *User) CanDecompress() bool {
return (u.Permission>>13)&1 == 1
}
func (u *User) CanShare() bool {
return (u.Permission>>14)&1 == 1
}
func (u *User) JoinPath(reqPath string) (string, error) {
return utils.JoinBasePath(u.BasePath, reqPath)
}
func StaticHash(password string) string {
return utils.HashData(utils.SHA256, []byte(fmt.Sprintf("%s-%s", password, StaticHashSalt)))
}
func HashPwd(static string, salt string) string {
return utils.HashData(utils.SHA256, []byte(fmt.Sprintf("%s-%s", static, salt)))
}
func TwoHashPwd(password string, salt string) string {
return HashPwd(StaticHash(password), salt)
}
func (u *User) WebAuthnID() []byte {
bs := make([]byte, 8)
binary.LittleEndian.PutUint64(bs, uint64(u.ID))
return bs
}
func (u *User) WebAuthnName() string {
return u.Username
}
func (u *User) WebAuthnDisplayName() string {
return u.Username
}
func (u *User) WebAuthnCredentials() []webauthn.Credential {
var res []webauthn.Credential
err := json.Unmarshal([]byte(u.Authn), &res)
if err != nil {
fmt.Println(err)
}
return res
}
func (u *User) WebAuthnIcon() string {
return "https://res.oplist.org/logo/logo.svg"
}