Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions agent/app/dto/request/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ type FileRemarkUpdate struct {
}

type FileShareCreate struct {
Path string `json:"path" validate:"required"`
ExpireMinutes int `json:"expireMinutes" validate:"min=0,max=10080"`
Password string `json:"password" validate:"omitempty,min=4,max=256"`
Path string `json:"path" validate:"required"`
ExpireMinutes int `json:"expireMinutes" validate:"min=0,max=10080"`
Password *string `json:"password"`
}
1 change: 1 addition & 0 deletions agent/app/dto/response/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ type FileShareInfo struct {
ExpiresAt int64 `json:"expiresAt"`
Permanent bool `json:"permanent"`
HasPassword bool `json:"hasPassword"`
Password string `json:"password,omitempty"`
}

type FileSharePublicInfo struct {
Expand Down
1 change: 1 addition & 0 deletions agent/app/model/file_share.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ type FileShare struct {
Token string `gorm:"not null;uniqueIndex" json:"token"`
FileName string `gorm:"not null" json:"fileName"`
ExpiresUnix int64 `json:"expiresUnix"`
PasswordEnc string `json:"-"`
PasswordSalt string `json:"passwordSalt"`
PasswordHash string `json:"passwordHash"`
MaxDownloads int `json:"maxDownloads"`
Expand Down
48 changes: 36 additions & 12 deletions agent/app/service/file_share.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
"gorm.io/gorm"
)

Expand Down Expand Up @@ -88,6 +89,17 @@ func shareModelToInfo(item model.FileShare) response.FileShareInfo {
}
}

func fillSharePassword(info *response.FileShareInfo, item model.FileShare) {
if info == nil || item.PasswordEnc == "" {
return
}
password, err := encrypt.StringDecrypt(item.PasswordEnc)
if err != nil {
return
}
info.Password = password
}

func shareModelToPublicInfo(item model.FileShare) response.FileSharePublicInfo {
return response.FileSharePublicInfo{
FileName: item.FileName,
Expand Down Expand Up @@ -156,19 +168,29 @@ func (s *FileShareService) Create(req request.FileShareCreate) (*response.FileSh
item.ExpiresUnix = time.Now().Add(time.Duration(req.ExpireMinutes) * time.Minute).Unix()
}

pw := strings.TrimSpace(req.Password)
item.PasswordSalt = ""
item.PasswordHash = ""
if pw != "" {
if utf8.RuneCountInString(pw) < 4 {
return nil, buserr.New("ErrFileSharePasswordPolicy")
}
salt, err := randomSalt()
if err != nil {
return nil, err
if req.Password != nil {
pw := strings.TrimSpace(*req.Password)
if pw == "" {
item.PasswordEnc = ""
item.PasswordSalt = ""
item.PasswordHash = ""
} else {
pwLen := utf8.RuneCountInString(pw)
if pwLen < 4 || pwLen > 256 {
return nil, buserr.New("ErrFileSharePasswordPolicy")
}
enc, err := encrypt.StringEncrypt(pw)
if err != nil {
return nil, err
}
item.PasswordEnc = enc
salt, err := randomSalt()
if err != nil {
return nil, err
}
item.PasswordSalt = salt
item.PasswordHash = hashPassword(salt, pw)
}
item.PasswordSalt = salt
item.PasswordHash = hashPassword(salt, pw)
}

if isNew {
Expand All @@ -182,6 +204,7 @@ func (s *FileShareService) Create(req request.FileShareCreate) (*response.FileSh
}

res := shareModelToInfo(item)
fillSharePassword(&res, item)
return &res, nil
}

Expand Down Expand Up @@ -227,6 +250,7 @@ func (s *FileShareService) GetByPath(path string) (*response.FileShareInfo, erro
return nil, nil
}
info := shareModelToInfo(item)
fillSharePassword(&info, item)
return &info, nil
}

Expand Down
2 changes: 1 addition & 1 deletion agent/init/migration/migrations/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -1220,7 +1220,7 @@ var AddFileManageAISettings = &gormigrate.Migration{
}

var AddFileShareTable = &gormigrate.Migration{
ID: "20260407-add-file-share-table",
ID: "20260410-add-file-share-table",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(&model.FileShare{})
},
Expand Down
1 change: 1 addition & 0 deletions core/constant/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ var DynamicRoutes = []string{
`^/databases/mysql/setting/[^/]+/[^/]+$`,
`^/databases/postgresql/setting/[^/]+/[^/]+$`,
`^/websites/[^/]+/config/[^/]+$`,
`^/s/[A-Za-z0-9]{10,16}$`,
}

var CertStore atomic.Value
Expand Down
10 changes: 10 additions & 0 deletions core/utils/security/security.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (
"github.com/gin-gonic/gin"
)

var publicSharePagePattern = regexp.MustCompile(`^/s/[A-Za-z0-9]{10,16}$`)

func HandleNotRoute(c *gin.Context) bool {
if !checkBindDomain(c) {
HandleNotSecurity(c, "err_domain")
Expand Down Expand Up @@ -137,13 +139,21 @@ func checkFrontendPath(c *gin.Context) bool {
if !isFrontendPath(c) {
return false
}
if isPublicFileSharePagePath(c.Request.URL.Path) {
return true
}
authService := service.NewIAuthService()
if authService.GetSecurityEntrance() != "" {
return authService.IsLogin(c)
}
return true
}

func isPublicFileSharePagePath(path string) bool {
reqUri := strings.TrimSuffix(path, "/")
return publicSharePagePattern.MatchString(reqUri)
}

func checkBindDomain(c *gin.Context) bool {
settingRepo := repo.NewISettingRepo()
status, _ := settingRepo.Get(repo.WithByKey("BindDomain"))
Expand Down
1 change: 1 addition & 0 deletions frontend/src/api/interface/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ export namespace File {
expiresAt: number;
permanent: boolean;
hasPassword: boolean;
password?: string;
}

export interface FileSharePublicInfo {
Expand Down
71 changes: 58 additions & 13 deletions frontend/src/views/host/file-management/share/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
<el-option v-for="opt in expireOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
</el-form-item>
<el-form-item :label="$t('file.sharePassword')" prop="password">
<el-form-item :label="$t('file.sharePassword')" prop="sharePassword">
<el-input
v-model="form.password"
v-model="form.sharePassword"
type="password"
show-password
clearable
Expand Down Expand Up @@ -54,7 +54,7 @@
</div>
</el-form-item>
<el-form-item :label="$t('file.shareExpiresAt')">
<span>{{ expiresAtText }}</span>
<el-input :model-value="expiresAtText" readonly />
</el-form-item>
</template>
</el-form>
Expand Down Expand Up @@ -102,7 +102,7 @@ import i18n from '@/lang';
import { GlobalStore } from '@/store';
import { buildFileSharePageUrl, buildFileShareQrCodeUrl, copyText, dateFormat as formatDateTime } from '@/utils/util';
import type { FormInstance, FormRules } from 'element-plus';
import { computed, reactive, ref } from 'vue';
import { computed, reactive, ref, watch } from 'vue';

interface ShareProps {
path: string;
Expand All @@ -119,9 +119,12 @@ const expiresAtText = ref('');
const qrCodeUrl = ref('');
const qrDialogOpen = ref(false);
const shareFormRef = ref<FormInstance>();
const syncingPassword = ref(false);
const initialSharePassword = ref('');
const passwordTouched = ref(false);
const form = reactive({
expireMinutes: 1440,
password: '',
sharePassword: '',
});

const emit = defineEmits(['close']);
Expand Down Expand Up @@ -149,9 +152,22 @@ const validatePassword = (_rule, value, callback) => {
};

const rules = reactive<FormRules>({
password: [{ validator: validatePassword, trigger: 'blur' }],
sharePassword: [{ validator: validatePassword, trigger: 'blur' }],
});

watch(
() => form.sharePassword,
(val) => {
if (syncingPassword.value) {
return;
}
if (val === initialSharePassword.value) {
return;
}
passwordTouched.value = true;
},
);

const mapExpireMinutes = (info: File.FileShareInfo) => {
if (info.permanent || info.expiresAt === 0) {
return 0;
Expand All @@ -169,7 +185,11 @@ const applyShareInfo = (info: File.FileShareInfo | null) => {
qrCodeUrl.value = '';
qrDialogOpen.value = false;
form.expireMinutes = 1440;
form.password = '';
form.sharePassword = '';
syncingPassword.value = true;
initialSharePassword.value = '';
passwordTouched.value = false;
syncingPassword.value = false;
return;
}
shareUrl.value = buildFileSharePageUrl(info.code, globalStore.currentNode);
Expand All @@ -178,7 +198,11 @@ const applyShareInfo = (info: File.FileShareInfo | null) => {
? i18n.global.t('website.ever')
: formatDateTime(null, null, info.expiresAt * 1000);
form.expireMinutes = mapExpireMinutes(info);
form.password = '';
syncingPassword.value = true;
form.sharePassword = info.password || '';
initialSharePassword.value = form.sharePassword;
passwordTouched.value = false;
syncingPassword.value = false;
};

const loadShareDetail = async () => {
Expand Down Expand Up @@ -207,12 +231,21 @@ const generate = async () => {
}
loading.value = true;
try {
const pw = form.password.trim();
const res = await createFileShare({
const pw = form.sharePassword.trim();
const payload: File.FileShareCreate = {
path: filePath.value,
expireMinutes: form.expireMinutes,
...(pw.length > 0 ? { password: pw } : {}),
});
};

// Only send password when user explicitly changes it.
if (passwordTouched.value) {
payload.password = pw; // empty string means "clear password"
} else if (!shareInfo.value?.hasPassword && pw.length > 0) {
// New share: allow setting password when it wasn't loaded from server.
payload.password = pw;
}

const res = await createFileShare(payload);
applyShareInfo(res.data as File.FileShareInfo);
changed.value = true;
} finally {
Expand All @@ -233,7 +266,19 @@ const cancelShare = async () => {

const copyLink = () => {
if (shareUrl.value) {
copyText(shareUrl.value);
const password = form.sharePassword.trim();
const content = password
? `${i18n.global.t('file.shareLinkLabel')}:${shareUrl.value},${i18n.global.t(
'file.sharePassword',
)}:${password}`
: `${i18n.global.t('file.shareLinkLabel')}:${shareUrl.value}`;
copyText(content);
}
};

const openShareUrl = () => {
if (shareUrl.value) {
window.open(shareUrl.value, '_blank', 'noopener,noreferrer');
}
};

Expand Down
Loading