@beforesemicolon/site-builder 0.34.0 → 0.36.0
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/cjs/build-templates.js +5 -5
- package/dist/esm/build-templates.js +5 -5
- package/netlify/functions/auth-middleware.js +183 -0
- package/netlify/functions/auth0-config.js +77 -0
- package/netlify/functions/build.js +214 -0
- package/netlify/functions/copy-file-local.js +88 -0
- package/netlify/functions/github.js +771 -0
- package/netlify/functions/validate-user.js +109 -0
- package/netlify/functions/widgets.js +403 -0
- package/netlify.toml +67 -0
- package/package.json +2 -2
- package/scaffolds/.env.example +18 -0
- package/scaffolds/_redirects +12 -0
- package/scaffolds/admin/app.js +244 -0
- package/scaffolds/admin/auth-manager.js +275 -0
- package/scaffolds/admin/controls/code.control.js +22 -0
- package/scaffolds/admin/controls/controls.js +249 -0
- package/scaffolds/admin/controls/file.control.js +829 -0
- package/scaffolds/admin/controls/html.control.js +43 -0
- package/scaffolds/admin/controls/markdown.control.js +31 -0
- package/scaffolds/admin/data.js +543 -0
- package/scaffolds/admin/flashbar.js +104 -0
- package/scaffolds/admin/index.html +44 -0
- package/scaffolds/admin/modal.js +123 -0
- package/scaffolds/admin/preview-widget.js +102 -0
- package/scaffolds/admin/preview.js +197 -0
- package/scaffolds/admin/repository-manager.js +329 -0
- package/scaffolds/admin/styles.css +1526 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var ye=Object.create;var j=Object.defineProperty;var he=Object.getOwnPropertyDescriptor;var Se=Object.getOwnPropertyNames;var je=Object.getPrototypeOf,xe=Object.prototype.hasOwnProperty;var x=(s,n)=>j(s,"name",{value:n,configurable:!0});var be=(s,n)=>{for(var a in n)j(s,a,{get:n[a],enumerable:!0})},te=(s,n,a,g)=>{if(n&&typeof n=="object"||typeof n=="function")for(let i of Se(n))!xe.call(s,i)&&i!==a&&j(s,i,{get:()=>n[i],enumerable:!(g=he(n,i))||g.enumerable});return s};var W=(s,n,a)=>(a=s!=null?ye(je(s)):{},te(n||!s||!s.__esModule?j(a,"default",{value:s,enumerable:!0}):a,s)),Ce=s=>te(j({},"__esModule",{value:!0}),s);var We={};be(We,{buildTemplates:()=>Pe});module.exports=Ce(We);var e=W(require("path"),1),l=W(require("fs"),1),ne=require("module"),oe=W(require("esbuild"),1),ie=require("./utils/merge-objects.js"),ae=require("./parse-template.js"),re=W(require("clean-css"),1),le=require("./types.js"),ce=require("html-minifier"),me=require("./utils/flatten-object.js");const Te=new re.default,{writeFile:b,readFile:C,readdir:T,mkdir:y,cp:p,rm:se}=l.default.promises;async function Oe(s,n){try{const a=e.default.resolve(s,"templates"),g=e.default.resolve(s,"widgets"),i=await T(a),c=[];let O=null;for(const f of i)if(f.endsWith(".json")){const w=e.default.join(a,f),P=await C(w,"utf-8"),u=JSON.parse(P);f==="base.json"?O=u:c.push({id:u.id,name:u.name||u.id,url:`/${u.id}.html`,template:f,widgetsData:u.widgetsData||{}})}const $=l.default.existsSync(g)?await T(g):[],h=[];for(const f of $)if(f.endsWith(".js")){const w=e.default.basename(f,".js");h.push({id:w,name:w.charAt(0).toUpperCase()+w.slice(1).replace(/-/g," "),file:f})}const E={meta:O,pages:c,widgets:h},F=e.default.join(n,"config.json");await b(F,JSON.stringify(E,null,2)),console.log("Generated config.json with",c.length,"pages and",h.length,"widgets")}catch(a){throw console.error("Error generating admin config:",a),a}}x(Oe,"generateAdminConfig");const Pe=x(async({publicDir:s,srcDir:n,prod:a=!0})=>{console.log("Building templates:",{publicDir:s,srcDir:n,prod:a});const g=(0,ne.createRequire)(e.default.resolve(process.cwd(),"package.json"));let i="";try{i=e.default.dirname(g.resolve("@beforesemicolon/site-builder/package.json"))}catch{i=""}const c=typeof __dirname=="string"?__dirname:e.default.dirname(process.argv[1]||process.cwd()),O=[i?e.default.resolve(i,"scaffolds","admin"):"",e.default.resolve(c,"..","scaffolds","admin"),e.default.resolve(c,"..","..","scaffolds","admin")],$=[i?e.default.resolve(i,"netlify"):"",e.default.resolve(c,"..","netlify"),e.default.resolve(c,"..","..","netlify")],h=[i?e.default.resolve(i,"netlify.toml"):"",e.default.resolve(c,"..","netlify.toml"),e.default.resolve(c,"..","..","netlify.toml")],E=[i?e.default.resolve(i,"scaffolds","_redirects"):"",e.default.resolve(c,"..","scaffolds","_redirects"),e.default.resolve(c,"..","..","scaffolds","_redirects")],F=[i?e.default.resolve(i,"scaffolds",".env.example"):"",e.default.resolve(c,"..","scaffolds",".env.example"),e.default.resolve(c,"..","..","scaffolds",".env.example")],f=O.find(t=>t&&l.default.existsSync(t)),w=$.find(t=>t&&l.default.existsSync(t)),P=h.find(t=>t&&l.default.existsSync(t)),u=E.find(t=>t&&l.default.existsSync(t)),A=F.find(t=>t&&l.default.existsSync(t));if(!f)throw new Error("Could not find bundled admin scaffold directory");if(!w)throw new Error("Could not find bundled netlify scaffold directory");if(!P)throw new Error("Could not find bundled netlify.toml file");if(!u)throw new Error("Could not find bundled _redirects file");if(!A)throw new Error("Could not find bundled .env.example file");const fe=e.default.join(s,"_redirects"),U=e.default.resolve(s,"scripts"),B=e.default.resolve(s,"stylesheets"),q=e.default.resolve(n,"assets"),L=e.default.resolve(n,"widgets"),G=e.default.resolve(s,"admin","widgets"),de=e.default.resolve(s,"assets"),I=e.default.resolve(n,"data"),pe=e.default.resolve(s,"data"),K=e.default.resolve(n,"assets","robots.txt"),ue=e.default.join(s,"robots.txt"),X=e.default.resolve(n,"assets","sitemap.xml");l.default.existsSync(s)&&await se(s,{recursive:!0,force:!0}),await y(s,{recursive:!0}),await y(U,{recursive:!0}),await y(B,{recursive:!0}),await p(u,fe),l.default.existsSync(q)&&await p(q,de,{recursive:!0}),l.default.existsSync(I)&&await p(I,pe,{recursive:!0});const z=e.default.resolve(n,".netlify");await se(z,{recursive:!0,force:!0}),await p(w,z,{recursive:!0}),await p(P,e.default.resolve(n,"netlify.toml")),await p(A,e.default.resolve(n,".env.example"));const _=e.default.resolve(s,"admin");await y(_,{recursive:!0}),await p(f,_,{recursive:!0}),await Oe(n,_),a||(await y(G,{recursive:!0}),await p(L,G,{recursive:!0}));const we=await T(e.default.resolve(n,"templates")),N=e.default.resolve(n,"locales"),H={};if(l.default.existsSync(N)){const t=await T(N);for(const o of t)if(o.endsWith(".json")){const d=JSON.parse(await C(e.default.join(N,o),"utf8")),m=e.default.basename(o,".json");H[m]=(0,me.flattenObject)(d)}}const M=await Promise.all(we.filter(t=>t.endsWith(".json")).map(async t=>{const o=e.default.basename(t);return{dir:e.default.dirname(t),name:o.replace(".json",""),content:JSON.parse(await C(e.default.resolve(n,"templates",t),"utf8"))}})),Q=M.reduce((t,o)=>({...t,[o.name]:o}),{}),J=M.filter(({content:t})=>t.type===le.TemplateType.Page&&t.excluded!==!0),R=e.default.resolve(n,"components");let V={};if(l.default.existsSync(R)){const t=await T(R),o=await Promise.all(t.filter(d=>d.endsWith(".json")).map(async d=>{const m=JSON.parse(await C(e.default.join(R,d),"utf8"));return[m.id,m]}));V=Object.fromEntries(o)}const Y=x(async t=>{let o=typeof t=="string"?t:"";t&&typeof t=="object"&&"src"in t&&(o=String(t.src)),!o.startsWith("http")&&o.endsWith(".js")&&await oe.default.build({entryPoints:[e.default.resolve(n,"scripts",o)],minify:!0,outfile:e.default.join(U,o)})},"handleScript"),Z=x(async t=>{let o=typeof t=="string"?t:"";if(t&&typeof t=="object"&&"href"in t&&(o=String(t.href)),!o.startsWith("http")&&o.endsWith(".css")){const d=await C(e.default.resolve(n,"stylesheets",o),"utf8");await b(e.default.join(B,o),Te.minify(d).styles,"utf-8")}},"handleStylesheet"),D=[];for(const{name:t,dir:o,content:d}of J){let m=d;if(m.extends){const r=Q[m.extends];m=(0,ie.mergeObjects)(r.content,m)}const ee=d.domain||"",k=d.route||"";if(k){const r=ee?`${ee.replace(/\/$/,"")}/${k.replace(/^\//,"")}`:`/${k.replace(/^\//,"")}`;D.push(r)}await y(e.default.join(s,o),{recursive:!0});for(let r=0;r<(m.scripts?.length??0);r++)await Y(m.scripts[r]);for(let r=0;r<(m.stylesheets?.length??0);r++)await Z(m.stylesheets[r]);const ge=await(0,ae.parseTemplate)(m,{prod:a,components:V,locales:H,fetchTemplate:r=>Q[r].content,fetchWidget:async r=>{const S=(await import(e.default.resolve(L,`${r}.js`))).default;for(let v=0;v<(S.scripts?.length??0);v++)await Y(S.scripts[v]);for(let v=0;v<(S.stylesheets?.length??0);v++)await Z(S.stylesheets[v]);return S}}),ve=(0,ce.minify)(ge,{collapseWhitespace:!0,removeComments:!0,minifyCSS:!0,minifyJS:!0});await b(e.default.join(s,o,`${t}.html`),ve)}if(l.default.existsSync(K))await p(K,e.default.join(s,"robots.txt"));else{const o=`User-agent: *
|
|
2
2
|
Disallow:
|
|
3
|
-
Sitemap: ${
|
|
4
|
-
`;await
|
|
3
|
+
Sitemap: ${J[0]?.content.domain?`${J[0].content.domain.replace(/\/$/,"")}/sitemap.xml`:"/sitemap.xml"}
|
|
4
|
+
`;await b(ue,o,"utf-8")}if(l.default.existsSync(X))await p(X,e.default.join(s,"sitemap.xml"));else{const t=`<?xml version="1.0" encoding="UTF-8"?>
|
|
5
5
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
6
|
-
`+
|
|
6
|
+
`+D.map(o=>` <url>
|
|
7
7
|
<loc>${o}</loc>
|
|
8
8
|
</url>`).join(`
|
|
9
9
|
`)+`
|
|
10
10
|
</urlset>
|
|
11
|
-
`;await
|
|
11
|
+
`;await b(e.default.join(s,"sitemap.xml"),t,"utf-8")}},"buildTemplates");0&&(module.exports={buildTemplates});
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
var
|
|
1
|
+
var re=Object.defineProperty;var S=(n,o)=>re(n,"name",{value:o,configurable:!0});import e from"path";import c from"fs";import{createRequire as le}from"module";import ce from"esbuild";import{mergeObjects as me}from"./utils/merge-objects.js";import{parseTemplate as fe}from"./parse-template.js";import de from"clean-css";import{TemplateType as pe}from"./types.js";import{minify as ue}from"html-minifier";import{flattenObject as we}from"./utils/flatten-object.js";const ge=new de,{writeFile:j,readFile:x,readdir:b,mkdir:v,cp:d,rm:D}=c.promises;async function ve(n,o){try{const p=e.resolve(n,"templates"),C=e.resolve(n,"widgets"),a=await b(p),r=[];let T=null;for(const m of a)if(m.endsWith(".json")){const w=e.join(p,m),O=await x(w,"utf-8"),u=JSON.parse(O);m==="base.json"?T=u:r.push({id:u.id,name:u.name||u.id,url:`/${u.id}.html`,template:m,widgetsData:u.widgetsData||{}})}const P=c.existsSync(C)?await b(C):[],y=[];for(const m of P)if(m.endsWith(".js")){const w=e.basename(m,".js");y.push({id:w,name:w.charAt(0).toUpperCase()+w.slice(1).replace(/-/g," "),file:m})}const W={meta:T,pages:r,widgets:y},$=e.join(o,"config.json");await j($,JSON.stringify(W,null,2)),console.log("Generated config.json with",r.length,"pages and",y.length,"widgets")}catch(p){throw console.error("Error generating admin config:",p),p}}S(ve,"generateAdminConfig");const Ne=S(async({publicDir:n,srcDir:o,prod:p=!0})=>{console.log("Building templates:",{publicDir:n,srcDir:o,prod:p});const C=le(e.resolve(process.cwd(),"package.json"));let a="";try{a=e.dirname(C.resolve("@beforesemicolon/site-builder/package.json"))}catch{a=""}const r=typeof __dirname=="string"?__dirname:e.dirname(process.argv[1]||process.cwd()),T=[a?e.resolve(a,"scaffolds","admin"):"",e.resolve(r,"..","scaffolds","admin"),e.resolve(r,"..","..","scaffolds","admin")],P=[a?e.resolve(a,"netlify"):"",e.resolve(r,"..","netlify"),e.resolve(r,"..","..","netlify")],y=[a?e.resolve(a,"netlify.toml"):"",e.resolve(r,"..","netlify.toml"),e.resolve(r,"..","..","netlify.toml")],W=[a?e.resolve(a,"scaffolds","_redirects"):"",e.resolve(r,"..","scaffolds","_redirects"),e.resolve(r,"..","..","scaffolds","_redirects")],$=[a?e.resolve(a,"scaffolds",".env.example"):"",e.resolve(r,"..","scaffolds",".env.example"),e.resolve(r,"..","..","scaffolds",".env.example")],m=T.find(t=>t&&c.existsSync(t)),w=P.find(t=>t&&c.existsSync(t)),O=y.find(t=>t&&c.existsSync(t)),u=W.find(t=>t&&c.existsSync(t)),R=$.find(t=>t&&c.existsSync(t));if(!m)throw new Error("Could not find bundled admin scaffold directory");if(!w)throw new Error("Could not find bundled netlify scaffold directory");if(!O)throw new Error("Could not find bundled netlify.toml file");if(!u)throw new Error("Could not find bundled _redirects file");if(!R)throw new Error("Could not find bundled .env.example file");const ee=e.join(n,"_redirects"),k=e.resolve(n,"scripts"),A=e.resolve(n,"stylesheets"),U=e.resolve(o,"assets"),B=e.resolve(o,"widgets"),q=e.resolve(n,"admin","widgets"),te=e.resolve(n,"assets"),L=e.resolve(o,"data"),se=e.resolve(n,"data"),G=e.resolve(o,"assets","robots.txt"),ne=e.join(n,"robots.txt"),I=e.resolve(o,"assets","sitemap.xml");c.existsSync(n)&&await D(n,{recursive:!0,force:!0}),await v(n,{recursive:!0}),await v(k,{recursive:!0}),await v(A,{recursive:!0}),await d(u,ee),c.existsSync(U)&&await d(U,te,{recursive:!0}),c.existsSync(L)&&await d(L,se,{recursive:!0});const K=e.resolve(o,".netlify");await D(K,{recursive:!0,force:!0}),await d(w,K,{recursive:!0}),await d(O,e.resolve(o,"netlify.toml")),await d(R,e.resolve(o,".env.example"));const E=e.resolve(n,"admin");await v(E,{recursive:!0}),await d(m,E,{recursive:!0}),await ve(o,E),p||(await v(q,{recursive:!0}),await d(B,q,{recursive:!0}));const oe=await b(e.resolve(o,"templates")),F=e.resolve(o,"locales"),X={};if(c.existsSync(F)){const t=await b(F);for(const s of t)if(s.endsWith(".json")){const f=JSON.parse(await x(e.join(F,s),"utf8")),l=e.basename(s,".json");X[l]=we(f)}}const z=await Promise.all(oe.filter(t=>t.endsWith(".json")).map(async t=>{const s=e.basename(t);return{dir:e.dirname(t),name:s.replace(".json",""),content:JSON.parse(await x(e.resolve(o,"templates",t),"utf8"))}})),H=z.reduce((t,s)=>({...t,[s.name]:s}),{}),_=z.filter(({content:t})=>t.type===pe.Page&&t.excluded!==!0),N=e.resolve(o,"components");let M={};if(c.existsSync(N)){const t=await b(N),s=await Promise.all(t.filter(f=>f.endsWith(".json")).map(async f=>{const l=JSON.parse(await x(e.join(N,f),"utf8"));return[l.id,l]}));M=Object.fromEntries(s)}const Q=S(async t=>{let s=typeof t=="string"?t:"";t&&typeof t=="object"&&"src"in t&&(s=String(t.src)),!s.startsWith("http")&&s.endsWith(".js")&&await ce.build({entryPoints:[e.resolve(o,"scripts",s)],minify:!0,outfile:e.join(k,s)})},"handleScript"),V=S(async t=>{let s=typeof t=="string"?t:"";if(t&&typeof t=="object"&&"href"in t&&(s=String(t.href)),!s.startsWith("http")&&s.endsWith(".css")){const f=await x(e.resolve(o,"stylesheets",s),"utf8");await j(e.join(A,s),ge.minify(f).styles,"utf-8")}},"handleStylesheet"),Y=[];for(const{name:t,dir:s,content:f}of _){let l=f;if(l.extends){const i=H[l.extends];l=me(i.content,l)}const Z=f.domain||"",J=f.route||"";if(J){const i=Z?`${Z.replace(/\/$/,"")}/${J.replace(/^\//,"")}`:`/${J.replace(/^\//,"")}`;Y.push(i)}await v(e.join(n,s),{recursive:!0});for(let i=0;i<(l.scripts?.length??0);i++)await Q(l.scripts[i]);for(let i=0;i<(l.stylesheets?.length??0);i++)await V(l.stylesheets[i]);const ie=await fe(l,{prod:p,components:M,locales:X,fetchTemplate:i=>H[i].content,fetchWidget:async i=>{const h=(await import(e.resolve(B,`${i}.js`))).default;for(let g=0;g<(h.scripts?.length??0);g++)await Q(h.scripts[g]);for(let g=0;g<(h.stylesheets?.length??0);g++)await V(h.stylesheets[g]);return h}}),ae=ue(ie,{collapseWhitespace:!0,removeComments:!0,minifyCSS:!0,minifyJS:!0});await j(e.join(n,s,`${t}.html`),ae)}if(c.existsSync(G))await d(G,e.join(n,"robots.txt"));else{const s=`User-agent: *
|
|
2
2
|
Disallow:
|
|
3
|
-
Sitemap: ${
|
|
4
|
-
`;await
|
|
3
|
+
Sitemap: ${_[0]?.content.domain?`${_[0].content.domain.replace(/\/$/,"")}/sitemap.xml`:"/sitemap.xml"}
|
|
4
|
+
`;await j(ne,s,"utf-8")}if(c.existsSync(I))await d(I,e.join(n,"sitemap.xml"));else{const t=`<?xml version="1.0" encoding="UTF-8"?>
|
|
5
5
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
6
|
-
`+
|
|
6
|
+
`+Y.map(s=>` <url>
|
|
7
7
|
<loc>${s}</loc>
|
|
8
8
|
</url>`).join(`
|
|
9
9
|
`)+`
|
|
10
10
|
</urlset>
|
|
11
|
-
`;await
|
|
11
|
+
`;await j(e.join(n,"sitemap.xml"),t,"utf-8")}},"buildTemplates");export{Ne as buildTemplates};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import jwt from 'jsonwebtoken'
|
|
2
|
+
import jwksClient from 'jwks-rsa'
|
|
3
|
+
|
|
4
|
+
// In-memory cache for admin emails
|
|
5
|
+
let adminEmailCache = null
|
|
6
|
+
let cacheTimestamp = null
|
|
7
|
+
const CACHE_TTL = 5 * 60 * 1000 // 5 minutes
|
|
8
|
+
|
|
9
|
+
// JWKS client for Auth0 token verification
|
|
10
|
+
const client = jwksClient({
|
|
11
|
+
jwksUri: `https://${process.env.AUTH0_DOMAIN}/.well-known/jwks.json`,
|
|
12
|
+
cache: true,
|
|
13
|
+
cacheMaxAge: 86400000, // 24 hours
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get signing key for JWT verification
|
|
18
|
+
* @param {Object} header - JWT header
|
|
19
|
+
* @param {Function} callback - Callback function
|
|
20
|
+
*/
|
|
21
|
+
function getKey(header, callback) {
|
|
22
|
+
client.getSigningKey(header.kid, (err, key) => {
|
|
23
|
+
if (err) {
|
|
24
|
+
callback(err)
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
const signingKey = key.publicKey || key.rsaPublicKey
|
|
28
|
+
callback(null, signingKey)
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Verify Auth0 JWT token
|
|
34
|
+
* @param {string} token - JWT token from Authorization header
|
|
35
|
+
* @returns {Promise<{email: string, sub: string}>}
|
|
36
|
+
*/
|
|
37
|
+
export async function verifyAuth0Token(token) {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
jwt.verify(
|
|
40
|
+
token,
|
|
41
|
+
getKey,
|
|
42
|
+
{
|
|
43
|
+
audience: process.env.AUTH0_AUDIENCE,
|
|
44
|
+
issuer: `https://${process.env.AUTH0_DOMAIN}/`,
|
|
45
|
+
algorithms: ['RS256'],
|
|
46
|
+
},
|
|
47
|
+
(err, decoded) => {
|
|
48
|
+
if (err) {
|
|
49
|
+
reject(err)
|
|
50
|
+
} else {
|
|
51
|
+
resolve(decoded)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Fetch user list from Netlify Identity
|
|
60
|
+
* Uses Netlify API endpoint
|
|
61
|
+
* @returns {Promise<Array<{email: string, id: string}>>}
|
|
62
|
+
*/
|
|
63
|
+
function getAdminEmailList() {
|
|
64
|
+
const raw = process.env.ADMIN_EMAILS
|
|
65
|
+
if (!raw) {
|
|
66
|
+
return []
|
|
67
|
+
}
|
|
68
|
+
return raw
|
|
69
|
+
.split(',')
|
|
70
|
+
.map((entry) => entry.trim().toLowerCase())
|
|
71
|
+
.filter(Boolean)
|
|
72
|
+
.map((email) => ({ email, id: email }))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get cached or fresh admin email list
|
|
77
|
+
* Cache expires after 5 minutes
|
|
78
|
+
* @returns {Promise<Array<{email: string, id: string}>>}
|
|
79
|
+
*/
|
|
80
|
+
export async function getUserList() {
|
|
81
|
+
const now = Date.now()
|
|
82
|
+
|
|
83
|
+
// Return cached data if valid
|
|
84
|
+
if (adminEmailCache && cacheTimestamp && now - cacheTimestamp < CACHE_TTL) {
|
|
85
|
+
return adminEmailCache
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const list = getAdminEmailList()
|
|
89
|
+
cacheTimestamp = now
|
|
90
|
+
adminEmailCache = list
|
|
91
|
+
|
|
92
|
+
if (!list.length) {
|
|
93
|
+
console.error('ADMIN_EMAILS is not configured or empty')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return list
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Check if email is in authorized list
|
|
101
|
+
* @param {string} email - Email to check
|
|
102
|
+
* @returns {Promise<boolean>}
|
|
103
|
+
*/
|
|
104
|
+
export async function isEmailAuthorized(email) {
|
|
105
|
+
if (!email) {
|
|
106
|
+
return false
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const normalizedEmail = email.toLowerCase()
|
|
110
|
+
const userList = await getUserList()
|
|
111
|
+
|
|
112
|
+
return userList.some((user) => user.email === normalizedEmail)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Middleware wrapper for protecting functions
|
|
117
|
+
* @param {Function} handler - Original function handler
|
|
118
|
+
* @returns {Function} - Wrapped handler with authorization
|
|
119
|
+
*/
|
|
120
|
+
export function withAuth(handler) {
|
|
121
|
+
return async (event, context) => {
|
|
122
|
+
// Skip auth in local development
|
|
123
|
+
const isLocalDev = process.env.CONTEXT === 'dev' || !process.env.CONTEXT
|
|
124
|
+
|
|
125
|
+
if (isLocalDev) {
|
|
126
|
+
return handler(event, context)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Extract token from Authorization header
|
|
130
|
+
const authHeader =
|
|
131
|
+
event.headers.authorization || event.headers.Authorization
|
|
132
|
+
|
|
133
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
134
|
+
return {
|
|
135
|
+
statusCode: 401,
|
|
136
|
+
body: JSON.stringify({
|
|
137
|
+
error: 'Unauthorized - No token provided',
|
|
138
|
+
}),
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const token = authHeader.substring(7) // Remove 'Bearer '
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
// Verify Auth0 token
|
|
146
|
+
const decoded = await verifyAuth0Token(token)
|
|
147
|
+
const email = decoded.email
|
|
148
|
+
|
|
149
|
+
if (!email) {
|
|
150
|
+
return {
|
|
151
|
+
statusCode: 401,
|
|
152
|
+
body: JSON.stringify({
|
|
153
|
+
error: 'Unauthorized - No email in token',
|
|
154
|
+
}),
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Check if email is authorized
|
|
159
|
+
const authorized = await isEmailAuthorized(email)
|
|
160
|
+
|
|
161
|
+
if (!authorized) {
|
|
162
|
+
return {
|
|
163
|
+
statusCode: 403,
|
|
164
|
+
body: JSON.stringify({
|
|
165
|
+
error: 'Forbidden - Email not authorized',
|
|
166
|
+
}),
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Add user info to context for logging
|
|
171
|
+
context.authorizedUser = { email }
|
|
172
|
+
|
|
173
|
+
// Call original handler
|
|
174
|
+
return handler(event, context)
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.error('Authorization error:', error)
|
|
177
|
+
return {
|
|
178
|
+
statusCode: 401,
|
|
179
|
+
body: JSON.stringify({ error: 'Unauthorized - Invalid token' }),
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Netlify function to return Auth0 config without exposing values in repo.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const ALLOWED_ORIGINS = [
|
|
6
|
+
'https://eliteflooringandtile.com',
|
|
7
|
+
'https://www.eliteflooringandtile.com',
|
|
8
|
+
process.env.URL,
|
|
9
|
+
process.env.DEPLOY_PRIME_URL,
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
if (process.env.CONTEXT === 'dev' || !process.env.CONTEXT) {
|
|
13
|
+
ALLOWED_ORIGINS.push('http://localhost:3000')
|
|
14
|
+
ALLOWED_ORIGINS.push('http://localhost:8888')
|
|
15
|
+
ALLOWED_ORIGINS.push('http://127.0.0.1:3000')
|
|
16
|
+
ALLOWED_ORIGINS.push('http://127.0.0.1:8888')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function checkOrigin(origin) {
|
|
20
|
+
if (!origin) return false
|
|
21
|
+
return ALLOWED_ORIGINS.some(
|
|
22
|
+
(allowed) => allowed && origin.startsWith(allowed)
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function handler(event) {
|
|
27
|
+
const origin = event.headers.origin || event.headers.Origin
|
|
28
|
+
|
|
29
|
+
const corsHeaders = {
|
|
30
|
+
'Access-Control-Allow-Origin': checkOrigin(origin) ? origin : 'null',
|
|
31
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
32
|
+
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
|
33
|
+
'Access-Control-Allow-Credentials': 'true',
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (event.httpMethod === 'OPTIONS') {
|
|
37
|
+
return { statusCode: 200, headers: corsHeaders, body: '' }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (event.httpMethod !== 'GET') {
|
|
41
|
+
return {
|
|
42
|
+
statusCode: 405,
|
|
43
|
+
headers: corsHeaders,
|
|
44
|
+
body: JSON.stringify({ error: 'Method not allowed' }),
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!checkOrigin(origin)) {
|
|
49
|
+
return {
|
|
50
|
+
statusCode: 403,
|
|
51
|
+
headers: corsHeaders,
|
|
52
|
+
body: JSON.stringify({ error: 'Forbidden - Invalid origin' }),
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const domain = process.env.AUTH0_DOMAIN
|
|
57
|
+
const clientId = process.env.AUTH0_CLIENT_ID
|
|
58
|
+
const audience = process.env.AUTH0_AUDIENCE
|
|
59
|
+
|
|
60
|
+
if (!domain || !clientId) {
|
|
61
|
+
return {
|
|
62
|
+
statusCode: 500,
|
|
63
|
+
headers: corsHeaders,
|
|
64
|
+
body: JSON.stringify({ error: 'Auth0 configuration not set' }),
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
statusCode: 200,
|
|
70
|
+
headers: corsHeaders,
|
|
71
|
+
body: JSON.stringify({
|
|
72
|
+
domain,
|
|
73
|
+
clientId,
|
|
74
|
+
audience,
|
|
75
|
+
}),
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Netlify Function to check build status after auto-build from commit
|
|
3
|
+
* Requires NETLIFY_AUTH_TOKEN and NETLIFY_SITE_ID environment variables
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const NETLIFY_AUTH_TOKEN = process.env.NETLIFY_AUTH_TOKEN
|
|
7
|
+
const NETLIFY_SITE_ID = process.env.NETLIFY_SITE_ID
|
|
8
|
+
const API_BASE = 'https://api.netlify.com/api/v1'
|
|
9
|
+
|
|
10
|
+
// Allowed origins (your domain)
|
|
11
|
+
const ALLOWED_ORIGINS = [
|
|
12
|
+
'https://eliteflooringandtile.com',
|
|
13
|
+
'https://www.eliteflooringandtile.com',
|
|
14
|
+
process.env.URL, // Netlify deploy URL
|
|
15
|
+
process.env.DEPLOY_PRIME_URL, // Netlify branch deploy URL
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
// Add localhost for development
|
|
19
|
+
if (process.env.CONTEXT === 'dev' || !process.env.CONTEXT) {
|
|
20
|
+
ALLOWED_ORIGINS.push('http://localhost:3000')
|
|
21
|
+
ALLOWED_ORIGINS.push('http://localhost:8888')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Check CORS and origin
|
|
26
|
+
*/
|
|
27
|
+
function checkOrigin(origin) {
|
|
28
|
+
if (!origin) return false
|
|
29
|
+
return ALLOWED_ORIGINS.some(
|
|
30
|
+
(allowed) => allowed && origin.startsWith(allowed)
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get latest build for the site
|
|
36
|
+
* Used after commit to find the auto-triggered build
|
|
37
|
+
*/
|
|
38
|
+
async function getLatestBuild() {
|
|
39
|
+
const url = `${API_BASE}/sites/${NETLIFY_SITE_ID}/builds?per_page=1`
|
|
40
|
+
|
|
41
|
+
const response = await fetch(url, {
|
|
42
|
+
headers: {
|
|
43
|
+
Authorization: `Bearer ${NETLIFY_AUTH_TOKEN}`,
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
const error = await response.text()
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Failed to get latest build: ${response.status} ${error}`
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const builds = await response.json()
|
|
55
|
+
|
|
56
|
+
if (builds.length === 0) {
|
|
57
|
+
throw new Error('No builds found for this site')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const latestBuild = builds[0]
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
buildId: latestBuild.id,
|
|
64
|
+
status: latestBuild.state,
|
|
65
|
+
createdAt: latestBuild.created_at,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Check build status
|
|
71
|
+
*/
|
|
72
|
+
async function checkBuildStatus(buildId) {
|
|
73
|
+
const url = `${API_BASE}/builds/${buildId}`
|
|
74
|
+
|
|
75
|
+
const response = await fetch(url, {
|
|
76
|
+
headers: {
|
|
77
|
+
Authorization: `Bearer ${NETLIFY_AUTH_TOKEN}`,
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
const error = await response.text()
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Failed to check build status: ${response.status} ${error}`
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const data = await response.json()
|
|
89
|
+
|
|
90
|
+
const done = data.done || data.state === 'ready' || data.state === 'error'
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
status: data.state,
|
|
94
|
+
done,
|
|
95
|
+
error: data.error,
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Main handler
|
|
101
|
+
*/
|
|
102
|
+
export async function handler(event, context) {
|
|
103
|
+
const origin = event.headers.origin || event.headers.Origin
|
|
104
|
+
|
|
105
|
+
// CORS headers
|
|
106
|
+
const corsHeaders = {
|
|
107
|
+
'Access-Control-Allow-Origin': checkOrigin(origin) ? origin : 'null',
|
|
108
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
109
|
+
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
|
110
|
+
'Access-Control-Allow-Credentials': 'true',
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Handle preflight
|
|
114
|
+
if (event.httpMethod === 'OPTIONS') {
|
|
115
|
+
return {
|
|
116
|
+
statusCode: 200,
|
|
117
|
+
headers: corsHeaders,
|
|
118
|
+
body: '',
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Only allow POST requests
|
|
123
|
+
if (event.httpMethod !== 'POST') {
|
|
124
|
+
return {
|
|
125
|
+
statusCode: 405,
|
|
126
|
+
headers: corsHeaders,
|
|
127
|
+
body: JSON.stringify({ error: 'Method not allowed' }),
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check origin
|
|
132
|
+
if (!checkOrigin(origin)) {
|
|
133
|
+
console.error('Blocked request from unauthorized origin:', origin)
|
|
134
|
+
return {
|
|
135
|
+
statusCode: 403,
|
|
136
|
+
headers: corsHeaders,
|
|
137
|
+
body: JSON.stringify({ error: 'Forbidden - Invalid origin' }),
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const isLocalDev = process.env.CONTEXT === 'dev' || !process.env.CONTEXT
|
|
142
|
+
if (isLocalDev) {
|
|
143
|
+
console.log('Local development mode - skipping authentication checks')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Check if Netlify credentials are configured
|
|
147
|
+
if (!NETLIFY_AUTH_TOKEN || !NETLIFY_SITE_ID) {
|
|
148
|
+
console.error('Netlify credentials not configured')
|
|
149
|
+
return {
|
|
150
|
+
statusCode: 500,
|
|
151
|
+
headers: corsHeaders,
|
|
152
|
+
body: JSON.stringify({
|
|
153
|
+
error: 'Netlify API credentials not configured on server',
|
|
154
|
+
}),
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const { action, data } = JSON.parse(event.body)
|
|
160
|
+
|
|
161
|
+
// Log the action for audit purposes
|
|
162
|
+
const userEmail = isLocalDev
|
|
163
|
+
? 'local-dev'
|
|
164
|
+
: context.clientContext?.user?.email || 'unknown'
|
|
165
|
+
console.log(`Action: ${action}, User: ${userEmail}, Origin: ${origin}`)
|
|
166
|
+
|
|
167
|
+
switch (action) {
|
|
168
|
+
case 'getLatestBuild': {
|
|
169
|
+
const result = await getLatestBuild()
|
|
170
|
+
console.log(
|
|
171
|
+
`Latest build fetched by ${userEmail}, Build ID: ${result.buildId}`
|
|
172
|
+
)
|
|
173
|
+
return {
|
|
174
|
+
statusCode: 200,
|
|
175
|
+
headers: corsHeaders,
|
|
176
|
+
body: JSON.stringify(result),
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
case 'checkBuildStatus': {
|
|
181
|
+
const { buildId } = data
|
|
182
|
+
|
|
183
|
+
if (!buildId) {
|
|
184
|
+
return {
|
|
185
|
+
statusCode: 400,
|
|
186
|
+
headers: corsHeaders,
|
|
187
|
+
body: JSON.stringify({ error: 'Build ID required' }),
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const result = await checkBuildStatus(buildId)
|
|
192
|
+
return {
|
|
193
|
+
statusCode: 200,
|
|
194
|
+
headers: corsHeaders,
|
|
195
|
+
body: JSON.stringify(result),
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
default:
|
|
200
|
+
return {
|
|
201
|
+
statusCode: 400,
|
|
202
|
+
headers: corsHeaders,
|
|
203
|
+
body: JSON.stringify({ error: 'Invalid action' }),
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.error('Netlify build function error:', error)
|
|
208
|
+
return {
|
|
209
|
+
statusCode: 500,
|
|
210
|
+
headers: corsHeaders,
|
|
211
|
+
body: JSON.stringify({ error: error.message }),
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|