From 0de10c74361b3ef7d6ec42f3ea8228c1af7308c0 Mon Sep 17 00:00:00 2001 From: williamd5 Date: Wed, 2 Nov 2022 09:37:32 +0200 Subject: [PATCH 1/2] include credentials if operation requires token --- browser/Cloudnode.js | 4 +++- browser/Cloudnode.min.js | 2 +- gen/templates/main.mustache | 4 +++- src/Cloudnode.js | 4 +++- src/Cloudnode.ts | 4 +++- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/browser/Cloudnode.js b/browser/Cloudnode.js index 66e0c47..22660e4 100644 --- a/browser/Cloudnode.js +++ b/browser/Cloudnode.js @@ -47,8 +47,10 @@ class Cloudnode { options.body = JSON.stringify(body); options.headers["Content-Type"] = "application/json"; } - if (this.#token) + if (this.#token && operation.token !== undefined) options.headers["Authorization"] = `Bearer ${this.#token}`; + if (operation.token !== undefined) + options.credentials = "include"; const response = await fetch(url.toString(), options); if (response.status === 204) return undefined; diff --git a/browser/Cloudnode.min.js b/browser/Cloudnode.min.js index a2f6dd9..e1342dc 100644 --- a/browser/Cloudnode.min.js +++ b/browser/Cloudnode.min.js @@ -1 +1 @@ -class Cloudnode{#e;#t;constructor(e,t="https://api.cloudnode.pro/v5/"){this.#e=e,this.#t=t}#r=async(e,t,r,s)=>{const o=new URL(e.path.replace(/^\/+/,""),this.#t);for(const[e,r]of Object.entries(t))o.pathname=o.pathname.replaceAll(`/:${e}`,`/${r}`);for(const[e,t]of Object.entries(r))o.searchParams.append(e,t);const i={method:e.method,headers:{}};s&&!["GET","HEAD"].includes(e.method)&&(i.body=JSON.stringify(s),i.headers["Content-Type"]="application/json"),this.#e&&(i.headers.Authorization=`Bearer ${this.#e}`);const a=await fetch(o.toString(),i);if(204!==a.status){if(a.headers.get("Content-Type")?.startsWith("application/json")){const e=await a.json();if(a.ok)return e;throw e}{const e=await a.text();if(a.ok)return e;throw e}}};newsletter={list:async(e=10,t=1)=>await this.#r({type:"operation",description:"List newsletters",method:"GET",path:"/newsletter",parameters:{query:{limit:{description:"The number of newsletters to return per page. No more than 50.",default:"10",type:"number",required:!1},page:{description:"The page number. No more than 2³² (4294967296).",default:"1",type:"number",required:!1}}},returns:[{status:200,type:"Newsletter[]"},{status:500,type:'Error & {code: "INTERNAL_SERVER_ERROR"}'},{status:429,type:'Error & {code: "RATE_LIMITED"}'}]},{},{limit:`${e}`,page:`${t}`},{}),get:async e=>await this.#r({type:"operation",description:"Get newsletter",method:"GET",path:"/newsletter/:id",parameters:{path:{id:{description:"A newsletter ID",type:"string",required:!0}}},returns:[{status:200,type:"Newsletter"},{status:404,type:'Error & {code: "RESOURCE_NOT_FOUND"}'},{status:500,type:'Error & {code: "INTERNAL_SERVER_ERROR"}'},{status:429,type:'Error & {code: "RATE_LIMITED"}'}]},{id:`${e}`},{},{}),subscribe:async(e,t,r)=>await this.#r({type:"operation",description:"Subscribe to newsletter",method:"POST",path:"/newsletter/:id/subscribe",parameters:{path:{id:{description:"A newsletter ID",type:"string",required:!0}},body:{email:{description:"Subscriber's email address",type:"string",required:!0},data:{description:"Additional data that this newsletter requires",type:"Record",required:!1}}},returns:[{status:201,type:"NewsletterSubscription"},{status:404,type:'Error & {code: "RESOURCE_NOT_FOUND"}'},{status:422,type:'Error & {code: "INVALID_DATA"}'},{status:409,type:'Error & {code: "CONFLICT"}'},{status:500,type:'Error & {code: "INTERNAL_SERVER_ERROR"}'},{status:429,type:'Error & {code: "RATE_LIMITED"}'}]},{id:`${e}`},{},{email:`${t}`,data:`${r}`})};newsletters={unsubscribe:async e=>await this.#r({type:"operation",description:"Revoke a subscription (unsubscribe)",method:"POST",path:"/newsletters/unsubscribe",parameters:{body:{subscription:{description:"The ID of the subscription to revoke",type:"string",required:!0}}},returns:[{status:204,type:"void"},{status:404,type:'Error & {code: "RESOURCE_NOT_FOUND"}'},{status:422,type:'Error & {code: "INVALID_DATA"}'},{status:500,type:'Error & {code: "INTERNAL_SERVER_ERROR"}'},{status:429,type:'Error & {code: "RATE_LIMITED"}'}]},{},{},{subscription:`${e}`}),listSubscriptions:async(e=10,t=1)=>await this.#r({type:"operation",description:"List subscriptions of the authenticated user",token:"newsletter.subscriptions.list.own",method:"GET",path:"/newsletters/subscriptions",parameters:{query:{limit:{description:"The number of subscriptions to return per page. No more than 50.",default:"10",type:"number",required:!1},page:{description:"The page number. No more than 2³² (4294967296).",default:"1",type:"number",required:!1}}},returns:[{status:200,type:"DatedNewsletterSubscription[]"},{status:401,type:'Error & {code: "UNAUTHORIZED"}'},{status:403,type:'Error & {code: "NO_PERMISSION"}'},{status:500,type:'Error & {code: "INTERNAL_SERVER_ERROR"}'},{status:429,type:'Error & {code: "RATE_LIMITED"}'}]},{},{limit:`${e}`,page:`${t}`},{})}} \ No newline at end of file +class Cloudnode{#e;#t;constructor(e,t="https://api.cloudnode.pro/v5/"){this.#e=e,this.#t=t}#r=async(e,t,r,s)=>{const o=new URL(e.path.replace(/^\/+/,""),this.#t);for(const[e,r]of Object.entries(t))o.pathname=o.pathname.replaceAll(`/:${e}`,`/${r}`);for(const[e,t]of Object.entries(r))o.searchParams.append(e,t);const i={method:e.method,headers:{}};s&&!["GET","HEAD"].includes(e.method)&&(i.body=JSON.stringify(s),i.headers["Content-Type"]="application/json"),this.#e&&void 0!==e.token&&(i.headers.Authorization=`Bearer ${this.#e}`),void 0!==e.token&&(i.credentials="include");const n=await fetch(o.toString(),i);if(204!==n.status){if(n.headers.get("Content-Type")?.startsWith("application/json")){const e=await n.json();if(n.ok)return e;throw e}{const e=await n.text();if(n.ok)return e;throw e}}};newsletter={list:async(e=10,t=1)=>await this.#r({type:"operation",description:"List newsletters",method:"GET",path:"/newsletter",parameters:{query:{limit:{description:"The number of newsletters to return per page. No more than 50.",default:"10",type:"number",required:!1},page:{description:"The page number. No more than 2³² (4294967296).",default:"1",type:"number",required:!1}}},returns:[{status:200,type:"Newsletter[]"},{status:500,type:'Error & {code: "INTERNAL_SERVER_ERROR"}'},{status:429,type:'Error & {code: "RATE_LIMITED"}'}]},{},{limit:`${e}`,page:`${t}`},{}),get:async e=>await this.#r({type:"operation",description:"Get newsletter",method:"GET",path:"/newsletter/:id",parameters:{path:{id:{description:"A newsletter ID",type:"string",required:!0}}},returns:[{status:200,type:"Newsletter"},{status:404,type:'Error & {code: "RESOURCE_NOT_FOUND"}'},{status:500,type:'Error & {code: "INTERNAL_SERVER_ERROR"}'},{status:429,type:'Error & {code: "RATE_LIMITED"}'}]},{id:`${e}`},{},{}),subscribe:async(e,t,r)=>await this.#r({type:"operation",description:"Subscribe to newsletter",method:"POST",path:"/newsletter/:id/subscribe",parameters:{path:{id:{description:"A newsletter ID",type:"string",required:!0}},body:{email:{description:"Subscriber's email address",type:"string",required:!0},data:{description:"Additional data that this newsletter requires",type:"Record",required:!1}}},returns:[{status:201,type:"NewsletterSubscription"},{status:404,type:'Error & {code: "RESOURCE_NOT_FOUND"}'},{status:422,type:'Error & {code: "INVALID_DATA"}'},{status:409,type:'Error & {code: "CONFLICT"}'},{status:500,type:'Error & {code: "INTERNAL_SERVER_ERROR"}'},{status:429,type:'Error & {code: "RATE_LIMITED"}'}]},{id:`${e}`},{},{email:`${t}`,data:`${r}`})};newsletters={unsubscribe:async e=>await this.#r({type:"operation",description:"Revoke a subscription (unsubscribe)",method:"POST",path:"/newsletters/unsubscribe",parameters:{body:{subscription:{description:"The ID of the subscription to revoke",type:"string",required:!0}}},returns:[{status:204,type:"void"},{status:404,type:'Error & {code: "RESOURCE_NOT_FOUND"}'},{status:422,type:'Error & {code: "INVALID_DATA"}'},{status:500,type:'Error & {code: "INTERNAL_SERVER_ERROR"}'},{status:429,type:'Error & {code: "RATE_LIMITED"}'}]},{},{},{subscription:`${e}`}),listSubscriptions:async(e=10,t=1)=>await this.#r({type:"operation",description:"List subscriptions of the authenticated user",token:"newsletter.subscriptions.list.own",method:"GET",path:"/newsletters/subscriptions",parameters:{query:{limit:{description:"The number of subscriptions to return per page. No more than 50.",default:"10",type:"number",required:!1},page:{description:"The page number. No more than 2³² (4294967296).",default:"1",type:"number",required:!1}}},returns:[{status:200,type:"DatedNewsletterSubscription[]"},{status:401,type:'Error & {code: "UNAUTHORIZED"}'},{status:403,type:'Error & {code: "NO_PERMISSION"}'},{status:500,type:'Error & {code: "INTERNAL_SERVER_ERROR"}'},{status:429,type:'Error & {code: "RATE_LIMITED"}'}]},{},{limit:`${e}`,page:`${t}`},{})}} \ No newline at end of file diff --git a/gen/templates/main.mustache b/gen/templates/main.mustache index 39dbbc0..c047128 100644 --- a/gen/templates/main.mustache +++ b/gen/templates/main.mustache @@ -52,8 +52,10 @@ class {{config.name}} { options.body = JSON.stringify(body); options.headers["Content-Type"] = "application/json"; } - if (this.#token) + if (this.#token && operation.token !== undefined) options.headers["Authorization"] = `Bearer ${this.#token}`; + if (operation.token !== undefined) + options.credentials = "include"; const response = await fetch(url.toString(), options); if (response.status === 204) return undefined as any; if (response.headers.get("Content-Type")?.startsWith("application/json")) { diff --git a/src/Cloudnode.js b/src/Cloudnode.js index 15b3421..61a244a 100644 --- a/src/Cloudnode.js +++ b/src/Cloudnode.js @@ -47,8 +47,10 @@ class Cloudnode { options.body = JSON.stringify(body); options.headers["Content-Type"] = "application/json"; } - if (this.#token) + if (this.#token && operation.token !== undefined) options.headers["Authorization"] = `Bearer ${this.#token}`; + if (operation.token !== undefined) + options.credentials = "include"; const response = await fetch(url.toString(), options); if (response.status === 204) return undefined; diff --git a/src/Cloudnode.ts b/src/Cloudnode.ts index 363f2b8..ce6b91c 100644 --- a/src/Cloudnode.ts +++ b/src/Cloudnode.ts @@ -52,8 +52,10 @@ class Cloudnode { options.body = JSON.stringify(body); options.headers["Content-Type"] = "application/json"; } - if (this.#token) + if (this.#token && operation.token !== undefined) options.headers["Authorization"] = `Bearer ${this.#token}`; + if (operation.token !== undefined) + options.credentials = "include"; const response = await fetch(url.toString(), options); if (response.status === 204) return undefined as any; if (response.headers.get("Content-Type")?.startsWith("application/json")) { From 2f264a9bdae7853f698013ef4b1b2d08f62830cb Mon Sep 17 00:00:00 2001 From: williamd5 Date: Wed, 2 Nov 2022 09:51:09 +0200 Subject: [PATCH 2/2] add docs for usage in browser --- README.md | 10 +++++++++- README.template.md | 10 +++++++++- gen/Config.ts | 5 +++++ gen/config.json | 3 ++- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 672e580..9191180 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,15 @@ const newsletter = await cloudnode.newsletter.get("newsletter_123asd"); ``` #### Browser -Coming soon! +Download the browser SDK from `browser/Cloudnode.js` or use our hosted version. +```html + +const cloudnode = new Cloudnode(); + +// get a newsletter +const newsletter = await cloudnode.newsletter.get("newsletter_123asd"); +``` +> **Warning**: You most likely don't want to set your private token in a public front-end website, as this will allow anyone who sees your front-end JavaScript code to use it for possibly malicious purposes. We advise you use a back-end server to proxy requests to our API, so you do not expose your token to the public. ### TypeScript ```ts diff --git a/README.template.md b/README.template.md index 7d7e514..54ba53b 100644 --- a/README.template.md +++ b/README.template.md @@ -33,7 +33,15 @@ const newsletter = await {{config.instanceName}}.newsletter.get("newsletter_123a ``` #### Browser -Coming soon! +Download the browser SDK from `browser/{{config.name}}.js` or use our hosted version. +```html + +const {{config.instanceName}} = new {{config.name}}(); + +// get a newsletter +const newsletter = await {{config.instanceName}}.newsletter.get("newsletter_123asd"); +``` +> **Warning**: You most likely don't want to set your private token in a public front-end website, as this will allow anyone who sees your front-end JavaScript code to use it for possibly malicious purposes. We advise you use a back-end server to proxy requests to our API, so you do not expose your token to the public. ### TypeScript ```ts diff --git a/gen/Config.ts b/gen/Config.ts index a3cea4c..53fcc2d 100644 --- a/gen/Config.ts +++ b/gen/Config.ts @@ -18,4 +18,9 @@ export interface Config { * Version of the API this client was generated for */ apiVersion: string; + + /** + * Link to hosted browser SDK + */ + browserSdkUrl: string; } diff --git a/gen/config.json b/gen/config.json index 2cf5dba..e8f6d86 100644 --- a/gen/config.json +++ b/gen/config.json @@ -2,5 +2,6 @@ "name": "Cloudnode", "instanceName": "cloudnode", "baseUrl": "https://api.cloudnode.pro/v5/", - "apiVersion": "5.6.2" + "apiVersion": "5.6.2", + "browserSdkUrl": "https://cloudnode.pro/assets/js/sdk.min.js" }