Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
VUE_APP_BASE_URL=https://staging.disfactory.tw/api
VUE_APP_IMGUR_FALLBACK_URL=https://api.disfactory.tw/imgur

# Image upload configuration
# Set to 'backend' to use the backend filesystem upload, or 'imgur' to use Imgur (default)
VUE_APP_IMAGE_UPLOAD_PROVIDER=imgur
# Backend upload URL (only used when VUE_APP_IMAGE_UPLOAD_PROVIDER=backend)
VUE_APP_IMAGE_UPLOAD_URL=https://api.disfactory.tw/api/upload
53 changes: 50 additions & 3 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

// Update base URL whenever currentBaseURL changes
const updateBaseURL = () => {
instance.defaults.baseURL = currentBaseURL.value

Check warning on line 15 in src/api/index.ts

View workflow job for this annotation

GitHub Actions / lint

Unsafe assignment of an `any` value
}

// Set initial base URL and watch for changes
Expand All @@ -28,10 +28,15 @@
src: string // used for preview images
}[]

// Image upload provider configuration
type ImageUploadProvider = 'imgur' | 'backend'
const IMAGE_UPLOAD_PROVIDER: ImageUploadProvider = (process.env.VUE_APP_IMAGE_UPLOAD_PROVIDER as ImageUploadProvider) || 'imgur'

Check warning on line 33 in src/api/index.ts

View workflow job for this annotation

GitHub Actions / lint

Unsafe member access .VUE_APP_IMAGE_UPLOAD_PROVIDER on an `any` value

Copilot AI Dec 5, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The environment variable VUE_APP_IMAGE_UPLOAD_PROVIDER is cast to ImageUploadProvider type without validation. If an invalid value (e.g., 'typo' or 'cloudinary') is provided, it will be assigned to IMAGE_UPLOAD_PROVIDER and only fall back to 'imgur' if the value is falsy. This could lead to unexpected behavior in the uploadImage function.

Consider adding validation:

const rawProvider = process.env.VUE_APP_IMAGE_UPLOAD_PROVIDER
const IMAGE_UPLOAD_PROVIDER: ImageUploadProvider = 
  (rawProvider === 'imgur' || rawProvider === 'backend') ? rawProvider : 'imgur'

This ensures only valid provider values are used.

Suggested change
const IMAGE_UPLOAD_PROVIDER: ImageUploadProvider = (process.env.VUE_APP_IMAGE_UPLOAD_PROVIDER as ImageUploadProvider) || 'imgur'
const rawProvider = process.env.VUE_APP_IMAGE_UPLOAD_PROVIDER
const IMAGE_UPLOAD_PROVIDER: ImageUploadProvider = (rawProvider === 'imgur' || rawProvider === 'backend') ? rawProvider : 'imgur'

Copilot uses AI. Check for mistakes.
const IMAGE_UPLOAD_URL = process.env.VUE_APP_IMAGE_UPLOAD_URL || ''

Check warning on line 34 in src/api/index.ts

View workflow job for this annotation

GitHub Actions / lint

Unsafe member access .VUE_APP_IMAGE_UPLOAD_URL on an `any` value

Check warning on line 34 in src/api/index.ts

View workflow job for this annotation

GitHub Actions / lint

Unsafe assignment of an `any` value

export async function getFactories (range: number, lng: number, lat: number): Promise<FactoriesResponse> {
try {
const { data } = await instance.get(`/factories?range=${range}&lng=${lng}&lat=${lat}`)

Check warning on line 38 in src/api/index.ts

View workflow job for this annotation

GitHub Actions / lint

Unsafe array destructuring of a tuple element with an `any` value
return data

Check warning on line 39 in src/api/index.ts

View workflow job for this annotation

GitHub Actions / lint

Unsafe return of an `any` typed value
} catch (err) {
console.error(err)
throw new TypeError('Get factory failed')
Expand All @@ -40,8 +45,8 @@

export async function getFactory (factoryId: string): Promise<FactoryData> {
try {
const { data } = await instance.get(`/factories/${factoryId}`)

Check warning on line 48 in src/api/index.ts

View workflow job for this annotation

GitHub Actions / lint

Unsafe array destructuring of a tuple element with an `any` value
return data

Check warning on line 49 in src/api/index.ts

View workflow job for this annotation

GitHub Actions / lint

Unsafe return of an `any` typed value
} catch (err) {
console.error(err)
throw new TypeError('Get factory failed')
Expand All @@ -50,11 +55,17 @@

const IMGUR_CLIENT_ID = '39048813b021935'

async function uploadToImgur (file: File) {
type ImageUploadResult = {
link: string,
deletehash: string,
file: File
}

async function uploadToImgur (file: File): Promise<ImageUploadResult> {
const formData = new FormData()
formData.append('image', file)

const { data } = await axios({

Check warning on line 68 in src/api/index.ts

View workflow job for this annotation

GitHub Actions / lint

Unsafe array destructuring of a tuple element with an `any` value
method: 'POST',
url: 'https://api.imgur.com/3/image',
data: formData,
Expand All @@ -65,12 +76,48 @@
})

return {
link: data.data.link as string,

Check warning on line 79 in src/api/index.ts

View workflow job for this annotation

GitHub Actions / lint

Unsafe member access .data on an `any` value
deletehash: data.data.deletehash as string,
file
}
}

async function uploadToBackend (file: File): Promise<ImageUploadResult> {
const formData = new FormData()
formData.append('image', file)

// Use the configured backend upload URL, or construct from current base URL
const uploadUrl = IMAGE_UPLOAD_URL || `${currentBaseURL.value}/upload`

const { data } = await axios({
method: 'POST',
url: uploadUrl,
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})

// Backend returns Imgur-compatible response format
if (!data.success) {
throw new Error(data.data?.error || 'Image upload failed')
}

return {
link: data.data.link as string,
deletehash: data.data.deletehash as string,
file
}
}

// Upload image using configured provider
async function uploadImage (file: File): Promise<ImageUploadResult> {
if (IMAGE_UPLOAD_PROVIDER === 'backend') {
return uploadToBackend(file)
}
return uploadToImgur(file)
}
Comment on lines +85 to +119

Copilot AI Dec 5, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new upload functionality (uploadToBackend, uploadImage) lacks test coverage. Since the repository uses Jest for testing (as evidenced by src/components/__tests__/TutorialModal.spec.ts), consider adding tests to verify:

  1. The uploadImage function correctly routes to the appropriate provider based on IMAGE_UPLOAD_PROVIDER
  2. The uploadToBackend function handles successful and failed uploads properly
  3. The fallback behavior when IMAGE_UPLOAD_URL is not provided

This would ensure the new feature works as expected and prevent regressions.

Copilot uses AI. Check for mistakes.

const convertTurple2Number = (input: [number, number, number]) => input[0] + (input[1] / 60) + (input[2] / 3600)

type ExifData = { DateTimeOriginal?: string, GPSLatitude?: [number, number, number], GPSLongitude?: [number, number, number] }
Expand Down Expand Up @@ -120,13 +167,13 @@

export async function uploadImages (files: FileList): Promise<UploadedImages> {
return Promise.all(
Array.from(files).map((file) => uploadToImgur(file).then((el) => uploadExifAndGetToken(el)))
Array.from(files).map((file) => uploadImage(file).then((el) => uploadExifAndGetToken(el)))
)
}

export async function updateFactoryImages (factoryId: string, files: FileList, { nickname, contact }: { nickname?: string, contact?: string }) {
return Promise.all(
Array.from(files).map((file) => uploadToImgur(file).then((el) => (async () => {
Array.from(files).map((file) => uploadImage(file).then((el) => (async () => {
const exifData = await readImageExif(el.file)
const { data }: { data: FactoryImage } = await instance.post(`/factories/${factoryId}/images`, { url: el.link, ...exifData, nickname, contact, deletehash: el.deletehash })
data.image_path = el.link
Expand Down