Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/mamena2020/nodemi

Boilerplate for Nodejs. Handling routing, Rule validation, ORM, Media library, File request handling, Jwt auth, Role & Permissions, Resources, Locale, Mail, Seeder & more..
https://github.com/mamena2020/nodemi

backend backend-api backend-nodejs backend-template boilerplate boilerplate-backend boilerplate-node boilerplate-nodejs express-js jwt-auth node-orm node-resources node-validator nodejs role-and-permission sequelize-orm validatorjs

Last synced: about 1 month ago
JSON representation

Boilerplate for Nodejs. Handling routing, Rule validation, ORM, Media library, File request handling, Jwt auth, Role & Permissions, Resources, Locale, Mail, Seeder & more..

Awesome Lists containing this project

README

        

# Nodemi

Boilerplate for nodejs. base on express js.

- ### Features

- Model - ORM

Create model via cli and make relation between.

- Media library

Binding media to any Model, so any model can own the media, and will able to save media, get media, and destroy media.
Media can be stored to `Local storage` or `Firebase Storage`.

- File request handling

Not worry about handling uploaded file, just upload from client side, and you can access file in request, ex: `req.body.avatar`.

- Request validation

Determine if request passes the rule.
You can create `custom rule` via cli.

- Role and Permissions

Binding to any model, any model can have a role and permissions, set role, checking access.

- Resources

Create custom resource from resources.

- Auth - JWT/Basic Auth

Create token, re generate token, and set middleware authorization for certain routes.

- Locale

Enabled or disabled locale or just enabled on certain routes.

- Mail

Create mail via cli, and send mail with html, file, or just text.

- Firebase Cloud Messaging

Sending push notification from server to client device.

- Seeder

Running seeder via cli.

- ### Live demo

| Action | Method | Auth | Body | EndPoint |
| --------------- | ------ | ------ | ---------------- | ----------------------------------------------------- |
| Login | POST | | email | https://nodemi.onrender.com/api/login |
| | | | password | |
| | | | | |
| Register | POST | | email | https://nodemi.onrender.com/api/register |
| | | | name | |
| | | | password | |
| | | | confirm_password | |
| | | | | |
| Token | GET | | | https://nodemi.onrender.com/api/token |
| | | | | |
| | | | | |
| Logout | DELETE | | | https://nodemi.onrender.com/api/logout |
| | | | | |
| Get User | GET | Bearer | | https://nodemi.onrender.com/api/user |
| | | token | | |
| | | | | |
| Get Users | GET | Bearer | | https://nodemi.onrender.com/api/users |
| | | token | | |
| | | | | |
| Forgot Password | POST | | email | https://nodemi.onrender.com/api/forgot-password |
| | | | | |
| Reset Password | PUT | | new_password | https://nodemi.onrender.com/api/reset-password/:token |

# Getting Started

- Clone this repo `https` or `SSH`

Clone and move to directory project and run `npm install`

```
git clone [email protected]:Mamena2020/nodemi.git

```

- ### Create database

Create database `mysql` or `pgsql`.

```
#mysql example

mysql -u root -p
# enter your password

create database mydatabase;

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#pgsql example

createdb -h localhost -p 5432 -U myPgUser mydatabase

```

- ### Setup .env

After creating your database, you can fill in the .env file and start your code.

```
cp .env.example .env

```

# Model

- ### Create new model via cli.

```
npx nodemi make:model Product
```

The model will be created in the `models` directory.

```

import { Model, DataTypes } from "sequelize";
import db from "../core/database/Database.js"

class Product extends Model {}
Product.init({
name: {
type: DataTypes.STRING,
allowNull: false
},
}, {
sequelize: db,
tableName: 'products',
modelName: 'Product',
timestamps: true
}
);

export default Product

```

Automatically registered in the `loadModels` function in the `models/Models.js` file.

```

const loadModels = async () => {

await Product.sync({
alter: true, // not recomended on production mode
})

....

```

Full documentation of ORM

- ### Noted

All relationships between models should be defined in the `loadModels` function.
When a model is removed from the `models` directory, it is important to also remove its corresponding relationship from the `loadModels` function in the `models/Models.js` file.

# Media

Any model can own media by binding the model to the media inside the `loadModels` function using `hasMedia(YourModel)`.

```

const loadModels = async () => {

await Product.sync({
alter: true, // not recomended on production mode
})

await hasMedia(Product)

```

- ### Save a file

After binding model using `hasMedia(YourModel)`, then your model will able to save a file using `instance.saveMedia(file,mediaName)`. If the instance already has a file with the same name, then the file will be replaced with a new file.

```

const product = await Product.findOne({
where: {
id: 1
}
})

await product.saveMedia(req.body.file,"thumbnail") // if success then will return media object

```

You can save files to either `Local` storage or `Firebase` storage.

To save to `Local` storage, just set your .env file `MEDIA_STORAGE=local` , and local storage directory name `MEDIA_LOCAL_STORAGE_DIR_NAME=storage`.

```
MEDIA_STORAGE=local
MEDIA_LOCAL_STORAGE_DIR_NAME=storage
```

To save to `Firebase` storage, first create your `Service Account .json` on firebase Firebase Console, and download and convert to `base64` string, then setup the .env file.

```
MEDIA_STORAGE=firebase # set to firebase
FIREBASE_STORAGE_BUCKET=gs://xxxxxx.appspot.com # your firebase storage bucket
FIREBASE_SERVICE_ACCOUNT_BASE64= # base64 string of your firebase service account .json
```

- ### Get media

Get all media by calling `instance.getMedia()`.

```

const product = await Product.findOne({
where: {
id: 1
}
})

product.getMedia() // return list of object

```

Get media by name, params is media name

```
product.getMediaByName("thumbnail") // return single object
product.getMediaByName("thumbnail").url // return single object url

```

Get media first media

```
product.getFirstMedia() // return single object
product.getFirstMedia().url // return first media url

```

Get media with exception, params can be `string` or `array` of string

```
product.getMediaExcept("thumbnail_mobile") // return list of object with exception

```

Get all media url,

```
product.getMediaUrl() // return list of media url

```

Get all media url with exception, params can be `string` or `array` of string

```
product.getMediaUrlExcept(['thumbnail_mobile']) // return list of url

```

Get url from media object

```
product.getFirstMedia().getUrl()

```

- ### Destroy media

Destroy media by calling `instance.destroyMedia(mediaName)`. return status deleted in boolean

```

const product = await Product.findOne({
where: {
id: 1
}
})

await product.destroyMedia("thumbnail")

```

- ### Noted

All media files will be automatically deleted whenever `instance` of your model is deleted.

# Request & Upload Files

Handling Content-Type header for

- application/json
- application/form-data
- application/x-www-form-urlencoded

Handling all upload files on `POST` and `PUT` method, and nested fields.

- ### File properties

Uploaded file will have this properties.

```

name -> file name,
encoding -> file encoding,
type -> file mimeType,
size -> file size,
sizeUnit -> file size in bytes
extension -> file extension
tempDir -> file temporary directory

```

# Rule Validation

- ### Create Request validation via cli.

```
npx nodemi make:request ProductRequest
```

The Request will be created in the `requests` directory.

```

import RequestValidation from "../core/validation/RequestValidation.js"

class ProductRequest extends RequestValidation {

constructor(req) {
super(req).load(this)
}

/**
* Get the validation rules that apply to the request.
*
* @return object
*/
rules() {
return {

}
}
}

export default ProductRequest

```

- ### Basic usage.

```

const request = new ProductRequest(req)

await request.check()

if (request.isError)
return request.responseError(res) // or return res.status(422).json(request.errors)

```

- ### Example html form.

```



Item






Item 1





Item 2














Submit


```

- ### Example rules

```

rules() {
return {
"name": {
"rules": ["required"]
},
"discount": {
"rules": ["required", "float", "min:3", "max:4"]
},
"expired_date": {
"rules": ["required", "date", "date_after:now"]
},
"product_image": {
"rules": ["required", "image", "max_file:1,MB"]
},
"item.*.name": {
"rules": ["required"]
},
"item.*.description": {
"rules": ["required"]
},
"price.*": {
"rules": ["required", "float"]
},
"comments.*": {
"rules": ["required"]
},
"seo.title": {
"rules": ["required"]
},
"seo.description.long": {
"rules": ["required"]
},
"seo.description.short": {
"rules": ["required"]
}
}
}

```

- ### Example error messages

```

{
"errors": {
"name": [
"The Name is required"
],
"discount": [
"The Discount is required",
"The Discount must be valid format of float",
"The Discount should be more or equal than 3",
"The Discount should be less or equal than 4"
],
"expired_date": [
"The Expired date is required",
"The Expired date must be valid format of date",
"The Expired date date must be after the now's date"
],
"product_image": [
"The Product image is required"
],
"item.0.name": [
"The Item.0.name is required"
],
"item.1.name": [
"The Item.1.name is required"
],
"item.0.description": [
"The Item.0.description is required"
],
"item.1.description": [
"The Item.1.description is required"
],
"price.0": [
"The Price.0 is required",
"The Price.0 must be valid format of float"
],
"price.1": [
"The Price.1 is required",
"The Price.1 must be valid format of float"
],
"comments.0": [
"The Comments.0 is required"
],
"comments.1": [
"The Comments.1 is required"
],
"comments.2": [
"The Comments.2 is required"
]
"seo.title": [
"The Seo.title is required"
],
"seo.description.long": [
"The Seo.description.long is required"
],
"seo.description.short": [
"The Seo.description.short is required"
]
}
}

```

- ### Basic rules

| Rule | Description | Example |
| -------------------- | --------------------------------------------- | ----------------------------------------------------------- |
| required | check empty value | "required" |
| email | check email formats | "email" |
| match | check match value with other value | "match:password" |
| exists | check value exists in the database | "exists:users,email" or "exists:users,email,"+super.body.id |
| unique | check value is unique in database | "unique:users,email" or "unique:users,email,"+super.body.id |
| string | check value is an string | "string" |
| float | check value is an float | "float" |
| integer | check value is an ineteger | "integer" |
| max | count maximum value of numeric, | "max:12" |
| | if string/array its count the length | |
| min | count minimum value of numeric, | "min:5" |
| | if string/array its count the length | |
| date | check value is date format | "date" |
| array | check value is an array | "array" |
| mimetypes | check file mimetypes | "mimetypes:image/webp,image/x-icon,video/mp4" |
| mimes | check file extension | "mimes:jpg,png,jpeg" |
| max_file | check maximum file size, | "max_file:1,GB" or "max_file:1,MB" or "max_file:1,Byte" |
| | param can be `GB`, `MB`, `KB` or `Byte` | |
| image | check file is an image format | "image" |
| date_after | check value after particular date | "date_after:now" or "date_after:birthdate" |
| | param can be `now`, or other field name | |
| date_after_or_equal | check value after or equal particular date | "date_after_or_equal:now" |
| | param can be `now`, or other field name | |
| date_before | check value before particular date | "date_before:now" or "date_before:birthdate" |
| | param can be `now`, or other field name | |
| date_before_or_equal | check value before or equal particular date | "date_before_or_equal:now" |
| | param can be `now`, or other field name | |
| boolean | check value is an boolean | "boolean" |
| in_array | check value exist in array | "in_array:1,3,4,1,4,5" |
| not_in_array | check value is not include in array | "not_in_array:1,3,4,1,4,5" |
| ip | check value is as ip address | "ip" |
| url | check value is as url | "url" |
| json | check value is as json format | "json" |
| digits | check value digits, | "digits:4" |
| max_digits | check maximum digits of value | "max_digits:20" |
| min_digits | check minumum digits of value | "min_digits:20" |
| digits_between | check digits bewteen of value | "digits_between:5,10" |
| age_lt | check value is less than param | "age_lt:17" |
| | value must be an date format | |
| age_lte | check value is less than or equal to param | "age_lte:17" |
| | value must be an date format | |
| age_gt | check value is greater than param | "age_gt:17" |
| | value must be an date format | |
| age_gte | check value is greater than or equal to param | "age_gte:17" |
| | value must be an date format | |

- ### Custom

Custom validation `messages` and `attribute`

```

rules() {
return {
"name": {
"rules": ["required"],
"attribute": "Product name"
},
"discount": {
"rules": ["required", "float", "min:3", "max:4"],
"messages": {
"required": "The _attribute_ need discount",
"float": "The data must be numeric"
},
"attribute": "DISCOUNT"
}
}
}

```

- ### Direct add error messages

Direct add error message required key and error message.

```
const request = new ProductRequest(req)
await request.check()

if (request.isError)
{
request.addError("name","Name have to .....")
request.addError("name","Name must be .....")

```

# Custom Rule

- ### Create Custom Rule via cli.

```
npx nodemi make:rule GmailRule
```

The Rule will be created in the `rules` directory.

```
class GmailRule {

constructor() {
}

/**
* Determine if the validation rule passes.
* @param {*} attribute
* @param {*} value
* @returns bolean
*/
passes(attribute, value) {

return value.includes("@gmail.com")
}

/**
* Get the validation error message.
*
* @return string
*/
message() {
return 'The _attribute_ must be using @gmail.com'
}
}

export default GmailRule

```

- ### Custom rule usage

```
rules() {
return {
"email": {
"rules": [ new GmailRule, "required","email" ]
}
}
}
```

- ### Noted

Default error messages outputs are dependent on the locale. If you haven't set up the locale as a middleware, it will be set to English `en` by default.

# Role and Permissions

A user model can have a role by binding using `hasRole(YourModel)` function inside `loadModels` in `models/Models.js` file.

```

const loadModels = async () => {

await User.sync()

await hasRole(User)

```

- ### Set users role

If the user instance already has a role, then the user role will be replaced with a new role. `instance.setRole(params)` params can be role `id` or `name`, and will return status in boolean.

```

const user = await User.create({
name: name,
email: email,
password: hashPassword
})

await user.setRole("customer") // params is role id or name

```

- ### Get role

Get role object by calling `instance.getRole()`, or direcly access role name `instance.getRole().name`.

```

user.getRole() // role object
user.getRole().name // role name

```

- ### Get permissions

Get permission by calling `instance.getPermissions()` will get array of object, or `instance.getPermissionsName()` will get array of permissions name.

```

user.getPermissions() // array of permissions object
user.getPermissionsName() // array of permissions name [ "user-create","user-stored"]

```

- ### Remove role

```

user.removeRole()

```

- ### Check user access

Limitation user access using `GateAccess(userInstance,permissionNames)`, `permissionNames` must be an array of permission names.

```

if (!GateAccess(user, ["user-create","user-stored","user-access"]))
return res.sendStatus(403) // return forbidden status code

```

- ### Add permissions

```

const permissions = [
"user-create",
"user-stored",
"user-edit",
"user-update",
"user-delete",
"user-search"
]

for (let permission of permissions) {
await Permission.create({ name: permission })
}

```

- ### Add Role

```

const roles = [ "admin","customer" ]

for (let role of roles) {
await Role.create({ name: role })
}

```

- ### Assigning Permissions to Roles

Assign permissions to a role by using `roleInstance.assignPermissions(params)`, params can be a list of permissions `name` or `id`.

```

const permissions = [
"user-create",
"user-stored"
]

const admin = await Role.findOne({ where: { name: "admin" } })

if (admin) {
await admin.assignPermissions(permissions)
}

```

# Resource

- ### Create new resource via cli.

```
npx nodemi make:resource UserResource
```

The Resource will be created in `resources` directory.

```

import Resource from "../core/resource/Resource.js"
class UserResource extends Resource {
constructor() {
super().load(this)
}

/**
* Transform the resource into custom object.
*
* @return
*/
toArray(data) {
return {}
}
}

export default UserResource

```

- ### Basic usage

To create resources from a single object use `make` or `collection` for an array of objects.

```

const userResource = new UserResource().make(user) // for single object

const userResources = new UserResource().collection(users) // for array of object

```

- ### Example user resource

```

class UserResource extends Resource {
constructor() {
super().load(this)
}

toArray(data) {
return {
"id": data.id,
"name": data.name,
"email": data.email,
"image": data.getMediaByName("avatar")?.url ?? '',
"role": data.getRole()?.name ?? '',
"permissions": new PermissionResource().collection(data.getPermissions() ?? []),
}
}
}

```

- ### Example permissions resource

```

class PermissionResource extends Resource {
constructor() {
super().load(this)
}

toArray(data) {
return {
"id": data.id,
"name": data.name
}
}
}

```

- ### Example usage

```

const user = await User.findOne({
where: {
id: 1
}
})

const userResource = new UserResource().make(user)

res.json(userResource)

```

- ### Example result

```

{
"id": 1,
"name": "Andre",
"email": "[email protected]",
"image": "http://localhost:5000/User-1/287d735a-2880-4d4f-9851-5055d1ba1aae.jpg",
"role": "customer",
"permissions": [
{
"id": 1,
"name": "user-create"
},
{
"id": 2,
"name": "user-stored"
}
]
}

```

# Auth Jwt

- ### Create token

Create token by calling `JwtAuth.createToken()`, that will return `refreshToken` and `accessToken`.

```
const payload = {
id: user.id,
name: user.name,
email: user.email
}

const token = JwtAuth.createToken(payload)

console.log(token.refreshToken)
console.log(token.accessToken)

```

- ### Regenerate access token

Regenerate access token by calling `JwtAuth.regenerateAccessToken(refreshToken)`, that will return new access token.

```

const accessToken = JwtAuth.regenerateAccessToken(refreshToken)

```

- ### Get Auth user

Get authenticated user by `calling JwtAuth.getUser(req)`, that will get user by refresh token on request cookies.

```

const user = await JwtAuth.getUser(req)

```

Or you just setup the .env `AUTH_GET_CURRENT_USER_ON_REQUEST=true` and you can access current authenticated user by access
`req.user`.

Before using `JwtAuth.GetUser()`, ensure that you have set up your `User` model inside the `AuthConfig` in the `core/config/Auth.js` file. It is crucial that your User model has a `refresh_token` column, as `JwtAuth.GetUser()` will retrieve the user instance based on the `refresh_token` by default. However, if you prefer to retrieve the current authenticated user in a different manner, you can modify the `JwtAuth.GetUser()` function to suit your needs in `core/auth/JwtAuth.js` file.

```
class AuthConfig {

/**
* Default user model for auth
* @returns
*/
static user = User
```

- ### Use Middleware - Auth Jwt

For secure access to controller by adding `JwtAuthPass` to your router.

```
import JwtAuthPass from '../core/middleware/JwtAuthPass.js';

routerAuth.use(JwtAuthPass)
routerAuth.get("/upload", UserController.upload)

app.use("/api",routerAuth)

```

Header Request

```
Authorization: 'Bearer ' + accessToken
```

- ### Use Middleware - Basic auth

For secure access to controller by adding `BasicAuthPass` to your router.

```
import BasicAuthPass from '../core/middleware/BasicAuthPass.js';

routerAuth.use(BasicAuthPass)
routerAuth.get("/upload", UserController.upload)

app.use("/api",routerAuth)

```

Before using this, make sure already set username and password for basic auth in `.env` file.

```
AUTH_BASIC_AUTH_USERNAME=myBasicUsername
AUTH_BASIC_AUTH_PASSWORD=myBasicPassword
```

Header Request

```
Authorization: 'Basic ' + encodeBase64(myBasicUsername+':'+myBasicPassword)
```

# Locale

- Config

Setup locale in `core/config/Locale.js`. by default locale setup to english `en`, support locale of `english`, `indonesian`, `spanish`, `hindi`, `portuguese`, `russian`, `chinese`, `japanese`,

```

defaultLocale: "en",
useLocale: useLocale,
locales: ["en", "id"]

```

You can add more locale Code to `locales`. By default `locales` are only available for English `en`, and for Indonesia `id`.

- Default validation error Messages

After adding additional `locales`, it is important to update the validation error messages in the `core/locale/LangValidation.js` file, as the messages generated will depend on the selected locale.

```

const langValidation = Object.freeze({
required: {
en: "The _attribute_ is required",
id: "_attribute_ wajib di isi",
//ja: "_attribute_ ........." -> adding new validation messages for code ja
},
email: {
en: "The _attribute_ must in E-mail format",
id: "_attribute_ harus dalam format E-mail",
},
match: {
en: "The _attribute_ must be match with _param1_",
id: "_attribute_ harus sama dengan _param1_"
},
......

```

- Use Locale

Its easy to use locale, just setup .env `LOCALE_USE=true`, then this will effect to `all` routes, so that have to has a params for locale, for the API router it should be `/api/:locale` and for the web router it should be `/:locale`.

```

// example for api route
const routerAuth = express.Router()

routerAuth.use(JwtAuthPass)
routerAuth.get("/user", UserController.getUser)
routerAuth.post("/upload", UserController.upload)s

app.use("/api/:locale", routerAuth)

// http://localhost:5000/api/en/endpoint | http://localhost:5000/api/id/endpoint

```

If you don't want to set the locale for all routes, only for a particular route, then simply set up the .env as `LOCALE_USE=false`. Then you can use the `LocalePass` middleware directly to your route.

```

// example for web route
app.get("/:locale",LocalePass, (req, res) => {

// http://localhost:5000/en | http://localhost:5000/id

// example for api route
app.get("/api/:locale/login",LocalePass, (req, res) => {

// http://localhost:5000/api/en/login | http://localhost:5000/api/id/login

```

- Noted

All routers that using `LocalePass` will have the locale Code on req, accessible via `req.locale`.

# Mail

Create mail via cli.

```
npx nodemi make:mail AccountVerify
```

The mail will be created in the `mails` directory, with `examplefile.txt` and `template.ejs`

```

import Mail from "../../core/mail/Mail.js"

class AccountVerify extends Mail {
constructor(from = String, to = [], subject = String) {
super().load({
from: from,
to: to,
subject: subject,
text: "Just need to verify that this is your email address.",
attachments: [
{
filename: "theFile.txt",
path: "mails/AccountVerify/examplefile.txt"
},
],
html: {
path: "mails/AccountVerify/template.ejs",
data: {
title: "Welcome to the party!",
message: "Just need to verify that this is your email address."
}
},
})
}
}

export default AccountVerify

```

The `template.ejs` using express view engine `ejs` to render html into mail content.

```







<%= title %>



Hello!



<%= message %>



Regards.



Nodemi



```

To use this `template.ejs`, you need to add an `html` object with a `path` and `data` (if needed) into `super().load()` method.

```
html: {
path: "mails/AccountVerify/template.ejs", // path is required
data: // data is optional base on your template.ejs
{
title: "Welcome to the party!",
message: "Just need to verify that this is your email address."
}
}
```

- Basic usage

To send email by calling `instance.send()`

```
const sendMail = new AccountVerify("[email protected]",["[email protected]"],"Verify Account")

await sendMail.send()

```

- Send file

To send files, you need to add an `attachments` to `super().load()`. See full doc.

```
attachments: [
{
filename: "theFile.txt",
path: "mails/AccountVerify/examplefile.txt"
}
]

```

- Mail message options

Message options that you can add into `super().load()`.

| Name | Description | Type | Required |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------ | ------ | -------- |
| from | The e-mail address of the sender. All e-mail addresses can be plain '[email protected]' | string | `Yes` |
| to | Recipients e-mail addresses that will appear on the To | array | `Yes` |
| subject | Subject of the e-mail | string | No |
| text | If you are using HTML for the body of the email, then this text will not be used again | string | No |
| html | The HTML version of the message | object | No |
| attachments | An array of objects is used for the purpose of sending files. See full doc | array | No |
| cc | Recipients e-mail addresses that will appear on the Cc | array | No |
| bcc | Recipients e-mail addresses that will appear on the Bcc | array | No |
| sender | E-mail address that will appear on the Sender | string | No |
| replyTo | An array of e-mail addresses that will appear on the Reply-To | array | No |
| alternatives | An array of alternative text contents. See full doc | array | No |
| encoding | optional transfer encoding for the textual parts. | string | No |

- Noted

Before using mail, make sure you already setup .env file

```
MAIL_HOST= #example: smtp.gmail.com | smtp-relay.sendinblue.com
MAIL_PORT=587
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_FROM=

```

# Firebase Cloud Messaging

- Send message

```
const message = {
title: "Notification", // notification title
body: "Hello there", // notification body
data: {
// payload
},
registrationTokens: ["token1","token2"] // target token
}

await FirebaseService.sendMessage(message)

```

- Noted

Before using FCM, make sure you already `enable` Firebase Cloud Messaging API on Google Cloud Console, by selecting your project and navigating to `APIs & Services`. Once you have enabled the API, you can set up your .env

```
FIREBASE_SERVICE_ACCOUNT_BASE64= # base64 of firebase service account (.json)
FIREBASE_CLOUD_MESSAGING_SERVER_KEY= #fcm server key

```

# Seeder

- Running seeder

Running seeder via cli

```
npx nodemi seed:run
```

You can put your seeder code inside `seeder` function in the `core/seeder/Seeder.js` file

```

const seeder = async () => {

// put code here..

}

```

# Cors

The configuration for Cross-Origin Resource Sharing (CORS) can be found in the `core/config/Cors.js` file.