Skip to content

Commit c2bd75a

Browse files
committed
fix: Generate links using permanent credentials rustfs/rustfs#1647
1 parent d87ca7e commit c2bd75a

2 files changed

Lines changed: 109 additions & 35 deletions

File tree

‎components/object/info.vue‎

Lines changed: 80 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ const { t } = useI18n()
244244
const message = useMessage()
245245
const dialog = useDialog()
246246
const { $s3Client } = useNuxtApp()
247+
const { credentials, permanentCredentials } = useAuth()
247248
248249
const props = defineProps<{
249250
bucketName: string
@@ -287,114 +288,127 @@ const openDrawer = async (_bucket: string, key: string) => {
287288
message.error(t('Failed to fetch object info'))
288289
}
289290
}
290-
// 有效期输入(天数、小时、分钟)
291+
// Expiration input (days, hours, minutes)
291292
const expirationDays = ref<number>(0)
292293
const expirationHours = ref<number>(0)
293294
const expirationMinutes = ref<number>(0)
294295
const expirationError = ref<string>('')
295296
const totalExpirationSeconds = ref<number>(0)
296297
const isExpirationValid = ref<boolean>(false)
297298
298-
// 一周的秒数
299+
// Seconds in a week
299300
const ONE_WEEK_SECONDS = 7 * 24 * 60 * 60 // 604800
300301
301-
// 计算总时长(秒数)
302+
// Calculate total duration (seconds)
302303
const calculateTotalSeconds = (days: number, hours: number, minutes: number): number => {
303304
return days * 24 * 60 * 60 + hours * 60 * 60 + minutes * 60
304305
}
305306
306-
// 验证有效期输入
307+
// Format duration display
308+
const formatDuration = (seconds: number): string => {
309+
if (seconds === 0) return ''
310+
311+
const days = Math.floor(seconds / (24 * 60 * 60))
312+
const hours = Math.floor((seconds % (24 * 60 * 60)) / (60 * 60))
313+
const minutes = Math.floor((seconds % (60 * 60)) / 60)
314+
315+
const parts: string[] = []
316+
if (days > 0) parts.push(`${days} ${t('Days')}`)
317+
if (hours > 0) parts.push(`${hours} ${t('Hours')}`)
318+
if (minutes > 0) parts.push(`${minutes} ${t('Minutes')}`)
319+
320+
return parts.join(' ')
321+
}
322+
323+
// Validate expiration input
307324
const validateExpiration = (): boolean => {
308325
expirationError.value = ''
309326
310-
// 处理空值和非数字值,转换为0
327+
// Handle empty values and non-numeric values, convert to 0
311328
const days = Number(expirationDays.value) || 0
312329
const hours = Number(expirationHours.value) || 0
313330
const minutes = Number(expirationMinutes.value) || 0
314331
315-
// 检查是否为有效数字
332+
// Check if valid numbers
316333
if (isNaN(days) || isNaN(hours) || isNaN(minutes)) {
317334
return false
318335
}
319336
320-
// 检查分钟范围 (0-59)
337+
// Check minutes range (0-59)
321338
if (minutes < 0 || minutes > 59) {
322339
expirationError.value = t('Minutes must be between 0 and 59')
323340
return false
324341
}
325342
326-
// 检查小时范围
343+
// Check hours range
327344
if (hours < 0) {
328345
expirationError.value = t('Hours must be between 0 and 23')
329346
return false
330347
}
331348
332-
// 如果天数大于0,小时不能超过23
349+
// If days > 0, hours cannot exceed 23
333350
if (days > 0 && hours > 23) {
334351
expirationError.value = t('Hours must be between 0 and 23')
335352
return false
336353
}
337354
338-
// 如果天数为0,小时不能超过24
355+
// If days is 0, hours cannot exceed 24
339356
if (days === 0 && hours > 24) {
340357
expirationError.value = t('Hours must be between 0 and 24 when days is 0')
341358
return false
342359
}
343360
344-
// 检查天数范围 (0-7)
361+
// Check days range (0-7)
345362
if (days < 0 || days > 7) {
346363
expirationError.value = t('Days must be between 0 and 7')
347364
return false
348365
}
349366
350-
// 如果小时为24,自动转换为1天0小时(在计算时处理)
367+
// If hours is 24, automatically convert to 1 day 0 hours (handled in calculation)
351368
let finalDays = days
352369
let finalHours = hours
353370
if (hours === 24 && days === 0) {
354371
finalDays = 1
355372
finalHours = 0
356373
}
357374
358-
// 计算总时长
375+
// Calculate total duration
359376
const totalSeconds = calculateTotalSeconds(finalDays, finalHours, minutes)
360377
361-
// 检查总时长不能超过一周
378+
// Check total duration cannot exceed one week
362379
if (totalSeconds > ONE_WEEK_SECONDS) {
363380
expirationError.value = t('Total duration cannot exceed 7 days')
364381
return false
365382
}
366383
367-
// 检查至少有一个值大于0
384+
// Check at least one value is greater than 0
368385
if (totalSeconds === 0) {
369386
return false
370387
}
371388
389+
// Check if exceeds current session expiration
390+
if (credentials.value?.Expiration && !permanentCredentials.value) {
391+
const expirationDate = new Date(credentials.value.Expiration)
392+
const now = new Date()
393+
const remainingSeconds = Math.floor((expirationDate.getTime() - now.getTime()) / 1000)
394+
395+
// If remaining time <= 0, session expired
396+
if (remainingSeconds <= 0) {
397+
expirationError.value = t('Session expired, please login again')
398+
return false
399+
}
400+
}
401+
372402
totalExpirationSeconds.value = totalSeconds
373403
return true
374404
}
375405
376-
// 验证并更新有效期
406+
// Validate and update expiration
377407
const validateAndUpdateExpiration = () => {
378408
isExpirationValid.value = validateExpiration()
379409
}
380410
381-
// 格式化时长显示
382-
const formatDuration = (seconds: number): string => {
383-
if (seconds === 0) return ''
384-
385-
const days = Math.floor(seconds / (24 * 60 * 60))
386-
const hours = Math.floor((seconds % (24 * 60 * 60)) / (60 * 60))
387-
const minutes = Math.floor((seconds % (60 * 60)) / 60)
388-
389-
const parts: string[] = []
390-
if (days > 0) parts.push(`${days} ${t('Days')}`)
391-
if (hours > 0) parts.push(`${hours} ${t('Hours')}`)
392-
if (minutes > 0) parts.push(`${minutes} ${t('Minutes')}`)
393-
394-
return parts.join(' ')
395-
}
396-
397-
// 生成临时链接
411+
// Generate temporary URL
398412
const generateTemporaryUrl = async () => {
399413
if (!object.value?.Key) return
400414
@@ -404,7 +418,38 @@ const generateTemporaryUrl = async () => {
404418
}
405419
406420
try {
407-
const url = await getSignedUrl(object.value.Key, totalExpirationSeconds.value)
421+
let url = ''
422+
if (permanentCredentials.value) {
423+
// If permanent credentials exist, use them to generate signed URL
424+
// Dynamic import to avoid large initial bundle, or since we are client-side, we can use AWS SDK directly
425+
// But here we need to create a new Client instance, because global $s3Client is STS-based (possibly)
426+
// Or we could reuse getSignedUrl logic in useObject, but need to pass different client
427+
const { S3Client, GetObjectCommand } = await import('@aws-sdk/client-s3')
428+
const { getSignedUrl: presignGetObject } = await import('@aws-sdk/s3-request-presigner')
429+
430+
const { $siteConfig } = useNuxtApp()
431+
const siteConfig = $siteConfig as any
432+
433+
const tempClient = new S3Client({
434+
endpoint: siteConfig.s3.endpoint,
435+
region: siteConfig.s3.region || 'us-east-1',
436+
forcePathStyle: true,
437+
credentials: {
438+
accessKeyId: permanentCredentials.value.AccessKeyId!,
439+
secretAccessKey: permanentCredentials.value.SecretAccessKey!,
440+
},
441+
})
442+
443+
const command = new GetObjectCommand({
444+
Bucket: props.bucketName,
445+
Key: object.value.Key,
446+
})
447+
448+
url = await presignGetObject(tempClient, command, { expiresIn: totalExpirationSeconds.value })
449+
} else {
450+
url = await getSignedUrl(object.value.Key, totalExpirationSeconds.value)
451+
}
452+
408453
signedUrl.value = url
409454
message.success(t('URL generated successfully'))
410455
} catch (error: any) {
@@ -416,7 +461,7 @@ const loadObjectInfo = async (key: string) => {
416461
const info = await getObjectInfo(key)
417462
object.value = info
418463
lockStatus.value = info?.ObjectLockLegalHoldStatus === 'ON'
419-
// 默认不生成分享链接,重置有效期输入
464+
// Default not to generate share link, reset expiration input
420465
expirationDays.value = 0
421466
expirationHours.value = 0
422467
expirationMinutes.value = 0

‎composables/useAuth.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,16 @@ interface Credentials {
1313
export function useAuth() {
1414
const store = useLocalStorage('auth.credentials', {})
1515
const isAdminStore = useLocalStorage('auth.isAdmin', false)
16+
const permanentStore = useLocalStorage<Credentials | undefined>('auth.permanent', undefined)
1617

1718
const setCredentials = (credentials: Credentials) => {
1819
store.value = credentials
1920
}
2021

22+
const setPermanentCredentials = (credentials: Credentials) => {
23+
permanentStore.value = credentials
24+
}
25+
2126
const getCredentials = () => {
2227
if (!isValidCredentials(store.value)) {
2328
return
@@ -26,6 +31,10 @@ export function useAuth() {
2631
return store.value
2732
}
2833

34+
const getPermanentCredentials = () => {
35+
return permanentStore.value
36+
}
37+
2938
const setIsAdmin = (value: boolean) => {
3039
isAdminStore.value = value
3140
}
@@ -57,11 +66,30 @@ export function useAuth() {
5766
Expiration: credentialsResponse.Expiration?.toISOString(),
5867
})
5968

69+
// If it's not an STS login (i.e., no SessionToken), save it as permanent credentials.
70+
// Or if it's an STS login but without a SessionToken (theoretically shouldn't happen).
71+
// In reality, AwsCredentialIdentity might contain a SessionToken.
72+
// We only save it when explicitly logging in with AccessKey/SecretKey (usually no SessionToken, or user intent is clear).
73+
// Simple check here: if no SessionToken, or STS login with very long expiration?
74+
// Usually STS login has a SessionToken.
75+
// Note: The passed credentials parameter can be a function (IdentityProvider) or an object.
76+
// If it's an object and has no SessionToken, we consider it permanent credentials.
77+
if (typeof credentials === 'object' && !('sessionToken' in credentials && credentials.sessionToken)) {
78+
setPermanentCredentials({
79+
AccessKeyId: credentials.accessKeyId,
80+
SecretAccessKey: credentials.secretAccessKey,
81+
})
82+
} else {
83+
// Clear old permanent credentials to avoid confusion
84+
permanentStore.value = undefined
85+
}
86+
6087
return credentialsResponse
6188
}
6289

6390
const logout = () => {
6491
store.value = {}
92+
permanentStore.value = undefined
6593
isAdminStore.value = false
6694
}
6795

@@ -77,6 +105,7 @@ export function useAuth() {
77105
setIsAdmin,
78106
getIsAdmin,
79107
credentials: ref<Credentials | undefined>(getCredentials()),
108+
permanentCredentials: ref<Credentials | undefined>(getPermanentCredentials()),
80109
isAuthenticated: computed(() => isValidCredentials(store.value)),
81110
isAdmin: computed(() => isAdminStore.value),
82111
}

0 commit comments

Comments
 (0)