mirror of
https://github.com/rikkaneko/paste.git
synced 2025-08-06 06:00:17 +01:00
Add edit-info API (v2)
Update frontend ui Signed-off-by: Joe Ma <rikkaneko23@gmail.com>
This commit is contained in:
parent
0301a9d9f8
commit
a2347f9f94
6 changed files with 115 additions and 21 deletions
|
@ -243,7 +243,7 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<td class="text-center col-3 text-nowrap">Access Times</td>
|
||||
<td class="text-center col-6"><span id="paste_info_access_n">-</span> / <span id="paste_info_access_max_access_n">99999</span></td>
|
||||
<td class="text-center col-6"><span id="paste_info_access_n">-</span><span id="paste_info_access_separator"> / </span><span id="paste_info_max_access_n"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-center col-3 text-nowrap">Created</td>
|
||||
|
|
|
@ -109,9 +109,17 @@ function build_paste_modal(paste_info, show_qrcode = true, saved = true, one_tim
|
|||
|
||||
Object.entries(paste_info).forEach(([key, val]) => {
|
||||
if (key.includes('link')) return;
|
||||
$(`#paste_info_${key}`).text(val ?? '-');
|
||||
$(`#paste_info_${key}`).text(val ?? '');
|
||||
});
|
||||
|
||||
if (paste_info.max_access_n !== undefined) {
|
||||
$('#paste_info_access_separator').show();
|
||||
$('#paste_info_max_access_n').show();
|
||||
} else {
|
||||
$('#paste_info_access_separator').hide();
|
||||
$('#paste_info_max_access_n').hide();
|
||||
}
|
||||
|
||||
let modal = new bootstrap.Modal(paste_modal.modal);
|
||||
if (!build_only) modal.show();
|
||||
paste_modal.forget_btn.prop('disabled', one_time_only);
|
||||
|
|
|
@ -21,7 +21,7 @@ import { sha256 } from 'js-sha256';
|
|||
import { Router, error, cors } from 'itty-router';
|
||||
import { ERequest, Env } from './types';
|
||||
import { serve_static } from './proxy';
|
||||
import { check_password_rules, get_paste_info, get_basic_auth, gen_id } from './utils';
|
||||
import { check_password_rules, get_paste_info, get_auth, gen_id } from './utils';
|
||||
import constants, { fetch_constant } from './constant';
|
||||
import { get_presign_url, router as large_upload } from './api/large_upload';
|
||||
import v2api from './v2/api';
|
||||
|
@ -316,7 +316,7 @@ router.get('/:uuid/:option?', async (request, env, ctx) => {
|
|||
// Check password if needed
|
||||
if (descriptor.password !== undefined) {
|
||||
if (headers.has('Authorization')) {
|
||||
let cert = get_basic_auth(headers);
|
||||
let cert = get_auth(headers, 'Basic');
|
||||
// Error occurred when parsing the header
|
||||
if (cert === null) {
|
||||
return new Response('Invalid Authorization header.', {
|
||||
|
|
29
src/utils.ts
29
src/utils.ts
|
@ -150,28 +150,31 @@ export function check_password_rules(password: string): boolean {
|
|||
return password.match('^[A-z0-9]{1,}$') !== null;
|
||||
}
|
||||
// Extract username and password from Basic Authorization header
|
||||
export function get_basic_auth(headers: Headers): [string, string] | null {
|
||||
export function get_auth(headers: Headers, required_scheme: string): string | [string, string] | null {
|
||||
if (headers.has('Authorization')) {
|
||||
const auth = headers.get('Authorization');
|
||||
const [scheme, encoded] = auth!.split(' ');
|
||||
// Validate authorization header format
|
||||
if (!encoded || scheme !== 'Basic') {
|
||||
if (!encoded || scheme !== required_scheme) {
|
||||
return null;
|
||||
}
|
||||
// Decode base64 to string (UTF-8)
|
||||
const buffer = Uint8Array.from(atob(encoded), (character) => character.charCodeAt(0));
|
||||
const decoded = new TextDecoder().decode(buffer).normalize();
|
||||
const index = decoded.indexOf(':');
|
||||
if (scheme == 'Basic') {
|
||||
// Decode base64 to string (UTF-8)
|
||||
const buffer = Uint8Array.from(atob(encoded), (character) => character.charCodeAt(0));
|
||||
const decoded = new TextDecoder().decode(buffer).normalize();
|
||||
const index = decoded.indexOf(':');
|
||||
|
||||
// Check if user & password are split by the first colon and MUST NOT contain control characters.
|
||||
if (index === -1 || decoded.match('[\\0-\x1F\x7F]')) {
|
||||
return null;
|
||||
// Check if user & password are split by the first colon and MUST NOT contain control characters.
|
||||
if (index === -1 || decoded.match('[\\0-\x1F\x7F]')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [decoded.slice(0, index), decoded.slice(index + 1)];
|
||||
} else if (scheme == 'Bearer') {
|
||||
return encoded;
|
||||
}
|
||||
|
||||
return [decoded.slice(0, index), decoded.slice(index + 1)];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function to_human_readable_size(bytes: number): string {
|
||||
|
|
|
@ -9,8 +9,10 @@ import {
|
|||
PasteIndexEntry,
|
||||
PasteCreateUploadResponse,
|
||||
PasteType,
|
||||
PasteInfoUpdateParams,
|
||||
PasteInfoUpdateParamsValidator,
|
||||
} from './schema';
|
||||
import { gen_id } from '../utils';
|
||||
import { gen_id, get_auth } from '../utils';
|
||||
import { AwsClient } from 'aws4fetch';
|
||||
import { sha256 } from 'js-sha256';
|
||||
import { xml2js } from 'xml-js';
|
||||
|
@ -37,6 +39,71 @@ router.get('/info/:uuid', async (req, env, ctx) => {
|
|||
return PasteAPIRepsonse.info(descriptor);
|
||||
});
|
||||
|
||||
/* POST /info/:uuid
|
||||
* Header: Authorization: Basic <password>
|
||||
*
|
||||
* Response:
|
||||
* <empty> | <Error>
|
||||
*/
|
||||
router.post('/info/:uuid', async (req, env, ctx) => {
|
||||
const { uuid } = req.params;
|
||||
if (uuid.length !== constants.UUID_LENGTH) {
|
||||
new PasteAPIRepsonse();
|
||||
return PasteAPIRepsonse.build(442, 'Invalid UUID.');
|
||||
}
|
||||
const val = await env.PASTE_INDEX.get(uuid);
|
||||
if (val === null) {
|
||||
return PasteAPIRepsonse.build(404, 'Paste not found.');
|
||||
}
|
||||
const descriptor: PasteIndexEntry = JSON.parse(val);
|
||||
|
||||
let params: PasteInfoUpdateParams | undefined;
|
||||
try {
|
||||
const _params: PasteInfoUpdateParams = await req.json();
|
||||
if (!PasteInfoUpdateParamsValidator.test(_params)) {
|
||||
return PasteAPIRepsonse.build(400, 'Invalid request fields.');
|
||||
}
|
||||
params = _params;
|
||||
} catch (e) {
|
||||
return PasteAPIRepsonse.build(400, 'Invalid request.');
|
||||
}
|
||||
|
||||
// Check password if needed
|
||||
if (descriptor.password !== undefined) {
|
||||
const { headers } = req;
|
||||
let cert = get_auth(headers, 'Bearer');
|
||||
// Error occurred when parsing the header
|
||||
if (cert === null) {
|
||||
return PasteAPIRepsonse.build(
|
||||
403,
|
||||
'This paste is password-protected. You must provide the current access credentials to update its metadata.'
|
||||
);
|
||||
}
|
||||
// Check password and username should be empty
|
||||
if (cert.length != 0 || descriptor.password !== sha256(cert).slice(0, 16)) {
|
||||
return PasteAPIRepsonse.build(403, 'Invalid access credentials.');
|
||||
}
|
||||
}
|
||||
|
||||
if (descriptor.upload_track?.pending_upload) {
|
||||
return PasteAPIRepsonse.build(400, 'This paste is not yet finalized.');
|
||||
}
|
||||
|
||||
// Change paste info logic
|
||||
// Explict assign the fields
|
||||
const updated_descriptor = {
|
||||
...descriptor,
|
||||
password: params.password ? sha256(params.password).slice(0, 16) : undefined,
|
||||
max_access_n: params.max_access_n,
|
||||
title: params.title,
|
||||
mime_type: params.mime_type,
|
||||
expired_at: params.expired_at ? params.expired_at : descriptor.expired_at,
|
||||
};
|
||||
|
||||
ctx.waitUntil(env.PASTE_INDEX.put(uuid, JSON.stringify(updated_descriptor), { expirationTtl: 2419200 }));
|
||||
return PasteAPIRepsonse.info(updated_descriptor);
|
||||
});
|
||||
|
||||
/* POST /create
|
||||
* Body: <PasteCreateLargeParams>
|
||||
*
|
||||
|
|
|
@ -70,8 +70,8 @@ export interface PasteCreateParams {
|
|||
expired_at?: number;
|
||||
}
|
||||
|
||||
export const PasteCreateParamsValidator = new Validator({
|
||||
password: new Rule({ type: 'string', optional: true, notEmpty: true }),
|
||||
const param_rules = {
|
||||
password: new Rule({ type: 'password', optional: true, notEmpty: true, maxLength: 40 }),
|
||||
max_access_n: new Rule({ type: 'int', optional: true, min: 1 }),
|
||||
title: new Rule({ type: 'string', optional: true, notEmpty: true }),
|
||||
mime_type: new Rule({ type: 'string', optional: true, notEmpty: true }),
|
||||
|
@ -83,7 +83,9 @@ export const PasteCreateParamsValidator = new Validator({
|
|||
min: Date.now(),
|
||||
max: new Date(Date.now() + 2419200 * 1000).getTime(), // max. 28 days
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export const PasteCreateParamsValidator = new Validator(param_rules);
|
||||
|
||||
export interface PasteCreateUploadResponse {
|
||||
uuid: string;
|
||||
|
@ -95,6 +97,19 @@ export interface PasteCreateUploadResponse {
|
|||
};
|
||||
}
|
||||
|
||||
export interface PasteInfoUpdateParams {
|
||||
password?: string;
|
||||
max_access_n?: number;
|
||||
title?: string;
|
||||
mime_type?: string;
|
||||
expired_at?: number;
|
||||
}
|
||||
|
||||
// Omit non-editable fields
|
||||
const { file_size, file_hash, ...editabe_fiels } = param_rules;
|
||||
|
||||
export const PasteInfoUpdateParamsValidator = new Validator(editabe_fiels);
|
||||
|
||||
export class PasteAPIRepsonse {
|
||||
static build(
|
||||
status_code: number = 200,
|
||||
|
@ -129,6 +144,7 @@ export class PasteAPIRepsonse {
|
|||
static info(descriptor: PasteIndexEntry) {
|
||||
const paste_info: PasteInfo = {
|
||||
uuid: descriptor.uuid,
|
||||
title: descriptor.title,
|
||||
paste_type: descriptor.paste_type,
|
||||
file_size: descriptor.file_size,
|
||||
mime_type: descriptor.mime_type,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue