@axium/server 0.42.1 → 0.43.1
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.
- package/dist/api/images.d.ts +4 -9
- package/dist/api/images.js +23 -33
- package/dist/auth.js +1 -1
- package/dist/config.d.ts +9 -2
- package/dist/config.js +11 -11
- package/package.json +2 -2
- package/routes/account/+page.svelte +1 -2
- package/routes/admin/users/[id]/+page.svelte +1 -2
- package/schemas/config.json +3 -2
package/dist/api/images.d.ts
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
/** Max size in KB */
|
|
4
|
-
max_size: number;
|
|
5
|
-
/** Max pixels per dimension */
|
|
6
|
-
max_length: number;
|
|
7
|
-
}
|
|
8
|
-
export declare function checkImageUpload(request: Request, cfg: ImageUploadConfig, userId: string): Promise<{
|
|
1
|
+
import { type ImageUploadConfig } from '../config.js';
|
|
2
|
+
export interface PreparedImageUpload {
|
|
9
3
|
data: Uint8Array<ArrayBuffer>;
|
|
10
4
|
type: string;
|
|
11
|
-
}
|
|
5
|
+
}
|
|
6
|
+
export declare function prepareImageUpload(request: Request, cfg: ImageUploadConfig, userId: string): Promise<PreparedImageUpload>;
|
package/dist/api/images.js
CHANGED
|
@@ -1,32 +1,13 @@
|
|
|
1
|
+
import { debug } from 'ioium';
|
|
1
2
|
import { sql } from 'kysely';
|
|
2
|
-
import
|
|
3
|
-
import { execSync } from 'node:child_process';
|
|
3
|
+
import sharp from 'sharp';
|
|
4
4
|
import * as z from 'zod';
|
|
5
5
|
import { checkAuthForUser } from '../auth.js';
|
|
6
6
|
import { config } from '../config.js';
|
|
7
7
|
import { database as db } from '../database.js';
|
|
8
8
|
import { error, withError } from '../requests.js';
|
|
9
9
|
import { addRoute } from '../routes.js';
|
|
10
|
-
|
|
11
|
-
try {
|
|
12
|
-
const mod = await import('image-size');
|
|
13
|
-
imageSize = mod.imageSize;
|
|
14
|
-
}
|
|
15
|
-
catch {
|
|
16
|
-
try {
|
|
17
|
-
// Fall back to `identify` from ImageMagick
|
|
18
|
-
execSync('command -v identify');
|
|
19
|
-
imageSize = function identifyImageSize(input) {
|
|
20
|
-
const stdout = execSync('identify -ping -format "%w %h" -', { input, timeout: 1000 });
|
|
21
|
-
const [width, height] = stdout.toString().trim().split(' ').map(Number);
|
|
22
|
-
return { width, height };
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
warn('Can not determine profile picture dimensions because neither image-size or ImageMagick is available');
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
export async function checkImageUpload(request, cfg, userId) {
|
|
10
|
+
export async function prepareImageUpload(request, cfg, userId) {
|
|
30
11
|
const { enabled, max_size, max_length } = cfg;
|
|
31
12
|
if (!enabled)
|
|
32
13
|
error(503, 'Image uploads are disabled');
|
|
@@ -36,20 +17,29 @@ export async function checkImageUpload(request, cfg, userId) {
|
|
|
36
17
|
error(400, 'Missing Content-Type header');
|
|
37
18
|
if (!type.startsWith('image/'))
|
|
38
19
|
error(415, 'Only image files are allowed');
|
|
39
|
-
const
|
|
40
|
-
if (!Number.isSafeInteger(
|
|
20
|
+
const contentLength = Number(request.headers.get('content-length'));
|
|
21
|
+
if (!Number.isSafeInteger(contentLength))
|
|
41
22
|
error(400, 'Invalid Content-Length header');
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const data = await request.bytes();
|
|
45
|
-
if (data.byteLength != size)
|
|
23
|
+
const rawData = await request.bytes();
|
|
24
|
+
if (rawData.byteLength != contentLength)
|
|
46
25
|
error(400, 'Content-Length does not match actual data size');
|
|
47
|
-
const {
|
|
48
|
-
|
|
26
|
+
const { data, info } = await sharp(rawData)
|
|
27
|
+
.autoOrient()
|
|
28
|
+
.timeout({ seconds: 10 })
|
|
29
|
+
.resize({ width: max_length, height: max_length, fit: 'cover', withoutEnlargement: true })
|
|
30
|
+
.toBuffer({ resolveWithObject: true });
|
|
31
|
+
const { width, height, size } = info;
|
|
32
|
+
if (!width || !height)
|
|
49
33
|
error(400, 'Invalid image dimensions');
|
|
50
|
-
|
|
34
|
+
debug(`Prepared image upload: ${Math.round(size / 1000)}KB @ ${width}x${height}`);
|
|
35
|
+
if (max_size && size / 1000 > max_size)
|
|
36
|
+
error(413, `Image must be smaller than ${max_size} KB (got ${Math.round(size / 1000)} KB)`);
|
|
37
|
+
if (max_length && (width > max_length || height > max_length)) {
|
|
51
38
|
error(413, `Image must be smaller than ${max_length}x${max_length} pixels`);
|
|
52
|
-
|
|
39
|
+
}
|
|
40
|
+
if (!(data.buffer instanceof ArrayBuffer))
|
|
41
|
+
error(500, 'Unexpectedly got a shared buffer from sharp.');
|
|
42
|
+
return { data: data, type };
|
|
53
43
|
}
|
|
54
44
|
addRoute({
|
|
55
45
|
path: '/raw/pfp/:id',
|
|
@@ -101,7 +91,7 @@ addRoute({
|
|
|
101
91
|
});
|
|
102
92
|
},
|
|
103
93
|
async POST(request, { id: userId }) {
|
|
104
|
-
const { data, type } = await
|
|
94
|
+
const { data, type } = await prepareImageUpload(request, config.user_pfp, userId);
|
|
105
95
|
const { isInsert } = await db
|
|
106
96
|
.insertInto('profile_pictures')
|
|
107
97
|
.values({ userId, data, type })
|
package/dist/auth.js
CHANGED
|
@@ -181,7 +181,7 @@ export async function authSessionForItem(itemType, itemId, permissions, session,
|
|
|
181
181
|
* This will fetch the item, ACLs, users, and the authenticating session.
|
|
182
182
|
*/
|
|
183
183
|
export async function authRequestForItem(request, itemType, itemId, permissions, recursive = false) {
|
|
184
|
-
let session
|
|
184
|
+
let session;
|
|
185
185
|
try {
|
|
186
186
|
const token = getToken(request, false);
|
|
187
187
|
if (!token)
|
package/dist/config.d.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { type DeepRequired } from 'utilium';
|
|
2
2
|
import * as z from 'zod';
|
|
3
|
+
export declare const ImageUploadConfig: z.ZodObject<{
|
|
4
|
+
enabled: z.ZodBoolean;
|
|
5
|
+
max_size: z.ZodNumber;
|
|
6
|
+
max_length: z.ZodInt;
|
|
7
|
+
}, z.core.$strip>;
|
|
8
|
+
export interface ImageUploadConfig extends z.infer<typeof ImageUploadConfig> {
|
|
9
|
+
}
|
|
3
10
|
export declare const Config: z.ZodObject<{
|
|
4
11
|
admin_api: z.ZodOptional<z.ZodBoolean>;
|
|
5
12
|
allow_new_users: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -45,7 +52,7 @@ export declare const Config: z.ZodObject<{
|
|
|
45
52
|
user_pfp: z.ZodOptional<z.ZodObject<{
|
|
46
53
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
47
54
|
max_size: z.ZodOptional<z.ZodNumber>;
|
|
48
|
-
max_length: z.ZodOptional<z.
|
|
55
|
+
max_length: z.ZodOptional<z.ZodInt>;
|
|
49
56
|
}, z.core.$loose>>;
|
|
50
57
|
verifications: z.ZodOptional<z.ZodObject<{
|
|
51
58
|
timeout: z.ZodOptional<z.ZodNumber>;
|
|
@@ -127,7 +134,7 @@ export declare const ConfigFile: z.ZodObject<{
|
|
|
127
134
|
user_pfp: z.ZodOptional<z.ZodOptional<z.ZodObject<{
|
|
128
135
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
129
136
|
max_size: z.ZodOptional<z.ZodNumber>;
|
|
130
|
-
max_length: z.ZodOptional<z.
|
|
137
|
+
max_length: z.ZodOptional<z.ZodInt>;
|
|
131
138
|
}, z.core.$loose>>>;
|
|
132
139
|
verifications: z.ZodOptional<z.ZodOptional<z.ZodObject<{
|
|
133
140
|
timeout: z.ZodOptional<z.ZodNumber>;
|
package/dist/config.js
CHANGED
|
@@ -9,6 +9,14 @@ import * as z from 'zod';
|
|
|
9
9
|
import { dirs, logger, systemDir } from './io.js';
|
|
10
10
|
import { _duplicateStateWarnings, _unique } from './state.js';
|
|
11
11
|
const audit_severity_levels = ['emergency', 'alert', 'critical', 'error', 'warning', 'notice', 'info', 'debug'];
|
|
12
|
+
export const ImageUploadConfig = z.object({
|
|
13
|
+
/** Whether images can be uploaded */
|
|
14
|
+
enabled: z.boolean(),
|
|
15
|
+
/** Max image size in KB. Set to zero for no limit */
|
|
16
|
+
max_size: z.number().min(0),
|
|
17
|
+
/** Max dimensions on a side. Set to zero for no limit */
|
|
18
|
+
max_length: z.int().min(0),
|
|
19
|
+
});
|
|
12
20
|
export const Config = z
|
|
13
21
|
.looseObject({
|
|
14
22
|
/** Whether /api/admin is enabled */
|
|
@@ -63,16 +71,8 @@ export const Config = z
|
|
|
63
71
|
show_duplicate_state: z.boolean(),
|
|
64
72
|
/** Who can use the user discovery API. For example, setting to `admin` means regular users need to type a full email in the ACL dialog and won't be shown results */
|
|
65
73
|
user_discovery: z.literal(['disabled', 'admin', 'user', 'public']),
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
/** Whether user's can upload custom profile pictures */
|
|
69
|
-
enabled: z.boolean(),
|
|
70
|
-
/** Max PFP size in KB. Set to zero for no limit */
|
|
71
|
-
max_size: z.number().min(0),
|
|
72
|
-
/** Max dimensions on a side. Set to zero for no limit */
|
|
73
|
-
max_length: z.number().min(0),
|
|
74
|
-
})
|
|
75
|
-
.partial(),
|
|
74
|
+
/** Configuration for user profile pictures */
|
|
75
|
+
user_pfp: ImageUploadConfig.loose().partial(),
|
|
76
76
|
verifications: z
|
|
77
77
|
.looseObject({
|
|
78
78
|
/** In minutes */
|
|
@@ -143,7 +143,7 @@ export const defaultConfig = {
|
|
|
143
143
|
user_pfp: {
|
|
144
144
|
enabled: true,
|
|
145
145
|
max_size: 500,
|
|
146
|
-
max_length:
|
|
146
|
+
max_length: 750,
|
|
147
147
|
},
|
|
148
148
|
verifications: {
|
|
149
149
|
timeout: 60,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axium/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.1",
|
|
4
4
|
"author": "James Prevett <axium@jamespre.dev>",
|
|
5
5
|
"funding": {
|
|
6
6
|
"type": "individual",
|
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
"ioium": "^0.1.0",
|
|
63
63
|
"logzen": "^0.7.0",
|
|
64
64
|
"pg": "^8.14.1",
|
|
65
|
+
"sharp": "^0.34.5",
|
|
65
66
|
"svelte": "^5.36.0"
|
|
66
67
|
},
|
|
67
68
|
"devDependencies": {
|
|
@@ -69,7 +70,6 @@
|
|
|
69
70
|
"vite-plugin-mkcert": "^1.17.8"
|
|
70
71
|
},
|
|
71
72
|
"optionalDependencies": {
|
|
72
|
-
"image-size": "^2.0.2",
|
|
73
73
|
"ua-parser-js": "^2.0.9"
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { ClipboardCopy, FormDialog, Icon, Logout, Popover, SessionList, UserPFP, ZodForm } from '@axium/client/components';
|
|
4
4
|
import '@axium/client/styles/account';
|
|
5
5
|
import { createPasskey, deletePasskey, deleteUser, sendVerificationEmail, updatePasskey, updateUser } from '@axium/client/user';
|
|
6
|
-
import {
|
|
6
|
+
import { Preferences } from '@axium/core/preferences';
|
|
7
7
|
import type { PageProps } from './$types';
|
|
8
8
|
import { toast, toastStatus } from '@axium/client/toast';
|
|
9
9
|
import { contextMenu } from '@axium/client/attachments';
|
|
@@ -221,7 +221,6 @@
|
|
|
221
221
|
bind:rootValue={user.preferences}
|
|
222
222
|
idPrefix="preferences"
|
|
223
223
|
schema={Preferences}
|
|
224
|
-
labels={preferenceLabels}
|
|
225
224
|
updateValue={(preferences: Preferences) => fetchAPI('PATCH', 'users/:id', { preferences }, user.id)}
|
|
226
225
|
/>
|
|
227
226
|
</div>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { ClipboardCopy, FormDialog, Icon, SessionList, UserPFP, ZodForm, ZodInput } from '@axium/client/components';
|
|
4
4
|
import { toast } from '@axium/client/toast';
|
|
5
5
|
import '@axium/client/styles/account';
|
|
6
|
-
import {
|
|
6
|
+
import { Preferences, User } from '@axium/core';
|
|
7
7
|
import { formatDateRange } from '@axium/core/format';
|
|
8
8
|
|
|
9
9
|
const { data } = $props();
|
|
@@ -134,7 +134,6 @@
|
|
|
134
134
|
<ZodForm
|
|
135
135
|
bind:rootValue={user.preferences}
|
|
136
136
|
schema={Preferences}
|
|
137
|
-
labels={preferenceLabels}
|
|
138
137
|
updateValue={(preferences: Preferences) => fetchAPI('PATCH', 'users/:id', { preferences }, user.id)}
|
|
139
138
|
/>
|
|
140
139
|
</div>
|