@astro-utils/forms
Version:
Server component for Astro (call server functions from client side with validation and state management)
86 lines (85 loc) • 3.32 kB
JavaScript
import superjson from 'superjson';
import { parseFormData } from '../../form-tools/post.js';
import { getFormOptions } from '../../settings.js';
import snappy from 'snappy';
import { getSomeProps, omitProps } from '../props-utils.js';
import crypto from 'crypto';
const CRYPTO_ALGORITHM = 'aes-256-ctr';
export default class ViewStateManager {
get filedName() {
if (!this._FORM_OPTIONS.forms) {
throw new Error('Forms options not set');
}
return this._FORM_OPTIONS.forms.viewStateFormFiled + this._bindId;
}
get stateProp() {
return this._astro.props.state ?? true;
}
get omitProps() {
return this._astro.props.omitState;
}
get useState() {
return this.stateProp;
}
constructor(_bind, _elementsState, _astro, _bindId) {
this._bind = _bind;
this._elementsState = _elementsState;
this._astro = _astro;
this._bindId = _bindId;
this._FORM_OPTIONS = getFormOptions(_astro);
if (!this._FORM_OPTIONS.secret) {
throw new Error('Secret not set in form options');
}
this._initKey();
}
_initKey() {
const repeat = Math.ceil(32 / this._FORM_OPTIONS.secret.length);
this._VALID_KEY = this._FORM_OPTIONS.secret.repeat(repeat).slice(0, 32);
}
async _extractStateFromForm() {
const form = await parseFormData(this._astro.request);
return form.get(this.filedName)?.toString();
}
async _parseState() {
try {
const state = await this._extractStateFromForm();
if (state == null)
return;
const [iv, content] = state.split('.');
const decipher = crypto.createDecipheriv(CRYPTO_ALGORITHM, this._VALID_KEY, Buffer.from(iv, 'base64'));
const decrypted = Buffer.concat([decipher.update(Buffer.from(content, 'base64')), decipher.final()]);
const uncompress = await snappy.uncompress(decrypted);
return superjson.parse(uncompress.toString());
}
catch (error) {
this._FORM_OPTIONS.logs?.('warn', `ViewStateManager: ${error.message}`);
}
}
async loadState() {
if (!this.useState || this._astro.request.method !== 'POST') {
return false;
}
const state = await this._parseState();
if (!state)
return false;
if (state.bind && state.elements) {
Object.assign(this._bind, state.bind);
Object.assign(this._elementsState, state.elements);
}
return true;
}
async createViewState() {
const data = this.useState ? {
bind: this.omitProps ?
omitProps(this._bind.__getState(), this.omitProps) :
getSomeProps(this._bind.__getState(), this.stateProp),
elements: this._elementsState
} : {};
const stringify = superjson.stringify(data);
const compress = await snappy.compress(stringify, {});
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(CRYPTO_ALGORITHM, this._VALID_KEY, iv);
const encrypted = Buffer.concat([cipher.update(compress), cipher.final()]);
return `${iv.toString('base64')}.${encrypted.toString('base64')}`;
}
}