2026-07-06 17:28:59 +00:00
|
|
|
import { Router } from 'express';
|
|
|
|
|
import fs from 'fs';
|
|
|
|
|
import path from 'path';
|
2026-07-17 12:00:16 +00:00
|
|
|
import { requireAuth } from '../../auth-connector/source/server/auth';
|
2026-07-31 18:43:08 +00:00
|
|
|
import { checkAccess } from '../projectAccess';
|
2026-07-14 11:31:11 +00:00
|
|
|
import { ROOT } from '../rootDir';
|
2026-07-06 17:28:59 +00:00
|
|
|
|
|
|
|
|
const router = Router();
|
|
|
|
|
router.use( requireAuth );
|
|
|
|
|
|
2026-07-14 11:31:11 +00:00
|
|
|
const LAYOUTS_DIR = path.join( ROOT, 'build', 'data', 'storage', 'layouts' );
|
2026-07-06 17:28:59 +00:00
|
|
|
|
2026-07-31 18:43:08 +00:00
|
|
|
function safeId( s: string ): string
|
2026-07-06 17:28:59 +00:00
|
|
|
{
|
2026-07-31 18:43:08 +00:00
|
|
|
return s.replace( /[^a-zA-Z0-9_-]/g, '_' );
|
2026-07-06 17:28:59 +00:00
|
|
|
}
|
|
|
|
|
|
2026-07-31 18:43:08 +00:00
|
|
|
function resolveLayoutPath( req: any ): string | null
|
2026-07-06 17:28:59 +00:00
|
|
|
{
|
|
|
|
|
const deviceId = req.query.deviceId as string;
|
2026-07-31 18:43:08 +00:00
|
|
|
if ( !deviceId ) return null;
|
|
|
|
|
|
|
|
|
|
const projectId = req.query.projectId as string | undefined;
|
|
|
|
|
const localRoot = req.query.localRoot as string | undefined;
|
|
|
|
|
const remoteProject = req.query.remoteProject as string | undefined;
|
|
|
|
|
|
|
|
|
|
if ( projectId )
|
|
|
|
|
{
|
|
|
|
|
const dir = path.join( ROOT, 'build', 'data', 'storage', projectId, 'root', '.roject' );
|
|
|
|
|
if ( !fs.existsSync( dir ) ) fs.mkdirSync( dir, { recursive: true } );
|
|
|
|
|
return path.join( dir, `layout-${ safeId( deviceId ) }.json` );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( localRoot )
|
|
|
|
|
{
|
|
|
|
|
const resolved = path.resolve( localRoot );
|
|
|
|
|
const dir = path.join( resolved, '.roject' );
|
|
|
|
|
if ( !fs.existsSync( dir ) ) fs.mkdirSync( dir, { recursive: true } );
|
|
|
|
|
return path.join( dir, `layout-${ safeId( deviceId ) }.json` );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( remoteProject )
|
|
|
|
|
{
|
|
|
|
|
// Remote project opened in Electron: store locally, keyed by device + remote project id
|
|
|
|
|
const userId = req.auth!.userId;
|
|
|
|
|
const dir = path.join( LAYOUTS_DIR, safeId( userId ) );
|
|
|
|
|
if ( !fs.existsSync( dir ) ) fs.mkdirSync( dir, { recursive: true } );
|
|
|
|
|
return path.join( dir, `${ safeId( deviceId ) }-${ safeId( remoteProject ) }.json` );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
router.get( '/', ( req, res ) =>
|
|
|
|
|
{
|
|
|
|
|
const projectId = req.query.projectId as string | undefined;
|
|
|
|
|
if ( projectId )
|
|
|
|
|
{
|
|
|
|
|
const denied = checkAccess( projectId, req.auth!, 'view' );
|
|
|
|
|
if ( denied ) { res.status( denied.status ).json( { error: denied.error } ); return; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fp = resolveLayoutPath( req );
|
|
|
|
|
if ( !fp ) { res.json( null ); return; }
|
2026-07-06 17:28:59 +00:00
|
|
|
if ( !fs.existsSync( fp ) ) { res.json( null ); return; }
|
|
|
|
|
try { res.json( JSON.parse( fs.readFileSync( fp, 'utf8' ) ) ); }
|
|
|
|
|
catch { res.json( null ); }
|
|
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
router.put( '/', ( req, res ) =>
|
|
|
|
|
{
|
2026-07-31 18:43:08 +00:00
|
|
|
const projectId = req.query.projectId as string | undefined;
|
|
|
|
|
if ( projectId )
|
|
|
|
|
{
|
|
|
|
|
const denied = checkAccess( projectId, req.auth!, 'edit' );
|
|
|
|
|
if ( denied ) { res.status( denied.status ).json( { error: denied.error } ); return; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fp = resolveLayoutPath( req );
|
|
|
|
|
if ( !fp ) { res.status( 400 ).json( { error: 'Missing project identifier or deviceId' } ); return; }
|
2026-07-06 17:28:59 +00:00
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
fs.writeFileSync( fp, JSON.stringify( req.body ), 'utf8' );
|
|
|
|
|
res.json( { ok: true } );
|
|
|
|
|
}
|
|
|
|
|
catch ( e ) { res.status( 500 ).json( { error: String( e ) } ); }
|
|
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
export default router;
|