create-nara 1.0.16 → 1.0.18

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.16",
3
+ "version": "1.0.18",
4
4
  "description": "CLI to scaffold NARA projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,8 +1,22 @@
1
- import { BaseController, jsonSuccess, ValidationError } from '@nara-web/core';
1
+ import { BaseController, jsonSuccess, jsonError, ValidationError } from '@nara-web/core';
2
2
  import type { NaraRequest, NaraResponse } from '@nara-web/core';
3
3
  import bcrypt from 'bcrypt';
4
+ import sharp from 'sharp';
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import crypto from 'crypto';
4
8
 
5
9
  export class ProfileController extends BaseController {
10
+ private avatarDir = './uploads/avatars';
11
+
12
+ constructor() {
13
+ super();
14
+ // Ensure avatar directory exists
15
+ if (!fs.existsSync(this.avatarDir)) {
16
+ fs.mkdirSync(this.avatarDir, { recursive: true });
17
+ }
18
+ }
19
+
6
20
  async update(req: NaraRequest, res: NaraResponse) {
7
21
  const user = req.user;
8
22
  if (!user) {
@@ -64,19 +78,58 @@ export class ProfileController extends BaseController {
64
78
  return res.status(401).json({ success: false, message: 'Unauthorized' });
65
79
  }
66
80
 
67
- // TODO: Handle file upload
68
- // This requires multipart form handling which depends on the upload middleware
69
- // For now, return a placeholder response
81
+ try {
82
+ // Get raw body as buffer
83
+ const buffer = await req.buffer();
70
84
 
71
- // Example implementation:
72
- // const file = await req.file();
73
- // const filename = `avatars/${user.id}-${Date.now()}.${file.extension}`;
74
- // await saveFile(file, filename);
75
- // await UserModel.updateAvatar(user.id, `/uploads/${filename}`);
76
- // return jsonSuccess(res, { url: `/uploads/${filename}` }, 'Avatar uploaded successfully');
85
+ if (!buffer || buffer.length === 0) {
86
+ return jsonError(res, 'No file uploaded', 400);
87
+ }
77
88
 
78
- return jsonSuccess(res, {
79
- url: '/uploads/avatars/default.png'
80
- }, 'Avatar uploaded successfully');
89
+ // Extract image from multipart form data
90
+ const boundary = req.headers['content-type']?.split('boundary=')[1];
91
+ if (!boundary) {
92
+ return jsonError(res, 'Invalid multipart form data', 400);
93
+ }
94
+
95
+ // Find image data in multipart
96
+ const parts = buffer.toString('binary').split(`--${boundary}`);
97
+ let imageBuffer: Buffer | null = null;
98
+
99
+ for (const part of parts) {
100
+ if (part.includes('Content-Type: image/')) {
101
+ const dataStart = part.indexOf('\r\n\r\n') + 4;
102
+ const dataEnd = part.lastIndexOf('\r\n');
103
+ if (dataStart > 4 && dataEnd > dataStart) {
104
+ imageBuffer = Buffer.from(part.slice(dataStart, dataEnd), 'binary');
105
+ break;
106
+ }
107
+ }
108
+ }
109
+
110
+ if (!imageBuffer) {
111
+ return jsonError(res, 'No image found in upload', 400);
112
+ }
113
+
114
+ // Generate unique filename
115
+ const filename = `${crypto.randomUUID()}.webp`;
116
+ const filepath = path.join(this.avatarDir, filename);
117
+
118
+ // Process and save image with sharp
119
+ await sharp(imageBuffer)
120
+ .resize(400, 400, { fit: 'cover' })
121
+ .webp({ quality: 80 })
122
+ .toFile(filepath);
123
+
124
+ const avatarUrl = `/uploads/avatars/${filename}`;
125
+
126
+ // TODO: Update user avatar in database
127
+ // await UserModel.updateAvatar(user.id, avatarUrl);
128
+
129
+ return jsonSuccess(res, { url: avatarUrl }, 'Avatar uploaded successfully');
130
+ } catch (error) {
131
+ console.error('Avatar upload error:', error);
132
+ return jsonError(res, 'Failed to upload avatar', 500);
133
+ }
81
134
  }
82
135
  }
@@ -1,8 +1,8 @@
1
1
  <script lang="ts">
2
2
  import { fly } from 'svelte/transition';
3
3
  import axios from "axios";
4
- import Header from "../Components/Header.svelte";
5
- import { api, Toast } from "../Components/helper";
4
+ import Header from "../components/Header.svelte";
5
+ import { api, Toast } from "../components/helper";
6
6
 
7
7
  interface User {
8
8
  id: string;
@@ -25,26 +25,21 @@
25
25
  function handleAvatarChange(event: Event): void {
26
26
  const target = event.target as HTMLInputElement;
27
27
  const file = target.files?.[0];
28
- if (file) console.log('[AVATAR DEBUG] Uploading file:', file.name, 'Size:', file.size, 'Type:', file.type);
29
28
  if (file) {
30
29
  const formData = new FormData();
31
30
  formData.append("file", file);
32
31
  isLoading = true;
33
- console.log('[AVATAR DEBUG] Sending request to /assets/avatar');
34
32
  axios
35
- .post("/assets/avatar", formData)
33
+ .post("/api/profile/avatar", formData)
36
34
  .then((response) => {
37
- console.log('[AVATAR DEBUG] Upload response:', response.data);
38
35
  setTimeout(() => {
39
36
  isLoading = false;
40
- previewUrl = response.data.data.url + "?v=" + Date.now();
41
- console.log('[AVATAR DEBUG] previewUrl set to:', previewUrl);
37
+ previewUrl = response.data.data?.url + "?v=" + Date.now();
42
38
  }, 500);
43
- user.avatar = response.data.data.url + "?v=" + Date.now();
39
+ user.avatar = response.data.data?.url;
44
40
  Toast("Avatar berhasil diupload", "success");
45
41
  })
46
- .catch((error) => {
47
- console.log('[AVATAR DEBUG] Upload failed:', error);
42
+ .catch(() => {
48
43
  isLoading = false;
49
44
  Toast("Gagal mengupload avatar", "error");
50
45
  });
@@ -1,7 +1,31 @@
1
1
  import type { NaraApp } from '@nara-web/core';
2
2
  import { webAuthMiddleware, guestMiddleware } from '../app/middlewares/auth.js';
3
+ import fs from 'fs';
4
+ import path from 'path';
3
5
 
4
6
  export function registerRoutes(app: NaraApp) {
7
+ // Serve uploaded files
8
+ app.get('/uploads/*', (req, res) => {
9
+ const requestPath = req.path?.replace('/uploads/', '') || '';
10
+ const filePath = path.join(process.cwd(), 'uploads', requestPath);
11
+
12
+ if (fs.existsSync(filePath)) {
13
+ const ext = path.extname(filePath).toLowerCase();
14
+ const mimeTypes: Record<string, string> = {
15
+ '.webp': 'image/webp',
16
+ '.png': 'image/png',
17
+ '.jpg': 'image/jpeg',
18
+ '.jpeg': 'image/jpeg',
19
+ '.gif': 'image/gif',
20
+ };
21
+ res.setHeader('Content-Type', mimeTypes[ext] || 'application/octet-stream');
22
+ res.setHeader('Cache-Control', 'public, max-age=31536000');
23
+ res.send(fs.readFileSync(filePath));
24
+ } else {
25
+ res.status(404).send('Not found');
26
+ }
27
+ });
28
+
5
29
  // Public routes
6
30
  app.get('/', (req, res) => {
7
31
  res.inertia?.('landing', {
@@ -1,226 +0,0 @@
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();