Skip to content

Commit 3280f28

Browse files
kek-SecGeorge Petrakis
andauthored
Fix/file upload (#19)
* Refactor form handling in CreateSend and improve PasswordInput component * Replace v-select with v-btn-toggle for type selection in Create.vue * chore: update changelog for version 1.0.9 and enhance file upload validation --------- Co-authored-by: George Petrakis <g.petrakis@natechbanking.com>
1 parent 9d1b539 commit 3280f28

5 files changed

Lines changed: 77 additions & 49 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# Changelog
22

3+
## [1.0.9]
4+
5+
### Changed
6+
- Improved the "Type" selector on the Create Secret page to use a visually appealing slider toggle with icons for "Text" and "File".
7+
- After creating a secret, all input fields are now cleared for a better user experience.
8+
- Enhanced file upload validation to reliably detect selected files and prevent false "Please select a file" errors.
9+
10+
### Fixed
11+
- Fixed an issue where file uploads would sometimes incorrectly show "Please select a file" even when a file was chosen.
12+
13+
### UI/UX
14+
- The form now resets when navigating to the Create page via the logo or Create+ button.
15+
316
## [1.0.8]
417
### UI Enhancements
518
- **Redesigned Header and Footer**: Modernized header/footer with theme toggle and improved visual consistency.

internal/handlers/handlers.go

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package handlers
33

44
import (
55
"fmt"
6-
"io/ioutil"
6+
"io"
77
"log"
88
"net/http"
99
"os"
@@ -22,10 +22,19 @@ import (
2222
// It accepts form data for type (text/file), optional password, one-time use, and expiration.
2323
func CreateSend(cfg config.Config, db *gorm.DB) gin.HandlerFunc {
2424
return func(c *gin.Context) {
25-
stype := c.PostForm("type")
26-
pw := c.PostForm("password")
27-
ot := c.PostForm("onetime")
28-
exp := c.PostForm("expires")
25+
// Explicitly parse the multipart form before accessing form values.
26+
// This is crucial for handling mixed file/text forms reliably in Gin.
27+
if err := c.Request.ParseMultipartForm(cfg.MaxFileSize + 1024*1024); err != nil { // Add buffer to max size
28+
log.Println("Error parsing multipart form:", err)
29+
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid form data or file too large"})
30+
return
31+
}
32+
33+
// Use c.Request.FormValue now that the form is parsed.
34+
stype := c.Request.FormValue("type")
35+
pw := c.Request.FormValue("password")
36+
ot := c.Request.FormValue("onetime")
37+
exp := c.Request.FormValue("expires")
2938

3039
log.Println("CreateSend called with type:", stype)
3140

@@ -63,7 +72,7 @@ func CreateSend(cfg config.Config, db *gorm.DB) gin.HandlerFunc {
6372
key := deriveKey(pw, cfg)
6473

6574
if stype == "text" {
66-
text := c.PostForm("data")
75+
text := c.Request.FormValue("data")
6776
if text == "" {
6877
log.Println("Error: 'data' field is empty for text type")
6978
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Data field is required for text type"})
@@ -92,30 +101,24 @@ func CreateSend(cfg config.Config, db *gorm.DB) gin.HandlerFunc {
92101
}
93102

94103
if stype == "file" {
95-
file, err := c.FormFile("file")
104+
// Use c.Request.FormFile now that the form is parsed.
105+
file, header, err := c.Request.FormFile("file")
96106
if err != nil {
97107
log.Println("Error retrieving file from form data:", err)
98108
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Failed to retrieve file from form data"})
99109
return
100110
}
111+
defer file.Close()
101112

102-
log.Println("Received file:", file.Filename, "Size:", file.Size)
113+
log.Println("Received file:", header.Filename, "Size:", header.Size)
103114

104-
if file.Size > cfg.MaxFileSize {
105-
log.Printf("Error: File size (%d bytes) exceeds maximum allowed size (%d bytes)\n", file.Size, cfg.MaxFileSize)
115+
if header.Size > cfg.MaxFileSize {
116+
log.Printf("Error: File size (%d bytes) exceeds maximum allowed size (%d bytes)\n", header.Size, cfg.MaxFileSize)
106117
c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, gin.H{"error": "File size exceeds the maximum allowed limit"})
107118
return
108119
}
109120

110-
f, err := file.Open()
111-
if err != nil {
112-
log.Println("Error opening uploaded file:", err)
113-
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Failed to open uploaded file"})
114-
return
115-
}
116-
defer f.Close()
117-
118-
data, err := ioutil.ReadAll(f)
121+
data, err := io.ReadAll(file)
119122
if err != nil {
120123
log.Println("Error reading file data:", err)
121124
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Failed to read file data"})
@@ -130,8 +133,7 @@ func CreateSend(cfg config.Config, db *gorm.DB) gin.HandlerFunc {
130133
}
131134

132135
fp := filepath.Join(cfg.StoragePath, hash)
133-
err = ioutil.WriteFile(fp, []byte(enc), 0600)
134-
if err != nil {
136+
if err := os.WriteFile(fp, []byte(enc), 0600); err != nil {
135137
log.Println("Error writing encrypted file to storage:", err)
136138
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Failed to write encrypted file to storage"})
137139
return
@@ -143,7 +145,7 @@ func CreateSend(cfg config.Config, db *gorm.DB) gin.HandlerFunc {
143145
Hash: hash,
144146
Type: "file",
145147
FilePath: fp,
146-
FileName: file.Filename,
148+
FileName: header.Filename,
147149
Password: pw,
148150
OneTime: oneTime,
149151
ExpiresAt: expiresAt,
@@ -192,7 +194,7 @@ func GetSend(cfg config.Config, db *gorm.DB) gin.HandlerFunc {
192194
}
193195
c.String(http.StatusOK, string(d))
194196
} else {
195-
d, err := ioutil.ReadFile(s.FilePath)
197+
d, err := os.ReadFile(s.FilePath)
196198
if err != nil {
197199
c.AbortWithStatus(http.StatusInternalServerError)
198200
return
@@ -246,4 +248,4 @@ func CheckPasswordProtection(db *gorm.DB) gin.HandlerFunc {
246248
// Return whether the send requires a password
247249
c.JSON(http.StatusOK, gin.H{"requiresPassword": s.Password != ""})
248250
}
249-
}
251+
}

ui/src/components/PasswordInput.vue

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,20 @@
44
:type="showPassword ? 'text' : 'password'"
55
:model-value="modelValue"
66
@update:modelValue="$emit('update:modelValue', $event)"
7+
variant="outlined"
8+
prepend-inner-icon="mdi-lock"
79
>
810
<template v-slot:append-inner>
911
<v-tooltip text="Toggle Password Visibility">
10-
<template v-slot:activator="{ on, attrs }">
11-
<v-btn icon v-bind="attrs" v-on="on" @click="showPassword = !showPassword" size="small">
12+
<template v-slot:activator="{ props }">
13+
<v-btn icon v-bind="props" @click="showPassword = !showPassword" size="small">
1214
<v-icon>{{ showPassword ? 'mdi-eye-off' : 'mdi-eye' }}</v-icon>
1315
</v-btn>
1416
</template>
1517
</v-tooltip>
1618
<v-tooltip text="Generate Random Password">
17-
<template v-slot:activator="{ on, attrs }">
18-
<v-btn icon color="primary" v-bind="attrs" v-on="on" @click="generateNewPassword" size="small" style="margin-left: 4px">
19+
<template v-slot:activator="{ props }">
20+
<v-btn icon color="primary" v-bind="props" @click="generateNewPassword" size="small" style="margin-left: 4px">
1921
<v-icon>mdi-refresh</v-icon>
2022
</v-btn>
2123
</template>

ui/src/pages/Create.vue

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,21 @@
44
<v-card-title class="text-h5 text-md-h4 font-weight-bold text-center mb-4">Create a New Secret 🔑</v-card-title>
55
<v-card-text>
66
<v-form @submit.prevent="handleSubmit">
7-
<v-select
8-
label="Type"
7+
<v-btn-toggle
98
v-model="type"
10-
:items="['text', 'file']"
11-
required
12-
variant="outlined"
13-
class="mb-2"
14-
></v-select>
9+
mandatory
10+
class="mb-4 d-flex justify-center"
11+
color="primary"
12+
rounded
13+
group
14+
>
15+
<v-btn value="text" class="px-6" rounded>
16+
<v-icon left>mdi-text</v-icon> Text
17+
</v-btn>
18+
<v-btn value="file" class="px-6" rounded>
19+
<v-icon left>mdi-file</v-icon> File
20+
</v-btn>
21+
</v-btn-toggle>
1522

1623
<v-textarea
1724
v-if="type === 'text'"
@@ -88,11 +95,11 @@
8895
import { ref, watch } from 'vue';
8996
import { createSend } from '../services/api.js';
9097
import PasswordInput from '../components/PasswordInput.vue';
91-
import { formStore } from '../stores/formStore.js'; // Import the store
98+
import { formStore } from '../stores/formStore.js';
9299
93100
const type = ref('text');
94101
const textSecret = ref('');
95-
const fileBlob = ref(null);
102+
// v-file-input uses an array for its model, so initialize it as such.
96103
const files = ref([]);
97104
const password = ref('');
98105
const oneTime = ref(false);
@@ -112,14 +119,6 @@ const expirationOptions = [
112119
{ title: '1 Week', value: '168h' }
113120
];
114121
115-
watch(files, (newFiles) => {
116-
if (Array.isArray(newFiles) && newFiles.length > 0) {
117-
fileBlob.value = newFiles[0];
118-
} else {
119-
fileBlob.value = null;
120-
}
121-
});
122-
123122
function resetForm() {
124123
type.value = 'text';
125124
textSecret.value = '';
@@ -152,12 +151,17 @@ async function handleSubmit() {
152151
}
153152
formData.append('data', textSecret.value);
154153
} else if (type.value === 'file') {
155-
if (!fileBlob.value) {
156-
errorMessage.value = 'Please select a file';
154+
// Debug log
155+
console.log('files.value:', files.value);
156+
// Ensure files.value is an array and has a File object
157+
const fileArr = Array.isArray(files.value) ? files.value : (files.value ? [files.value] : []);
158+
if (!fileArr.length || !(fileArr[0] instanceof File)) {
159+
errorMessage.value = 'Please select a file 😟';
157160
loading.value = false;
158161
return;
159162
}
160-
formData.append('file', fileBlob.value);
163+
const fileToUpload = fileArr[0];
164+
formData.append('file', fileToUpload, fileToUpload.name);
161165
}
162166
163167
if (password.value.trim()) {
@@ -171,6 +175,13 @@ async function handleSubmit() {
171175
try {
172176
const result = await createSend(formData);
173177
resultHash.value = result.hash;
178+
// Clear form inputs but keep the result hash visible
179+
type.value = 'text';
180+
textSecret.value = '';
181+
files.value = [];
182+
password.value = '';
183+
oneTime.value = false;
184+
expires.value = '24h';
174185
} catch (err) {
175186
errorMessage.value = err.message || 'Failed to create secret';
176187
} finally {

version.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
#Application version following https://semver.org/
2-
version: 1.0.7
2+
version: 1.0.9

0 commit comments

Comments
 (0)