create-nara 1.0.14 → 1.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of create-nara might be problematic. Click here for more details.

package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nara",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
4
4
  "description": "CLI to scaffold NARA projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,226 @@
1
+ import { randomUUID } from "crypto";
2
+ import type { NaraRequest, NaraResponse } from "@core";
3
+ import { BaseController, jsonError, jsonServerError, jsonSuccess } from "@core";
4
+ import fs from "fs";
5
+ import path from "path";
6
+ import sharp from "sharp";
7
+ import { Asset, User } from "@models";
8
+ import Logger from "@services/Logger";
9
+ import { Storage } from '@services';
10
+ import { UPLOAD } from '@config/constants';
11
+
12
+
13
+
14
+ // Cache object to store file contents in memory
15
+ let cache: { [key: string]: Buffer } = {};
16
+
17
+ class AssetController extends BaseController {
18
+ /**
19
+ * Serves assets from the dist folder (compiled assets)
20
+ * - Handles CSS and JS files with proper content types
21
+ * - Implements file caching for better performance
22
+ * - Sets appropriate cache headers for browser caching
23
+ */
24
+
25
+ public async uploadAsset(request: NaraRequest, response: NaraResponse) {
26
+ this.requireAuth(request);
27
+
28
+ // Store user reference for use in nested callbacks
29
+ const userId = request.user.id;
30
+ console.log('[AVATAR DEBUG] Upload started for user:', userId);
31
+
32
+ try {
33
+ let isValidFile = true;
34
+
35
+ await request.multipart(async (field: any) => {
36
+ if (field.file) {
37
+ if (!field.mime_type.includes("image")) {
38
+ console.log('[AVATAR DEBUG] File mime_type:', field.mime_type);
39
+ isValidFile = false;
40
+ return;
41
+ }
42
+
43
+ const id = randomUUID();
44
+ const fileName = `${id}.webp`;
45
+
46
+ // Create a buffer to store the image data
47
+ const chunks: Buffer[] = [];
48
+ const readable = field.file.stream;
49
+
50
+ readable.on('data', (chunk: Buffer) => {
51
+ chunks.push(chunk);
52
+ });
53
+
54
+ readable.on('end', async () => {
55
+ const buffer = Buffer.concat(chunks);
56
+ console.log('[AVATAR DEBUG] Total buffer length received:', buffer.length);
57
+
58
+ try {
59
+ // Process image with Sharp and get buffer
60
+ const processedBuffer = await sharp(buffer)
61
+ .webp({ quality: 80 }) // Convert to WebP with 80% quality
62
+ .resize(1200, 1200, { // Resize to max 1200x1200 while maintaining aspect ratio
63
+ fit: 'inside',
64
+ withoutEnlargement: true
65
+ })
66
+ .toBuffer();
67
+ console.log('[AVATAR DEBUG] Processed buffer length (WebP):', processedBuffer.length);
68
+
69
+ // Store processed image using Storage service
70
+ const storedFile = await Storage.put(processedBuffer, {
71
+ directory: UPLOAD.AVATAR_DIR,
72
+ name: id,
73
+ extension: 'webp'
74
+ console.log('[AVATAR DEBUG] File stored at:', storedFile.url);
75
+ });
76
+
77
+ // Build public URL for the saved file
78
+ const publicUrl = storedFile.url;
79
+
80
+ // Save to assets table with local file reference
81
+ await Asset.create({
82
+ id,
83
+ type: 'image',
84
+ url: publicUrl,
85
+ mime_type: 'image/webp',
86
+ name: fileName,
87
+ size: processedBuffer.length,
88
+ user_id: userId,
89
+ });
90
+
91
+ // Update user avatar in users table
92
+ await User.updateAvatar(userId, publicUrl);
93
+
94
+ // Return success response with public URL
95
+ console.log('[AVATAR DEBUG] Returning public URL:', publicUrl);
96
+ jsonSuccess(response, 'Avatar berhasil diupload', { url: publicUrl });
97
+ } catch (err) {
98
+ Logger.error('Error processing and uploading image', err as Error);
99
+ jsonServerError(response, 'Gagal memproses dan mengupload gambar');
100
+ }
101
+ });
102
+ }
103
+ });
104
+
105
+ if (!isValidFile) {
106
+ return jsonError(response, "Invalid file type. Only images are allowed.", 400, "INVALID_FILE_TYPE");
107
+ }
108
+
109
+ } catch (error) {
110
+ Logger.error('Error uploading asset', error as Error);
111
+ return jsonServerError(response, "Internal server error");
112
+ }
113
+ }
114
+
115
+ public async distFolder(request: NaraRequest, response: NaraResponse) {
116
+ const file = request.params.file;
117
+
118
+ try {
119
+ const filePath = `dist/assets/${file}`;
120
+
121
+ // Set appropriate content type based on file extension
122
+ if (file.endsWith(".css")) {
123
+ response.setHeader("Content-Type", "text/css");
124
+ } else if (file.endsWith(".js")) {
125
+ response.setHeader("Content-Type", "application/javascript");
126
+ } else {
127
+ response.setHeader("Content-Type", "application/octet-stream");
128
+ }
129
+
130
+ // Set cache control header for browser caching (1 year)
131
+ response.setHeader("Cache-Control", "public, max-age=31536000");
132
+
133
+ // Return cached content if available
134
+ if (cache[file]) {
135
+ return response.send(cache[file]);
136
+ }
137
+
138
+ // Check if file exists and serve it
139
+ if (await fs.promises.access(filePath).then(() => true).catch(() => false)) {
140
+ const fileContent = await fs.promises.readFile(filePath);
141
+
142
+ // Cache the file content
143
+ cache[file] = fileContent;
144
+
145
+ return response.send(fileContent);
146
+ }
147
+
148
+ return response.status(404).send("File not found");
149
+ } catch (error) {
150
+ Logger.error('Error serving dist file', error as Error);
151
+ return response.status(500).send("Internal server error");
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Serves static files from the public folder
157
+ * - Implements security by checking allowed file extensions
158
+ * - Prevents directory traversal attacks
159
+ * - Handles various file types (images, fonts, documents, etc.)
160
+ */
161
+ public async publicFolder(request: NaraRequest, response: NaraResponse) {
162
+ // List of allowed file extensions for security
163
+ const allowedExtensions = [
164
+ '.ico', '.png', '.jpeg', '.jpg', '.gif', '.svg', '.webp',
165
+ '.txt', '.pdf', '.css', '.js',
166
+ '.woff', '.woff2', '.ttf', '.eot',
167
+ '.mp4', '.webm', '.mp3', '.wav'
168
+ ];
169
+
170
+ // Get the requested path and decode URL encoding
171
+ const requestedPath = decodeURIComponent(request.path);
172
+
173
+ // Security: Remove leading slash and normalize
174
+ const relativePath = requestedPath.replace(/^\/+/, '');
175
+
176
+ // Security: Check for path traversal attempts BEFORE any path operations
177
+ // Block any path containing .. or encoded variants
178
+ if (relativePath.includes('..') ||
179
+ relativePath.includes('%2e') ||
180
+ relativePath.includes('%2E') ||
181
+ relativePath.includes('\0')) {
182
+ Logger.logSecurity('Path traversal attempt blocked', {
183
+ requestedPath,
184
+ ip: request.ip
185
+ });
186
+ return response.status(403).send('Access denied');
187
+ }
188
+
189
+ // Check if the path has any extension
190
+ if (!relativePath.includes('.')) {
191
+ return response.status(404).send('Page not found');
192
+ }
193
+
194
+ // Security check: validate file extension
195
+ if (!allowedExtensions.some(ext => relativePath.toLowerCase().endsWith(ext))) {
196
+ return response.status(403).send('File type not allowed');
197
+ }
198
+
199
+ // Resolve the absolute path and verify it's within public directory
200
+ const publicDir = path.resolve(process.cwd(), 'public');
201
+ const storageDir = path.resolve(process.cwd(), 'storage');
202
+ const resolvedPath = path.resolve(process.cwd(), relativePath);
203
+
204
+ // Security: Ensure the resolved path is within the public or storage directory
205
+ if (!resolvedPath.startsWith(publicDir) && !resolvedPath.startsWith(storageDir)) {
206
+ Logger.logSecurity('Path traversal attempt blocked (resolved path escape)', {
207
+ requestedPath,
208
+ resolvedPath,
209
+ publicDir,
210
+ storageDir,
211
+ ip: request.ip
212
+ });
213
+ return response.status(403).send('Access denied');
214
+ }
215
+
216
+ // Check if file exists
217
+ if (!fs.existsSync(resolvedPath)) {
218
+ return response.status(404).send('File not found');
219
+ }
220
+
221
+ // Serve the file
222
+ return response.download(resolvedPath);
223
+ }
224
+ }
225
+
226
+ export default new AssetController();
@@ -1,7 +1,8 @@
1
1
  <script lang="ts">
2
2
  import { fly } from 'svelte/transition';
3
- import Header from "../components/Header.svelte";
4
- import { Toast } from "../components/helper";
3
+ import axios from "axios";
4
+ import Header from "../Components/Header.svelte";
5
+ import { api, Toast } from "../Components/helper";
5
6
 
6
7
  interface User {
7
8
  id: string;
@@ -21,104 +22,64 @@
21
22
  let isLoading: boolean = false;
22
23
  let previewUrl: string | null = user.avatar || null;
23
24
 
24
- async function handleAvatarChange(event: Event): Promise<void> {
25
+ function handleAvatarChange(event: Event): void {
25
26
  const target = event.target as HTMLInputElement;
26
27
  const file = target.files?.[0];
28
+ if (file) console.log('[AVATAR DEBUG] Uploading file:', file.name, 'Size:', file.size, 'Type:', file.type);
27
29
  if (file) {
28
30
  const formData = new FormData();
29
31
  formData.append("file", file);
30
32
  isLoading = true;
31
-
32
- try {
33
- const response = await fetch('/api/profile/avatar', {
34
- method: 'POST',
35
- body: formData
33
+ console.log('[AVATAR DEBUG] Sending request to /assets/avatar');
34
+ axios
35
+ .post("/assets/avatar", formData)
36
+ .then((response) => {
37
+ console.log('[AVATAR DEBUG] Upload response:', response.data);
38
+ setTimeout(() => {
39
+ isLoading = false;
40
+ previewUrl = response.data.data.url + "?v=" + Date.now();
41
+ console.log('[AVATAR DEBUG] previewUrl set to:', previewUrl);
42
+ }, 500);
43
+ user.avatar = response.data.data.url + "?v=" + Date.now();
44
+ Toast("Avatar berhasil diupload", "success");
45
+ })
46
+ .catch((error) => {
47
+ console.log('[AVATAR DEBUG] Upload failed:', error);
48
+ isLoading = false;
49
+ Toast("Gagal mengupload avatar", "error");
36
50
  });
37
- const result = await response.json();
38
-
39
- if (result.success) {
40
- previewUrl = result.data?.url + "?v=" + Date.now();
41
- user.avatar = result.data?.url;
42
- Toast(result.message || "Avatar uploaded successfully", "success");
43
- } else {
44
- Toast(result.message || "Failed to upload avatar", "error");
45
- }
46
- } catch (err: unknown) {
47
- const errorMessage = err instanceof Error ? err.message : 'Network error';
48
- Toast(errorMessage, "error");
49
- } finally {
50
- isLoading = false;
51
- }
52
51
  }
53
52
  }
54
53
 
55
54
  async function changeProfile(): Promise<void> {
56
55
  isLoading = true;
57
- try {
58
- const response = await fetch('/api/profile/update', {
59
- method: 'POST',
60
- headers: { 'Content-Type': 'application/json' },
61
- body: JSON.stringify({ name: user.name, email: user.email, phone: user.phone })
62
- });
63
- const result = await response.json();
64
-
65
- if (result.success) {
66
- Toast(result.message || 'Profile updated successfully', 'success');
67
- } else {
68
- if (result.errors) {
69
- const errorMessages = Object.values(result.errors).flat() as string[];
70
- Toast(errorMessages[0] || result.message || 'Failed to update profile', 'error');
71
- } else {
72
- Toast(result.message || 'Failed to update profile', 'error');
73
- }
74
- }
75
- } catch (err: unknown) {
76
- const errorMessage = err instanceof Error ? err.message : 'Network error';
77
- Toast(errorMessage, 'error');
78
- } finally {
79
- isLoading = false;
80
- }
56
+ await api(() => axios.post("/change-profile", user));
57
+ isLoading = false;
81
58
  }
82
59
 
83
60
  async function changePassword(): Promise<void> {
84
- if (new_password !== confirm_password) {
85
- Toast("Passwords do not match", "error");
61
+ if (new_password != confirm_password) {
62
+ Toast("Password tidak cocok", "error");
86
63
  return;
87
64
  }
88
65
 
89
66
  if (!current_password || !new_password || !confirm_password) {
90
- Toast("Please fill in all fields", "error");
67
+ Toast("Mohon isi semua field", "error");
91
68
  return;
92
69
  }
93
70
 
94
71
  isLoading = true;
95
- try {
96
- const response = await fetch('/api/profile/password', {
97
- method: 'POST',
98
- headers: { 'Content-Type': 'application/json' },
99
- body: JSON.stringify({ current_password, new_password })
100
- });
101
- const result = await response.json();
102
-
103
- if (result.success) {
104
- Toast(result.message || 'Password changed successfully', 'success');
105
- current_password = "";
106
- new_password = "";
107
- confirm_password = "";
108
- } else {
109
- if (result.errors) {
110
- const errorMessages = Object.values(result.errors).flat() as string[];
111
- Toast(errorMessages[0] || result.message || 'Failed to change password', 'error');
112
- } else {
113
- Toast(result.message || 'Failed to change password', 'error');
114
- }
115
- }
116
- } catch (err: unknown) {
117
- const errorMessage = err instanceof Error ? err.message : 'Network error';
118
- Toast(errorMessage, 'error');
119
- } finally {
120
- isLoading = false;
72
+ const result = await api(() => axios.post("/change-password", {
73
+ current_password,
74
+ new_password,
75
+ }));
76
+
77
+ if (result.success) {
78
+ current_password = "";
79
+ new_password = "";
80
+ confirm_password = "";
121
81
  }
82
+ isLoading = false;
122
83
  }
123
84
  </script>
124
85