migrate-launch-fixes.mjs 3.0 KB

123456789101112131415161718
  1. #!/usr/bin/env node
  2. // --plan is offline. --apply is a publisher action and requires a backup plus a real operator session.
  3. import {readFile} from 'node:fs/promises';import {randomUUID} from 'node:crypto';
  4. const files=['account-security.sql','payroll-integrity.sql','payroll-cutoff.sql','account-deletion.sql'];
  5. const sql=(await Promise.all(files.map(name=>readFile(new URL('./sql/'+name,import.meta.url),'utf8')))).join('\n');
  6. const fields={legacyPasswordMigrationAllowed:{type:'Boolean'},credentialRevision:{type:'Number'},credentialExpectedRevision:{type:'Number'},credentialMigrationAttempt:{type:'Boolean'},passwordResetRequired:{type:'Boolean'}};
  7. if(!process.argv.includes('--apply')){console.log(JSON.stringify({apply:false,files,parseClass:'_User',fields:Object.keys(fields),requires:'Back up database, apply existing payroll and direct-sync prerequisites, then run --apply before publishing gateways.'},null,2));process.exit(0);}
  8. const master=process.env.XIAOSHU_MASTER_KEY,token=process.env.XIAOSHU_MIGRATION_OPERATOR_TOKEN;
  9. if(!master||!token)throw Error('Require XIAOSHU_MASTER_KEY and XIAOSHU_MIGRATION_OPERATOR_TOKEN in environment');
  10. const base=(process.env.XIAOSHU_PARSE_URL||'https://server.xiaoshu.pro/parse').replace(/\/$/,''),app=process.env.XIAOSHU_PARSE_APP_ID||'7pIbDBJmKx_main';
  11. async function api(path,method='GET',body,session=false){const r=await fetch(base+path,{method,headers:{'Content-Type':'application/json','X-Parse-Application-Id':app,...(session?{'X-Parse-Session-Token':token}:{'X-Parse-Master-Key':master})},body:body?JSON.stringify(body):undefined,signal:AbortSignal.timeout(120000)});const data=await r.json();if(!r.ok)throw Error('Migration API failed ('+r.status+')');return data;}
  12. const user=await api('/users/me','GET',undefined,true);if(user.isDisabled||user.isDeleted||!(user.roles||[]).includes('super-admin'))throw Error('An active super-admin session is required');
  13. const schema=await api('/schemas/_User');const missing=Object.fromEntries(Object.entries(fields).filter(([key])=>!schema.fields[key]));if(Object.keys(missing).length)await api('/schemas/_User','PUT',{fields:missing});
  14. const path='xiaoshu/system/launch-fixes-'+randomUUID();let id;
  15. try{const code=`async function handler(request,response){if(request.user?.id!==${JSON.stringify(user.objectId)})return response.status(403).json({success:false});try{await Psql.tx(t=>t.none(${JSON.stringify(sql)}));return response.json({success:true});}catch(e){return response.status(500).json({success:false,message:String(e.message)});}}`;
  16. id=(await api('/classes/Function','POST',{name:path,path,code,type:'standalone',enabled:true,params:[],paramList:[],respType:'json'})).objectId;
  17. const r=await fetch(base.replace(/\/parse$/,'/api/functions/')+path,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token,params:{}}),signal:AbortSignal.timeout(120000)});const result=await r.json();if(!r.ok||!result.success)throw Error(result.message||'Migration failed');console.log(JSON.stringify({applied:true,files}));
  18. }finally{if(id)await api('/classes/Function/'+id,'DELETE');}