nitro-web 0.0.1

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.
Files changed (152) hide show
  1. package/.editorconfig +9 -0
  2. package/.eslintrc.json +86 -0
  3. package/_example/.env-example +16 -0
  4. package/_example/client/config.ts +5 -0
  5. package/_example/client/css/index.css +35 -0
  6. package/_example/client/fonts/Roboto-Bold.ttf +0 -0
  7. package/_example/client/fonts/Roboto-BoldItalic.ttf +0 -0
  8. package/_example/client/fonts/Roboto-Italic.ttf +0 -0
  9. package/_example/client/fonts/Roboto-Medium.ttf +0 -0
  10. package/_example/client/fonts/Roboto-MediumItalic.ttf +0 -0
  11. package/_example/client/fonts/Roboto-Regular.ttf +0 -0
  12. package/_example/client/fonts/inter-v13-latin-300.woff2 +0 -0
  13. package/_example/client/fonts/inter-v13-latin-500.woff2 +0 -0
  14. package/_example/client/fonts/inter-v13-latin-600.woff2 +0 -0
  15. package/_example/client/fonts/inter-v13-latin-700.woff2 +0 -0
  16. package/_example/client/fonts/inter-v13-latin-800.woff2 +0 -0
  17. package/_example/client/fonts/inter-v13-latin-900.woff2 +0 -0
  18. package/_example/client/fonts/inter-v13-latin-regular.woff2 +0 -0
  19. package/_example/client/imgs/android-chrome-512x512.png +0 -0
  20. package/_example/client/imgs/favicon.png +0 -0
  21. package/_example/client/imgs/icons/calendar.svg +3 -0
  22. package/_example/client/imgs/icons/email.svg +6 -0
  23. package/_example/client/imgs/icons/eye-open.svg +4 -0
  24. package/_example/client/imgs/icons/eye.svg +5 -0
  25. package/_example/client/imgs/icons/filter.svg +7 -0
  26. package/_example/client/imgs/icons/left-circle.svg +3 -0
  27. package/_example/client/imgs/icons/left.svg +3 -0
  28. package/_example/client/imgs/icons/line-options.svg +5 -0
  29. package/_example/client/imgs/icons/line.svg +3 -0
  30. package/_example/client/imgs/icons/person.svg +7 -0
  31. package/_example/client/imgs/icons/plus-circle.svg +5 -0
  32. package/_example/client/imgs/icons/plus.svg +5 -0
  33. package/_example/client/imgs/icons/right-circle.svg +3 -0
  34. package/_example/client/imgs/icons/right.svg +3 -0
  35. package/_example/client/imgs/icons/search.svg +3 -0
  36. package/_example/client/imgs/icons/shield.svg +6 -0
  37. package/_example/client/imgs/icons/tick-circle-solid.svg +8 -0
  38. package/_example/client/imgs/icons/tick-circle.svg +6 -0
  39. package/_example/client/imgs/icons/tick.svg +5 -0
  40. package/_example/client/imgs/icons/up2-small.svg +4 -0
  41. package/_example/client/imgs/icons/up2.svg +4 -0
  42. package/_example/client/imgs/icons/updown.svg +6 -0
  43. package/_example/client/imgs/icons/v-big-dark.svg +3 -0
  44. package/_example/client/imgs/icons/v-dark.svg +3 -0
  45. package/_example/client/imgs/icons/v.svg +3 -0
  46. package/_example/client/imgs/icons/v2-active.svg +6 -0
  47. package/_example/client/imgs/icons/x1.svg +4 -0
  48. package/_example/client/imgs/logo/logo-white.svg +20 -0
  49. package/_example/client/imgs/logo/logo.svg +20 -0
  50. package/_example/client/imgs/no-image.jpg +0 -0
  51. package/_example/client/imgs/user.jpg +0 -0
  52. package/_example/client/index.html +12 -0
  53. package/_example/client/index.ts +47 -0
  54. package/_example/components/auth.api.js +1 -0
  55. package/_example/components/index.tsx +225 -0
  56. package/_example/components/partials/layouts.tsx +5 -0
  57. package/_example/components/settings.api.js +1 -0
  58. package/_example/server/config.js +120 -0
  59. package/_example/server/email/welcome.html +27 -0
  60. package/_example/server/index.js +32 -0
  61. package/_example/tailwind.config.js +84 -0
  62. package/_example/tsconfig.json +32 -0
  63. package/_example/types.d.ts +7 -0
  64. package/_example/webpack.config.js +4 -0
  65. package/client/app.js +300 -0
  66. package/client/css/components.css +84 -0
  67. package/client/css/fonts.css +67 -0
  68. package/client/imgs/icons/calendar.svg +3 -0
  69. package/client/imgs/icons/email.svg +6 -0
  70. package/client/imgs/icons/eye-open.svg +4 -0
  71. package/client/imgs/icons/eye.svg +5 -0
  72. package/client/imgs/icons/filter.svg +7 -0
  73. package/client/imgs/icons/left-circle.svg +3 -0
  74. package/client/imgs/icons/left.svg +3 -0
  75. package/client/imgs/icons/line-options.svg +5 -0
  76. package/client/imgs/icons/line.svg +3 -0
  77. package/client/imgs/icons/person.svg +7 -0
  78. package/client/imgs/icons/plus-circle.svg +5 -0
  79. package/client/imgs/icons/plus.svg +5 -0
  80. package/client/imgs/icons/right-circle.svg +3 -0
  81. package/client/imgs/icons/right.svg +3 -0
  82. package/client/imgs/icons/search.svg +3 -0
  83. package/client/imgs/icons/shield.svg +6 -0
  84. package/client/imgs/icons/tick-circle-solid.svg +8 -0
  85. package/client/imgs/icons/tick-circle.svg +6 -0
  86. package/client/imgs/icons/tick.svg +5 -0
  87. package/client/imgs/icons/up2-small.svg +4 -0
  88. package/client/imgs/icons/up2.svg +4 -0
  89. package/client/imgs/icons/updown.svg +6 -0
  90. package/client/imgs/icons/v-big-dark.svg +3 -0
  91. package/client/imgs/icons/v-dark.svg +3 -0
  92. package/client/imgs/icons/v.svg +3 -0
  93. package/client/imgs/icons/v2-active.svg +6 -0
  94. package/client/imgs/icons/x1.svg +4 -0
  95. package/client.js +42 -0
  96. package/components/auth/auth.api.js +419 -0
  97. package/components/auth/reset.jsx +88 -0
  98. package/components/auth/signin.jsx +74 -0
  99. package/components/auth/signup.jsx +62 -0
  100. package/components/billing/stripe.api.js +267 -0
  101. package/components/partials/element/accordion.jsx +82 -0
  102. package/components/partials/element/avatar.jsx +28 -0
  103. package/components/partials/element/button.jsx +66 -0
  104. package/components/partials/element/dropdown.jsx +185 -0
  105. package/components/partials/element/initials.jsx +56 -0
  106. package/components/partials/element/message.jsx +124 -0
  107. package/components/partials/element/modal.jsx +229 -0
  108. package/components/partials/element/sidebar.jsx +166 -0
  109. package/components/partials/element/tooltip.jsx +146 -0
  110. package/components/partials/element/topbar.jsx +25 -0
  111. package/components/partials/form/checkbox.jsx +74 -0
  112. package/components/partials/form/drop-handler.jsx +62 -0
  113. package/components/partials/form/drop.jsx +125 -0
  114. package/components/partials/form/form-error.jsx +21 -0
  115. package/components/partials/form/input-color.jsx +77 -0
  116. package/components/partials/form/input-currency.jsx +133 -0
  117. package/components/partials/form/input-date.jsx +223 -0
  118. package/components/partials/form/input.jsx +131 -0
  119. package/components/partials/form/location.jsx +212 -0
  120. package/components/partials/form/select.jsx +369 -0
  121. package/components/partials/form/toggle.jsx +46 -0
  122. package/components/partials/is-first-render.js +15 -0
  123. package/components/partials/layout/layout1.jsx +32 -0
  124. package/components/partials/layout/layout2.jsx +47 -0
  125. package/components/partials/not-found.jsx +7 -0
  126. package/components/partials/styleguide.jsx +252 -0
  127. package/components/settings/settings-account.jsx +143 -0
  128. package/components/settings/settings-business.jsx +121 -0
  129. package/components/settings/settings-team--member.jsx +108 -0
  130. package/components/settings/settings-team.jsx +76 -0
  131. package/components/settings/settings.api.js +54 -0
  132. package/package.json +175 -0
  133. package/readme.md +43 -0
  134. package/server/email/index.js +192 -0
  135. package/server/email/partials/email.css +153 -0
  136. package/server/email/partials/layout1.swig +92 -0
  137. package/server/email/partials/line.swig +8 -0
  138. package/server/email/partials/vert-10.swig +8 -0
  139. package/server/email/partials/vert-15.swig +8 -0
  140. package/server/email/partials/vert-20.swig +8 -0
  141. package/server/email/partials/vert-25.swig +8 -0
  142. package/server/email/partials/vert-30.swig +8 -0
  143. package/server/email/partials/vert-35.swig +8 -0
  144. package/server/email/partials/vert-50.swig +8 -0
  145. package/server/email/reset-password.html +21 -0
  146. package/server/email/welcome.html +21 -0
  147. package/server/models/company.js +76 -0
  148. package/server/models/user.js +45 -0
  149. package/server/router.js +355 -0
  150. package/server.js +20 -0
  151. package/util.js +1145 -0
  152. package/webpack.config.js +302 -0
@@ -0,0 +1,355 @@
1
+ import fs from 'fs'
2
+ import path, { dirname } from 'path'
3
+ import http from 'http'
4
+ import { fileURLToPath } from 'url'
5
+ import compression from 'compression'
6
+ import expressFileUpload from 'express-fileupload'
7
+ import express from 'express'
8
+ import bodyParser from 'body-parser'
9
+ import sortRouteAddressesNodeps from 'sort-route-addresses-nodeps'
10
+
11
+ import { sendEmail } from './email/index.js'
12
+ import * as util from '../util.js'
13
+
14
+ const _dirname = dirname(fileURLToPath(import.meta.url)) + '/'
15
+
16
+ export async function setupRouter (config) {
17
+ const { componentsDir, distDir, emailTemplateDir, env, middleware, version } = config
18
+ const expressApp = express()
19
+ const server = http.createServer(expressApp)
20
+ const apiRoutes = {}
21
+ const controllers = {}
22
+ const allMiddleware = { ...defaultMiddleware, ...(middleware || {}) }
23
+
24
+ if (!componentsDir) {
25
+ throw new Error('setupRouter: `config.componentsDir` missing')
26
+ } else if (!env) {
27
+ throw new Error('setupRouter: `config.env` missing')
28
+ } else if (!emailTemplateDir) {
29
+ throw new Error('setupRouter: `config.emailTemplateDir` missing')
30
+ }
31
+
32
+ // Extend request/response with our custom error responses
33
+ setupErrorResponses(expressApp)
34
+
35
+ // Extend request with version
36
+ expressApp.use((req, res, next) => {
37
+ req.version = version
38
+ next()
39
+ })
40
+
41
+ // Load in API routes & controllers
42
+ let filepaths = getFiles(componentsDir, /\.api\.js$/)
43
+ // console.log(filepaths, componentsDir)
44
+ for (let filepath of filepaths) {
45
+ let file = (await import(filepath)).default
46
+ let name = filepath.replace(/^.*[\\\/]|\.api\.js$/g, '') // eslint-disable-line
47
+ controllers[name] = file
48
+
49
+ if (file.setup) file.setup(allMiddleware, config)
50
+ if (file.routes) {
51
+ util.each(file.routes, (_middleware, key) => {
52
+ apiRoutes[key] = {
53
+ middleware: util.toArray(_middleware),
54
+ filename: name,
55
+ path: key.replace(/^[a-z]+\s+/, ''),
56
+ verb: key.match(/^[a-z]+/)? key.match(/^[a-z]+/)[0] : 'get',
57
+ }
58
+ })
59
+ }
60
+ }
61
+
62
+ // Register the base middleware
63
+ for (let name of allMiddleware.order) {
64
+ if (name == 'loadAssets') {
65
+ expressApp.use('/favicon.png', express.static(distDir + 'favicon.png', { maxage: '7d' }))
66
+ expressApp.use('/assets', compression()) // gzip
67
+ expressApp.use('/assets', express.static(distDir + 'assets/', { maxage: '365d' }))
68
+ } else if (!allMiddleware[name]) {
69
+ continue
70
+ } else {
71
+ expressApp.use(allMiddleware[name])
72
+ }
73
+ }
74
+
75
+ // Register the API routes
76
+ expressApp.use('/api', compression()) // gzip
77
+ for (let key of sortRouteAddressesNodeps(Object.keys(apiRoutes))) {
78
+ let route = apiRoutes[key]
79
+ if (!route.verb.match(/get|post|put|delete/)) throw Error(`The API route verb '${route.verb}' is invalid`)
80
+ route.middleware = route.middleware
81
+ .map((o, i) => resolveMiddleware(controllers, allMiddleware, route, o, i+1==route.middleware.length))
82
+ .filter(o => o)
83
+ // express uses path-to-regex for URL interpretation
84
+ // console.log(route.verb, route.path, route.middleware)
85
+ expressApp[route.verb](route.path, [ ...route.middleware])
86
+ }
87
+
88
+ // If the index file is missing that means webpack-dev-server is being used which
89
+ // stores the bundled files in memory
90
+ let indexExists = !!fs.existsSync(distDir + 'index.html')
91
+
92
+ // Register email routes for development
93
+ // E.g. http://localhost:3001/email/welcome
94
+ if (env == 'development') {
95
+ expressApp.get('/email/partials/email.css', (req, res) => {
96
+ // first check if there is a custom email.css in the emailTemplateDir
97
+ // if not, return the default nitro email.css
98
+ console.log(_dirname)
99
+ if (fs.existsSync(emailTemplateDir + '/partials/email.css')) res.sendFile(emailTemplateDir + '/partials/email.css')
100
+ else res.sendFile(_dirname + 'email/partials/email.css')
101
+ })
102
+ expressApp.get('/email/:name', async (req, res) => {
103
+ try {
104
+ const html = await sendEmail({
105
+ config: config,
106
+ subject: 'Development email',
107
+ template: req.params.name,
108
+ test: true,
109
+ skipCssInline: true,
110
+ to: 'Ricky<test@gmail.com>',
111
+ })
112
+ res.send(html)
113
+ } catch (e) {
114
+ console.error(e)
115
+ res.error(e.message)
116
+ }
117
+ })
118
+ }
119
+
120
+ // Ping, pong, useful for webpack
121
+ expressApp.get('/ping', (req, res) => {
122
+ res.send('pong')
123
+ })
124
+
125
+ // Catch all remaining routes, i.e 404
126
+ expressApp.get('*', (req, res) => {
127
+ if (indexExists) res.sendFile(distDir + 'index.html')
128
+ else { res.status(404); res.notFound() }
129
+ })
130
+
131
+ return server
132
+ }
133
+
134
+ function setupErrorResponses (expressApp) {
135
+ /**
136
+ * Extend the express response object with custom formatted error responses
137
+ * @param {object} express - expressApp to extend
138
+ */
139
+
140
+ Object.assign(expressApp.response, {
141
+ error: function(a, b) { error.call(this, a, b, 400) },
142
+ unauthorized: function(a, b) { error.call(this, a, b, 401) },
143
+ forbidden: function(a, b) { error.call(this, a, b, 403) },
144
+ notFound: function(a, b) { error.call(this, a, b, 404) },
145
+ serverError: function(a, b) { error.call(this, a, b, 500) },
146
+ })
147
+
148
+ function duplicateKeyIndexAndValue(error) {
149
+ // https://github.com/Automattic/mongoose/issues/2129#issuecomment-280507821
150
+ // E.g. E11000 duplicate key error collection: anamata-production.person index:
151
+ // email_1 dup key: { email: "person1@gmail.com" }
152
+ let regex = /index: (?:.*\.)?\$?(?:([_a-z0-9]*)(?:_\d*)|([_a-z0-9]*))\s*dup key/i
153
+ let match = error.message.match(regex)
154
+ let index = match[1] || match[2]
155
+ let value = (error.message.match(/.*{.*?: (.*) }/i)[1]||'').replace(/"/g, '')
156
+ return [index, value]
157
+ }
158
+
159
+ function error(error, detail, status) {
160
+ /**
161
+ * Returns a formatted error
162
+ * @this = res
163
+ * @param {string | Error | Error[]} error - { code, title, detail }, or title
164
+ * @param {string} detail - used when error is a string
165
+ * @param {number} status
166
+ */
167
+
168
+ const res = this
169
+ const req = this.req
170
+ let errors = []
171
+ let _detail
172
+
173
+ status = parseInt(error && error.status || status) // parseInt until monastery removes or udpates status?
174
+ res.status(status)
175
+
176
+ // Default detail
177
+ if (status == 400) _detail = 'Bad request made.'
178
+ else if (status == 401) _detail = 'You are unauthorised to make this request.'
179
+ else if (status == 403) _detail = 'You are unauthorised to make this request.'
180
+ else if (status == 404) _detail = 'Sorry, nothing found here.'
181
+ else if (status == 500) _detail = 'Internal server error, please contact the admin.'
182
+
183
+ // Single error string
184
+ if (util.isString(error) || !error) {
185
+ if (detail) errors = [{ title: error, detail: detail }]
186
+ else errors = [{ detail: error || _detail }]
187
+
188
+ // Mongo error
189
+ } else if (util.isObject(error) && (error.name||'').match(/Mongo|BulkWriteError/)) {
190
+ if (error.code == 11000) {
191
+ let [name] = duplicateKeyIndexAndValue(error)
192
+ if (name == 'email') errors = [{ title: 'email', detail: 'That email is already linked to an account.' }]
193
+ else errors = [{ title: name, detail: `Cannot insert duplicate values for "${name}".` }]
194
+ } else {
195
+ errors = [{ title: 'mongo', detail: error.message }]
196
+ }
197
+
198
+ // Stripe error object
199
+ } else if (error instanceof Error && error.type?.match(/Stripe/)) {
200
+ errors = [{ title: 'error', detail: 'Stripe: ' + error.message }]
201
+
202
+ // Error object
203
+ } else if (error instanceof Error) {
204
+ if (error.response) console.log('Error:', error.response.data)
205
+ else console.error(error) // and stack
206
+ errors = [{ title: 'error', detail: error.message }]
207
+
208
+ // Mutliple errors passed
209
+ } else if (util.isObject(error) || util.isArray(error)) {
210
+ errors = error.errors? error.errors : util.toArray(error)
211
+ for (let o of errors) {
212
+ // detail can be an error object
213
+ if (o.detail instanceof Error) {
214
+ console.error(o.detail) // and stack
215
+ o.detail = o.detail.message
216
+ }
217
+ // Remove _ prefixed keys
218
+ for (let key in o) {
219
+ if (o.hasOwnProperty(key) && key.match(/^_/)) delete o[key]
220
+ }
221
+ }
222
+
223
+ // Invalid data
224
+ } else {
225
+ console.error('Invalid data parsed into response()')
226
+ }
227
+
228
+ // Add status to all errors.
229
+ for (let o of errors) {
230
+ if (!o.status) o.status = status
231
+ }
232
+
233
+ // Log error
234
+ let type = status == 500 ? 'error' : 'log'
235
+ console[type]('Sending ' + status + ' response: \n', errors)
236
+
237
+ // Display error json/html
238
+ if (req.json) res.json({ errors: errors })
239
+ else res.send('<p>' + errors.map(e => e.detail).join('<br>') + '</p>')
240
+ return new Error({ errors: errors })
241
+ }
242
+ }
243
+
244
+ function getFiles (dir, regexp) {
245
+ /**
246
+ * Recursivaly retreive all files
247
+ * @param {string} dir - directory to search (IS NOW FULL PATH)
248
+ * @return [path, ..]
249
+ */
250
+ let paths = []
251
+ // let dirname = path.dirname(fileURLToPath(import.meta.url))
252
+ // if (dir.match(/^\./)) dir = path.join(dirname, dir)
253
+
254
+ for (let filename of fs.readdirSync(dir)) {
255
+ let filepath = path.join(dir, '/', filename)
256
+ let stat = fs.statSync(filepath)
257
+ if (stat && stat.isDirectory()) {
258
+ paths = paths.concat(getFiles(filepath, regexp))
259
+ } else if (filepath.match(regexp)) {
260
+ paths.push(filepath)
261
+ }
262
+ }
263
+ return paths
264
+ }
265
+
266
+ function resolveMiddleware (controllers, middleware, route, item, last) {
267
+ /**
268
+ * Resolves a placeholder string into a function
269
+ * @param {object} route
270
+ * @param {fn|string} item
271
+ * @param {boolean} last - last item
272
+ * @return function(req, res){..}
273
+ */
274
+ if (util.isFunction(item)) {
275
+ return item
276
+
277
+ } else if (!util.isString(item)) {
278
+ console.error('Invalid middleware item:', item)
279
+ return
280
+
281
+ } else if (item.match(/\./) || last) { // e.g. user.read
282
+ let arr = item.split('.')
283
+ let controllerGroup = controllers[arr[1]? arr[0] : route.filename]
284
+ let controllerName = arr[1] || arr[0]
285
+ if (controllerGroup && controllerGroup[controllerName]) {
286
+ if (middleware.endpointSwitcher && controllerGroup[controllerName + 'Desktop']) {
287
+ return middleware.endpointSwitcher.bind(
288
+ null,
289
+ controllerGroup[controllerName].bind(controllerGroup),
290
+ controllerGroup[controllerName + 'Desktop'].bind(controllerGroup)
291
+ )
292
+ } else {
293
+ return controllerGroup[controllerName].bind(controllerGroup)
294
+ }
295
+ } else {
296
+ console.error(`The controller '${item}' defined in '${route.filename}.api' doesn't exist.`)
297
+ }
298
+
299
+ } else if (middleware[item]) {
300
+ return middleware[item]
301
+
302
+ } else {
303
+ console.error(`The middleware '${item}' defined in '${route.filename}.api' doesn't exist.`)
304
+ return
305
+ }
306
+ }
307
+
308
+ const defaultMiddleware = {
309
+ beforeAPIRoute: (req, res, next) => {
310
+ res.set('version', req.version)
311
+ next()
312
+ },
313
+
314
+ modifyRequest: (req, res, next) => {
315
+ // Handy boolean denoting that the request wants JSON returned
316
+ req.json = req.xhr || req.accepts(['html', 'json']) == 'json'
317
+ next()
318
+ },
319
+
320
+ // parse application/x-www-form-urlencoded (rawbody for stripe webhooks)
321
+ parseUrlEncoded: bodyParser.urlencoded({ extended: false, verify: (req, res, buf, encoding) => {
322
+ if (!buf || !buf.length) return
323
+ req.rawHeaders = req.headers
324
+ req.rawBody = buf.toString(encoding || 'utf8')
325
+ }}),
326
+
327
+ // parse application/json
328
+ parseJson: bodyParser.json({
329
+ verify: (req, res, buf, encoding) => {
330
+ if (!buf || !buf.length) return
331
+ req.rawHeaders = req.headers
332
+ req.rawBody = buf.toString(encoding || 'utf8')
333
+ },
334
+ limit: '40mb',
335
+ }),
336
+
337
+ // parse multipart/form-data
338
+ parseFile: (req, res, next) => {
339
+ req.files = {} // always ensure req.files is defined
340
+ // console.time('upload middleware')
341
+ expressFileUpload({
342
+ limits: { fileSize: 1000 * 1000 * 90, files: 10 }, // 90mb
343
+ })(req, res, () => { /*console.timeEnd('upload middleware'); */next() })
344
+ },
345
+
346
+ order: [
347
+ // Express middleware runtime order
348
+ 'loadAssets',
349
+ 'modifyRequest',
350
+ 'parseUrlEncoded',
351
+ 'parseJson',
352
+ 'parseFile',
353
+ 'beforeAPIRoute',
354
+ ],
355
+ }
package/server.js ADDED
@@ -0,0 +1,20 @@
1
+ // Export models
2
+ import userModel from './server/models/user.js'
3
+ import companyModel from './server/models/company.js'
4
+ async function setupDefaultModels(db) {
5
+ // Load default nitro models, if they don't exist already
6
+ if (!db.models.user) await db.model('user', userModel)
7
+ if (!db.models.company) await db.model('company', companyModel)
8
+ }
9
+ export { userModel, companyModel, setupDefaultModels }
10
+
11
+ // Export router
12
+ export { setupRouter } from './server/router.js'
13
+
14
+ // Export email util
15
+ export { sendEmail } from './server/email/index.js'
16
+
17
+ // Export api default controllers
18
+ export { default as auth } from './components/auth/auth.api.js'
19
+ export { default as settings } from './components/settings/settings.api.js'
20
+ export { default as stripe } from './components/billing/stripe.api.js'