├── mails ├── AccountVerify │ ├── examplefile.txt │ ├── template.ejs │ └── AccountVerify.js └── ForgotPassword │ ├── examplefile.txt │ ├── template.ejs │ └── ForgotPassword.js ├── .gitignore ├── core ├── config │ ├── Cors.js │ ├── Firebase.js │ ├── Mail.js │ ├── Locale.js │ ├── Database.js │ ├── Auth.js │ └── Media.js ├── middleware │ ├── CorsHandling.js │ ├── LocalePass.js │ ├── BasicAuthPass.js │ ├── Middleware.js │ ├── JwtAuthPass.js │ └── MediaRequestHandling.js ├── database │ └── Database.js ├── service │ ├── Firebase │ │ ├── __test │ │ │ └── testFirebase.js │ │ └── FirebaseService.js │ ├── RolePermission │ │ ├── UserHasRole.js │ │ ├── RoleHasPermission.js │ │ ├── Service.js │ │ ├── Permission.js │ │ └── Role.js │ └── Media │ │ └── MediaService.js ├── resource │ └── Resource.js ├── validation │ ├── ValidationDB.js │ ├── ___test │ │ ├── test.js │ │ ├── tst.html │ │ ├── TestValidation.js │ │ └── test.html │ └── RequestValidation.js ├── locale │ ├── Locale.js │ ├── Dictionary.js │ └── LangValidation.js ├── seeder │ └── Seeder.js ├── Core.js ├── auth │ └── JwtAuth.js └── mail │ └── Mail.js ├── routes ├── web.js └── api.js ├── middleware └── Requests.js ├── resources ├── PermissionResource.js └── UserResource.js ├── server.js ├── requests ├── user │ └── UploadRequest.js └── auth │ ├── ResetPasswordRequest.js │ ├── LoginRequest.js │ ├── ForgotPasswordRequest.js │ └── RegisterRequest.js ├── models ├── Models.js ├── UserDetail.js └── User.js ├── package.json ├── LICENSE.md ├── ___test ├── testLiveDemo.rest └── test.rest ├── .env.example ├── controllers ├── UserController.js └── AuthController.js └── README.md /mails/AccountVerify/examplefile.txt: -------------------------------------------------------------------------------- 1 | Hello world! -------------------------------------------------------------------------------- /mails/ForgotPassword/examplefile.txt: -------------------------------------------------------------------------------- 1 | Hello world! -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .env 3 | storage/* 4 | /core/cli -------------------------------------------------------------------------------- /core/config/Cors.js: -------------------------------------------------------------------------------- 1 | const corsConfig = { 2 | credentials: true, 3 | origin: ['http:localhost:3000'] 4 | } 5 | export default corsConfig -------------------------------------------------------------------------------- /core/middleware/CorsHandling.js: -------------------------------------------------------------------------------- 1 | import cors from 'cors' 2 | import corsConfig from "../config/Cors.js" 3 | 4 | const CorsHandling = (app) => { 5 | app.use(cors(corsConfig)) 6 | } 7 | 8 | export default CorsHandling -------------------------------------------------------------------------------- /routes/web.js: -------------------------------------------------------------------------------- 1 | import LocalePass from "../core/middleware/LocalePass.js"; 2 | 3 | export default function web(app) { 4 | app.get("/:locale", LocalePass, (req, res) => { 5 | res.json({ "message": "hellow" }) 6 | }) 7 | } 8 | 9 | -------------------------------------------------------------------------------- /core/config/Firebase.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv' 2 | dotenv.config() 3 | 4 | const firebaseConfig = { 5 | firebaseBucket: process.env.FIREBASE_STORAGE_BUCKET, 6 | ServiceAccountBase64: process.env.FIREBASE_SERVICE_ACCOUNT_BASE64, 7 | firebaseCloudMessagingServerKey: process.env.FIREBASE_CLOUD_MESSAGING_SERVER_KEY, 8 | } 9 | 10 | export default firebaseConfig -------------------------------------------------------------------------------- /core/database/Database.js: -------------------------------------------------------------------------------- 1 | import { Sequelize } from "sequelize"; 2 | import databaseConfig from "../config/Database.js"; 3 | 4 | const db = new Sequelize(databaseConfig.database, databaseConfig.username, databaseConfig.password, 5 | { 6 | host: databaseConfig.host, 7 | dialect: databaseConfig.dialect, 8 | logging: databaseConfig.logging 9 | }) 10 | 11 | export default db -------------------------------------------------------------------------------- /middleware/Requests.js: -------------------------------------------------------------------------------- 1 | import LoginRequest from "../requests/auth/LoginRequest.js" 2 | 3 | class Requests { 4 | 5 | 6 | static async login(req, res, next) { 7 | 8 | const valid = new LoginRequest(req) 9 | await valid.check() 10 | if (valid.isError) 11 | return valid.responseError(res) 12 | 13 | next() 14 | } 15 | 16 | } 17 | 18 | export default Requests -------------------------------------------------------------------------------- /resources/PermissionResource.js: -------------------------------------------------------------------------------- 1 | import Resource from "../core/resource/Resource.js" 2 | 3 | 4 | class PermissionResource extends Resource { 5 | constructor() { 6 | super().load(this) 7 | } 8 | 9 | toArray(data) { 10 | return { 11 | "id": data.id, 12 | "name": data.name 13 | } 14 | } 15 | } 16 | 17 | 18 | export default PermissionResource 19 | 20 | -------------------------------------------------------------------------------- /core/config/Mail.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv' 2 | dotenv.config() 3 | 4 | 5 | /** 6 | * Mail config 7 | */ 8 | const mailConfig = { 9 | host: process.env.MAIL_HOST, 10 | port: process.env.MAIL_PORT, 11 | username: process.env.MAIL_USERNAME, 12 | password: process.env.MAIL_PASSWORD, 13 | from: process.env.MAIL_FROM, 14 | // name: process.env.MAIL_FROM_NAME, 15 | testing: false, 16 | 17 | } 18 | 19 | export default mailConfig -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv' 2 | import express from "express"; 3 | import Load from './core/Core.js'; 4 | dotenv.config() 5 | const app = express() 6 | 7 | const port = process.env.APP_PORT || 5000 8 | 9 | Load(app).then((msg) => { 10 | console.log(msg) 11 | app.listen(port, () => { 12 | console.log(`Server running on: ${port}`) 13 | }) 14 | }).catch((error) => { 15 | console.log("\x1b[31m", error, "\x1b[0m"); 16 | }) 17 | 18 | 19 | -------------------------------------------------------------------------------- /core/service/Firebase/__test/testFirebase.js: -------------------------------------------------------------------------------- 1 | import FirebaseService from "../FirebaseService.js"; 2 | 3 | 4 | 5 | class testFirebase { 6 | 7 | 8 | static fcm() { 9 | FirebaseService.sendMessage({ 10 | body: "test body", 11 | title: " notif", 12 | data: { 13 | message: "this is message" 14 | }, 15 | registrationTokens: ["asdasdasdsad"] 16 | 17 | }) 18 | } 19 | 20 | } 21 | 22 | testFirebase.fcm() -------------------------------------------------------------------------------- /core/config/Locale.js: -------------------------------------------------------------------------------- 1 | let useLocale = false 2 | if (process.env.LOCALE_USE === "true" || process.env.LOCALE_USE === true) 3 | useLocale = true 4 | 5 | const locales = Object.freeze({ 6 | en: "en", 7 | id: "id", 8 | es: "es", 9 | hi: "hi", 10 | ru: "ru", 11 | pt: "pt", 12 | zh: "zh", 13 | ja: "ja" 14 | }) 15 | 16 | const localeConfig = { 17 | defaultLocale: locales.en, 18 | useLocale: useLocale, 19 | locales: locales 20 | } 21 | 22 | export default localeConfig -------------------------------------------------------------------------------- /requests/user/UploadRequest.js: -------------------------------------------------------------------------------- 1 | import RequestValidation from "../../core/validation/RequestValidation.js"; 2 | 3 | class UploadRequest extends RequestValidation { 4 | 5 | constructor(req) { 6 | super(req).load(this) 7 | } 8 | 9 | 10 | rules() { 11 | return { 12 | "file": { 13 | "rules": [ 14 | "required", 15 | "image", 16 | "max_file:1000,KB", 17 | ] 18 | }, 19 | }; 20 | } 21 | } 22 | 23 | export default UploadRequest -------------------------------------------------------------------------------- /core/config/Database.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv' 2 | dotenv.config() 3 | 4 | let logging = false 5 | if (process.env.DB_DEBUG_LOG === "true" || process.env.DB_DEBUG_LOG === true) 6 | logging = true 7 | 8 | /** 9 | * Database config 10 | */ 11 | const databaseConfig = { 12 | "username": process.env.DB_USERNAME || 'root', 13 | "password": process.env.DB_PASSWORD || null, 14 | "database": process.env.DB_NAME || "", 15 | "host": process.env.DB_HOST || "localhost", 16 | "dialect": process.env.DB_CONNECTION, 17 | "logging": logging 18 | } 19 | export default databaseConfig -------------------------------------------------------------------------------- /requests/auth/ResetPasswordRequest.js: -------------------------------------------------------------------------------- 1 | import RequestValidation from "../../core/validation/RequestValidation.js" 2 | 3 | 4 | class ResetPasswordRequest extends RequestValidation { 5 | constructor(req) { 6 | super(req).load(this) 7 | } 8 | 9 | /** 10 | * Get the validation rules that apply to the request. 11 | * 12 | * @return object 13 | */ 14 | rules() { 15 | return { 16 | "new_password": { 17 | "rules": ["required"], 18 | }, 19 | } 20 | } 21 | } 22 | 23 | 24 | export default ResetPasswordRequest -------------------------------------------------------------------------------- /requests/auth/LoginRequest.js: -------------------------------------------------------------------------------- 1 | import RequestValidation from "../../core/validation/RequestValidation.js"; 2 | 3 | 4 | class LoginRequest extends RequestValidation { 5 | 6 | constructor(req) { 7 | super(req).load(this) 8 | } 9 | 10 | rules() { 11 | return { 12 | "email": { 13 | "rules": ["required", "email", "exists:users,email"], 14 | "attribute": "E-mail" 15 | }, 16 | "password": { 17 | "rules": ["required"], 18 | "attribute": "Password" 19 | }, 20 | }; 21 | } 22 | } 23 | 24 | export default LoginRequest -------------------------------------------------------------------------------- /mails/AccountVerify/template.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | <%= title %> 11 | 12 | 13 | 14 | 15 |

Hello!

16 |

17 | <%= message %> 18 |

19 |

20 | Verify Account 21 |

22 |

23 | Regards. 24 |

25 |

26 | Nodemi 27 |

28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /requests/auth/ForgotPasswordRequest.js: -------------------------------------------------------------------------------- 1 | import RequestValidation from "../../core/validation/RequestValidation.js" 2 | 3 | 4 | class ForgotPasswordRequest extends RequestValidation { 5 | constructor(req) { 6 | super(req).load(this) 7 | } 8 | 9 | /** 10 | * Get the validation rules that apply to the request. 11 | * 12 | * @return object 13 | */ 14 | rules() { 15 | return { 16 | "email": { 17 | "rules": ["required", "email", "exists:users,email"], 18 | "attribute": "E-mail" 19 | }, 20 | } 21 | } 22 | } 23 | 24 | 25 | export default ForgotPasswordRequest -------------------------------------------------------------------------------- /core/middleware/LocalePass.js: -------------------------------------------------------------------------------- 1 | import Locale from "../locale/Locale.js" 2 | 3 | 4 | 5 | const LocalePass = async (req, res, next) => { 6 | 7 | const prefixs = req.originalUrl.split('/'); // prefixs=> [ '', 'api', 'id','endpoint' ] 8 | let locale = '' 9 | if (prefixs[1] === 'api') { 10 | locale = prefixs[2] || ''; 11 | } else { 12 | locale = prefixs[1] || '' 13 | } 14 | 15 | if (!Locale.isLocale(locale)) { 16 | return res.status(400).json({ "message": "Required valid locale code. Example: https://abc.com/api/en/endpoint or https://abc.com/en/endpoint" }) 17 | } 18 | 19 | req["locale"] = locale 20 | next() 21 | } 22 | 23 | export default LocalePass -------------------------------------------------------------------------------- /mails/ForgotPassword/template.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | <%= title %> 11 | 12 | 13 | 14 | 15 |

Hello!

16 |

17 | <%= message %> 18 |

19 |

20 | Click here to reset your password. 21 |

22 |

23 | If you don't do this, ignore this message and change your password immediately for security reasons. 24 |

25 |

26 | Regards, 27 |

28 |

29 | Nodemi 30 |

31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /resources/UserResource.js: -------------------------------------------------------------------------------- 1 | import Resource from "../core/resource/Resource.js" 2 | import PermissionResource from "./PermissionResource.js" 3 | 4 | class UserResource extends Resource { 5 | 6 | constructor() { 7 | super().load(this) 8 | } 9 | 10 | toArray(data) { 11 | return { 12 | "id": data.id, 13 | "name": data.name, 14 | "email": data.email, 15 | "avatar": data.getMediaByName("avatar")?.url ?? '', 16 | "media_urls": data.getMediaUrl(), 17 | "media_except": data.getMediaUrlExcept(["avatar2","test"]), 18 | "role": data.getRole()?.name ?? '', 19 | "permissions": new PermissionResource().collection(data.getPermissions() ?? []), 20 | } 21 | } 22 | 23 | } 24 | 25 | export default UserResource -------------------------------------------------------------------------------- /core/config/Auth.js: -------------------------------------------------------------------------------- 1 | import User from "../../models/User.js" 2 | 3 | const getUserOnRequest = () => { 4 | if (process.env.AUTH_GET_CURRENT_USER_ON_REQUEST === "true" || process.env.AUTH_GET_CURRENT_USER_ON_REQUEST === true) { 5 | return true; 6 | } 7 | return false; 8 | } 9 | const authEmailVerification = () => { 10 | if (process.env.AUTH_EMAIL_VERIFICATION === "true" || process.env.AUTH_EMAIL_VERIFICATION === true) { 11 | return true; 12 | } 13 | return false; 14 | } 15 | 16 | class AuthConfig { 17 | 18 | /** 19 | * Default user model for auth 20 | * @returns 21 | */ 22 | static user = User 23 | 24 | 25 | static getUserOnRequest = getUserOnRequest() 26 | 27 | 28 | static emailVerification = authEmailVerification() 29 | 30 | } 31 | 32 | export default AuthConfig 33 | -------------------------------------------------------------------------------- /core/config/Media.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv' 2 | import express from 'express' 3 | dotenv.config() 4 | 5 | 6 | 7 | const mediaStorages = Object.freeze({ 8 | local: "local", 9 | firebase: "firebase" 10 | }) 11 | 12 | let appUrl = process.env.APP_URL ?? "http://localhost::5000" 13 | 14 | const mediaConfig = { 15 | localStorageDirectory: process.env.MEDIA_LOCAL_STORAGE_DIR_NAME || "storage", 16 | mediaStorage: process.env.MEDIA_STORAGE || mediaStorages.local, 17 | rootMediaUrl: appUrl + "/" 18 | } 19 | 20 | 21 | /** 22 | * Setup dir as public where media is stored 23 | * @param {*} app express js app 24 | */ 25 | const routeStoragePublic = (app) => { 26 | app.use(express.static(process.env.MEDIA_LOCAL_STORAGE_DIR_NAME || "storage")); 27 | } 28 | 29 | export default mediaConfig 30 | export { routeStoragePublic, mediaStorages } -------------------------------------------------------------------------------- /models/Models.js: -------------------------------------------------------------------------------- 1 | import User from "./User.js" 2 | import UserDetail from "./UserDetail.js" 3 | import { hasMedia } from "../core/service/Media/MediaService.js" 4 | import { hasRole } from "../core/service/RolePermission/Role.js" 5 | 6 | 7 | /** 8 | * All model will load here 9 | */ 10 | const loadModels = async () => { 11 | 12 | 13 | await User.sync({ 14 | alter: true, 15 | // force: true 16 | }) 17 | await hasMedia(User) 18 | await hasRole(User) 19 | 20 | 21 | await UserDetail.sync({ 22 | alter: true, 23 | }) 24 | 25 | User.hasOne(UserDetail, { 26 | foreignKey: 'user_id', 27 | as: 'user_details' 28 | }); 29 | 30 | UserDetail.belongsTo(User, { 31 | foreignKey: 'user_id', 32 | as: 'user' 33 | }) 34 | 35 | 36 | } 37 | 38 | export default loadModels 39 | -------------------------------------------------------------------------------- /core/resource/Resource.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Resource for handling mapping data from collections of single object 4 | */ 5 | class Resource { 6 | 7 | constructor() { 8 | } 9 | load(child) { 10 | this.child = child 11 | } 12 | 13 | /** 14 | * create resources collection from an array of object 15 | * @param {*} list 16 | * @returns 17 | */ 18 | collection(list = []) { 19 | let newList = [] 20 | for (let data of list) { 21 | let newData = this.make(data) 22 | newList.push(newData) 23 | } 24 | return newList 25 | } 26 | 27 | /** 28 | * create resource from single object 29 | * @param {*} data 30 | * @returns 31 | */ 32 | make(data) { 33 | return this.child.toArray(data) 34 | } 35 | 36 | } 37 | 38 | export default Resource -------------------------------------------------------------------------------- /requests/auth/RegisterRequest.js: -------------------------------------------------------------------------------- 1 | import RequestValidation from "../../core/validation/RequestValidation.js"; 2 | 3 | 4 | class RegisterRequest extends RequestValidation { 5 | 6 | constructor(req) { 7 | super(req).load(this) 8 | } 9 | 10 | rules() { 11 | return { 12 | "name": { 13 | "rules": ["required",], 14 | }, 15 | "email": { 16 | "rules": ["required", "email","unique:users,email"], 17 | "attribute": "E-mail" 18 | }, 19 | "password": { 20 | "rules": ["required"], 21 | "attribute": "Password" 22 | }, 23 | "confirm_password": { 24 | "rules": ["required","match:password"], 25 | }, 26 | }; 27 | } 28 | } 29 | 30 | export default RegisterRequest -------------------------------------------------------------------------------- /models/UserDetail.js: -------------------------------------------------------------------------------- 1 | import { Model, DataTypes } from "sequelize"; 2 | import db from "../core/database/Database.js" 3 | 4 | class UserDetail extends Model { } 5 | 6 | UserDetail.init({ 7 | // Model attributes are defined here 8 | bio: { 9 | type: DataTypes.STRING, 10 | // allowNull: false, 11 | // defaultValue: 'new user' 12 | }, 13 | user_id: { 14 | type: DataTypes.INTEGER, 15 | allowNull: false, 16 | references: { 17 | model: "users", 18 | key: 'id' 19 | }, 20 | onDelete: "CASCADE" 21 | } 22 | }, { 23 | sequelize: db, // We need to pass the connection instance 24 | tableName: "user_details",// table name 25 | modelName: 'UserDetail', // model name 26 | timestamps: true, 27 | underscored: true 28 | }); 29 | 30 | 31 | 32 | export default UserDetail -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodemi", 3 | "version": "1.0.6", 4 | "description": "", 5 | "type": "module", 6 | "main": "server.js", 7 | "scripts": { 8 | "start": "node server.js", 9 | "dev": "nodemon server.js" 10 | }, 11 | "engines": { 12 | "node": "16.x.x" 13 | }, 14 | "author": "andre aipassa", 15 | "license": "MIT", 16 | "dependencies": { 17 | "bcrypt": "^5.1.0", 18 | "busboy": "^1.6.0", 19 | "cookie-parser": "^1.4.6", 20 | "cors": "^2.8.5", 21 | "dotenv": "^16.0.3", 22 | "ejs": "^3.1.8", 23 | "express": "^4.18.2", 24 | "firebase-admin": "^11.5.0", 25 | "fs-extra": "^11.1.0", 26 | "jsonwebtoken": "^9.0.0", 27 | "mysql2": "^3.0.1", 28 | "nodemailer": "^6.9.1", 29 | "pg": "^8.9.0", 30 | "pg-hstore": "^2.3.4", 31 | "sequelize": "^6.28.0", 32 | "uuid": "^9.0.0", 33 | "validator": "^13.7.0" 34 | }, 35 | "devDependencies": { 36 | "commander": "^10.0.0", 37 | "nodemi-cli": "^1.0.52", 38 | "nodemon": "^2.0.20" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /core/middleware/BasicAuthPass.js: -------------------------------------------------------------------------------- 1 | const BasicAuthPass = (req, res, next) => { 2 | // ----------------------------------------------------------------------- 3 | // authentication middleware 4 | const auth = { 5 | username: process.env.AUTH_BASIC_AUTH_USERNAME, 6 | password: process.env.AUTH_BASIC_AUTH_PASSWORD, 7 | }; 8 | 9 | // parse and password from headers 10 | const b64auth = (req.headers.authorization || '').split(' ')[1] || ''; 11 | const [username, password] = Buffer.from(b64auth, 'base64').toString().split(':'); 12 | 13 | // Verify login and password are set and correct 14 | if (username && password && username === auth.username && password === auth.password) { 15 | // Access granted... 16 | return next(); 17 | } 18 | 19 | // Access denied... 20 | res.set('WWW-Authenticate', 'Basic realm="401"'); 21 | return res.status(401).send('Authentication required.'); 22 | // ----------------------------------------------------------------------- 23 | }; 24 | 25 | export default BasicAuthPass; 26 | -------------------------------------------------------------------------------- /core/service/RolePermission/UserHasRole.js: -------------------------------------------------------------------------------- 1 | import { Model, DataTypes } from "sequelize"; 2 | import db from "../../database/Database.js"; 3 | 4 | class UserHasRole extends Model { 5 | 6 | } 7 | 8 | 9 | UserHasRole.init({ 10 | role_id: { 11 | type: DataTypes.INTEGER, 12 | allowNull: false, 13 | references: { 14 | model: "roles", 15 | key: 'id' 16 | }, 17 | onDelete: "CASCADE" 18 | }, 19 | roleable_id: { 20 | type: DataTypes.INTEGER, 21 | allowNull: false, 22 | }, 23 | roleable_type: { 24 | type: DataTypes.STRING, 25 | allowNull: false 26 | } 27 | }, 28 | { 29 | sequelize: db, // We need to pass the connection instance 30 | tableName: "user_has_roles", 31 | modelName: 'UserHasRole', // We need to choose the model name 32 | timestamps: true, 33 | underscored: true, 34 | createdAt: "created_at", 35 | updatedAt: "updated_at" 36 | } 37 | ) 38 | 39 | 40 | 41 | 42 | 43 | export default UserHasRole 44 | -------------------------------------------------------------------------------- /mails/AccountVerify/AccountVerify.js: -------------------------------------------------------------------------------- 1 | import Mail from "../../core/mail/Mail.js" 2 | 3 | 4 | class AccountVerify extends Mail { 5 | constructor(from = '', to = [], subject = '', token = '') { 6 | super().load({ 7 | from: from, 8 | to: to, 9 | subject: subject, 10 | text: "Just need to verify that this is your email address.", 11 | // attachments: [ 12 | // { 13 | // filename: "theFile.txt", 14 | // path: "mails/AccountVerify/examplefile.txt" 15 | // }, 16 | // ], 17 | html: { 18 | path: "mails/AccountVerify/template.ejs", 19 | data: { 20 | title: "Welcome to the party!", 21 | message: "Just need to verify that this is your email address.", 22 | verification_url: process.env.APP_URL + "/api/email-verification/" + token, 23 | } 24 | }, 25 | }) 26 | } 27 | } 28 | 29 | export default AccountVerify 30 | 31 | -------------------------------------------------------------------------------- /mails/ForgotPassword/ForgotPassword.js: -------------------------------------------------------------------------------- 1 | import Mail from "../../core/mail/Mail.js" 2 | 3 | 4 | class ForgotPassword extends Mail { 5 | constructor(from = '', to = [], subject = '', resetToken = '') { 6 | super().load({ 7 | from: from, 8 | to: to, 9 | subject: subject, 10 | text: "You received this email because you requested to reset your password.", 11 | // attachments: [ 12 | // { 13 | // filename: "theFile.txt", 14 | // path: "mails/forgotPassword/examplefile.txt" 15 | // }, 16 | // ], 17 | html: { 18 | path: "mails/ForgotPassword/template.ejs", 19 | data: { 20 | title: "Password Reset Request", 21 | message: "You received this email because you requested to reset your password.", 22 | reset_password_url: process.env.APP_URL + "/api/reset-password/" + resetToken, 23 | } 24 | }, 25 | }) 26 | } 27 | } 28 | 29 | export default ForgotPassword 30 | 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Andre Aipassa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /core/middleware/Middleware.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import cookieParser from "cookie-parser"; 3 | import CorsHandling from "./CorsHandling.js" 4 | import mediaRequestHandling from "./MediaRequestHandling.js" 5 | import localeConfig from "../config/Locale.js"; 6 | import LocalePass from "./LocalePass.js"; 7 | 8 | /** 9 | * Default middleware 10 | */ 11 | const defaultMiddleware = (app) => { 12 | 13 | CorsHandling(app) 14 | 15 | //------------------------------------------------------- 16 | // read cookie from client 17 | app.use(cookieParser()) 18 | //------------------------------------------------------- 19 | 20 | // read reques body json & formData 21 | app.use(express.json()) 22 | app.use(express.urlencoded({ extended: true })) 23 | 24 | //------------------------------------------------------- files upload handling & nested field 25 | app.use(mediaRequestHandling) 26 | //------------------------------------------------------- locale 27 | if (localeConfig.useLocale) { 28 | app.use(LocalePass) 29 | } 30 | //------------------------------------------------------- 31 | } 32 | 33 | export default defaultMiddleware -------------------------------------------------------------------------------- /core/middleware/JwtAuthPass.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import JwtAuth from "../auth/JwtAuth.js"; 3 | import AuthConfig from "../config/Auth.js"; 4 | 5 | 6 | 7 | /** 8 | * Jwt middleware checking access token from header bearer token 9 | * @param {*} req 10 | * @param {*} res 11 | * @param {*} next 12 | * @returns 13 | */ 14 | const JwtAuthPass = async (req, res, next) => { 15 | 16 | const authHeader = req.headers['authorization'] 17 | const token = authHeader && authHeader.split(' ')[1] 18 | if (!token) { 19 | return res.status(403).json({ message: "unauthorized" }) 20 | } 21 | await jwt.verify(token, process.env.AUTH_JWT_ACCESS_TOKEN_SECRET, async (err, decoded) => { 22 | 23 | if (err) return res.status(403).json({ message: "unauthorized" }) 24 | const currentDate = new Date() 25 | if (decoded.exp * 1000 < currentDate.getTime()) 26 | return res.status(410).json({ message: "access token expired" }) 27 | 28 | if (AuthConfig.getUserOnRequest) { 29 | req["user"] = await JwtAuth.getUser(req) 30 | } 31 | 32 | next() 33 | }) 34 | } 35 | 36 | 37 | 38 | export default JwtAuthPass 39 | -------------------------------------------------------------------------------- /models/User.js: -------------------------------------------------------------------------------- 1 | import { Model, DataTypes } from "sequelize"; 2 | import db from "../core/database/Database.js" 3 | 4 | 5 | class User extends Model { 6 | 7 | } 8 | 9 | User.init({ 10 | // Model attributes are defined here 11 | name: { 12 | type: DataTypes.STRING, 13 | allowNull: false 14 | }, 15 | email: { 16 | type: DataTypes.STRING, 17 | allowNull: false 18 | }, 19 | password: { 20 | type: DataTypes.TEXT, 21 | allowNull: false 22 | }, 23 | refresh_token: { 24 | type: DataTypes.STRING, 25 | allowNull: true 26 | }, 27 | verification_token: { 28 | type: DataTypes.STRING(100), 29 | allowNull: true 30 | }, 31 | verified_at: { 32 | type: DataTypes.DATE, 33 | allowNull: true 34 | }, 35 | reset_token: { 36 | type: DataTypes.STRING(100), 37 | allowNull: true 38 | }, 39 | reset_token_expires: { 40 | type: DataTypes.DATE, 41 | allowNull: true 42 | }, 43 | password_reset: { 44 | type: DataTypes.DATE, 45 | allowNull: true 46 | }, 47 | 48 | }, { 49 | sequelize: db, // We need to pass the connection instance 50 | tableName: 'users', // table name 51 | modelName: 'User', // model name 52 | timestamps: true, 53 | underscored: true 54 | }); 55 | 56 | export default User -------------------------------------------------------------------------------- /___test/testLiveDemo.rest: -------------------------------------------------------------------------------- 1 | 2 | ### install REST Client extentions, before use this 3 | ### register 4 | POST https://nodemi.onrender.com/api/register 5 | Content-Type: application/json 6 | 7 | { 8 | "name": "Tester ", 9 | "email": "Tester@gmail.com", 10 | "password": "1234", 11 | "confirmPassword": "1234" 12 | } 13 | 14 | ### login 15 | POST https://nodemi.onrender.com/api/login 16 | Content-Type: application/json 17 | 18 | { 19 | "email": "Tester@gmail.com", 20 | "password": "1234" 21 | } 22 | 23 | 24 | ### refresh token 25 | GET https://nodemi.onrender.com/api/token 26 | 27 | ### logout 28 | DELETE http://localhost:5000/logout 29 | 30 | ### get user 31 | GET https://nodemi.onrender.com/api/user 32 | Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwibmFtZSI6IlRlc3RlciAiLCJlbWFpbCI6IlRlc3RlckBnbWFpbC5jb20iLCJpYXQiOjE2OTE3MTA0MTIsImV4cCI6MTY5MTcxMTAxMn0.uufasynY26o7jBnV6IV_q4I1BgRej-E8mYg5rcKdJ6E 33 | 34 | ### get users 35 | GET https://nodemi.onrender.com/api/users 36 | Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwibmFtZSI6IlRlc3RlciAiLCJlbWFpbCI6IlRlc3RlckBnbWFpbC5jb20iLCJpYXQiOjE2OTE3MTA0MTIsImV4cCI6MTY5MTcxMTAxMn0.uufasynY26o7jBnV6IV_q4I1BgRej-E8mYg5rcKdJ6E 37 | 38 | # ### users no auth 39 | # GET https://nodemi.onrender.com/api/users2 40 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # ~~~~~~~~~~~~~~~~ APP 2 | APP_DEBUG=true 3 | APP_URL=http://localhost:5000 4 | APP_HOST=http://localhost 5 | APP_PORT=5000 6 | # ~~~~~~~~~~~~~~~~ AUTH 7 | AUTH_JWT_ACCESS_TOKEN_SECRET=mySecretCode 8 | AUTH_JWT_REFRESH_TOKEN_SECRET=mySecretCode 9 | AUTH_BASIC_AUTH_USERNAME=myBasicUsername 10 | AUTH_BASIC_AUTH_PASSWORD=myBasicPassword 11 | # check this for time formats-> https://github.com/vercel/ms 12 | AUTH_JWT_ACCESS_TOKEN_EXPIRED=1m 13 | AUTH_JWT_REFRESH_TOKEN_EXPIRED=1d 14 | AUTH_GET_CURRENT_USER_ON_REQUEST=true 15 | AUTH_EMAIL_VERIFICATION=false 16 | # ~~~~~~~~~~~~~~~~ DATABASE 17 | DB_CONNECTION=mysql 18 | DB_HOST=localhost 19 | DB_PORT=3306 20 | DB_NAME=mydatabase 21 | DB_USERNAME=root 22 | DB_PASSWORD= 23 | DB_DEBUG_LOG=false 24 | # ~~~~~~~~~~~~~~~~ MEDIA 25 | MEDIA_STORAGE=local # local | firebase 26 | MEDIA_LOCAL_STORAGE_DIR_NAME=storage 27 | # ~~~~~~~~~~~~~~~~ FIREBASE 28 | FIREBASE_STORAGE_BUCKET= # firebase storage bucket. example: gs://xxxxxx.appspot.com 29 | FIREBASE_SERVICE_ACCOUNT_BASE64= # base64 of firebaseServiceAccount.json 30 | FIREBASE_CLOUD_MESSAGING_SERVER_KEY= # fcm key 31 | # ~~~~~~~~~~~~~~~~ LOCALE 32 | LOCALE_USE=false 33 | # ~~~~~~~~~~~~~~~~ MAIL 34 | MAIL_HOST= # example: smtp.gmail.com | smtp-relay.sendinblue.com | smtp.mailtrap.io 35 | MAIL_PORT=587 36 | MAIL_USERNAME= 37 | MAIL_PASSWORD= 38 | MAIL_FROM= # nodemitest@gmail.com -------------------------------------------------------------------------------- /___test/test.rest: -------------------------------------------------------------------------------- 1 | 2 | ### install REST Client extentions, before use this 3 | ### register 4 | POST http://localhost:5000/api/register 5 | Content-Type: application/json 6 | 7 | { 8 | "name": "game2", 9 | "email": "andre2@gmail.com", 10 | "password": "1234", 11 | "confirm_password": "1234" 12 | } 13 | 14 | ### login 15 | POST http://localhost:5000/api/login 16 | Content-Type: application/json 17 | 18 | { 19 | "email": "andre2@gmail.com", 20 | "password": "1234" 21 | } 22 | 23 | 24 | ### refresh token 25 | GET http://localhost:5000/api/token 26 | 27 | ### logout 28 | DELETE http://localhost:5000/logout 29 | 30 | ### get user 31 | GET http://localhost:5000/api/user 32 | Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NywibmFtZSI6ImdhbWUyIiwiZW1haWwiOiJhbmRyZTJAZ21haWwuY29tIiwiaWF0IjoxNjkxNzA4MzMxLCJleHAiOjE2OTE3OTQ3MzF9.6sHc8MOWgC0vmsH1HjQcsNyF3MV1_e5Y3TIGtXSnjnU 33 | 34 | ### email verification 35 | GET http://localhost:5000/api/email-verification/asdasd 36 | 37 | 38 | ### users 39 | GET http://localhost:5000/api/users 40 | Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NywibmFtZSI6ImdhbWUyIiwiZW1haWwiOiJhbmRyZTJAZ21haWwuY29tIiwiaWF0IjoxNjkxNzA4MzMxLCJleHAiOjE2OTE3OTQ3MzF9.6sHc8MOWgC0vmsH1HjQcsNyF3MV1_e5Y3TIGtXSnjnU 41 | 42 | ### users basic auth 43 | GET http://localhost:5000/api/users2 44 | Authorization: Basic bXlCYXNpY1VzZXJuYW1lOm15QmFzaWNQYXNzd29yZA== 45 | -------------------------------------------------------------------------------- /core/validation/ValidationDB.js: -------------------------------------------------------------------------------- 1 | import db from "../database/Database.js" 2 | 3 | class ValidationDB { 4 | 5 | 6 | static async exists(tableName, column, field, exception) { 7 | let result = await db.query(`SELECT ${column} FROM ${tableName} WHERE ${column} = :field AND id != :exception limit 1`, { 8 | replacements: { 9 | field: field, 10 | exception: exception ?? -1 11 | } 12 | }).then((e) => { 13 | // console.log("exist", e) 14 | if (e[0].length == 0) return false 15 | return true 16 | }).catch((e) => { 17 | // console.log("error exists", e) 18 | return false 19 | }) 20 | return result 21 | } 22 | static async unique(tableName, column, field, exception) { 23 | let result = await db.query(`SELECT ${column} FROM ${tableName} WHERE ${column} = :field AND id != :exception limit 1`, { 24 | replacements: { 25 | field: field, 26 | exception: exception ?? -1 27 | } 28 | }).then((e) => { 29 | // console.log("unique", e) 30 | if (e[0].length == 0) return true 31 | return false 32 | }).catch((e) => { 33 | // console.log("error unique", e) 34 | return false 35 | }) 36 | return result 37 | } 38 | 39 | 40 | } 41 | 42 | export default ValidationDB 43 | 44 | 45 | -------------------------------------------------------------------------------- /core/service/RolePermission/RoleHasPermission.js: -------------------------------------------------------------------------------- 1 | import { Model, DataTypes } from "sequelize"; 2 | import db from "../../database/Database.js"; 3 | import Permission from "./Permission.js"; 4 | import Role from "./Role.js"; 5 | 6 | 7 | class RoleHasPermission extends Model { 8 | 9 | } 10 | 11 | 12 | RoleHasPermission.init({ 13 | role_id: { 14 | type: DataTypes.INTEGER, 15 | allowNull: false, 16 | references: { 17 | model: "roles", 18 | // model: "Role", 19 | key: 'id' 20 | }, 21 | onDelete: "CASCADE" 22 | }, 23 | permission_id: { 24 | type: DataTypes.INTEGER, 25 | allowNull: false, 26 | references: { 27 | model: "permissions", 28 | // model: Permission, 29 | key: 'id' 30 | }, 31 | onDelete: "CASCADE" 32 | } 33 | }, 34 | { 35 | sequelize: db, // We need to pass the connection instance 36 | tableName: "role_has_permissions", 37 | modelName: 'RoleHasPermission', // We need to choose the model name 38 | timestamps: true, 39 | underscored: true, 40 | createdAt: "created_at", 41 | updatedAt: "updated_at" 42 | } 43 | ) 44 | 45 | /** 46 | * Load RoleHasPermission 47 | * @param {*} alter 48 | */ 49 | const loadRoleHasPermission = async function (alter = false) { 50 | await RoleHasPermission.sync({ 51 | alter: alter 52 | }) 53 | } 54 | 55 | 56 | export default RoleHasPermission 57 | export { loadRoleHasPermission } -------------------------------------------------------------------------------- /core/locale/Locale.js: -------------------------------------------------------------------------------- 1 | import localeConfig from "../config/Locale.js" 2 | 3 | class Locale { 4 | 5 | /** 6 | * checking locale code 7 | * @param {*} locale 8 | * @returns bolean 9 | */ 10 | static isLocale = (locale) => { 11 | if (Object.values(localeConfig.locales).includes(locale)) 12 | return true 13 | 14 | return false 15 | } 16 | 17 | /** 18 | * create rule base on locale 19 | * @param {*} param0 20 | * @returns 21 | */ 22 | static createRule({ key = "", rules = [] }) { 23 | var req = {} 24 | Object.values(localeConfig.locales).forEach(e => { 25 | req[key + "_" + e] = {} 26 | req[key + "_" + e]["rules"] = rules 27 | }); 28 | return req 29 | } 30 | 31 | /** 32 | * create field base on data format like data { name_en :"data", name_id:"data"} to {en:"data",id:"data"} 33 | * @param {*} param0 34 | */ 35 | static createField({ key = "", data = {} }) { 36 | var fields = {} 37 | for (var locale in localeConfig.locales) { 38 | for (var d in data) { 39 | var splitData = d.split("_"); 40 | var lastChar = splitData.pop(); 41 | var prefix = splitData.join("_"); 42 | if (prefix === key && lastChar === locale) { 43 | fields[locale] = data[d] 44 | } 45 | } 46 | } 47 | return fields; 48 | } 49 | 50 | 51 | 52 | } 53 | 54 | export default Locale 55 | 56 | -------------------------------------------------------------------------------- /routes/api.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import AuthController from "../controllers/AuthController.js"; 3 | import UserController from "../controllers/UserController.js"; 4 | import JwtAuthPass from "../core/middleware/JwtAuthPass.js";; 5 | // import BasicAuthPass from "../core/middleware/BasicAuthPass.js";; 6 | // import Requests from "../middleware/Requests.js"; 7 | 8 | 9 | export default function api(app) { 10 | 11 | const routerGuest = express.Router() 12 | routerGuest.post("/login", AuthController.login) 13 | routerGuest.post("/register", AuthController.register) 14 | routerGuest.get("/email-verification/:token", AuthController.emailVerification) 15 | routerGuest.get("/token", AuthController.refreshToken) 16 | routerGuest.delete("/logout", AuthController.logout) 17 | routerGuest.post("/forgot-password", AuthController.forgotPassword) 18 | routerGuest.put("/reset-password/:token", AuthController.resetPassword) 19 | // routerGuest.get("/users2", BasicAuthPass, UserController.getUsers) //-> example basic auth 20 | app.use("/api", routerGuest) 21 | // routerGuest.get("/:locale/users", LocalePass, UserController.getUsers) //-> example locale 22 | 23 | const routerAuth = express.Router() 24 | routerAuth.get("/user", JwtAuthPass, UserController.getUser) 25 | routerAuth.get("/users", JwtAuthPass, UserController.getUsers) 26 | routerAuth.post("/avatar", JwtAuthPass, UserController.uploadAvatar) 27 | routerAuth.delete("/avatar", JwtAuthPass, UserController.deleteAvatar) 28 | 29 | app.use("/api", routerAuth) 30 | 31 | } -------------------------------------------------------------------------------- /core/seeder/Seeder.js: -------------------------------------------------------------------------------- 1 | import Permission from "./../service/RolePermission/Permission.js" 2 | import Role from "./../service/RolePermission/Role.js" 3 | 4 | // const permissions = [ 5 | // { name: "user-create" }, 6 | // { name: "user-stored" }, 7 | // { name: "user-edit" }, 8 | // { name: "user-update" }, 9 | // { name: "user-delete" }, 10 | // { name: "user-search" } 11 | // ] 12 | const permissions = [ 13 | "user-create", 14 | "user-stored", 15 | "user-edit", 16 | "user-update", 17 | "user-delete", 18 | "user-search" 19 | ] 20 | 21 | const roles = [ 22 | { name: "admin", }, 23 | { name: "customer" } 24 | ] 25 | 26 | /** 27 | * running seeder code in here 28 | * 29 | * Cli command: npx nodemi seed:run 30 | * @returns 31 | */ 32 | const Seeder = async () => { 33 | 34 | let alreadySeed = await Permission.findAll() 35 | 36 | if (alreadySeed.length > 0) { 37 | console.log("already Seeding") 38 | return 39 | } 40 | 41 | for (let permission of permissions) { 42 | console.log(permission) 43 | await Permission.create({ name: permission }) 44 | } 45 | 46 | await Role.bulkCreate(roles) 47 | 48 | let admin = await Role.findOne({ where: { name: "admin" } }) 49 | if (admin) { 50 | await admin.assignPermissions(permissions) 51 | } 52 | 53 | let customer = await Role.findOne({ where: { name: "customer" } }) 54 | if (customer) { 55 | await customer.assignPermissions([ 56 | "user-create", "user-stored", 57 | ]) 58 | } 59 | 60 | } 61 | 62 | export default Seeder -------------------------------------------------------------------------------- /core/validation/___test/test.js: -------------------------------------------------------------------------------- 1 | import RequestValidation from "../RequestValidation.js"; 2 | 3 | class UserRequest extends RequestValidation { 4 | constructor(req) { 5 | super(req).load(this) 6 | } 7 | 8 | rules() { 9 | return { 10 | "email": { 11 | "rules": ["email", "unique:users,email"] 12 | }, 13 | "npm": { 14 | "rules": ["required", "integer", "length:4", "digits_between:2,5"], 15 | }, 16 | "ipk": { 17 | "rules": ["required", "float", "min:1", "max:4"], 18 | }, 19 | "password": { 20 | "rules": ["required"], 21 | }, 22 | "birthdate": { 23 | "rules": ["required", "date"], 24 | }, 25 | "confirmPassword": { 26 | "rules": ["required", "match:password"], 27 | "attribute": "Confirm password", 28 | }, 29 | "hobby": { 30 | "rules": ["required", "array", "max:1"] 31 | } 32 | }; 33 | } 34 | } 35 | 36 | let birthdate = Date() 37 | var d = new UserRequest({ 38 | body: { 39 | "email": "andre@gmail.com", 40 | "npm": 1, 41 | "ipk": 5, 42 | "password": "12313ss", 43 | "birthdate": birthdate.toString(), 44 | // "birthdate": "2014-05-11", 45 | "confirmPassword": "12313s", 46 | "hobby": ["coding", "cooking"] 47 | } 48 | }) 49 | await d.check() 50 | 51 | console.log("================================= ERROR MESSAGE") 52 | console.log(d.errors) -------------------------------------------------------------------------------- /core/validation/___test/tst.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | { 31 | name: '', 32 | discount: '', 33 | expired_date: '', 34 | item: [ { name: '' , description: '' } , { name: '' , description: '' }], 35 | price: { '' , '' }, 36 | list: [ '' ], 37 | comment: [ '' , '' , '' , '' ], 38 | seo: { title: '' , description: '' , image: {url : '' , name: '' } } 39 | } 40 | 41 | { 42 | name: '', 43 | discount: '', 44 | expired_date: '', 45 | item: { 46 | '0': { name: '', description: '' }, 47 | '1': { name: '', description: '' } 48 | }, 49 | price: { '0': '', '1': '' }, 50 | list: '', 51 | comment: '', 52 | seo: { title: '', description: '', image: { url: '', name: '' } } 53 | } -------------------------------------------------------------------------------- /core/service/RolePermission/Service.js: -------------------------------------------------------------------------------- 1 | import Permission, { loadPermission } from "./Permission.js"; 2 | import Role, { loadRole } from "./Role.js"; 3 | import RoleHasPermission, { loadRoleHasPermission } from "./RoleHasPermission.js"; 4 | 5 | 6 | 7 | 8 | /** 9 | * load role & permissions model 10 | */ 11 | const loadRolePermission = async () => { 12 | 13 | 14 | await loadRole(true) 15 | 16 | await loadPermission(true) 17 | 18 | await loadRoleHasPermission(true) 19 | 20 | await Role.belongsToMany(Permission, { 21 | through: RoleHasPermission, 22 | foreignKey: "role_id", 23 | otherKey: "permission_id", 24 | constraints: false 25 | // through: 'rolehaspermissions', as: "permissions", 26 | }) 27 | 28 | 29 | await Permission.belongsToMany(Role, { 30 | through: RoleHasPermission, 31 | foreignKey: "permission_id", 32 | otherKey: "role_id", 33 | constraints: false 34 | }) 35 | 36 | 37 | } 38 | 39 | /** 40 | * Checking user that has particular permissions 41 | * @param {*} user user instance 42 | * @param {*} permissions ["product-access","product-stored"] 43 | * @returns 44 | */ 45 | const GateAccess = (user, permissions = []) => { 46 | 47 | if (!Array.isArray(permissions)) 48 | throw "permissions must be an array" 49 | let permissionsName = user.getPermissionsName() 50 | 51 | if (!permissionsName) 52 | return false 53 | 54 | let countValid = 0 55 | for (let permission of permissionsName) { 56 | if (permissions.includes(permission)) { 57 | countValid++ 58 | } 59 | } 60 | 61 | if (countValid !== permissions.length) 62 | return false 63 | 64 | return true 65 | } 66 | 67 | 68 | export default loadRolePermission 69 | export { 70 | GateAccess 71 | } 72 | -------------------------------------------------------------------------------- /core/service/RolePermission/Permission.js: -------------------------------------------------------------------------------- 1 | import { Model, DataTypes } from "sequelize"; 2 | import databaseConfig from "../../config/Database.js"; 3 | import db from "../../database/Database.js"; 4 | 5 | 6 | const permissionType = Object.freeze({ 7 | create: "create", 8 | store: "stored", 9 | edit: "edit", 10 | update: "update", 11 | delete: "delete", 12 | show: "show", 13 | search: "search" 14 | }) 15 | 16 | 17 | class Permission extends Model { 18 | 19 | } 20 | 21 | Permission.init({ 22 | name: { 23 | type: DataTypes.STRING, 24 | allowNull: true, 25 | unique: true 26 | }, 27 | }, 28 | { 29 | sequelize: db, // We need to pass the connection instance 30 | tableName: "permissions", 31 | modelName: 'Permission', // We need to choose the model name 32 | timestamps: true, 33 | underscored: true, 34 | createdAt: "created_at", 35 | updatedAt: "updated_at" 36 | } 37 | ) 38 | 39 | 40 | /** 41 | * Load permisssions model 42 | * @param {*} alter 43 | */ 44 | const loadPermission = async (alter = false) => { 45 | await alterTablePermissionHandling(alter) 46 | await Permission.sync({ 47 | alter: alter 48 | }) 49 | 50 | } 51 | 52 | /** 53 | * Used for handling multiple index before alter permissions table 54 | * @param {*} alter 55 | */ 56 | const alterTablePermissionHandling = async (alter = false) => { 57 | // handling for multiple index of url 58 | try { 59 | if (alter) { 60 | await db.query(`ALTER TABLE permissions DROP INDEX name`).then(() => { 61 | }) 62 | } 63 | } catch (error) { 64 | if (databaseConfig.dialect == "mysql") { 65 | console.log("Failed alter permissions drop index name, permissions not exist yet") 66 | } 67 | } 68 | } 69 | 70 | 71 | export default Permission 72 | export { 73 | permissionType, 74 | loadPermission 75 | } -------------------------------------------------------------------------------- /core/Core.js: -------------------------------------------------------------------------------- 1 | 2 | import express from "express"; 3 | import db from "./database/Database.js"; 4 | import { loadMedia } from "./service/Media/MediaService.js" 5 | import loadRolePermission from "./service/RolePermission/Service.js" 6 | import loadModels from "../models/Models.js" 7 | import defaultMiddleware from "./middleware/Middleware.js" 8 | import { routeStoragePublic } from "./config/Media.js" 9 | import api from "../routes/api.js"; 10 | import web from "../routes/web.js"; 11 | 12 | 13 | const Load = async (app) => { 14 | return await new Promise(async (resolve, reject) => { 15 | try { 16 | console.log("load core....") 17 | //------------------------------------------------------- Database 18 | 19 | await db.authenticate() 20 | 21 | // db.drop({cascade: true}) 22 | 23 | // await db.sync({alter: true}) 24 | 25 | //------------------------------------------------------- 26 | 27 | 28 | //------------------------------------------------------- Services 29 | 30 | await loadMedia() 31 | await loadRolePermission() 32 | 33 | //------------------------------------------------------- 34 | 35 | 36 | 37 | //------------------------------------------------------- Models 38 | 39 | await loadModels() // all model 40 | 41 | //------------------------------------------------------- 42 | 43 | 44 | 45 | //------------------------------------------------------- Middleware 46 | 47 | defaultMiddleware(app) 48 | 49 | //------------------------------------------------------- 50 | 51 | 52 | 53 | //------------------------------------------------------- Routers 54 | 55 | app.use(express.static("public")); 56 | 57 | routeStoragePublic(app) 58 | 59 | api(app) 60 | web(app) 61 | 62 | //------------------------------------------------------- 63 | return resolve("Ready") 64 | 65 | } catch (error) { 66 | return reject(error) 67 | } 68 | 69 | } 70 | 71 | ) 72 | } 73 | 74 | export default Load -------------------------------------------------------------------------------- /core/auth/JwtAuth.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import AuthConfig from "../config/Auth.js"; 3 | 4 | class JwtAuth { 5 | 6 | 7 | 8 | /** 9 | * Create access token & refresh token by using payload 10 | * @param {*} payload 11 | * @returns 12 | */ 13 | 14 | static createToken(payload) { 15 | let accessToken = jwt.sign(payload, process.env.AUTH_JWT_ACCESS_TOKEN_SECRET, { 16 | expiresIn: process.env.AUTH_JWT_ACCESS_TOKEN_EXPIRED 17 | }) 18 | let refreshToken = jwt.sign(payload, process.env.AUTH_JWT_REFRESH_TOKEN_SECRET, { 19 | expiresIn: process.env.AUTH_JWT_REFRESH_TOKEN_EXPIRED 20 | }) 21 | return { 22 | accessToken, 23 | refreshToken 24 | } 25 | } 26 | 27 | /** 28 | * generate access token using refresh token 29 | * @param {*} refreshToken 30 | * @returns 31 | */ 32 | 33 | static regenerateAccessToken(refreshToken) { 34 | return jwt.verify(refreshToken, process.env.AUTH_JWT_REFRESH_TOKEN_SECRET, (err, decoded) => { 35 | if (err) return 36 | 37 | const currentDate = new Date() 38 | if (decoded.exp * 1000 < currentDate.getTime()) 39 | return 40 | 41 | delete decoded.exp 42 | delete decoded.iat 43 | 44 | const accessToken = jwt.sign(decoded, process.env.AUTH_JWT_ACCESS_TOKEN_SECRET, { 45 | expiresIn: process.env.AUTH_JWT_ACCESS_TOKEN_EXPIRED 46 | }) 47 | return accessToken 48 | }) 49 | } 50 | 51 | 52 | /** 53 | * Get user object using refresh token from cookies 54 | * @param {*} req request 55 | * @returns 56 | */ 57 | static async getUser(req) { 58 | try { 59 | return await new Promise(async (resolve, reject) => { 60 | const refreshToken = req.cookies.refreshToken; 61 | if (!refreshToken) { 62 | return reject({ error: "no refresh token" }) 63 | } 64 | var user = await AuthConfig.user.findOne({ 65 | where: { 66 | refresh_token: refreshToken 67 | } 68 | }) 69 | 70 | if (!user) { 71 | console.log("auth user failed1") 72 | return reject({ error: "auth user failed" }) 73 | } 74 | return resolve(user) 75 | }) 76 | 77 | } catch (error) { 78 | console.log(error) 79 | return null 80 | } 81 | } 82 | 83 | } 84 | 85 | export default JwtAuth -------------------------------------------------------------------------------- /core/validation/___test/TestValidation.js: -------------------------------------------------------------------------------- 1 | import RequestValidation from "../RequestValidation.js"; 2 | 3 | 4 | class TestRequest extends RequestValidation { 5 | 6 | constructor(req) { 7 | super(req).load(this) 8 | } 9 | 10 | 11 | rules() { 12 | return { 13 | "name": { 14 | "rules": ["required", new MyRule] 15 | }, 16 | "discount": { 17 | "rules": ["required", "float", "min:3", "max:4"], 18 | "messages": { 19 | "required": "Need discount", 20 | "float": "data must be numeric" 21 | }, 22 | "attribute": "DISCOUNT" 23 | }, 24 | "expired_date": { 25 | "rules": ["required", "date", "date_after:now"] 26 | }, 27 | "product_image": { 28 | "rules": ["required", "image", "max_file:1,MB"] 29 | }, 30 | "item.*.name": { 31 | "rules": ["required", new MyRule, "digits_between:5,10"] 32 | }, 33 | "item.*.description": { 34 | "rules": ["required"] 35 | }, 36 | "price.*": { 37 | "rules": ["required", "float", "digits:2", "max:15"] 38 | }, 39 | "comment.*": { 40 | "rules": ["required"], 41 | "messages": { 42 | "required": "The _attribute_ is needed bro " 43 | } 44 | }, 45 | "seo.title": { 46 | "rules": ["required"] 47 | }, 48 | "seo.description.long": { 49 | "rules": ["required", "max:30"] 50 | }, 51 | "seo.description.short": { 52 | "rules": ["required", "max:20"] 53 | } 54 | } 55 | } 56 | 57 | 58 | } 59 | 60 | class MyRule { 61 | 62 | constructor() { 63 | } 64 | 65 | /** 66 | * Determine if the validation rule passes. 67 | * @param {*} attribute 68 | * @param {*} value 69 | * @returns bolean 70 | */ 71 | passes(attribute, value) { 72 | // console.log("attribute",value) 73 | return value.includes("and") 74 | } 75 | 76 | /** 77 | * Get the validation error message. 78 | * 79 | * @return string 80 | */ 81 | message() { 82 | return 'The _attribute_ must be have and' 83 | } 84 | 85 | } 86 | 87 | const TestValidation = async (req, res) => { 88 | 89 | // console.log("BODY REQ:") 90 | // console.dir(req.body, { depth: null }); 91 | 92 | const valid = new TestRequest(req) 93 | console.dir(req.body,{ depth: null }) 94 | await valid.check() 95 | 96 | if (valid.isError) { 97 | valid.addError("men", "ini adalah") 98 | valid.addError("men", "ini adalah2") 99 | return valid.responseError(res) 100 | } 101 | 102 | return res.json("success") 103 | 104 | } 105 | 106 | export default TestValidation -------------------------------------------------------------------------------- /core/locale/Dictionary.js: -------------------------------------------------------------------------------- 1 | import localeConfig from "../config/Locale.js" 2 | 3 | const Dictionary = Object.freeze({ 4 | "success": { 5 | "en": "success", 6 | "id": "sukses", 7 | "es": "éxito", 8 | "hi": "सफलता", 9 | "ru": "успех", 10 | "pt": "sucesso", 11 | "zh": "成功", 12 | "ja": "成功" 13 | }, 14 | "failed": { 15 | "en": "failed", 16 | "id": "gagal", 17 | "es": "fracaso", 18 | "hi": "विफल", 19 | "ru": "не удалось", 20 | "pt": "fracasso", 21 | "zh": "失败", 22 | "ja": "失敗" 23 | }, 24 | "error": { 25 | "en": "error", 26 | "id": "kesalahan", 27 | "es": "error", 28 | "hi": "त्रुटि", 29 | "ru": "ошибка", 30 | "pt": "erro", 31 | "zh": "错误", 32 | "ja": "エラー" 33 | }, 34 | "now": { 35 | "en": "now", 36 | "id": "sekarang", 37 | "es": "ahora", 38 | "hi": "अभी", 39 | "ru": "сейчас", 40 | "pt": "agora", 41 | "zh": "现在", 42 | "ja": "今" 43 | }, 44 | "yesterday": { 45 | "en": "yesterday", 46 | "id": "kemarin", 47 | "es": "ayer", 48 | "hi": "कल", 49 | "ru": "вчера", 50 | "pt": "ontem", 51 | "zh": "昨天", 52 | "ja": "昨日" 53 | }, 54 | "tomorrow": { 55 | "en": "tomorrow", 56 | "id": "besok", 57 | "es": "mañana", 58 | "hi": "कल", 59 | "ru": "завтра", 60 | "pt": "amanhã", 61 | "zh": "明天", 62 | "ja": "明日" 63 | }, 64 | "wrongPassword": { 65 | "en": "wrong password", 66 | "id": "kata sandi salah", 67 | "es": "contraseña incorrecta", 68 | "hi": "गलत पासवर्ड", 69 | "ru": "неправильный пароль", 70 | "pt": "senha incorreta", 71 | "zh": "密码错误", 72 | "ja": "間違ったパスワード" 73 | }, 74 | "accountSuspended": { 75 | "en": "account suspended", 76 | "id": "akun ditangguhkan", 77 | "es": "cuenta suspendida", 78 | "hi": "खाता निलंबित कर दिया गया है", 79 | "ru": "аккаунт заблокирован", 80 | "pt": "conta suspensa", 81 | "zh": "帐户暂停", 82 | "ja": "アカウントが一時停止" 83 | }, 84 | "accountBlocked": { 85 | "en": "account suspended", 86 | "id": "akun diblokir", 87 | "es": "cuenta bloqueada", 88 | "hi": "खाता अवरुद्ध", 89 | "ru": "аккаунт заблокирован", 90 | "pt": "conta bloqueada", 91 | "zh": "账户被封锁", 92 | "ja": "アカウントがブロックされました" 93 | }, 94 | "loginSuccess": { 95 | "en": "login success", 96 | "id": "berhasil masuk", 97 | "es": "Inicio de sesión exitoso", 98 | "hi": "लॉगिन की सफलता", 99 | "ru": "успешный вход в систему", 100 | "pt": "sucesso de login", 101 | "zh": "登录成功", 102 | "ja": "ログイン成功" 103 | }, 104 | "registerSuccess": { 105 | "en": "register success", 106 | "id": "berhasil daftar", 107 | "es": "registro exitoso", 108 | "hi": "सफलता दर्ज करें", 109 | "ru": "зарегистрируйте успех", 110 | "pt": "registrar sucesso", 111 | "zh": "注册成功", 112 | "ja": "登録成功" 113 | }, 114 | }) 115 | 116 | /** 117 | * Translate language to locale base on Dictionary 118 | * @param {*} key 119 | * @param {*} locale 120 | * @returns 121 | */ 122 | const Translate = (key, locale) => { 123 | return Dictionary[key] && Dictionary[key][locale ?? localeConfig.defaultLocale] || key 124 | } 125 | 126 | export default Translate 127 | export { Dictionary } -------------------------------------------------------------------------------- /core/validation/___test/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 11 | 12 | 13 | 14 | 15 |
16 | 17 |
18 |
19 | 20 | 21 | 22 | 23 | 24 |
25 |
26 | 27 | 28 | 29 | 30 |
31 |
32 | 33 | 34 | 35 | 36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
50 | 51 |
52 |
53 |
54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /controllers/UserController.js: -------------------------------------------------------------------------------- 1 | import User from "../models/User.js"; 2 | import UserDetail from "../models/UserDetail.js"; 3 | import { GateAccess } from "../core/service/RolePermission/Service.js"; 4 | import UploadRequest from "../requests/user/UploadRequest.js"; 5 | import UserResource from "../resources/UserResource.js"; 6 | 7 | 8 | export default class UserController { 9 | 10 | /** 11 | * get current user 12 | * @param {*} req express req 13 | * @param {*} res express res 14 | * @returns res 15 | */ 16 | static async getUser(req, res) { 17 | 18 | try { 19 | 20 | const refreshToken = req.cookies.refreshToken; 21 | const user = await User.findOne( 22 | { 23 | where: { 24 | refresh_token: refreshToken 25 | }, 26 | attributes: ['id', 'name', 'email'], 27 | include: [ 28 | { 29 | model: UserDetail, 30 | as: 'user_details', 31 | attributes: ['bio'] 32 | } 33 | ] 34 | } 35 | ) 36 | 37 | if (!user) return res.status(404).json({ message: "user not found" }) 38 | 39 | // example permission, should use-> user-access 40 | if (!GateAccess(user, ["user-create", "user-stored"])) return res.status(403).json({ message: "don't have permission" }) 41 | 42 | const userResource = new UserResource().make(user) 43 | 44 | res.json({ message: "get success", "user": userResource }) 45 | 46 | } catch (error) { 47 | console.log(error) 48 | res.status(409).json({ message: "something went wrong", reason: String(error) }) 49 | } 50 | } 51 | 52 | /** 53 | * get users 54 | * @param {*} req express req 55 | * @param {*} res express res 56 | * @returns res 57 | */ 58 | static async getUsers(req, res) { 59 | try { 60 | 61 | const users = await User.findAll() 62 | 63 | const resources = new UserResource().collection(users) 64 | 65 | res.json(resources) 66 | 67 | } catch (error) { 68 | console.log(error) 69 | res.status(409).json({ message: "something went wrong", reason: String(error) }) 70 | } 71 | } 72 | 73 | /** 74 | * upload current user avatar 75 | * @param {*} req express req 76 | * @param {*} res express res 77 | * @returns res 78 | */ 79 | static async uploadAvatar(req, res) { 80 | 81 | try { 82 | 83 | const valid = new UploadRequest(req) 84 | await valid.check() 85 | if (valid.isError) 86 | return valid.responseError(res) 87 | 88 | const user = req.user 89 | 90 | await user.saveMedia( 91 | req.body.file, 92 | "avatar" 93 | ) 94 | 95 | res.json("upload successfuly") 96 | 97 | } catch (error) { 98 | console.log(error) 99 | res.status(409).json({ message: "something went wrong", reason: String(error) }) 100 | } 101 | 102 | } 103 | 104 | 105 | /** 106 | * delete current user avatar 107 | * @param {*} req express req 108 | * @param {*} res express res 109 | * @returns res 110 | */ 111 | static async deleteAvatar(req, res) { 112 | 113 | try { 114 | 115 | const user = req.user 116 | 117 | await user.destroyMedia("avatar") 118 | 119 | res.json("media deleted") 120 | 121 | } catch (error) { 122 | console.log(error) 123 | res.status(409).json({ message: "something went wrong", reason: String(error) }) 124 | } 125 | 126 | } 127 | 128 | 129 | } 130 | -------------------------------------------------------------------------------- /core/service/Firebase/FirebaseService.js: -------------------------------------------------------------------------------- 1 | 2 | import admin from "firebase-admin" 3 | import { v4 as uuid4 } from 'uuid' 4 | import fse from "fs-extra" 5 | import firebaseConfig from "../../config/Firebase.js"; 6 | 7 | class FirebaseService { 8 | 9 | 10 | /** 11 | * Init firebase service to firebase admin 12 | * @returns 13 | */ 14 | static async init() { 15 | 16 | if (admin.apps.length) 17 | return 18 | 19 | await new Promise(async (resolve, reject) => { 20 | try { 21 | const jsonString = await Buffer.from(firebaseConfig.ServiceAccountBase64, 'base64').toString('ascii') 22 | const jsonData = await JSON.parse(jsonString) 23 | admin.initializeApp({ 24 | credential: admin.credential.cert(jsonData), 25 | storageBucket: firebaseConfig.firebaseBucket 26 | }); 27 | resolve() 28 | } catch (error) { 29 | console.log(error) 30 | reject() 31 | } 32 | }) 33 | } 34 | 35 | 36 | /** 37 | * Save single media to firebase storage 38 | * @param {*} file 39 | * @returns {url, path} 40 | */ 41 | static async saveMedia(file) { 42 | 43 | return await new Promise(async (resolve, reject) => { 44 | 45 | await this.init() 46 | 47 | const bucket = admin.storage().bucket(); 48 | 49 | const fileName = uuid4() + file.extension 50 | 51 | const fileFirebase = bucket.file(fileName); 52 | 53 | const stream = fileFirebase.createWriteStream({ 54 | resumable: true, 55 | public: true, 56 | timeout: 120000, // 2m 57 | }); 58 | 59 | stream.on('error', (err) => { 60 | console.error(err); 61 | reject() 62 | }); 63 | 64 | stream.on('finish', async () => { 65 | // uploaded 66 | resolve({ 67 | url: fileFirebase.publicUrl(), 68 | path: firebaseConfig.firebaseBucket + "/" + fileName 69 | }) 70 | }); 71 | 72 | fse.createReadStream(file.tempDir).pipe(stream); 73 | }) 74 | 75 | } 76 | 77 | 78 | /** 79 | * Delete single file from firebase storage 80 | * @param {*} path ex: gs://xxxxx.appspot.com/6e2b7970-f56d-4009-b0cb-f3464d8cc847.jpg 81 | * @returns 82 | */ 83 | static async deleteMedia(path) { 84 | 85 | return await new Promise(async (resolve, reject) => { 86 | 87 | await this.init() 88 | const fileName = path.split("/").pop(); 89 | const bucket = admin.storage().bucket(); 90 | const file = bucket.file(fileName); 91 | 92 | file.delete().then(() => { 93 | // console.log(`File deleted successfully.`); 94 | resolve(true) 95 | }).catch(error => { 96 | reject() 97 | console.error(`Error deleting file:`, error); 98 | }); 99 | }) 100 | } 101 | 102 | /** 103 | * Delete many media 104 | * @param {*} paths must be an array of path firebase storage 105 | * @returns 106 | */ 107 | static async deleteMedias(paths) { 108 | return await new Promise(async (resolve, reject) => { 109 | // console.log("start delete firebase files", paths) 110 | 111 | if (!Array.isArray(paths)) 112 | reject() 113 | 114 | for (let path of paths) { 115 | await this.deleteMedia(path) 116 | } 117 | resolve() 118 | }) 119 | } 120 | 121 | 122 | 123 | 124 | static async sendMessage({ 125 | title = '', body = '', data = {}, registrationTokens = [] 126 | }) { 127 | 128 | if (!Array.isArray(registrationTokens) || registrationTokens.length === 0) 129 | return 130 | 131 | await this.init() 132 | 133 | const message = {} 134 | const notification = {} 135 | notification["title"] = title 136 | notification["body"] = body 137 | message["notification"] = notification 138 | 139 | if (Object.keys(data).length > 0) { 140 | message["data"] = data 141 | } 142 | message["token"] = registrationTokens.length === 1 ? registrationTokens[0] : registrationTokens 143 | 144 | if (registrationTokens.length === 1) { 145 | await admin.messaging().send(message) 146 | .then((response) => { 147 | console.log("Successfully sent message:", response); 148 | }) 149 | .catch((error) => { 150 | console.log("Error sending message:", error); 151 | }) 152 | } 153 | else { 154 | await admin.messaging().sendMulticast(message) 155 | .then((response) => { 156 | console.log(`${response.successCount} messages were sent successfully`); 157 | }) 158 | .catch((error) => { 159 | console.log(`Error sending message: ${error}`); 160 | }); 161 | } 162 | 163 | 164 | } 165 | 166 | } 167 | 168 | 169 | export default FirebaseService -------------------------------------------------------------------------------- /core/middleware/MediaRequestHandling.js: -------------------------------------------------------------------------------- 1 | import fse from 'fs-extra' 2 | import path from "path"; 3 | import os from 'os' 4 | import busboy from "busboy"; 5 | 6 | 7 | // the body - parser middleware is configured to handle application / json 8 | // and application / x - www - form - urlencoded content types. 9 | // To handle form - data using middleware like multer or busboy 10 | 11 | 12 | const isArray = (name) => { 13 | const a = name.indexOf("[") 14 | const b = name.indexOf("]") 15 | if (a !== -1 && b !== -1) { 16 | if (b - 1 === a || b - 2 === a) { 17 | return true 18 | } 19 | } 20 | return false 21 | } 22 | 23 | const isArrayNested = (name) => { 24 | const a = name.indexOf("[") 25 | const b = name.indexOf("]") 26 | if (a !== -1 && b !== -1) { 27 | if (b - 1 === a) { 28 | return true 29 | } 30 | } 31 | return false 32 | } 33 | 34 | /** 35 | * start remove all temp files after response is send back to client 36 | * @param {*} res response of expres 37 | * @param {*} files files 38 | */ 39 | const clearTempFiles = (res, files) => { 40 | res.on("finish", () => { 41 | try { 42 | for (let fieldName in files) { 43 | try { 44 | if (fse.pathExistsSync(files[fieldName].tempDir)) 45 | fse.removeSync(files[fieldName].tempDir) 46 | } catch (error1) { 47 | console.log(error1) 48 | } 49 | } 50 | } catch (error) { 51 | console.log(error) 52 | } 53 | }) 54 | } 55 | 56 | 57 | const parseFields = (fieldName, value, req) => { 58 | const keys = fieldName.split(/[\[\]]+/).filter(key => key); 59 | let current = req.body; 60 | // console.log(keys) 61 | for (let i = 0; i < keys.length; i++) { 62 | let key = keys[i]; 63 | if (key.endsWith(']')) { 64 | key = key.slice(0, -1); 65 | } 66 | if (i === keys.length - 1) { 67 | if (Array.isArray(current)) { 68 | current.push(value); 69 | // console.log(key, 1) 70 | } else if (typeof current[key] === 'string') { 71 | current[key] = [current[key], value]; 72 | // console.log(key, 2) 73 | } else if (Array.isArray(current[key])) { 74 | current[key].push(value); 75 | // console.log(key, 3) 76 | } 77 | else { 78 | if (isArrayNested(fieldName)) { 79 | if (!current[key]) { 80 | current[key] = [value]; 81 | } 82 | else { 83 | current[key].push(value) 84 | } 85 | } 86 | else { 87 | current[key] = value; 88 | } 89 | // console.log(key, 4) 90 | } 91 | } else { 92 | current[key] = current[key] || (isNaN(keys[i + 1]) ? {} : []); 93 | current = current[key]; 94 | // console.log(key, 5) 95 | } 96 | // console.log("req body") 97 | // console.dir(req.body) 98 | } 99 | } 100 | 101 | 102 | 103 | 104 | 105 | /** 106 | * request handling for handling nested fields or file request 107 | * @param {*} req 108 | * @param {*} res 109 | * @param {*} next 110 | */ 111 | const mediaRequestHandling = async (req, res, next) => { 112 | 113 | if ( 114 | req.method === 'POST' && req.headers['content-type'] && req.headers['content-type'].startsWith('multipart/form-data') 115 | || 116 | req.method === 'POST' && req.headers['content-type'] && req.headers['content-type'].startsWith('application/x-www-form-urlencoded') 117 | || 118 | req.method === 'PUT' && req.headers['content-type'] && req.headers['content-type'].startsWith('multipart/form-data') 119 | || 120 | req.method === 'PUT' && req.headers['content-type'] && req.headers['content-type'].startsWith('application/x-www-form-urlencoded') 121 | ) { 122 | 123 | var bb = busboy({ headers: req.headers }) 124 | let tempFiles = {} 125 | 126 | bb.on('file', function (fieldName, file, info) { 127 | 128 | //ex: file[] 129 | if (isArray(fieldName)) { 130 | if (!req.body[fieldName]) { 131 | req.body[fieldName] = [] 132 | tempFiles[fieldName] = [] 133 | } 134 | } 135 | 136 | 137 | let tempDir = path.join(os.tmpdir(), info.filename ?? 'temp.temp'); 138 | 139 | let fileSize = 0 140 | file.pipe(fse.createWriteStream(tempDir)); 141 | 142 | file.on("data", (data) => { 143 | fileSize += data.length 144 | }) 145 | file.on('end', function () { 146 | 147 | let newFile = { 148 | name: info.filename, 149 | encoding: info.encoding, 150 | type: info.mimeType, 151 | size: fileSize, 152 | sizeUnit: 'bytes', 153 | extension: path.extname(info.filename ?? 'temp.temp'), 154 | tempDir: tempDir 155 | } 156 | 157 | if (isArray(fieldName) && Array.isArray(req.body[fieldName])) { 158 | if (info.filename) { 159 | req.body[fieldName].push(newFile) 160 | } 161 | tempFiles[fieldName].push(newFile) 162 | } 163 | else { 164 | if (info.filename) { 165 | req.body[fieldName] = newFile 166 | } 167 | tempFiles[fieldName] = newFile 168 | } 169 | }) 170 | }) 171 | 172 | bb.on('field', (fieldName, value) => { 173 | parseFields(fieldName, value, req) 174 | }) 175 | 176 | bb.on("finish", () => { 177 | // console.log(body) 178 | clearTempFiles(res, tempFiles) 179 | next() 180 | }) 181 | req.pipe(bb); 182 | } 183 | else { 184 | next() 185 | } 186 | } 187 | 188 | 189 | 190 | export default mediaRequestHandling 191 | 192 | -------------------------------------------------------------------------------- /core/mail/Mail.js: -------------------------------------------------------------------------------- 1 | import mailConfig from "../config/Mail.js"; 2 | import nodemailer from "nodemailer" 3 | import ejs from "ejs" 4 | 5 | /** 6 | * Store testing account 7 | */ 8 | var testingAccount 9 | const mailAccount = async () => { 10 | if (mailConfig.testing) { 11 | if (!testingAccount) { 12 | testingAccount = await nodemailer.createTestAccount() 13 | } 14 | return { 15 | user: testingAccount.user,// generated ethereal user 16 | pass: testingAccount.pass // generated ethereal password 17 | } 18 | } 19 | return { 20 | user: mailConfig.username, 21 | pass: mailConfig.password 22 | } 23 | } 24 | 25 | /** 26 | * create transporter for email 27 | * @returns 28 | */ 29 | const transporter = async () => { 30 | const mailAuth = await mailAccount() 31 | if (!mailAuth.user || !mailAuth.pass) { 32 | console.log("\x1b[31m", 'Mail credential invalid, Check your credential mail username or password', "\x1b[0m"); 33 | throw 'Credential invalid' 34 | } 35 | const transport = { 36 | host: mailConfig.host || "smtp.ethereal.email", 37 | port: mailConfig.port || 587, 38 | secure: mailConfig.port == 465 ? true : false, // true for 465, false for other ports 39 | auth: mailAuth 40 | } 41 | return nodemailer.createTransport(transport); 42 | } 43 | 44 | class Mail { 45 | 46 | /** 47 | * Load message options 48 | * @param {*} param0 49 | */ 50 | async load({ 51 | // ------------------- common fields 52 | from = '', 53 | to = [], 54 | subject = '', 55 | text = '', 56 | html = { 57 | path: '', 58 | data: {} 59 | }, 60 | attachments = [], 61 | cc = [], 62 | bcc = [], 63 | // ------------------- advance fields 64 | sender = '', 65 | replyTo = [], 66 | alternatives = [], 67 | encoding = '', 68 | // ------------------- 69 | } 70 | ) { 71 | if (!from) 72 | throw 'from email address is required' 73 | if (!Array.isArray(to) || to.length === 0) 74 | throw 'receivers is required and must be an array' 75 | if (!Array.isArray(attachments)) 76 | throw 'attachments must be an array of object, see doc: https://nodemailer.com/message/attachments' 77 | if (!Array.isArray(cc) || !Array.isArray(bcc)) 78 | throw 'cc & bcc must be an array email' 79 | if (!Array.isArray(alternatives)) 80 | throw 'alternatives must be an array of object, see doc: https://nodemailer.com/message/alternatives' 81 | if (!Array.isArray(replyTo)) 82 | throw 'replyTo must be an array of string' 83 | 84 | // ------------------- common fields 85 | this.from = from 86 | this.to = to 87 | this.subject = subject 88 | this.text = text 89 | this.attachments = attachments 90 | this.cc = cc 91 | this.bcc = bcc 92 | 93 | if (html && html.path) { 94 | this.html = await this.#renderHtml({ path: html.path.toString(), data: html.data }) 95 | } 96 | // ------------------- advance fields 97 | this.sender = sender 98 | this.replyTo = replyTo 99 | this.encoding = encoding 100 | this.alternatives = alternatives 101 | } 102 | 103 | 104 | /** 105 | * rendering html to string with data if exist 106 | * @param {*} {path and data} 107 | * @returns html string 108 | */ 109 | async #renderHtml({ path = String, data }) { 110 | return await new Promise(async (resolve, reject) => { 111 | await ejs.renderFile(path, data || {}, (err, html) => { 112 | if (err) { 113 | console.log("\x1b[31m", 'Error render html', err, "\x1b[0m"); 114 | reject(err) 115 | } 116 | resolve(html) 117 | }); 118 | }) 119 | } 120 | 121 | 122 | /** 123 | * preparing message options 124 | * @returns 125 | */ 126 | #messageOptions() { 127 | // ------------------- common fields 128 | let message = {} 129 | 130 | if (this.from) { 131 | message["from"] = this.from 132 | } 133 | if (this.to) { 134 | let _to = '' 135 | this.to.forEach((e, i) => { 136 | if (i > 0) { 137 | _to = _to + ", " + e 138 | } else { 139 | _to = _to + e 140 | } 141 | }) 142 | message["to"] = _to 143 | } 144 | if (this.subject) { 145 | message["subject"] = this.subject 146 | } 147 | if (this.text) { 148 | message["text"] = this.text 149 | } 150 | if (this.attachments) { 151 | message["attachments"] = this.attachments 152 | } 153 | if (this.html) { 154 | message["html"] = this.html 155 | } 156 | 157 | if (this.cc) { 158 | let _cc = '' 159 | this.cc.forEach((e, i) => { 160 | if (i > 0) { 161 | _cc = _cc + ", " + e 162 | } else { 163 | _cc = _cc + e 164 | } 165 | }) 166 | message['cc'] = _cc 167 | } 168 | 169 | if (this.bcc) { 170 | let _bcc = '' 171 | this.bcc.forEach((e, i) => { 172 | if (i > 0) { 173 | _bcc = _bcc + ", " + e 174 | } else { 175 | _bcc = _bcc + e 176 | } 177 | }) 178 | message['bcc'] = _bcc 179 | } 180 | // ------------------- advance fields 181 | if (this.sender) { 182 | message['sender'] = this.sender 183 | } 184 | if (this.replyTo) { 185 | let _replyTo = '' 186 | this.replyTo.forEach((e, i) => { 187 | if (i > 0) { 188 | _replyTo = _replyTo + ", " + e 189 | } else { 190 | _replyTo = _replyTo + e 191 | } 192 | }) 193 | message['replyTo'] = _replyTo 194 | } 195 | if (this.encoding) { 196 | message['encoding'] = this.encoding 197 | } 198 | if (this.alternatives) { 199 | message['alternatives'] = this.alternatives 200 | } 201 | 202 | return message 203 | } 204 | 205 | /** 206 | * sending mail 207 | * @returns 208 | */ 209 | async send() { 210 | const _transporter = await transporter() 211 | return await _transporter.sendMail(this.#messageOptions()) 212 | } 213 | 214 | } 215 | 216 | export default Mail 217 | 218 | -------------------------------------------------------------------------------- /core/service/RolePermission/Role.js: -------------------------------------------------------------------------------- 1 | import { Model, DataTypes, Sequelize } from "sequelize"; 2 | import databaseConfig from "../../config/Database.js"; 3 | import db from "../../database/Database.js"; 4 | import Permission from "./Permission.js"; 5 | import RoleHasPermission from "./RoleHasPermission.js"; 6 | import UserHasRole from "./UserHasRole.js"; 7 | 8 | class Role extends Model { 9 | 10 | } 11 | 12 | 13 | Role.init({ 14 | name: { 15 | type: DataTypes.STRING, 16 | allowNull: true, 17 | unique: true 18 | } 19 | }, 20 | { 21 | sequelize: db, // We need to pass the connection instance 22 | tableName: "roles", 23 | modelName: 'Role', // We need to choose the model name 24 | timestamps: true, 25 | underscored: true, 26 | createdAt: "created_at", 27 | updatedAt: "updated_at" 28 | } 29 | ) 30 | 31 | Role.prototype.assignPermissions = async function (permissions) { 32 | await assignPermissions(this, permissions) 33 | } 34 | 35 | 36 | 37 | /** 38 | * Binding any model to role, so any model that binding with this function will have role 39 | * @param {*} model 40 | */ 41 | const hasRole = async (model = Model) => { 42 | 43 | model.belongsToMany(Role, { 44 | through: UserHasRole, 45 | constraints: false, 46 | foreignKey: 'roleable_id' 47 | }) 48 | 49 | Role.belongsToMany(model, { 50 | through: UserHasRole, 51 | constraints: false, 52 | foreignKey: "role_id" 53 | }) 54 | 55 | let includeRolePermissions = { 56 | model: Role, 57 | include: { 58 | model: Permission 59 | } 60 | } 61 | 62 | model.addScope('withRole', { 63 | include: [includeRolePermissions] 64 | }) 65 | 66 | model.options.defaultScope = model.options.defaultScope || {} 67 | model.options.defaultScope.method = model.options.defaultScope.method || [] 68 | model.options.defaultScope.include = model.options.defaultScope.include || [] 69 | 70 | model.options.defaultScope.include.push(includeRolePermissions) 71 | 72 | model.prototype.getRole = function () { 73 | return this.Roles && this.Roles[0] || null 74 | } 75 | 76 | model.prototype.getPermissions = function () { 77 | return this.Roles && this.Roles[0] && this.Roles[0].Permissions || null 78 | } 79 | model.prototype.getPermissionsName = function () { 80 | if (!this.Roles || !this.Roles[0]) 81 | return 82 | 83 | let names = [] 84 | for (let p of this.Roles[0].Permissions) { 85 | names.push(p.name) 86 | } 87 | return names 88 | } 89 | 90 | 91 | model.prototype.setRole = async function (role) { 92 | await setRole( 93 | this, 94 | role 95 | ) 96 | } 97 | 98 | model.prototype.removeRole = async function () { 99 | await UserHasRole.destroy({ 100 | where: { 101 | roleable_id: this.id, 102 | roleable_type: this.constructor.options.name.singular 103 | } 104 | }) 105 | } 106 | 107 | 108 | model.beforeBulkDestroy(async (instance) => { 109 | let _models = await model.findAll({ 110 | where: instance.where 111 | }) 112 | if (_models && _models.length > 0) { 113 | // console.log("_models: ", _models) 114 | for (let _model of _models) { 115 | UserHasRole.destroy({ 116 | where: { 117 | roleable_id: _model.id, 118 | roleable_type: instance.model.options.name.singular 119 | } 120 | }) 121 | } 122 | } 123 | }) 124 | } 125 | 126 | /** 127 | * Binding role model to any model, so that model can have a role 128 | * @param {*} model 129 | * @param {*} role 130 | */ 131 | const setRole = async (model, role) => { 132 | 133 | try { 134 | 135 | const roleable_id = model.id 136 | const roleable_type = model.constructor.options.name.singular 137 | 138 | let roleId = -1 139 | if (isInt(role)) { 140 | roleId = role 141 | } 142 | 143 | const roleModel = await Role.findOne({ 144 | where: { 145 | [Sequelize.Op.or]: [ 146 | { id: roleId }, 147 | { name: role ?? '' } 148 | ] 149 | } 150 | }) 151 | 152 | if (!roleModel || !roleable_id || !roleable_type) 153 | throw "role not found" 154 | 155 | // console.log("roleModel.id", roleModel.id) 156 | // console.log("roleModel.name", roleModel.name) 157 | // console.log("roleable_id", roleable_id) 158 | // console.log("roleable_type", roleable_type) 159 | 160 | const userRole = await UserHasRole.findOne({ 161 | where: { 162 | roleable_id: roleable_id, 163 | roleable_type: roleable_type 164 | }, 165 | }) 166 | 167 | if (userRole) { 168 | // console.log("update role model") 169 | const updated = await UserHasRole.update({ 170 | role_id: roleModel.id 171 | }, { 172 | where: { 173 | roleable_id: roleable_id, 174 | roleable_type: roleable_type 175 | } 176 | }) 177 | if (updated > 0) return true 178 | } 179 | else { 180 | // console.log("create new role for user") 181 | const created = await UserHasRole.create({ 182 | role_id: roleModel.id, 183 | roleable_id: roleable_id, 184 | roleable_type: roleable_type 185 | }) 186 | if (created) return true 187 | } 188 | } catch (error) { 189 | console.error(error) 190 | } 191 | return false; 192 | } 193 | 194 | 195 | /** 196 | * Load role Model & UserHasModel 197 | * @param {*} alter 198 | */ 199 | const loadRole = async (alter = false) => { 200 | 201 | 202 | await alterTableRoleHandling(alter) 203 | 204 | await Role.sync({ 205 | alter: alter 206 | }) 207 | 208 | await UserHasRole.sync({ 209 | alter: alter 210 | }) 211 | 212 | 213 | // UserHasRole.belongsToMany(Role, { 214 | // foreignKey: 'role_id', 215 | // as: 'role', 216 | // through: "userhasroles", 217 | // constraints: false, 218 | // }) 219 | 220 | // Role.belongsToMany(UserHasRole, { 221 | // through: 'userhasroles', 222 | // as: 'user_has_role', 223 | // foreignKey: 'role_id', 224 | // constraints: false, 225 | // }); 226 | 227 | } 228 | 229 | /** 230 | * Used for handling multiple index before alter role table 231 | * @param {*} alter 232 | */ 233 | const alterTableRoleHandling = async (alter = false) => { 234 | // handling for multiple index of url 235 | try { 236 | if (alter) { 237 | await db.query(`ALTER TABLE roles DROP INDEX name`).then(() => { 238 | }) 239 | } 240 | } catch (error) { 241 | if (databaseConfig.dialect == "mysql") { 242 | console.log("Failed alter roles drop index name, roles not exist yet") 243 | } 244 | } 245 | } 246 | 247 | 248 | /** 249 | * Assigning permission to a role 250 | * @param {*} role 251 | * @param {*} permissions 252 | */ 253 | const assignPermissions = async (role, permissions) => { 254 | try { 255 | await RoleHasPermission.destroy({ 256 | where: { 257 | role_id: role.id 258 | } 259 | }) 260 | 261 | if (Array.isArray(permissions)) { 262 | if (permissions.every(element => typeof element === "number")) { 263 | for (let id of permissions) { 264 | try { 265 | await RoleHasPermission.create({ 266 | role_id: role.id, 267 | permission_id: id 268 | }) 269 | } catch (error) { 270 | console.error(error) 271 | } 272 | } 273 | } 274 | if (permissions.every(element => typeof element === "string")) { 275 | for (let name of permissions) { 276 | try { 277 | let permission = await Permission.findOne({ where: { name: name } }) 278 | if (permission) { 279 | await RoleHasPermission.create({ 280 | role_id: role.id, 281 | permission_id: permission.id 282 | }) 283 | } 284 | } catch (error) { 285 | console.error(error) 286 | } 287 | } 288 | } 289 | } 290 | else { 291 | 292 | } 293 | } catch (error) { 294 | console.error(error) 295 | } 296 | } 297 | 298 | function isInt(value) { 299 | return typeof value === 'number' && 300 | isFinite(value) && 301 | Math.floor(value) === value; 302 | } 303 | 304 | 305 | export default Role 306 | export { loadRole, hasRole } 307 | 308 | 309 | 310 | 311 | // const { User, Task } = require('./models'); 312 | 313 | // User.defaultScope(function(options) { 314 | // const { withTasks } = options; 315 | // const scopes = {}; 316 | 317 | // if (withTasks) { 318 | // scopes.include = [{ 319 | // model: Task 320 | // }]; 321 | // } 322 | 323 | // return scopes; 324 | // }); 325 | 326 | // User.findAll({ withTasks: true }) 327 | // .then(users => { 328 | // console.log(users.map(user => user.toJSON())); 329 | // }) 330 | // .catch(error => { 331 | // console.error(error); 332 | // }); -------------------------------------------------------------------------------- /controllers/AuthController.js: -------------------------------------------------------------------------------- 1 | import User from "../models/User.js" 2 | import bcrypt from 'bcrypt' 3 | import RegisterRequest from "../requests/auth/RegisterRequest.js" 4 | import JwtAuth from "../core/auth/JwtAuth.js" 5 | import AccountVerify from "../mails/AccountVerify/AccountVerify.js" 6 | import ForgotPassword from "../mails/ForgotPassword/ForgotPassword.js" 7 | import { Op } from "sequelize" 8 | import crypto from "crypto" 9 | import mailConfig from "../core/config/Mail.js" 10 | import AuthConfig from "../core/config/Auth.js" 11 | import ResetPasswordRequest from "../requests/auth/ResetPasswordRequest.js" 12 | import ForgotPasswordRequest from "../requests/auth/ForgotPasswordRequest.js" 13 | import LoginRequest from "../requests/auth/LoginRequest.js" 14 | 15 | export default class AuthController { 16 | 17 | /** 18 | * login 19 | * @param {*} req express req 20 | * @param {*} res express res 21 | * @returns res 22 | */ 23 | static async login(req, res) { 24 | 25 | try { 26 | 27 | const request = new LoginRequest(req) 28 | await request.check() 29 | if (request.isError) return request.responseError(res) 30 | 31 | const { email, password } = req.body 32 | 33 | const user = await User.findOne({ where: { email: email } }) 34 | 35 | const match = await bcrypt.compare(password, user.password) 36 | 37 | if (!match) { 38 | request.addError("password", "wrong password") 39 | return request.responseError(res) 40 | } 41 | 42 | if (AuthConfig.emailVerification && !user.verified_at) { 43 | return res.json({ message: "Verify your account first.." }) 44 | } 45 | 46 | const payload = { 47 | id: user.id, 48 | name: user.name, 49 | email: user.email 50 | } 51 | 52 | const token = JwtAuth.createToken(payload) 53 | 54 | await user.update({ 55 | refresh_token: token.refreshToken 56 | }) 57 | 58 | res.cookie('refreshToken', token.refreshToken, { 59 | httpOnly: true, 60 | maxAge: 24 * 60 * 60 * 1000, 61 | // secure: true 62 | }) 63 | res.json({ message: "Login success", "accessToken": token.accessToken }) 64 | 65 | } catch (error) { 66 | console.log(error) 67 | res.status(409).json({ message: "something went wrong", reason: String(error) }) 68 | } 69 | } 70 | 71 | /** 72 | * register 73 | * @param {*} req express req 74 | * @param {*} res express res 75 | * @returns res 76 | */ 77 | static async register(req, res) { 78 | try { 79 | 80 | const request = new RegisterRequest(req) 81 | await request.check() 82 | if (request.isError) return request.responseError(res) 83 | 84 | var { name, email, password } = req.body 85 | const verificationToken = randomTokenString() 86 | password = await passwordHash(password); 87 | const user = await User.create({ 88 | name: name, 89 | email: email, 90 | password: password, 91 | verification_token: verificationToken 92 | }) 93 | 94 | await user.setRole("customer") 95 | 96 | if (AuthConfig.emailVerification && mailConfig.host) { 97 | const sendMail = new AccountVerify(mailConfig.from, [email], "Verify Your Account", verificationToken) 98 | await sendMail.send() 99 | } 100 | 101 | res.json({ message: "Register success" }).status(200) 102 | 103 | } catch (error) { 104 | console.log(error) 105 | res.status(409).json({ message: "something went wrong", reason: String(error) }) 106 | } 107 | } 108 | 109 | /** 110 | * email verification 111 | * @param {*} req express req 112 | * @param {*} res express res 113 | * @returns res 114 | */ 115 | static async emailVerification(req, res) { 116 | try { 117 | 118 | const user = await User.findOne({ where: { verification_token: req.params.token } }) 119 | 120 | if (!user) { 121 | return res.json({ message: "Invalid token." }) 122 | } 123 | 124 | await user.update({ 125 | verification_token: '', 126 | verified_at: new Date() 127 | }) 128 | 129 | res.status(200).json({ message: "Email verification success" }) 130 | 131 | } catch (error) { 132 | console.log(error) 133 | res.status(409).json({ message: "something went wrong", reason: String(error) }) 134 | } 135 | } 136 | 137 | /** 138 | * refresh token 139 | * @param {*} req express req 140 | * @param {*} res express res 141 | * @returns res 142 | */ 143 | static async refreshToken(req, res) { 144 | 145 | try { 146 | 147 | const refreshToken = req.cookies.refreshToken 148 | 149 | if (!refreshToken) return res.sendStatus(401) 150 | 151 | const user = await User.findOne( 152 | { 153 | where: { 154 | refresh_token: refreshToken 155 | } 156 | } 157 | ) 158 | 159 | if (!user) return res.sendStatus(403) 160 | 161 | const accessToken = JwtAuth.regenerateAccessToken(refreshToken) 162 | 163 | if (!accessToken) res.status(403) 164 | 165 | res.json({ message: "Get token success", "accessToken": accessToken }) 166 | 167 | } catch (error) { 168 | console.log(error) 169 | res.status(409).json({ message: "something went wrong", reason: String(error) }) 170 | } 171 | } 172 | 173 | /** 174 | * logout 175 | * @param {*} req express req 176 | * @param {*} res express res 177 | * @returns res 178 | */ 179 | static async logout(req, res) { 180 | 181 | try { 182 | 183 | const refreshToken = req.cookies.refreshToken 184 | 185 | if (!refreshToken) return res.sendStatus(204) 186 | 187 | const user = await User.findOne( 188 | { 189 | where: { 190 | refresh_token: refreshToken 191 | } 192 | } 193 | ) 194 | 195 | if (!user) return res.sendStatus(204) 196 | 197 | await user.update({ 198 | refresh_token: null 199 | }) 200 | 201 | res.clearCookie("refreshToken") 202 | 203 | res.status(200).json({ message: "Logout success" }) 204 | 205 | } catch (error) { 206 | console.log(error) 207 | res.status(409).json({ message: "something went wrong", reason: String(error) }) 208 | } 209 | 210 | } 211 | 212 | /** 213 | * forgot password 214 | * @param {*} req express req 215 | * @param {*} res express res 216 | * @returns res 217 | */ 218 | static async forgotPassword(req, res) { 219 | 220 | try { 221 | 222 | if (!mailConfig.host) throw Error("You are not set SMTP at MAIL_HOST on .env") 223 | 224 | const request = new ForgotPasswordRequest(req) 225 | await request.check() 226 | if (request.isError) return request.responseError(res) 227 | 228 | const { email } = req.body 229 | 230 | const user = await User.findOne( 231 | { 232 | where: { 233 | email: email 234 | } 235 | } 236 | ) 237 | 238 | const resetToken = randomTokenString() 239 | user.reset_token = resetToken 240 | user.reset_token_expires = new Date(Date.now() + 24 * 60 * 60 * 1000) 241 | await user.save() 242 | 243 | const sendMail = new ForgotPassword(mailConfig.from, [email], "Password Reset Request", resetToken) 244 | await sendMail.send() 245 | 246 | res.json({ message: "Please check your email." }) 247 | 248 | } catch (error) { 249 | console.log(error) 250 | res.status(409).json({ message: "something went wrong", reason: String(error) }) 251 | } 252 | } 253 | 254 | /** 255 | * reset password 256 | * @param {*} req express req 257 | * @param {*} res express res 258 | * @returns res 259 | */ 260 | static async resetPassword(req, res) { 261 | 262 | try { 263 | 264 | const token = req.params.token 265 | 266 | const request = new ResetPasswordRequest(req) 267 | await request.check() 268 | if (request.isError) return request.responseError(res) 269 | 270 | const user = await User.findOne({ 271 | where: { 272 | reset_token: token, 273 | reset_token_expires: { [Op.gt]: Date.now() } 274 | } 275 | }) 276 | 277 | if (!user) { 278 | request.addError("token", "Inavalid token or token expired") 279 | return request.responseError(res) 280 | } 281 | 282 | const { new_password } = req.body 283 | 284 | user.password = await passwordHash(new_password) 285 | user.password_reset = Date.now() 286 | user.reset_token = null 287 | user.reset_token_expires = null 288 | await user.save() 289 | 290 | res.json({ message: "Your password has been successfully reset." }) 291 | 292 | } catch (error) { 293 | console.log(error) 294 | res.status(409).json({ message: "something went wrong", reason: String(error) }) 295 | } 296 | } 297 | } 298 | 299 | async function passwordHash(password) { 300 | const salt = await bcrypt.genSalt() 301 | return await bcrypt.hash(password, salt) 302 | } 303 | 304 | function randomTokenString() { 305 | return crypto.randomBytes(25).toString('hex') 306 | } 307 | -------------------------------------------------------------------------------- /core/service/Media/MediaService.js: -------------------------------------------------------------------------------- 1 | import { Model, DataTypes } from "sequelize"; 2 | import { v4 as uuid4 } from 'uuid' 3 | import path from 'path' 4 | import fse from 'fs-extra' 5 | import db from "../../database/Database.js" 6 | import mediaConfig, { mediaStorages } from "../../config/Media.js"; 7 | import FirebaseService from "../Firebase/FirebaseService.js"; 8 | 9 | 10 | class Media extends Model { 11 | 12 | // getMediatable(options) { 13 | // if (!this.mediatable_type) return Promise.resolve(null); 14 | // const mixinMethodName = `get${uppercaseFirst(this.mediatable_type)}`; 15 | // return this[mixinMethodName](options); 16 | // } 17 | 18 | getUrl() 19 | { 20 | if (this.media_storage === mediaStorages.local) { 21 | return normalizeLocalStorageToUrl(this.url) 22 | } 23 | return this.url 24 | } 25 | 26 | } 27 | 28 | Media.init({ 29 | name: { 30 | type: DataTypes.STRING, 31 | allowNull: true, 32 | }, 33 | description: { 34 | type: DataTypes.TEXT, 35 | allowNull: true, 36 | }, 37 | info: { 38 | type: DataTypes.JSON, 39 | allowNull: false, 40 | }, 41 | media_storage: { 42 | type: DataTypes.STRING, 43 | allowNull: true, 44 | }, 45 | path: { 46 | type: DataTypes.TEXT, 47 | allowNull: false, 48 | }, 49 | url: { 50 | type: DataTypes.TEXT, 51 | allowNull: false, 52 | // unique: true 53 | }, 54 | mediatable_id: { 55 | type: DataTypes.INTEGER, 56 | allowNull: false, 57 | }, 58 | mediatable_type: { 59 | type: DataTypes.STRING, 60 | allowNull: false 61 | } 62 | }, 63 | { 64 | sequelize: db, // We need to pass the connection instance 65 | tableName: "medias", 66 | modelName: 'Media', // We need to choose the model name 67 | timestamps: true, 68 | underscored: true, 69 | createdAt: "created_at", 70 | updatedAt: "updated_at" 71 | } 72 | ) 73 | 74 | 75 | /** 76 | * Load media model 77 | * @param {*} param0 78 | */ 79 | Media.loadSync = async function ({ alter = false }) { 80 | // // handling for multiple index of url 81 | // try { 82 | // if (alter) { 83 | // await db.query(`ALTER TABLE medias DROP INDEX url`).then(() => { 84 | // }) 85 | // } 86 | // } catch (error) { 87 | // if (databaseConfig.dialect == "mysql") { 88 | // console.log("Failed alter medias drop index url, medias not exist yet") 89 | // } 90 | // } 91 | await Media.sync({ 92 | alter: alter, 93 | }) 94 | } 95 | 96 | // ------------------------------------------------------------------------------------------- binding with any model 97 | /** 98 | * binding any model to media, so any model can have media 99 | * @param {*} model 100 | */ 101 | const hasMedia = async (model = Model) => { 102 | 103 | model.hasMany(Media, { 104 | // as: Media.name, 105 | foreignKey: 'mediatable_id', 106 | constraints: false, 107 | scope: { 108 | mediatable_type: model.options.name.singular 109 | } 110 | }) 111 | Media.belongsTo(model, { foreignKey: 'mediatable_id', constraints: false }) 112 | 113 | let includeMedia = { 114 | model: Media 115 | } 116 | 117 | model.addScope('withMedia', { 118 | include: [includeMedia] 119 | }) 120 | 121 | model.options.defaultScope = model.options.defaultScope || {}; 122 | model.options.defaultScope.method = model.options.defaultScope.method || []; 123 | model.options.defaultScope.include = model.options.defaultScope.include || [] 124 | 125 | model.options.defaultScope.include.push(includeMedia) 126 | 127 | 128 | model.prototype.getMedia = function () { 129 | return getMedia(this) 130 | } 131 | 132 | 133 | /** 134 | * get media object by name 135 | * @param {*} name 136 | * @returns array of media object 137 | */ 138 | model.prototype.getMediaByName = function (name) { 139 | let _medias = getMedia(this) 140 | if (!_medias || !name) 141 | return 142 | 143 | for (let m of _medias) { 144 | if (m.name === name) { 145 | return m 146 | } 147 | } 148 | return 149 | } 150 | /** 151 | * get single first media object 152 | * @returns media object 153 | */ 154 | model.prototype.getFirstMedia = function () { 155 | let _medias = getMedia(this) 156 | if (!_medias) 157 | return 158 | 159 | return _medias[0] || null 160 | } 161 | 162 | /** 163 | * get array of media object with exception 164 | * @param {*} except string or array if string 165 | * @returns array of media object 166 | */ 167 | model.prototype.getMediaExcept = function (except) { 168 | let _medias = getMedia(this) 169 | if (!_medias) 170 | return [] 171 | 172 | const data = [] 173 | 174 | if (typeof except === "string") { 175 | for (let media of _medias) { 176 | if (media.name != except) { 177 | data.push(media) 178 | } 179 | } 180 | } else 181 | if (Array.isArray(except)) { 182 | for (let media of _medias) { 183 | if (!except.includes(media.name)) { 184 | data.push(media) 185 | } 186 | } 187 | } 188 | return data 189 | } 190 | 191 | /** 192 | * get array of media url 193 | */ 194 | model.prototype.getMediaUrl = function () { 195 | let _medias = getMedia(this) 196 | if (!_medias) 197 | return [] 198 | 199 | const urls = [] 200 | for (let media of _medias) { 201 | urls.push(media.url) 202 | } 203 | return urls 204 | } 205 | 206 | /** 207 | * get array of media url with exception 208 | */ 209 | model.prototype.getMediaUrlExcept = function (except) { 210 | let _medias = this.getMediaExcept(except) 211 | if (!_medias) 212 | return [] 213 | 214 | const urls = [] 215 | for (let media of _medias) { 216 | urls.push(media.url) 217 | } 218 | return urls 219 | } 220 | 221 | 222 | 223 | /** 224 | * save media 225 | * @param {*} file 226 | * @param {*} name 227 | */ 228 | model.prototype.saveMedia = async function (file, name) { 229 | return await saveMedia({ 230 | model: this, 231 | file: file, 232 | name: name 233 | }) 234 | } 235 | 236 | /** 237 | * destroy media 238 | * @param {*} name 239 | * @returns 240 | */ 241 | model.prototype.destroyMedia = async function (name) { 242 | if (!name) 243 | throw "need media name" 244 | 245 | if (!this.Media) 246 | return false 247 | 248 | let _medias = await this.getMedia() 249 | let status = false; 250 | for (let i = 0; i < _medias.length; i++) { 251 | if (_medias[i].name === name) { 252 | await Media.destroy({ 253 | where: { 254 | name: name, 255 | mediatable_id: this.id, 256 | mediatable_type: this.constructor.name 257 | } 258 | }) 259 | try { 260 | if (_medias[i].media_storage === mediaStorages.local) { 261 | await fse.remove(_medias[i].path) 262 | } 263 | if (_medias[i].media_storage === mediaStorages.firebase) { 264 | await FirebaseService.deleteMedia(_medias[i].path) 265 | } 266 | status = true; 267 | } catch (error) { 268 | console.log("error", error) 269 | } 270 | if (status) { 271 | this.Media.splice(i, 1) 272 | break 273 | } 274 | } 275 | } 276 | return status 277 | } 278 | 279 | 280 | model.beforeBulkDestroy(async (instance) => { 281 | 282 | let _models = await model.findAll({ 283 | where: instance.where 284 | }) 285 | if (_models && _models.length > 0) { 286 | for (let _model of _models) { 287 | let _medias = await _model.getMedia() 288 | Media.destroy({ 289 | where: { 290 | mediatable_id: _model.id, 291 | mediatable_type: instance.model.options.name.singular 292 | } 293 | }) 294 | let paths = getPathsFirebaseFromMedias(_medias) 295 | if (paths.length) { 296 | FirebaseService.deleteMedias(paths) 297 | } 298 | 299 | let _pathlocalStorage = getPathLocalStorage(_medias) // result storage/user-1 300 | if (_pathlocalStorage) { 301 | try { 302 | fse.remove(_pathlocalStorage) 303 | } catch (error) { 304 | console.log(error) 305 | } 306 | } 307 | 308 | } 309 | } 310 | 311 | }) 312 | 313 | 314 | } 315 | 316 | 317 | /** 318 | * return list of dataValues of models 319 | * @param {*} _model any model that has binded with media model 320 | * @returns 321 | */ 322 | const getMedia = (_model) => { 323 | if (!_model.Media) // from include default scope 324 | return 325 | 326 | let _medias = [] 327 | let _media = {} 328 | for (let m of _model.Media) { 329 | _media = {} 330 | for (let key in m.dataValues) { 331 | _media[key] = m.dataValues[key] 332 | } 333 | if (_media.media_storage === mediaStorages.local) { 334 | // _media["path"] = _media["url"] 335 | _media["url"] = normalizeLocalStorageToUrl(_media["url"]) 336 | } 337 | _medias.push(_media) 338 | } 339 | return _medias 340 | } 341 | 342 | 343 | // ------------------------------------------------------------------------------------------- delete file 344 | 345 | 346 | // ------------------------------------------------------------------------------------------- store file functions 347 | /** 348 | * store media to local storage 349 | * @param {*} file 350 | * @param {*} mediatable_type 351 | * @param {*} mediatable_id 352 | * @returns { path, url } 353 | */ 354 | const saveToLocal = async (file, mediatable_type, mediatable_id) => { 355 | return await new Promise(async (resolve, reject) => { 356 | const folderName = mediaConfig.localStorageDirectory + "/" + mediatable_type + "-" + mediatable_id 357 | const fileName = uuid4() + file.extension 358 | const targetDir = path.join(folderName, fileName); 359 | // create folder if not exist 360 | if (!fse.existsSync(folderName)) { 361 | await fse.mkdirSync(folderName, { recursive: true }); 362 | } 363 | 364 | // moving from temporary dir 365 | await fse.move(file.tempDir, targetDir, (err) => { 366 | if (err) { 367 | console.log(err); 368 | return reject(); 369 | } 370 | return resolve({ path: targetDir, url: targetDir }) 371 | }) 372 | }) 373 | } 374 | 375 | 376 | 377 | /** 378 | * save a media 379 | * @param { model = Model, file = Object, name = String } 380 | * @returns media url 381 | */ 382 | const saveMedia = async ({ model = Model, file = Object, name = String }) => { 383 | if (!file || !file.extension || !name || !model) { 384 | console.log("Save media failed: require all params, Please check file or name") 385 | return 386 | } 387 | const mediatable_id = model.id 388 | const mediatable_type = model.constructor.options.name.singular 389 | 390 | 391 | var targetStorage 392 | if (mediaConfig.mediaStorage === mediaStorages.local) { 393 | targetStorage = await saveToLocal(file, mediatable_type, mediatable_id) 394 | } 395 | if (mediaConfig.mediaStorage === mediaStorages.firebase) { 396 | targetStorage = await FirebaseService.saveMedia(file) 397 | } 398 | 399 | 400 | if (targetStorage) { 401 | 402 | delete file.tempDir 403 | 404 | const media = await Media.findOne({ 405 | where: { 406 | mediatable_id: mediatable_id, 407 | mediatable_type: mediatable_type, 408 | name: name 409 | } 410 | }) 411 | var newMedia 412 | if (!media) { 413 | // create new media 414 | newMedia = await Media.create({ 415 | mediatable_id: mediatable_id, 416 | mediatable_type: mediatable_type, 417 | url: targetStorage.url, 418 | path: targetStorage.path, 419 | info: JSON.stringify(file), 420 | name: name, 421 | media_storage: mediaConfig.mediaStorage 422 | }) 423 | 424 | } 425 | else { 426 | // update media exists before with same name 427 | 428 | // remove old media file 429 | try { 430 | if (media.media_storage === mediaStorages.local) { 431 | fse.remove(media.path) 432 | } 433 | if (media.media_storage === mediaStorages.firebase) { 434 | FirebaseService.deleteMedia(media.path) 435 | } 436 | } catch (error) { 437 | } 438 | 439 | const updated = await media.update({ 440 | url: targetStorage.url, 441 | path: targetStorage.path, 442 | info: JSON.stringify(file), 443 | media_storage: mediaConfig.mediaStorage 444 | }) 445 | if (updated) { 446 | newMedia = media; 447 | newMedia["url"] = targetStorage.url 448 | newMedia["path"] = targetStorage.path 449 | newMedia["info"] = JSON.stringify(file) 450 | newMedia["media_storage"] = mediaConfig.mediaStorage 451 | } 452 | } 453 | 454 | if (newMedia) { 455 | if (mediaConfig.mediaStorage === mediaStorages.local) { 456 | newMedia["url"] = normalizeLocalStorageToUrl(targetStorage.url) 457 | } 458 | return newMedia 459 | } 460 | 461 | } 462 | return null 463 | } 464 | // ------------------------------------------------------------------------------------------- helpers 465 | /** 466 | * normalize media path to url 467 | * ex: path => /storage/user-1/image.jpg 468 | * result => http://localhost:8000/user-1/image.jpd 469 | * @param {*} filePath 470 | * @returns url 471 | */ 472 | 473 | const normalizeLocalStorageToUrl = (filePath) => { 474 | let directories = filePath.split(path.sep) 475 | directories = directories.slice(1) // remove first path -> /storage/ 476 | let newPath = directories.join(path.sep) 477 | return mediaConfig.rootMediaUrl + newPath.replace(/\\/g, "/") 478 | } 479 | // ------------------------------------------------------------------------------------------- 480 | /** 481 | * load media model 482 | */ 483 | const loadMedia = async () => { 484 | await Media.loadSync({ 485 | alter: true 486 | }) 487 | } 488 | 489 | 490 | /** 491 | * get path local storage if on there is a media stored on local storage 492 | * @param {*} medias 493 | * @returns ex: storage/user-1 494 | */ 495 | const getPathLocalStorage = (medias) => { 496 | if (!medias || !Array.isArray(medias)) 497 | return 498 | try { 499 | for (let m of medias) { 500 | if (m.media_storage === mediaStorages.local) { 501 | // console.log("m.path:", m.path) 502 | const parts = m.path.split(path.sep) 503 | return parts.slice(0, 2).join('/'); 504 | } 505 | } 506 | } catch (error) { 507 | console.log(error) 508 | } 509 | return 510 | } 511 | 512 | /** 513 | * get firebase paths from array of media object 514 | * @param {*} medias 515 | * @returns 516 | */ 517 | const getPathsFirebaseFromMedias = (medias) => { 518 | if (!medias || !Array.isArray(medias)) 519 | return [] 520 | 521 | let paths = [] 522 | 523 | try { 524 | for (let m of medias) { 525 | if (m.media_storage === mediaStorages.firebase) { 526 | paths.push(m.path) 527 | } 528 | } 529 | } catch (error) { 530 | console.log(error) 531 | } 532 | 533 | return paths 534 | 535 | } 536 | // ------------------------------------------------------------------------------------------- 537 | 538 | export default Media 539 | export { hasMedia, loadMedia, getPathLocalStorage } 540 | -------------------------------------------------------------------------------- /core/locale/LangValidation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Default error message 3 | */ 4 | const langValidation = Object.freeze({ 5 | required: { 6 | "en": "The _attribute_ is required", 7 | "id": "_attribute_ wajib di isi", 8 | "es": "El campo _attribute_ es obligatorio", 9 | "hi": "_attribute_ आवश्यक है", 10 | "ru": "Поле _attribute_ является обязательным", 11 | "pt": "O campo _attribute_ é obrigatório", 12 | "zh": "_attribute_ 必填", 13 | "ja": "_attribute_ は必須です" 14 | }, 15 | email: { 16 | "en": "The _attribute_ must be in E-mail format", 17 | "id": "_attribute_ harus dalam format E-mail", 18 | "es": "El campo _attribute_ debe ser un correo electrónico", 19 | "hi": "_attribute_ ईमेल फॉर्मेट में होना चाहिए", 20 | "ru": "Поле _attribute_ должно быть в формате электронной почты", 21 | "pt": "O campo _attribute_ deve estar em formato de e-mail", 22 | "zh": "_attribute_ 必须是电子邮件格式", 23 | "ja": "_attribute_ はメールアドレス形式である必要があります" 24 | }, 25 | match: { 26 | "en": "The _attribute_ must be match with _param1_", 27 | "id": "_attribute_ harus sama dengan _param1_", 28 | "es": "El campo _attribute_ debe coincidir con _param1_", 29 | "hi": "_attribute_ _param1_ से मेल खाना चाहिए", 30 | "ru": "Поле _attribute_ должно совпадать с _param1_", 31 | "pt": "O campo _attribute_ deve corresponder a _param1_", 32 | "zh": "_attribute_ 必须与 _param1_ 相匹配", 33 | "ja": "_attribute_ は _param1_ と一致する必要があります" 34 | }, 35 | string: { 36 | "en": "The _attribute_ must be in string format", 37 | "id": "_attribute_ harus dalam format string", 38 | "es": "El campo _attribute_ debe estar en formato de texto", 39 | "hi": "_attribute_ स्ट्रिंग फॉर्मेट में होना चाहिए", 40 | "ru": "Поле _attribute_ должно быть в формате строки", 41 | "pt": "O campo _attribute_ deve estar no formato de string", 42 | "zh": "_attribute_ 必须为字符串格式", 43 | "ja": "_attribute_ は文字列形式である必要があります" 44 | }, 45 | float: { 46 | "en": "The _attribute_ must be numeric", 47 | "id": "_attribute_ harus dalam format angka", 48 | "es": "El _attribute_ debe ser numérico", 49 | "hi": "_attribute_ संख्यात्मक होना चाहिए", 50 | "ru": "_attribute_ должно быть числовым", 51 | "pt": "O _attribute_ deve ser numérico", 52 | "zh": "_attribute_ 必须为数字", 53 | "ja": "_attribute_ は数値でなければなりません" 54 | }, 55 | integer: { 56 | "en": "The _attribute_ must be integer", 57 | "id": "_attribute_ harus bilangan bulat", 58 | "es": "El _attribute_ debe ser entero", 59 | "hi": "_attribute_ पूर्णांक होना चाहिए", 60 | "ru": "_attribute_ должно быть целочисленным", 61 | "pt": "O _attribute_ deve ser um número inteiro", 62 | "zh": "_attribute_ 必须是整数", 63 | "ja": "_attribute_ は整数でなければなりません" 64 | }, 65 | max: { 66 | "en": "The _attribute_ should be less or equal then _param1_", 67 | "id": "_attribute_ harus kurang atau sama dengan _param1_", 68 | "es": "El _attribute_ debe ser menor o igual que _param1_", 69 | "hi": "_attribute_ _param1_ से कम या बराबर होना चाहिए", 70 | "ru": "_attribute_ должно быть меньше или равно _param1_", 71 | "pt": "O _attribute_ deve ser menor ou igual a _param1_", 72 | "zh": "_attribute_ 应该小于或等于 _param1_", 73 | "ja": "_attribute_ は _param1_ 以下でなければなりません" 74 | }, 75 | min: { 76 | "en": "The _attribute_ should be more or equal then _param1_", 77 | "id": "_attribute_ harus lebih atau sama dengan _param1_", 78 | "es": "El _attribute_ debe ser mayor o igual que _param1_", 79 | "hi": "_attribute_ _param1_ से अधिक या बराबर होना चाहिए", 80 | "ru": "_attribute_ должно быть больше или равно _param1_", 81 | "pt": "O _attribute_ deve ser maior ou igual a _param1_", 82 | "zh": "_attribute_ 应该大于或等于 _param1_", 83 | "ja": "_attribute_ は _param1_ 以上でなければなりません" 84 | }, 85 | date: { 86 | "en": "The _attribute_ must be in date format", 87 | "id": "_attribute_ harus dalam format tanggal", 88 | "es": "El campo _attribute_ debe estar en formato fecha", 89 | "hi": "_attribute_ तिथि प्रारूप में होना चाहिए", 90 | "ru": "Поле _attribute_ должно быть в формате даты", 91 | "pt": "O campo _attribute_ deve estar em formato de data", 92 | "zh": "_attribute_必须是日期格式", 93 | "ja": "_attribute_は日付形式でなければなりません" 94 | }, 95 | array: { 96 | "en": "The _attribute_ must be in array format", 97 | "id": "_attribute_ harus dalam format array", 98 | "es": "El campo _attribute_ debe estar en formato de arreglo", 99 | "hi": "_attribute_ एक सरणी प्रारूप में होना चाहिए", 100 | "ru": "Поле _attribute_ должно быть в формате массива", 101 | "pt": "O campo _attribute_ deve estar em formato de array", 102 | "zh": "_attribute_必须是数组格式", 103 | "ja": "_attribute_は配列形式でなければなりません" 104 | }, 105 | exists: { 106 | "en": "The _attribute_ not recorded", 107 | "id": "_attribute_ tidak terdaftar", 108 | "es": "El _attribute_ no está registrado", 109 | "hi": "_attribute_ दर्ज नहीं हुआ", 110 | "ru": "_attribute_ не зарегистрирован", 111 | "pt": "O _attribute_ não está registrado", 112 | "zh": "_attribute_ 没有记录", 113 | "ja": "_attribute_ は記録されていません" 114 | }, 115 | unique: { 116 | "en": "The _attribute_ already used", 117 | "id": "_attribute_ sudah digunakan", 118 | "es": "El _attribute_ ya ha sido utilizado", 119 | "hi": "_attribute_ पहले से ही उपयोग में है", 120 | "ru": "_attribute_ уже используется", 121 | "pt": "O _attribute_ já está sendo usado", 122 | "zh": "_attribute_ 已经被使用", 123 | "ja": "_attribute_ はすでに使用されています" 124 | }, 125 | mimetypes: { 126 | "en": "The _attribute_ must be in file format of _param1_", 127 | "id": "_attribute_ harus dalam format file dari _param1_", 128 | "es": "El _attribute_ debe estar en el formato de archivo _param1_", 129 | "hi": "_attribute_ _param1_ फ़ाइल प्रारूप में होना चाहिए", 130 | "ru": "_attribute_ должен быть в формате файла _param1_", 131 | "pt": "O _attribute_ deve estar no formato de arquivo _param1_", 132 | "zh": "_attribute_ 必须是 _param1_ 文件格式", 133 | "ja": "_attribute_ はファイル形式が _param1_ でなければなりません" 134 | }, 135 | mimes: { 136 | "en": "The _attribute_ must be in have extention of _param1_", 137 | "id": "_attribute_ harus memiliki extensi _param1_", 138 | "es": "El _attribute_ debe tener la extensión _param1_", 139 | "hi": "_attribute_ के पास _param1_ एक्सटेंशन होना चाहिए", 140 | "ru": "_attribute_ должен иметь расширение _param1_", 141 | "pt": "O _attribute_ deve ter a extensão _param1_", 142 | "zh": "_attribute_ 必须有 _param1_ 扩展名", 143 | "ja": "_attribute_ は _param1_ 拡張子を持っている必要があります" 144 | }, 145 | max_file: { 146 | "en": "The _attribute_ should have file size less or equal then _param1_ _param2_", 147 | "id": "_attribute_ harus memiliki ukuran file kurang atau sama dengan _param1_ _param2_", 148 | "es": "El archivo _attribute_ debe tener un tamaño menor o igual que _param1_ _param2_", 149 | "hi": "फ़ाइल _attribute_ का आकार _param1_ _param2_ से कम या बराबर होना चाहिए", 150 | "ru": "Размер файла _attribute_ должен быть меньше или равен _param1_ _param2_", 151 | "pt": "O tamanho do arquivo _attribute_ deve ser menor ou igual a _param1_ _param2_", 152 | "zh": "_attribute_ 文件大小应小于等于 _param1_ _param2_", 153 | "ja": "_attribute_ のファイルサイズは _param1_ _param2_以下である必要があります" 154 | }, 155 | image: { 156 | "en": "The _attribute_ should be image format", 157 | "id": "_attribute_ harus dalam format gambar", 158 | "es": "El archivo _attribute_ debe ser de formato imagen", 159 | "hi": "_attribute_ का फ़ॉर्मेट छवि होना चाहिए", 160 | "ru": "Формат файла _attribute_ должен быть изображением", 161 | "pt": "O formato do arquivo _attribute_ deve ser de imagem", 162 | "zh": "_attribute_ 应为图像格式", 163 | "ja": "_attribute_ は画像形式である必要があります" 164 | }, 165 | date_after: { 166 | "en": "The _attribute_ date must be after _param1_'s date", 167 | "id": "_attribute_ harus setelah tanggal _param1_", 168 | "es": "La fecha de _attribute_ debe ser posterior a la fecha de _param1_", 169 | "hi": "_attribute_ तिथि _param1_ की तिथि के बाद होनी चाहिए", 170 | "ru": "Дата _attribute_ должна быть после даты _param1_", 171 | "pt": "A data de _attribute_ deve ser posterior à data de _param1_", 172 | "zh": "_attribute_ 日期必须在 _param1_ 日期之后", 173 | "ja": "_attribute_ の日付は_param1_の日付の後でなければなりません" 174 | }, 175 | date_after_or_equal: { 176 | "en": "The _attribute_ date must be after or equal than _param1_'s date", 177 | "id": "_attribute_ harus setelah atau sama dengan tanggal _param1_", 178 | "es": "La fecha de _attribute_ debe ser posterior o igual a la fecha de _param1_", 179 | "hi": "_attribute_ की तारीख _param1_ की तारीख के बाद या उससे बराबर होनी चाहिए", 180 | "ru": "Дата _attribute_ должна быть после или равна дате _param1_", 181 | "pt": "A data do(a) _attribute_ deve ser posterior ou igual à data do(a) _param1_", 182 | "zh": "_attribute_ 的日期必须在 _param1_ 的日期之后或相同", 183 | "ja": "_attribute_の日付は、_param1_の日付以降または同じでなければなりません" 184 | }, 185 | date_before: { 186 | "en": "The _attribute_ date must be before _param1_'s date", 187 | "id": "_attribute_ harus sebelum tanggal _param1_", 188 | "es": "La fecha de _attribute_ debe ser anterior a la fecha de _param1_", 189 | "hi": "_attribute_ की तारीख _param1_ की तारीख से पहले होनी चाहिए", 190 | "ru": "Дата _attribute_ должна быть до даты _param1_", 191 | "pt": "A data do(a) _attribute_ deve ser anterior à data do(a) _param1_", 192 | "zh": "_attribute_ 的日期必须在 _param1_ 的日期之前", 193 | "ja": "_attribute_の日付は、_param1_の日付よりも前でなければなりません" 194 | }, 195 | date_before_or_equal: { 196 | "en": "The _attribute_ date must be before or equal than _param1_'s date", 197 | "id": "_attribute_ harus sebelum atau sama dengan tanggal _param1_", 198 | "es": "La fecha de _attribute_ debe ser anterior o igual a la fecha de _param1_", 199 | "hi": "_attribute_ की तारीख _param1_ की तारीख से पहले या उससे बराबर होनी चाहिए", 200 | "ru": "Дата _attribute_ должна быть до или равна дате _param1_", 201 | "pt": "A data do(a) _attribute_ deve ser anterior ou igual à data do(a) _param1_", 202 | "zh": "_attribute_ 的日期必须在 _param1_ 的日期之前或相同", 203 | "ja": "_attribute_の日付は、_param1_の日付以前または同じでなければなりません" 204 | }, 205 | boolean: { 206 | "en": "The _attribute_ must be have true or false format", 207 | "id": "_attribute_ harus dalam format benar atau salah", 208 | "es": "El campo _attribute_ debe estar en formato verdadero o falso", 209 | "hi": "_attribute_ सही या गलत फ़ॉर्मेट में होना चाहिए", 210 | "ru": "_attribute_ должно быть в формате true или false", 211 | "pt": "_attribute_ deve estar no formato verdadeiro ou falso", 212 | "zh": "_attribute_ 必须是 true 或 false 格式", 213 | "ja": "_attribute_ は true または false の形式でなければなりません" 214 | }, 215 | in_array: { 216 | "en": "The _attribute_ selected field is invalid", 217 | "id": "_attribute_ terpilih tidak valid", 218 | "es": "El campo seleccionado en _attribute_ no es válido", 219 | "hi": "_attribute_ चयनित फ़ील्ड अमान्य है", 220 | "ru": "Выбранное поле в _attribute_ недопустимо", 221 | "pt": "O campo selecionado em _attribute_ não é válido", 222 | "zh": "_attribute_ 中选择的字段无效", 223 | "ja": "_attribute_ で選択されたフィールドが無効です" 224 | }, 225 | not_in_array: { 226 | "en": "The _attribute_ selected field is invalid", 227 | "id": "_attribute_ terpilih tidak valid", 228 | "es": "El campo seleccionado en _attribute_ no es válido", 229 | "hi": "_attribute_ चयनित फ़ील्ड अमान्य है", 230 | "ru": "Выбранное поле в _attribute_ недопустимо", 231 | "pt": "O campo selecionado em _attribute_ não é válido", 232 | "zh": "_attribute_ 中选择的字段无效", 233 | "ja": "_attribute_ で選択されたフィールドが無効です" 234 | }, 235 | ip: { 236 | "en": "The _attribute_ must an Ip address", 237 | "id": "_attribute_ harus dalam format Ip address", 238 | "es": "El campo _attribute_ debe ser una dirección IP", 239 | "hi": "_attribute_ एक आईपी ​​पता होना चाहिए", 240 | "ru": "_attribute_ должно быть IP-адресом", 241 | "pt": "_attribute_ deve ser um endereço de IP", 242 | "zh": "_attribute_ 必须是一个 IP 地址", 243 | "ja": "_attribute_ はIPアドレスでなければなりません" 244 | }, 245 | url: { 246 | "en": "The _attribute_ must be an Url", 247 | "id": "_attribute_ harus berupa Url", 248 | "es": "El _attribute_ debe ser una URL", 249 | "hi": "_attribute_ एक Url होना चाहिए", 250 | "ru": "_attribute_ должен быть Url", 251 | "pt": "O _attribute_ deve ser um Url", 252 | "zh": "_attribute_ 必须是一个 Url", 253 | "ja": "_attribute_はURLである必要があります" 254 | }, 255 | json: { 256 | "en": "The _attribute_ must be in Json format", 257 | "id": "_attribute_ harus berupa format Json", 258 | "es": "El _attribute_ debe estar en formato Json", 259 | "hi": "_attribute_ Json प्रारूप में होना चाहिए", 260 | "ru": "_attribute_ должен быть в формате Json", 261 | "pt": "O _attribute_ deve estar em formato Json", 262 | "zh": "_attribute_ 必须是Json格式", 263 | "ja": "_attribute_はJson形式である必要があります" 264 | }, 265 | digits: { 266 | "en": "The _attribute_ must be _param1_ digits", 267 | "id": "_attribute_ harus _param1_ digit", 268 | "es": "El _attribute_ debe tener _param1_ dígitos", 269 | "hi": "_attribute_ _param1_ अंक होना चाहिए", 270 | "ru": "_attribute_ должен быть _param1_ цифр", 271 | "pt": "O _attribute_ deve ter _param1_ dígitos", 272 | "zh": "_attribute_ 必须为_param1_位数字", 273 | "ja": "_attribute_は_param1_桁である必要があります" 274 | }, 275 | max_digits: { 276 | "en": "The _attribute_ must be less or equal than _param1_ digits", 277 | "id": "_attribute_ harus kurang atau sama dengan _param1_ digit", 278 | "es": "El _attribute_ debe tener menos o igual que _param1_ dígitos", 279 | "hi": "_attribute_ _param1_ अंक से कम या बराबर होना चाहिए", 280 | "ru": "_attribute_ должен быть меньше или равен _param1_ цифр", 281 | "pt": "O _attribute_ deve ter menos ou igual a _param1_ dígitos", 282 | "zh": "_attribute_ 必须少于或等于_param1_位数字", 283 | "ja": "_attribute_は_param1_桁以下である必要があります" 284 | }, 285 | min_digits: { 286 | "en": "The _attribute_ must be more or equal than _param1_ digits", 287 | "id": "_attribute_ harus lebih atau sama dengan _param1_ digit", 288 | "es": "El campo _attribute_ debe tener como mínimo _param1_ dígitos", 289 | "hi": "_attribute_ कम से कम _param1_ अंकों से अधिक होना चाहिए", 290 | "ru": "Длина поля _attribute_ должна быть не менее _param1_ цифр", 291 | "pt": "O campo _attribute_ deve ter no mínimo _param1_ dígitos", 292 | "zh": "_attribute_字段的长度不能小于_param1_个数字", 293 | "ja": "_attribute_は、最低でも_param1_桁以上必要です" 294 | }, 295 | digits_between: { 296 | "en": "The _attribute_ must be between _param1_ and _param2_ digits", 297 | "id": "_attribute_ harus diantara _param1_ dan _param2_ digit", 298 | "es": "El campo _attribute_ debe tener entre _param1_ y _param2_ dígitos", 299 | "hi": "_attribute_ कम से कम _param1_ और अधिकतम _param2_ अंकों के बीच होना चाहिए", 300 | "ru": "Длина поля _attribute_ должна быть между _param1_ и _param2_ цифрами", 301 | "pt": "O campo _attribute_ deve ter entre _param1_ e _param2_ dígitos", 302 | "zh": "_attribute_字段的长度应在_param1_和_param2_个数字之间", 303 | "ja": "_attribute_は_param1_桁以上_param2_桁以下でなければなりません" 304 | }, 305 | age_lt: { 306 | "en": "The _attribute_ must be less than _param1_ years", 307 | "id": "_attribute_ harus kurang dari _param1_ tahun", 308 | "es": "El _attribute_ debe ser menor que _param1_ años", 309 | "hi": "_attribute_ _param1_ साल से कम होना चाहिए", 310 | "ru": "_attribute_ должен быть меньше _param1_ лет", 311 | "pt": "O(a) _attribute_ deve ser menor que _param1_ anos", 312 | "zh": "_attribute_ 必须小于 _param1_ 岁", 313 | "ja": "_attribute_は_param1_歳以下でなければなりません" 314 | }, 315 | age_lte: { 316 | "en": "The _attribute_ must be less than or equal to _param1_ years", 317 | "id": "_attribute_ harus kurang atau sama dengan _param1_ tahun", 318 | "es": "El _attribute_ debe ser menor o igual a _param1_ años", 319 | "hi": "_attribute_ _param1_ साल से कम या बराबर होना चाहिए", 320 | "ru": "_attribute_ должен быть меньше или равен _param1_ годам", 321 | "pt": "O(a) _attribute_ deve ser menor ou igual a _param1_ anos", 322 | "zh": "_attribute_ 必须小于或等于 _param1_ 岁", 323 | "ja": "_attribute_は_param1_歳以下である必要があります" 324 | }, 325 | age_gt: { 326 | "en": "The _attribute_ must be greater than _param1_ years", 327 | "id": "_attribute_ harus lebih besar dari _param1_ tahun", 328 | "es": "El _attribute_ debe ser mayor que _param1_ años", 329 | "hi": "_attribute_ _param1_ साल से अधिक होना चाहिए", 330 | "ru": "Значение _attribute_ должно быть больше чем _param1_ лет", 331 | "pt": "O _attribute_ deve ser maior que _param1_ anos", 332 | "zh": "_attribute_ 必须大于 _param1_ 岁", 333 | "ja": "_attribute_は_param1_歳よりも大きくなければなりません" 334 | }, 335 | age_gte: { 336 | "en": "The _attribute_ must be greater than or equal to _param1_ years", 337 | "id": "_attribute_ harus lebih besar atau sama dengan _param1_ tahun", 338 | "es": "El _attribute_ debe ser mayor o igual que _param1_ años", 339 | "hi": "_attribute_ _param1_ साल से अधिक या उससे बराबर होना चाहिए", 340 | "ru": "Значение _attribute_ должно быть больше или равно _param1_ годам", 341 | "pt": "O _attribute_ deve ser maior ou igual a _param1_ anos", 342 | "zh": "_attribute_ 必须大于等于 _param1_ 岁", 343 | "ja": "_attribute_は_param1_歳以上でなければなりません" 344 | } 345 | 346 | }) 347 | export default langValidation -------------------------------------------------------------------------------- /core/validation/RequestValidation.js: -------------------------------------------------------------------------------- 1 | import validator from 'validator' 2 | import localeConfig from '../config/Locale.js' 3 | import Translate from '../locale/Dictionary.js' 4 | import ValidationDB from './ValidationDB.js' 5 | import langValidation from "../locale/LangValidation.js" 6 | 7 | /** 8 | * For add new rule 9 | * [1]. add on ValidationType 10 | * [2]. add message LangValidation in `core/locale/LangValidation` 11 | * [3]. add params if has params -> createOptionsParams() 12 | * [4]. add check validation on -> ValidationCheck() 13 | */ 14 | 15 | const ValidationType = Object.freeze({ 16 | required: "required", 17 | email: "email", 18 | match: "match", 19 | string: "string", 20 | float: "float", 21 | integer: "integer", 22 | max: "max", 23 | min: "min", 24 | date: "date", 25 | array: "array", 26 | exists: "exists", 27 | unique: "unique", 28 | mimetypes: "mimetypes", 29 | mimes: "mimes", 30 | max_file: "max_file", 31 | image: "image", 32 | date_after: "date_after", 33 | date_after_or_equal: "date_after_or_equal", 34 | date_before: "date_before", 35 | date_before_or_equal: "date_before_or_equal", 36 | boolean: "boolean", 37 | in_array: "in_array", 38 | not_in_array: "not_in_array", 39 | ip: "ip", 40 | url: "url", 41 | json: "json", 42 | digits: "digits", 43 | max_digits: "max_digits", 44 | min_digits: "min_digits", 45 | digits_between: "digits_between", 46 | age_lt: "age_lt", 47 | age_lte: "age_lte", 48 | age_gt: "age_gt", 49 | age_gte: "age_gte", 50 | }) 51 | 52 | 53 | class RequestValidation { 54 | 55 | errors = {} 56 | isError = false 57 | constructor(req) { 58 | this.body = req?.body ?? {} 59 | this.locale = req.locale || localeConfig.defaultLocale 60 | } 61 | 62 | 63 | async load(child) { 64 | this.rules = child.rules(); 65 | // await this.check() 66 | } 67 | 68 | async #checkError() { 69 | this.isError = true 70 | if (JSON.stringify(this.errors) === JSON.stringify({})) { 71 | this.isError = false 72 | } 73 | } 74 | 75 | responseError(res) { 76 | return res.status(422).json(this.errors) 77 | } 78 | 79 | 80 | async check() { 81 | this.errors = {} 82 | // console.log("--------------------------------------------------- field") 83 | // console.log(this.body) 84 | // console.log("--------------------------------------------------- rules") 85 | // console.log(this.rules) 86 | // console.log("=================================================== Checking") 87 | for (let fieldKey in this.rules) { 88 | 89 | if (this.#isNested(fieldKey)) { 90 | await this.#nestedProcess(fieldKey) 91 | } 92 | else { 93 | if (this.#hasData(fieldKey)) { 94 | await this.#checking(fieldKey, this.body[fieldKey]) 95 | } 96 | else { 97 | if (this.#hasRuleRequired(fieldKey)) 98 | this.#setError(fieldKey, "required") 99 | } 100 | } 101 | 102 | } 103 | await this.#checkError() 104 | // console.log("this.isError", this.isError) 105 | return this.isError 106 | } 107 | #hasRuleRequired(fieldKey) { 108 | let rules = this.rules[fieldKey].rules 109 | for (let ruleKey of rules) { 110 | if (ruleKey === "required") { 111 | return true 112 | } 113 | } 114 | return false 115 | } 116 | 117 | #isNested(fieldKey) { 118 | if (fieldKey.indexOf(".") !== -1) return true 119 | return false 120 | } 121 | 122 | #hasData(fieldKey) { 123 | for (let _fieldKey in this.body) { 124 | if (_fieldKey.toString() == fieldKey.toString()) { 125 | return true 126 | } 127 | } 128 | return false 129 | } 130 | #getData(key) { 131 | for (let fieldKey in this.body) { 132 | if (key.toString() == fieldKey.toString()) { 133 | return this.body[key] 134 | } 135 | } 136 | return null 137 | } 138 | 139 | 140 | /** 141 | * 142 | * @param {*} fieldKey ex: name, email, username 143 | * @param {*} rule ex: required, exist, match 144 | * @param {*} options params of rule that has params. ex: match:password, return password 145 | */ 146 | #setError(fieldKey, rule, attribute, options) { 147 | let message = this.#setErrorMessage(fieldKey, rule, attribute, options) 148 | 149 | let keyError = attribute ?? fieldKey 150 | 151 | this.addError(keyError, message) 152 | } 153 | 154 | 155 | addError(keyError, message) { 156 | if (Object.keys(this.errors).length === 0) { 157 | this.errors["errors"] = {} 158 | } 159 | if (!this.errors["errors"][keyError]) { 160 | this.errors["errors"][keyError] = [] 161 | } 162 | this.errors["errors"][keyError].push(message) 163 | } 164 | 165 | 166 | 167 | /** 168 | * 169 | * @param {*} fieldKey ex: name, username, birthdate 170 | * @param {*} rule ex: required, email, max,min 171 | * @param {*} options params of rule that has params. ex: match:password 172 | * @returns 173 | */ 174 | #setErrorMessage(fieldKey, rule, attribute, options) { 175 | 176 | attribute = this.rules[fieldKey].attribute ?? (attribute ?? fieldKey) 177 | // ---------- set custom message 178 | if (this.rules[fieldKey].messages && this.rules[fieldKey].messages[rule]) { 179 | return this.rules[fieldKey].messages[rule].replace("_attribute_", attribute) 180 | } 181 | // ---------- set default message 182 | return this.#defaultErrorMessage(rule, attribute, options) 183 | } 184 | 185 | /** 186 | * 187 | * @param {*} rule ex: required, match,float, min, max 188 | * @param {*} attribute ex: name, birthdate 189 | * @param {*} options ex: match:password -> ['password'] | digit_between:1,2 -> [1,2] 190 | */ 191 | #defaultErrorMessage(rule, attribute, options) { 192 | 193 | if (!langValidation[rule] || !langValidation[rule][this.locale]) 194 | throw "message no exist" 195 | attribute = attribute[0].toUpperCase() + attribute.slice(1) 196 | let message = langValidation[rule][this.locale].replace("_attribute_", attribute) 197 | 198 | if (options && Array.isArray(options)) { 199 | for (let i = 0; i < options.length; i++) { 200 | let translateParam = Translate(options[i], this.locale) 201 | message = message.replace(("_param" + (i + 1) + "_").toString(), translateParam) 202 | } 203 | } 204 | 205 | return message.split("_").join(" ") 206 | } 207 | 208 | 209 | /** 210 | * checking proccess by create params from rule, get rule name, and check validation of value 211 | * @param {*} fieldKey ex: name, birthdate, id, 212 | * @param {*} value value of field 213 | * @param {*} attribute attribute 214 | * @returns void 215 | */ 216 | async #checking(fieldKey, value, attribute) { 217 | 218 | let rules = this.rules[fieldKey].rules // ["required","match:password","min:1","max:2"] 219 | // console.log(">>>>--------------------------------------->>>>") 220 | // console.log(rules) 221 | if (!Array.isArray(rules)) { 222 | console.log("\x1b[31m", "validations not an array", fieldKey, "\x1b[0m"); 223 | return null; 224 | } 225 | var isValid = false; 226 | for (let rule of rules) { 227 | // val, ex: float, required, date etc... 228 | // console.log("--------------") 229 | // console.log(rule, value) 230 | 231 | if (typeof rule === "object") { 232 | // custom rule 233 | await this.#processCustomRule(rule, fieldKey, value, attribute) 234 | } 235 | if (typeof rule === "string") { 236 | let options 237 | let ruleName 238 | let hasParams = this.#isValidationHasParam(rule) 239 | let ruleParams 240 | // ex: max:3, min:5 241 | if (hasParams) { 242 | // console.log("CREATE PARAMS") 243 | options = this.#createOptionsParams(fieldKey, rule) 244 | ruleName = this.#getValidateNameFromValidationWithParams(rule) 245 | } 246 | else { 247 | ruleName = rule 248 | } 249 | isValid = await this.ValidationCheck(ruleName, value, { options: options }) 250 | if (!isValid) { 251 | if (hasParams) { 252 | ruleParams = this.#getValidateParams(rule) 253 | // console.log("validationParams", ruleParams) 254 | } 255 | this.#setError(fieldKey, ruleName, attribute, ruleParams) 256 | } 257 | } 258 | 259 | 260 | } 261 | // console.log("<<<<---------------------------------------<<<<") 262 | } 263 | 264 | /** 265 | * checking custom rule validation 266 | * @param {*} rule 267 | * @param {*} fieldKey 268 | * @param {*} value 269 | * @param {*} attribute 270 | * @returns 271 | */ 272 | async #processCustomRule(rule, fieldKey, value, attribute) { 273 | if (typeof rule.passes === 'undefined') 274 | throw 'Invalid Custom rule, dont have passes() method' 275 | if (typeof rule.message === 'undefined') 276 | throw 'Invalid Custom rule, dont have passes() message' 277 | const message = rule.message() 278 | if (typeof message != 'string') 279 | throw 'Invalid Custom rule, message() have to return string' 280 | attribute = attribute ?? fieldKey 281 | const valid = await rule.passes(attribute, value) 282 | if (typeof valid != "boolean") 283 | throw 'Invalid Custom rule, passes() have to return boolean' 284 | 285 | if (!valid) { 286 | this.addError(attribute, message.replace("_attribute_", attribute)) 287 | } 288 | } 289 | 290 | 291 | /** 292 | * check if a rule has params 293 | * @param {*} rule ex: match:oldPassword 294 | * @returns 295 | */ 296 | #isValidationHasParam(rule) { 297 | if (rule.indexOf(":") !== -1) return true 298 | return false 299 | } 300 | 301 | 302 | /** 303 | * get rule name if rule has params 304 | * @param {*} rule ex: match:oldPassword 305 | * @returns match 306 | */ 307 | #getValidateNameFromValidationWithParams(rule) { 308 | let arr = rule.split(":") 309 | return arr[0] 310 | } 311 | 312 | /** 313 | * get rule params 314 | * @param {*} rule ex: digit_between:1,2 315 | * @returns [1,2] 316 | */ 317 | #getValidateParams(rule) { 318 | let arr = rule.split(":") 319 | arr = arr.splice(1, 1); 320 | // console.log("arr", arr) 321 | // console.log("rule", rule) 322 | if (arr[0].indexOf(",") !== -1) { 323 | return arr[0].split(",") 324 | } 325 | return arr 326 | } 327 | 328 | /** 329 | * create options from rule params 330 | * @param {*} rule ex: match:password, max:2, min:3 331 | * @param {*} fieldKey 332 | */ 333 | #createOptionsParams(fieldKey, rule) { 334 | 335 | let arr = rule.split(":") 336 | let options = {} 337 | try { 338 | if (arr.length > 1) { 339 | if (arr[0] === ValidationType.match) { 340 | let fieldMatch = this.#getData(arr[1]) 341 | // if (!fieldMatch) 342 | // throw "Not right format of validation: " + rule 343 | options["fieldMatch"] = fieldMatch 344 | } 345 | if (arr[0] === ValidationType.max || arr[0] === ValidationType.min) { 346 | let param = arr[1] 347 | if (!param) throw "Not right format of validation: " + rule 348 | 349 | if (arr[0] === ValidationType.max) 350 | options["fieldMax"] = param 351 | if (arr[0] === ValidationType.min) 352 | options["fieldMin"] = param 353 | } 354 | if (arr[0] === ValidationType.exists) { 355 | let params = arr[1].split(",") 356 | if (params.length < 2) throw "Not right format of validation: " + rule 357 | 358 | 359 | options["fieldTableName"] = params[0] 360 | options["fieldColumnName"] = params[1] 361 | 362 | if (params[2]) 363 | options["fieldException"] = params[2] 364 | } 365 | if (arr[0] === ValidationType.unique) { 366 | let params = arr[1].split(",") 367 | if (params.length < 2) throw "Not right format of validation: " + rule 368 | 369 | 370 | options["fieldTableName"] = params[0] 371 | options["fieldColumnName"] = params[1] 372 | 373 | if (params[2]) 374 | options["fieldException"] = params[2] 375 | 376 | } 377 | 378 | if (arr[0] === ValidationType.mimetypes || arr[0] === ValidationType.mimes) { 379 | let params = arr[1].split(",") 380 | options["fieldMimetypes"] = params 381 | } 382 | 383 | if (arr[0] === ValidationType.max_file) { 384 | let params = arr[1].split(",") 385 | if (params.length < 1) throw "Not right format of validation: " + rule 386 | 387 | 388 | if (!validator.isInt(params[0]) || !this.#isValidFileUnit(params[1])) 389 | throw "Not right format of validation: " + rule + ". Valid max_file:1000,MB -> [GB,MB,KB,Byte]" 390 | 391 | options["fieldMaxSize"] = params[0] 392 | options["fieldUnit"] = params[1] 393 | } 394 | 395 | if (arr[0] === ValidationType.date_after || arr[0] === ValidationType.date_before || 396 | arr[0] === ValidationType.date_after_or_equal || arr[0] === ValidationType.date_before_or_equal) { 397 | let params = arr[1] 398 | let targetDate = this.#getData(params) 399 | if (!targetDate && params == "now") 400 | targetDate = new Date() 401 | options["fieldDate"] = targetDate 402 | } 403 | 404 | if (arr[0] === ValidationType.in_array || arr[0] === ValidationType.not_in_array) { 405 | let params = arr[1].split(",") 406 | if (!params) 407 | throw "Not right format of validation: " + rule 408 | options["fieldArray"] = params 409 | } 410 | 411 | if (arr[0] === ValidationType.digits || arr[0] === ValidationType.max_digits || arr[0] === ValidationType.min_digits) { 412 | let params = arr[1] 413 | if (!validator.isInt(params)) 414 | throw "Not right format of validation: " + rule 415 | options["fieldDigits"] = params 416 | } 417 | 418 | if (arr[0] === ValidationType.digits_between) { 419 | let params = arr[1].split(",") 420 | if (!params || params.length < 2 || !validator.isInt(params[0]) || !validator.isInt(params[1])) 421 | throw "Not right format of validation: " + rule 422 | 423 | options["fieldDigitsFirst"] = params[0] 424 | options["fieldDigitsLast"] = params[1] 425 | } 426 | if (arr[0] === ValidationType.age_lt || arr[0] === ValidationType.age_lte || arr[0] === ValidationType.age_gt || arr[0] === ValidationType.age_gte) { 427 | let params = arr[1] 428 | if (!params || params < 1 || !validator.isInt(params)) 429 | throw "Not right format of validation: " + rule 430 | options["fieldAge"] = params 431 | } 432 | 433 | } 434 | else { 435 | throw "Not right format of validation: " + rule 436 | } 437 | } catch (error) { 438 | console.log("\x1b[31m", error, "\x1b[0m"); 439 | } 440 | 441 | return options 442 | 443 | } 444 | 445 | 446 | /** 447 | * Validation check value and rule 448 | * @param {*} ruleName ex: required, float 449 | * @param {*} value value 450 | * @param {*} options ex: {fieldMax: 3 } 451 | * @returns boolean 452 | */ 453 | async ValidationCheck(ruleName, value, { options }) { 454 | 455 | // console.log("ruleName...", ruleName) 456 | // console.log("field...", field) 457 | // console.log("options...", options) 458 | 459 | //------------------------------------------------------ database 460 | if (ruleName == ValidationType.exists) { 461 | let d = await ValidationDB.exists(options.fieldTableName, options.fieldColumnName, value, 462 | options.fieldException 463 | ) 464 | return d 465 | } 466 | if (ruleName == ValidationType.unique) { 467 | return await ValidationDB.unique(options.fieldTableName, options.fieldColumnName, value, 468 | options.fieldException 469 | ) 470 | } 471 | 472 | //------------------------------------------------------ has params 473 | 474 | if (ruleName === ValidationType.digits) { 475 | if (!value) 476 | return true 477 | 478 | return (value.toString().length === parseInt(options.fieldDigits)) 479 | } 480 | if (ruleName === ValidationType.digits_between) { 481 | if (!value) 482 | return true 483 | 484 | return (value.toString().length >= parseInt(options.fieldDigitsFirst) && value.toString().length <= parseInt(options.fieldDigitsLast)) 485 | } 486 | if (ruleName === ValidationType.max_digits || ruleName === ValidationType.min_digits) { 487 | if (!value) 488 | return false 489 | if (ruleName === ValidationType.max_digits) 490 | return (value.toString().length <= parseInt(options.fieldDigits)) 491 | 492 | return (value.toString().length >= parseInt(options.fieldDigits)) 493 | } 494 | 495 | if (ruleName === ValidationType.max_file) { 496 | 497 | if (!value) 498 | return true 499 | 500 | let size = this.#convertByteToAnyUnit(value.size, options.fieldUnit) 501 | 502 | // console.log("size:", size) 503 | // console.log("options.fieldMaxSize:", options.fieldMaxSize) 504 | return parseFloat(size) <= parseFloat(options.fieldMaxSize) 505 | } 506 | 507 | if (ruleName === ValidationType.match) { 508 | if (!value && options.fieldMatch || value && !options.fieldMatch || !value && !options.fieldMatch) return false 509 | return value.toString() === options.fieldMatch.toString() 510 | } 511 | 512 | if (ruleName === ValidationType.max) { 513 | if (value === null || value === undefined) 514 | return false 515 | if (Array.isArray(value)) 516 | return value.length <= options.fieldMax 517 | if (validator.isNumeric(value.toString())) 518 | return validator.isFloat(value.toString() ?? "0", { max: options.fieldMax ?? " " }) 519 | 520 | return value.toString().length <= options.fieldMax 521 | 522 | } 523 | 524 | if (ruleName === ValidationType.min) { 525 | if (value === null || value === undefined) 526 | return false 527 | if (Array.isArray(value)) 528 | return value.length >= options.fieldMin 529 | if (validator.isNumeric(value.toString())) 530 | return validator.isFloat(value.toString() ?? "0", { min: options.fieldMin ?? " " }) 531 | 532 | return value.toString().length >= options.fieldMax 533 | } 534 | 535 | if (ruleName === ValidationType.mimetypes) { 536 | if (!Array.isArray(options.fieldMimetypes) || !value.type) { 537 | return false 538 | } 539 | return validator.isIn(value.type, options.fieldMimetypes) 540 | } 541 | 542 | if (ruleName === ValidationType.mimes) { 543 | if (!Array.isArray(options.fieldMimetypes) || !value.extension) return false 544 | 545 | return validator.isIn(value.extension.split('.').join(""), options.fieldMimetypes) 546 | } 547 | if (ruleName === ValidationType.date_after || ruleName === ValidationType.date_before) { 548 | if (!options.fieldDate || !value) return false 549 | let _date = this.#formatDate(value) 550 | let _dateCompare = this.#formatDate(options.fieldDate) 551 | if (ruleName == ValidationType.date_before) 552 | return validator.isBefore(_date, _dateCompare) 553 | return validator.isAfter(_date, _dateCompare) 554 | } 555 | if (ruleName === ValidationType.date_after_or_equal || ruleName === ValidationType.date_before_or_equal) { 556 | if (!options.fieldDate || !value) return false 557 | 558 | let _date = this.#formatDate(value) 559 | let _dateCompare = this.#formatDate(options.fieldDate) 560 | 561 | if (ruleName == ValidationType.date_before_or_equal) { 562 | let isBefore = validator.isBefore(_date, _dateCompare) 563 | if (isBefore || !isBefore && validator.equals(_date, _dateCompare)) 564 | return true 565 | return false 566 | } 567 | let isAfter = validator.isAfter(_date, _dateCompare) 568 | if (isAfter || !isAfter && validator.equals(_date, _dateCompare)) 569 | return true 570 | return false 571 | } 572 | 573 | if (ruleName === ValidationType.in_array) { 574 | return validator.isIn(value, options.fieldArray) 575 | } 576 | if (ruleName === ValidationType.not_in_array) { 577 | return !validator.isIn(value, options.fieldArray) 578 | } 579 | 580 | if (ruleName === ValidationType.age_lt) { 581 | let newDate = this.#formatDate(value) 582 | if (!validator.isDate(newDate.toString())) 583 | return false 584 | return this.#getAge(value) < options.fieldAge 585 | } 586 | if (ruleName === ValidationType.age_lte) { 587 | let newDate = this.#formatDate(value) 588 | if (!validator.isDate(newDate.toString())) 589 | return false 590 | return this.#getAge(value) <= options.fieldAge 591 | } 592 | if (ruleName === ValidationType.age_gt) { 593 | let newDate = this.#formatDate(value) 594 | if (!validator.isDate(newDate.toString())) 595 | return false 596 | return this.#getAge(value) > options.fieldAge 597 | } 598 | if (ruleName === ValidationType.age_gte) { 599 | let newDate = this.#formatDate(value) 600 | if (!validator.isDate(newDate.toString())) 601 | return false 602 | return this.#getAge(value) >= options.fieldAge 603 | } 604 | 605 | 606 | //------------------------------------------------------ has no params 607 | 608 | if (ruleName === ValidationType.image) { 609 | if (!value || !value.extension) 610 | return false 611 | return validator.isIn(value.extension.split('.').join("").toLowerCase(), Object.values(this.imageFormats)) 612 | } 613 | 614 | if (ruleName === ValidationType.required) { 615 | if (value === undefined || value === null || value === "") 616 | return false 617 | } 618 | 619 | if (ruleName === ValidationType.email) 620 | return validator.isEmail(value.toString()) 621 | 622 | if (ruleName === ValidationType.boolean) 623 | return validator.isBoolean(value.toString()) 624 | 625 | if (ruleName === ValidationType.float) 626 | return validator.isFloat((value ?? '').toString()) 627 | 628 | if (ruleName === ValidationType.integer) 629 | return validator.isInt((value ?? "").toString()) 630 | 631 | if (ruleName === ValidationType.date) { 632 | let newDate = this.#formatDate(value) 633 | return validator.isDate(newDate.toString()) 634 | } 635 | 636 | if (ruleName === ValidationType.string) 637 | return (typeof value === "string") 638 | 639 | if (ruleName === ValidationType.array) 640 | return (Array.isArray(value)) 641 | 642 | if (ruleName === ValidationType.ip) 643 | return validator.isIP(value) 644 | 645 | if (ruleName === ValidationType.url) 646 | return validator.isURL(value) 647 | 648 | if (ruleName === ValidationType.json) 649 | return validator.isJSON(value) 650 | 651 | 652 | 653 | return true 654 | } 655 | 656 | /** 657 | * change date format so can be using for validation check 658 | * @param {*} date 659 | * @returns 660 | */ 661 | #formatDate(date) { 662 | try { 663 | var d = new Date(date), 664 | month = '' + (d.getMonth() + 1), 665 | day = '' + d.getDate(), 666 | year = d.getFullYear(); 667 | 668 | if (month.length < 2) 669 | month = '0' + month; 670 | if (day.length < 2) 671 | day = '0' + day; 672 | return [year, month, day].join('/'); 673 | } catch (error) { 674 | 675 | } 676 | return date 677 | } 678 | 679 | /** 680 | * file size units 681 | */ 682 | fileUnits = { 683 | GB: "GB", MB: "MB", KB: "KB", Byte: "Byte" 684 | } 685 | 686 | /** 687 | * image formats 688 | */ 689 | imageFormats = { 690 | jpg: "jpg", 691 | jpeg: "jpeg", 692 | png: "png", 693 | bmp: "bmp", 694 | gif: "gif", 695 | svg: "svg", 696 | webp: "webp", 697 | } 698 | 699 | 700 | #getAge(dateString) { 701 | var today = new Date(); 702 | var birthDate = new Date(dateString); 703 | var age = today.getFullYear() - birthDate.getFullYear(); 704 | var m = today.getMonth() - birthDate.getMonth(); 705 | if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) { 706 | age--; 707 | } 708 | return age; 709 | } 710 | 711 | 712 | /** 713 | * check if unit input is valid 714 | * @param {*} unitFile 715 | * @returns 716 | */ 717 | #isValidFileUnit(unitFile) { 718 | 719 | for (let unit in this.fileUnits) { 720 | if (unitFile == unit) 721 | return true 722 | } 723 | return false 724 | } 725 | 726 | /** 727 | * convert size in byte to any unit 728 | * @param {*} sizeInByte 729 | * @param {*} unit 730 | * @returns 731 | */ 732 | #convertByteToAnyUnit(sizeInByte, unit) { 733 | 734 | if (unit === this.fileUnits.KB) { 735 | return (sizeInByte / 1024).toFixed(2) 736 | } 737 | 738 | if (unit === this.fileUnits.MB) 739 | return (sizeInByte / 1048576).toFixed(2) 740 | 741 | if (unit === this.fileUnits.GB) 742 | return (sizeInByte / 1073741824).toFixed(2) 743 | 744 | return sizeInByte 745 | } 746 | 747 | 748 | // -------------------------------------------------------------------------------------------------------- nested process 749 | 750 | /** 751 | * If rule is nested, then proccess to get value begins here 752 | * @param {*} fieldKey 753 | */ 754 | async #nestedProcess(fieldKey) { 755 | // console.log("start nested validation for " + fieldKey) 756 | let fieldArray = fieldKey.split(".") 757 | await this.#recursizeNested(fieldKey, fieldArray, this.body, "", 0) 758 | } 759 | 760 | /** 761 | * recursive function to check into deep nested value. 762 | * the purpose is found the value from field body 763 | * @param {*} fieldKey item.*.name 764 | * @param {*} fieldArray [item, * , name] 765 | * @param {*} attribute default is "" 766 | * @param {*} currentField this.body[item] | this.body[item][0] | this.body[item][0][name] 767 | * @param {*} indexNested default is 0 768 | * @returns 769 | */ 770 | async #recursizeNested(fieldKey, fieldArray, currentField, attribute, indexNested) { 771 | 772 | // console.log("-----------------------------------" + indexNested) 773 | // console.log("fieldArray", fieldArray) 774 | // console.log("currentField", currentField) 775 | // console.log("----------------------------------.") 776 | // if (!currentField) { 777 | // console.log("field not found", currentField) 778 | // return 779 | // } 780 | 781 | if (!indexNested <= fieldArray.length) { 782 | // validation in here 783 | if (indexNested === fieldArray.length) { 784 | // console.log("validation: ",) 785 | // console.log("data-> ", currentField) 786 | // console.log("attribute-> ",) 787 | 788 | // fieldKey -> item.*.name -> used for get validation rule 789 | // currentField -> value of name from this.body object 790 | // attribute slice 1 means-> .item.0.name -> item.0.name 791 | await this.#checking(fieldKey, currentField, attribute.slice(1)) 792 | } 793 | else { 794 | if (fieldArray[indexNested] === "*") { 795 | if (!Array.isArray(currentField)) { 796 | // console.log("current field not an array") 797 | return 798 | } 799 | for (let i = 0; i < currentField.length; i++) { 800 | 801 | await this.#recursizeNested(fieldKey, fieldArray, currentField[i], attribute + "." + i, indexNested + 1) 802 | } 803 | } 804 | else { 805 | if (currentField) 806 | return await this.#recursizeNested(fieldKey, fieldArray, currentField[fieldArray[indexNested]], attribute + "." + fieldArray[indexNested], indexNested + 1) 807 | } 808 | } 809 | return 810 | } 811 | } 812 | 813 | 814 | 815 | 816 | 817 | } 818 | 819 | export default RequestValidation 820 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nodemi 2 | 3 | Boilerplate for nodejs. base on express js. 4 | 5 | - ### Features 6 | 7 | - Model - ORM 8 | 9 | Create model via cli and make relation between. 10 | 11 | - Media library 12 | 13 | Binding media to any Model, so any model can own the media, and will able to save media, get media, and destroy media. 14 | Media can be stored to `Local storage` or `Firebase Storage`. 15 | 16 | - File request handling 17 | 18 | Not worry about handling uploaded file, just upload from client side, and you can access file in request, ex: `req.body.avatar`. 19 | 20 | - Request validation 21 | 22 | Determine if request passes the rule. 23 | You can create `custom rule` via cli. 24 | 25 | - Role and Permissions 26 | 27 | Binding to any model, any model can have a role and permissions, set role, checking access. 28 | 29 | - Resources 30 | 31 | Create custom resource from resources. 32 | 33 | - Auth - JWT/Basic Auth 34 | 35 | Create token, re generate token, and set middleware authorization for certain routes. 36 | 37 | - Locale 38 | 39 | Enabled or disabled locale or just enabled on certain routes. 40 | 41 | - Mail 42 | 43 | Create mail via cli, and send mail with html, file, or just text. 44 | 45 | - Firebase Cloud Messaging 46 | 47 | Sending push notification from server to client device. 48 | 49 | - Seeder 50 | 51 | Running seeder via cli. 52 | 53 | - ### Live demo 54 | 55 | | Action | Method | Auth | Body | EndPoint | 56 | | --------------- | ------ | ------ | ---------------- | ----------------------------------------------------- | 57 | | Login | POST | | email | https://nodemi.onrender.com/api/login | 58 | | | | | password | | 59 | | | | | | | 60 | | Register | POST | | email | https://nodemi.onrender.com/api/register | 61 | | | | | name | | 62 | | | | | password | | 63 | | | | | confirm_password | | 64 | | | | | | | 65 | | Token | GET | | | https://nodemi.onrender.com/api/token | 66 | | | | | | | 67 | | | | | | | 68 | | Logout | DELETE | | | https://nodemi.onrender.com/api/logout | 69 | | | | | | | 70 | | Get User | GET | Bearer | | https://nodemi.onrender.com/api/user | 71 | | | | token | | | 72 | | | | | | | 73 | | Get Users | GET | Bearer | | https://nodemi.onrender.com/api/users | 74 | | | | token | | | 75 | | | | | | | 76 | | Forgot Password | POST | | email | https://nodemi.onrender.com/api/forgot-password | 77 | | | | | | | 78 | | Reset Password | PUT | | new_password | https://nodemi.onrender.com/api/reset-password/:token | 79 | 80 | # Getting Started 81 | 82 | - Clone this repo `https` or `SSH` 83 | 84 | Clone and move to directory project and run `npm install` 85 | 86 | ``` 87 | git clone git@github.com:Mamena2020/nodemi.git 88 | 89 | ``` 90 | 91 | - ### Create database 92 | 93 | Create database `mysql` or `pgsql`. 94 | 95 | ``` 96 | #mysql example 97 | 98 | mysql -u root -p 99 | # enter your password 100 | 101 | create database mydatabase; 102 | 103 | #pgsql example 104 | 105 | createdb -h localhost -p 5432 -U myPgUser mydatabase 106 | 107 | ``` 108 | 109 | - ### Setup .env 110 | 111 | After creating your database, you can fill in the .env file and start your code. 112 | 113 | ``` 114 | cp .env.example .env 115 | 116 | ``` 117 | 118 | # Model 119 | 120 | - ### Create new model via cli. 121 | 122 | ``` 123 | npx nodemi make:model Product 124 | ``` 125 | 126 | The model will be created in the `models` directory. 127 | 128 | ``` js 129 | 130 | import { Model, DataTypes } from "sequelize"; 131 | import db from "../core/database/Database.js" 132 | 133 | class Product extends Model {} 134 | Product.init({ 135 | name: { 136 | type: DataTypes.STRING, 137 | allowNull: false 138 | }, 139 | }, { 140 | sequelize: db, 141 | tableName: 'products', 142 | modelName: 'Product', 143 | timestamps: true 144 | } 145 | ); 146 | 147 | export default Product 148 | 149 | ``` 150 | 151 | Automatically registered in the `loadModels` function in the `models/Models.js` file. 152 | 153 | ``` js 154 | 155 | const loadModels = async () => { 156 | 157 | await Product.sync({ 158 | alter: true, // not recomended on production mode 159 | }) 160 | 161 | .... 162 | 163 | ``` 164 | 165 | Full documentation of ORM 166 | 167 | - ### Noted 168 | 169 | All relationships between models should be defined in the `loadModels` function. 170 | 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. 171 | 172 | # Media 173 | 174 | Any model can own media by binding the model to the media inside the `loadModels` function using `hasMedia(YourModel)`. 175 | 176 | ``` js 177 | 178 | const loadModels = async () => { 179 | 180 | await Product.sync({ 181 | alter: true, // not recomended on production mode 182 | }) 183 | 184 | await hasMedia(Product) 185 | 186 | ``` 187 | 188 | - ### Save a file 189 | 190 | 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. 191 | 192 | ``` js 193 | 194 | const product = await Product.findOne({ 195 | where: { 196 | id: 1 197 | } 198 | }) 199 | 200 | await product.saveMedia(req.body.file,"thumbnail") // if success then will return media object 201 | 202 | ``` 203 | 204 | You can save files to either `Local` storage or `Firebase` storage. 205 | 206 | To save to `Local` storage, just set your .env file `MEDIA_STORAGE=local` , and local storage directory name `MEDIA_LOCAL_STORAGE_DIR_NAME=storage`. 207 | 208 | ``` env 209 | MEDIA_STORAGE=local 210 | MEDIA_LOCAL_STORAGE_DIR_NAME=storage 211 | ``` 212 | 213 | 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. 214 | 215 | ``` env 216 | MEDIA_STORAGE=firebase # set to firebase 217 | FIREBASE_STORAGE_BUCKET=gs://xxxxxx.appspot.com # your firebase storage bucket 218 | FIREBASE_SERVICE_ACCOUNT_BASE64= # base64 string of your firebase service account .json 219 | ``` 220 | 221 | - ### Get media 222 | 223 | Get all media by calling `instance.getMedia()`. 224 | 225 | ``` js 226 | 227 | const product = await Product.findOne({ 228 | where: { 229 | id: 1 230 | } 231 | }) 232 | 233 | product.getMedia() // return list of object 234 | 235 | 236 | ``` 237 | 238 | Get media by name, params is media name 239 | 240 | ``` js 241 | product.getMediaByName("thumbnail") // return single object 242 | product.getMediaByName("thumbnail").url // return single object url 243 | 244 | ``` 245 | 246 | Get media first media 247 | 248 | ``` js 249 | product.getFirstMedia() // return single object 250 | product.getFirstMedia().url // return first media url 251 | 252 | ``` 253 | 254 | Get media with exception, params can be `string` or `array` of string 255 | 256 | ``` js 257 | product.getMediaExcept("thumbnail_mobile") // return list of object with exception 258 | 259 | ``` 260 | 261 | Get all media url, 262 | 263 | ``` js 264 | product.getMediaUrl() // return list of media url 265 | 266 | ``` 267 | 268 | Get all media url with exception, params can be `string` or `array` of string 269 | 270 | ``` js 271 | product.getMediaUrlExcept(['thumbnail_mobile']) // return list of url 272 | 273 | ``` 274 | 275 | Get url from media object 276 | 277 | ``` js 278 | product.getFirstMedia().getUrl() 279 | 280 | ``` 281 | 282 | - ### Destroy media 283 | 284 | Destroy media by calling `instance.destroyMedia(mediaName)`. return status deleted in boolean 285 | 286 | ``` js 287 | 288 | const product = await Product.findOne({ 289 | where: { 290 | id: 1 291 | } 292 | }) 293 | 294 | await product.destroyMedia("thumbnail") 295 | 296 | ``` 297 | 298 | - ### Noted 299 | 300 | All media files will be automatically deleted whenever `instance` of your model is deleted. 301 | 302 | # Request & Upload Files 303 | 304 | Handling Content-Type header for 305 | 306 | - application/json 307 | - application/form-data 308 | - application/x-www-form-urlencoded 309 | 310 | Handling all upload files on `POST` and `PUT` method, and nested fields. 311 | 312 | - ### File properties 313 | 314 | Uploaded file will have this properties. 315 | 316 | ``` 317 | 318 | name -> file name, 319 | encoding -> file encoding, 320 | type -> file mimeType, 321 | size -> file size, 322 | sizeUnit -> file size in bytes 323 | extension -> file extension 324 | tempDir -> file temporary directory 325 | 326 | ``` 327 | 328 | # Rule Validation 329 | 330 | - ### Create Request validation via cli. 331 | 332 | ``` 333 | npx nodemi make:request ProductRequest 334 | ``` 335 | 336 | The Request will be created in the `requests` directory. 337 | 338 | ``` js 339 | 340 | import RequestValidation from "../core/validation/RequestValidation.js" 341 | 342 | class ProductRequest extends RequestValidation { 343 | 344 | constructor(req) { 345 | super(req).load(this) 346 | } 347 | 348 | /** 349 | * Get the validation rules that apply to the request. 350 | * 351 | * @return object 352 | */ 353 | rules() { 354 | return { 355 | 356 | } 357 | } 358 | } 359 | 360 | export default ProductRequest 361 | 362 | ``` 363 | 364 | - ### Basic usage. 365 | 366 | ``` js 367 | 368 | const request = new ProductRequest(req) 369 | 370 | await request.check() 371 | 372 | if (request.isError) 373 | return request.responseError(res) // or return res.status(422).json(request.errors) 374 | 375 | ``` 376 | 377 | - ### Example html form. 378 | 379 | ``` html 380 | 381 |
382 |
383 | 384 |
385 | 386 | 387 | 388 | 389 | 390 |
391 |
392 | 393 | 394 | 395 | 396 |
397 |
398 | 399 | 400 | 401 | 402 |
403 |
404 | 405 | 406 | 407 |
408 |
409 | 410 | 411 | 412 |
413 | 414 | 415 |
416 | 417 |
418 | 419 |
420 |
421 | 422 | ``` 423 | 424 | - ### Example rules 425 | 426 | ``` js 427 | 428 | rules() { 429 | return { 430 | "name": { 431 | "rules": ["required"] 432 | }, 433 | "discount": { 434 | "rules": ["required", "float", "min:3", "max:4"] 435 | }, 436 | "expired_date": { 437 | "rules": ["required", "date", "date_after:now"] 438 | }, 439 | "product_image": { 440 | "rules": ["required", "image", "max_file:1,MB"] 441 | }, 442 | "item.*.name": { 443 | "rules": ["required"] 444 | }, 445 | "item.*.description": { 446 | "rules": ["required"] 447 | }, 448 | "price.*": { 449 | "rules": ["required", "float"] 450 | }, 451 | "comments.*": { 452 | "rules": ["required"] 453 | }, 454 | "seo.title": { 455 | "rules": ["required"] 456 | }, 457 | "seo.description.long": { 458 | "rules": ["required"] 459 | }, 460 | "seo.description.short": { 461 | "rules": ["required"] 462 | } 463 | } 464 | } 465 | 466 | ``` 467 | 468 | - ### Example error messages 469 | 470 | ``` js 471 | 472 | { 473 | "errors": { 474 | "name": [ 475 | "The Name is required" 476 | ], 477 | "discount": [ 478 | "The Discount is required", 479 | "The Discount must be valid format of float", 480 | "The Discount should be more or equal than 3", 481 | "The Discount should be less or equal than 4" 482 | ], 483 | "expired_date": [ 484 | "The Expired date is required", 485 | "The Expired date must be valid format of date", 486 | "The Expired date date must be after the now's date" 487 | ], 488 | "product_image": [ 489 | "The Product image is required" 490 | ], 491 | "item.0.name": [ 492 | "The Item.0.name is required" 493 | ], 494 | "item.1.name": [ 495 | "The Item.1.name is required" 496 | ], 497 | "item.0.description": [ 498 | "The Item.0.description is required" 499 | ], 500 | "item.1.description": [ 501 | "The Item.1.description is required" 502 | ], 503 | "price.0": [ 504 | "The Price.0 is required", 505 | "The Price.0 must be valid format of float" 506 | ], 507 | "price.1": [ 508 | "The Price.1 is required", 509 | "The Price.1 must be valid format of float" 510 | ], 511 | "comments.0": [ 512 | "The Comments.0 is required" 513 | ], 514 | "comments.1": [ 515 | "The Comments.1 is required" 516 | ], 517 | "comments.2": [ 518 | "The Comments.2 is required" 519 | ] 520 | "seo.title": [ 521 | "The Seo.title is required" 522 | ], 523 | "seo.description.long": [ 524 | "The Seo.description.long is required" 525 | ], 526 | "seo.description.short": [ 527 | "The Seo.description.short is required" 528 | ] 529 | } 530 | } 531 | 532 | ``` 533 | 534 | - ### Basic rules 535 | 536 | | Rule | Description | Example | 537 | | -------------------- | --------------------------------------------- | ----------------------------------------------------------- | 538 | | required | check empty value | "required" | 539 | | email | check email formats | "email" | 540 | | match | check match value with other value | "match:password" | 541 | | exists | check value exists in the database | "exists:users,email" or "exists:users,email,"+super.body.id | 542 | | unique | check value is unique in database | "unique:users,email" or "unique:users,email,"+super.body.id | 543 | | string | check value is an string | "string" | 544 | | float | check value is an float | "float" | 545 | | integer | check value is an ineteger | "integer" | 546 | | max | count maximum value of numeric, | "max:12" | 547 | | | if string/array its count the length | | 548 | | min | count minimum value of numeric, | "min:5" | 549 | | | if string/array its count the length | | 550 | | date | check value is date format | "date" | 551 | | array | check value is an array | "array" | 552 | | mimetypes | check file mimetypes | "mimetypes:image/webp,image/x-icon,video/mp4" | 553 | | mimes | check file extension | "mimes:jpg,png,jpeg" | 554 | | max_file | check maximum file size, | "max_file:1,GB" or "max_file:1,MB" or "max_file:1,Byte" | 555 | | | param can be `GB`, `MB`, `KB` or `Byte` | | 556 | | image | check file is an image format | "image" | 557 | | date_after | check value after particular date | "date_after:now" or "date_after:birthdate" | 558 | | | param can be `now`, or other field name | | 559 | | date_after_or_equal | check value after or equal particular date | "date_after_or_equal:now" | 560 | | | param can be `now`, or other field name | | 561 | | date_before | check value before particular date | "date_before:now" or "date_before:birthdate" | 562 | | | param can be `now`, or other field name | | 563 | | date_before_or_equal | check value before or equal particular date | "date_before_or_equal:now" | 564 | | | param can be `now`, or other field name | | 565 | | boolean | check value is an boolean | "boolean" | 566 | | in_array | check value exist in array | "in_array:1,3,4,1,4,5" | 567 | | not_in_array | check value is not include in array | "not_in_array:1,3,4,1,4,5" | 568 | | ip | check value is as ip address | "ip" | 569 | | url | check value is as url | "url" | 570 | | json | check value is as json format | "json" | 571 | | digits | check value digits, | "digits:4" | 572 | | max_digits | check maximum digits of value | "max_digits:20" | 573 | | min_digits | check minumum digits of value | "min_digits:20" | 574 | | digits_between | check digits bewteen of value | "digits_between:5,10" | 575 | | age_lt | check value is less than param | "age_lt:17" | 576 | | | value must be an date format | | 577 | | age_lte | check value is less than or equal to param | "age_lte:17" | 578 | | | value must be an date format | | 579 | | age_gt | check value is greater than param | "age_gt:17" | 580 | | | value must be an date format | | 581 | | age_gte | check value is greater than or equal to param | "age_gte:17" | 582 | | | value must be an date format | | 583 | 584 | - ### Custom 585 | 586 | Custom validation `messages` and `attribute` 587 | 588 | ``` js 589 | 590 | rules() { 591 | return { 592 | "name": { 593 | "rules": ["required"], 594 | "attribute": "Product name" 595 | }, 596 | "discount": { 597 | "rules": ["required", "float", "min:3", "max:4"], 598 | "messages": { 599 | "required": "The _attribute_ need discount", 600 | "float": "The data must be numeric" 601 | }, 602 | "attribute": "DISCOUNT" 603 | } 604 | } 605 | } 606 | 607 | ``` 608 | 609 | - ### Direct add error messages 610 | 611 | Direct add error message required key and error message. 612 | 613 | ``` js 614 | const request = new ProductRequest(req) 615 | await request.check() 616 | 617 | if (request.isError) 618 | { 619 | request.addError("name","Name have to .....") 620 | request.addError("name","Name must be .....") 621 | 622 | 623 | ``` 624 | 625 | # Custom Rule 626 | 627 | - ### Create Custom Rule via cli. 628 | 629 | ``` 630 | npx nodemi make:rule GmailRule 631 | ``` 632 | 633 | The Rule will be created in the `rules` directory. 634 | 635 | ``` js 636 | class GmailRule { 637 | 638 | constructor() { 639 | } 640 | 641 | /** 642 | * Determine if the validation rule passes. 643 | * @param {*} attribute 644 | * @param {*} value 645 | * @returns bolean 646 | */ 647 | passes(attribute, value) { 648 | 649 | return value.includes("@gmail.com") 650 | } 651 | 652 | /** 653 | * Get the validation error message. 654 | * 655 | * @return string 656 | */ 657 | message() { 658 | return 'The _attribute_ must be using @gmail.com' 659 | } 660 | } 661 | 662 | export default GmailRule 663 | 664 | ``` 665 | 666 | - ### Custom rule usage 667 | 668 | ``` js 669 | rules() { 670 | return { 671 | "email": { 672 | "rules": [ new GmailRule, "required","email" ] 673 | } 674 | } 675 | } 676 | ``` 677 | 678 | - ### Noted 679 | 680 | 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. 681 | 682 | # Role and Permissions 683 | 684 | A user model can have a role by binding using `hasRole(YourModel)` function inside `loadModels` in `models/Models.js` file. 685 | 686 | ``` js 687 | 688 | const loadModels = async () => { 689 | 690 | await User.sync() 691 | 692 | await hasRole(User) 693 | 694 | ``` 695 | 696 | - ### Set users role 697 | 698 | 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. 699 | 700 | ``` js 701 | 702 | const user = await User.create({ 703 | name: name, 704 | email: email, 705 | password: hashPassword 706 | }) 707 | 708 | await user.setRole("customer") // params is role id or name 709 | 710 | ``` 711 | 712 | - ### Get role 713 | 714 | Get role object by calling `instance.getRole()`, or direcly access role name `instance.getRole().name`. 715 | 716 | ``` js 717 | 718 | user.getRole() // role object 719 | user.getRole().name // role name 720 | 721 | 722 | ``` 723 | 724 | - ### Get permissions 725 | 726 | Get permission by calling `instance.getPermissions()` will get array of object, or `instance.getPermissionsName()` will get array of permissions name. 727 | 728 | ``` js 729 | 730 | user.getPermissions() // array of permissions object 731 | user.getPermissionsName() // array of permissions name [ "user-create","user-stored"] 732 | 733 | ``` 734 | 735 | - ### Remove role 736 | 737 | ``` js 738 | 739 | user.removeRole() 740 | 741 | ``` 742 | 743 | - ### Check user access 744 | 745 | Limitation user access using `GateAccess(userInstance,permissionNames)`, `permissionNames` must be an array of permission names. 746 | 747 | ``` js 748 | 749 | if (!GateAccess(user, ["user-create","user-stored","user-access"])) 750 | return res.sendStatus(403) // return forbidden status code 751 | 752 | ``` 753 | 754 | - ### Add permissions 755 | 756 | ``` js 757 | 758 | const permissions = [ 759 | "user-create", 760 | "user-stored", 761 | "user-edit", 762 | "user-update", 763 | "user-delete", 764 | "user-search" 765 | ] 766 | 767 | for (let permission of permissions) { 768 | await Permission.create({ name: permission }) 769 | } 770 | 771 | ``` 772 | 773 | - ### Add Role 774 | 775 | ``` js 776 | 777 | const roles = [ "admin","customer" ] 778 | 779 | for (let role of roles) { 780 | await Role.create({ name: role }) 781 | } 782 | 783 | ``` 784 | 785 | - ### Assigning Permissions to Roles 786 | 787 | Assign permissions to a role by using `roleInstance.assignPermissions(params)`, params can be a list of permissions `name` or `id`. 788 | 789 | ``` js 790 | 791 | const permissions = [ 792 | "user-create", 793 | "user-stored" 794 | ] 795 | 796 | const admin = await Role.findOne({ where: { name: "admin" } }) 797 | 798 | if (admin) { 799 | await admin.assignPermissions(permissions) 800 | } 801 | 802 | ``` 803 | 804 | # Resource 805 | 806 | - ### Create new resource via cli. 807 | 808 | ``` 809 | npx nodemi make:resource UserResource 810 | ``` 811 | 812 | The Resource will be created in `resources` directory. 813 | 814 | ``` js 815 | 816 | import Resource from "../core/resource/Resource.js" 817 | class UserResource extends Resource { 818 | constructor() { 819 | super().load(this) 820 | } 821 | 822 | /** 823 | * Transform the resource into custom object. 824 | * 825 | * @return 826 | */ 827 | toArray(data) { 828 | return {} 829 | } 830 | } 831 | 832 | export default UserResource 833 | 834 | ``` 835 | 836 | - ### Basic usage 837 | 838 | To create resources from a single object use `make` or `collection` for an array of objects. 839 | 840 | ``` js 841 | 842 | const userResource = new UserResource().make(user) // for single object 843 | 844 | const userResources = new UserResource().collection(users) // for array of object 845 | 846 | ``` 847 | 848 | - ### Example user resource 849 | 850 | ``` js 851 | 852 | class UserResource extends Resource { 853 | constructor() { 854 | super().load(this) 855 | } 856 | 857 | toArray(data) { 858 | return { 859 | "id": data.id, 860 | "name": data.name, 861 | "email": data.email, 862 | "image": data.getMediaByName("avatar")?.url ?? '', 863 | "role": data.getRole()?.name ?? '', 864 | "permissions": new PermissionResource().collection(data.getPermissions() ?? []), 865 | } 866 | } 867 | } 868 | 869 | ``` 870 | 871 | - ### Example permissions resource 872 | 873 | ``` js 874 | 875 | class PermissionResource extends Resource { 876 | constructor() { 877 | super().load(this) 878 | } 879 | 880 | toArray(data) { 881 | return { 882 | "id": data.id, 883 | "name": data.name 884 | } 885 | } 886 | } 887 | 888 | ``` 889 | 890 | - ### Example usage 891 | 892 | ``` js 893 | 894 | const user = await User.findOne({ 895 | where: { 896 | id: 1 897 | } 898 | }) 899 | 900 | const userResource = new UserResource().make(user) 901 | 902 | res.json(userResource) 903 | 904 | ``` 905 | 906 | - ### Example result 907 | 908 | ``` js 909 | 910 | { 911 | "id": 1, 912 | "name": "Andre", 913 | "email": "andre@gmail.com", 914 | "image": "http://localhost:5000/User-1/287d735a-2880-4d4f-9851-5055d1ba1aae.jpg", 915 | "role": "customer", 916 | "permissions": [ 917 | { 918 | "id": 1, 919 | "name": "user-create" 920 | }, 921 | { 922 | "id": 2, 923 | "name": "user-stored" 924 | } 925 | ] 926 | } 927 | 928 | ``` 929 | 930 | # Auth Jwt 931 | 932 | - ### Create token 933 | 934 | Create token by calling `JwtAuth.createToken()`, that will return `refreshToken` and `accessToken`. 935 | 936 | ``` js 937 | const payload = { 938 | id: user.id, 939 | name: user.name, 940 | email: user.email 941 | } 942 | 943 | const token = JwtAuth.createToken(payload) 944 | 945 | console.log(token.refreshToken) 946 | console.log(token.accessToken) 947 | 948 | 949 | ``` 950 | 951 | - ### Regenerate access token 952 | 953 | Regenerate access token by calling `JwtAuth.regenerateAccessToken(refreshToken)`, that will return new access token. 954 | 955 | ``` js 956 | 957 | const accessToken = JwtAuth.regenerateAccessToken(refreshToken) 958 | 959 | ``` 960 | 961 | - ### Get Auth user 962 | 963 | Get authenticated user by `calling JwtAuth.getUser(req)`, that will get user by refresh token on request cookies. 964 | 965 | ``` js 966 | 967 | const user = await JwtAuth.getUser(req) 968 | 969 | ``` 970 | 971 | Or you just setup the .env `AUTH_GET_CURRENT_USER_ON_REQUEST=true` and you can access current authenticated user by access 972 | `req.user`. 973 | 974 | 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. 975 | 976 | ``` js 977 | class AuthConfig { 978 | 979 | /** 980 | * Default user model for auth 981 | * @returns 982 | */ 983 | static user = User 984 | ``` 985 | 986 | - ### Use Middleware - Auth Jwt 987 | 988 | For secure access to controller by adding `JwtAuthPass` to your router. 989 | 990 | ``` js 991 | import JwtAuthPass from '../core/middleware/JwtAuthPass.js'; 992 | 993 | routerAuth.use(JwtAuthPass) 994 | routerAuth.get("/upload", UserController.upload) 995 | 996 | app.use("/api",routerAuth) 997 | 998 | ``` 999 | 1000 | Header Request 1001 | 1002 | ``` js 1003 | Authorization: 'Bearer ' + accessToken 1004 | ``` 1005 | 1006 | - ### Use Middleware - Basic auth 1007 | 1008 | For secure access to controller by adding `BasicAuthPass` to your router. 1009 | 1010 | ``` js 1011 | import BasicAuthPass from '../core/middleware/BasicAuthPass.js'; 1012 | 1013 | routerAuth.use(BasicAuthPass) 1014 | routerAuth.get("/upload", UserController.upload) 1015 | 1016 | app.use("/api",routerAuth) 1017 | 1018 | ``` 1019 | 1020 | Before using this, make sure already set username and password for basic auth in `.env` file. 1021 | 1022 | ``` env 1023 | AUTH_BASIC_AUTH_USERNAME=myBasicUsername 1024 | AUTH_BASIC_AUTH_PASSWORD=myBasicPassword 1025 | ``` 1026 | 1027 | Header Request 1028 | 1029 | ``` js 1030 | Authorization: 'Basic ' + encodeBase64(myBasicUsername+':'+myBasicPassword) 1031 | ``` 1032 | 1033 | # Locale 1034 | 1035 | - Config 1036 | 1037 | 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`, 1038 | 1039 | ``` js 1040 | 1041 | defaultLocale: "en", 1042 | useLocale: useLocale, 1043 | locales: ["en", "id"] 1044 | 1045 | ``` 1046 | 1047 | You can add more locale Code to `locales`. By default `locales` are only available for English `en`, and for Indonesia `id`. 1048 | 1049 | - Default validation error Messages 1050 | 1051 | 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. 1052 | 1053 | ``` js 1054 | 1055 | const langValidation = Object.freeze({ 1056 | required: { 1057 | en: "The _attribute_ is required", 1058 | id: "_attribute_ wajib di isi", 1059 | //ja: "_attribute_ ........." -> adding new validation messages for code ja 1060 | }, 1061 | email: { 1062 | en: "The _attribute_ must in E-mail format", 1063 | id: "_attribute_ harus dalam format E-mail", 1064 | }, 1065 | match: { 1066 | en: "The _attribute_ must be match with _param1_", 1067 | id: "_attribute_ harus sama dengan _param1_" 1068 | }, 1069 | ...... 1070 | 1071 | ``` 1072 | 1073 | - Use Locale 1074 | 1075 | 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`. 1076 | 1077 | ``` js 1078 | 1079 | // example for api route 1080 | const routerAuth = express.Router() 1081 | 1082 | routerAuth.use(JwtAuthPass) 1083 | routerAuth.get("/user", UserController.getUser) 1084 | routerAuth.post("/upload", UserController.upload)s 1085 | 1086 | app.use("/api/:locale", routerAuth) 1087 | 1088 | // http://localhost:5000/api/en/endpoint | http://localhost:5000/api/id/endpoint 1089 | 1090 | ``` 1091 | 1092 | 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. 1093 | 1094 | ``` js 1095 | 1096 | // example for web route 1097 | app.get("/:locale",LocalePass, (req, res) => { 1098 | 1099 | // http://localhost:5000/en | http://localhost:5000/id 1100 | 1101 | 1102 | // example for api route 1103 | app.get("/api/:locale/login",LocalePass, (req, res) => { 1104 | 1105 | // http://localhost:5000/api/en/login | http://localhost:5000/api/id/login 1106 | 1107 | ``` 1108 | 1109 | - Noted 1110 | 1111 | All routers that using `LocalePass` will have the locale Code on req, accessible via `req.locale`. 1112 | 1113 | # Mail 1114 | 1115 | Create mail via cli. 1116 | 1117 | ``` 1118 | npx nodemi make:mail AccountVerify 1119 | ``` 1120 | 1121 | The mail will be created in the `mails` directory, with `examplefile.txt` and `template.ejs` 1122 | 1123 | ``` js 1124 | 1125 | import Mail from "../../core/mail/Mail.js" 1126 | 1127 | class AccountVerify extends Mail { 1128 | constructor(from = String, to = [], subject = String) { 1129 | super().load({ 1130 | from: from, 1131 | to: to, 1132 | subject: subject, 1133 | text: "Just need to verify that this is your email address.", 1134 | attachments: [ 1135 | { 1136 | filename: "theFile.txt", 1137 | path: "mails/AccountVerify/examplefile.txt" 1138 | }, 1139 | ], 1140 | html: { 1141 | path: "mails/AccountVerify/template.ejs", 1142 | data: { 1143 | title: "Welcome to the party!", 1144 | message: "Just need to verify that this is your email address." 1145 | } 1146 | }, 1147 | }) 1148 | } 1149 | } 1150 | 1151 | export default AccountVerify 1152 | 1153 | ``` 1154 | 1155 | The `template.ejs` using express view engine `ejs` to render html into mail content. 1156 | 1157 | ``` ejs 1158 | 1159 | 1160 | 1161 | 1162 | 1163 | 1164 | 1165 | <%= title %> 1166 | 1167 | 1168 | 1169 |

Hello!

1170 |

1171 | <%= message %> 1172 |

1173 |

1174 | Regards. 1175 |

1176 |

1177 | Nodemi 1178 |

1179 | 1180 | 1181 | 1182 | ``` 1183 | 1184 | To use this `template.ejs`, you need to add an `html` object with a `path` and `data` (if needed) into `super().load()` method. 1185 | 1186 | ``` js 1187 | html: { 1188 | path: "mails/AccountVerify/template.ejs", // path is required 1189 | data: // data is optional base on your template.ejs 1190 | { 1191 | title: "Welcome to the party!", 1192 | message: "Just need to verify that this is your email address." 1193 | } 1194 | } 1195 | ``` 1196 | 1197 | - Basic usage 1198 | 1199 | To send email by calling `instance.send()` 1200 | 1201 | ``` js 1202 | const sendMail = new AccountVerify("from@gmail.com",["receiver@gmail.com"],"Verify Account") 1203 | 1204 | await sendMail.send() 1205 | 1206 | ``` 1207 | 1208 | - Send file 1209 | 1210 | To send files, you need to add an `attachments` to `super().load()`. See full doc. 1211 | 1212 | ``` js 1213 | attachments: [ 1214 | { 1215 | filename: "theFile.txt", 1216 | path: "mails/AccountVerify/examplefile.txt" 1217 | } 1218 | ] 1219 | 1220 | ``` 1221 | 1222 | - Mail message options 1223 | 1224 | Message options that you can add into `super().load()`. 1225 | 1226 | | Name | Description | Type | Required | 1227 | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------ | ------ | -------- | 1228 | | from | The e-mail address of the sender. All e-mail addresses can be plain 'sender@server.com' | string | `Yes` | 1229 | | to | Recipients e-mail addresses that will appear on the To | array | `Yes` | 1230 | | subject | Subject of the e-mail | string | No | 1231 | | text | If you are using HTML for the body of the email, then this text will not be used again | string | No | 1232 | | html | The HTML version of the message | object | No | 1233 | | attachments | An array of objects is used for the purpose of sending files. See full doc | array | No | 1234 | | cc | Recipients e-mail addresses that will appear on the Cc | array | No | 1235 | | bcc | Recipients e-mail addresses that will appear on the Bcc | array | No | 1236 | | sender | E-mail address that will appear on the Sender | string | No | 1237 | | replyTo | An array of e-mail addresses that will appear on the Reply-To | array | No | 1238 | | alternatives | An array of alternative text contents. See full doc | array | No | 1239 | | encoding | optional transfer encoding for the textual parts. | string | No | 1240 | 1241 | - Noted 1242 | 1243 | Before using mail, make sure you already setup .env file 1244 | 1245 | ``` env 1246 | MAIL_HOST= #example: smtp.gmail.com | smtp-relay.sendinblue.com 1247 | MAIL_PORT=587 1248 | MAIL_USERNAME= 1249 | MAIL_PASSWORD= 1250 | MAIL_FROM= 1251 | 1252 | ``` 1253 | 1254 | # Firebase Cloud Messaging 1255 | 1256 | - Send message 1257 | 1258 | ``` js 1259 | const message = { 1260 | title: "Notification", // notification title 1261 | body: "Hello there", // notification body 1262 | data: { 1263 | // payload 1264 | }, 1265 | registrationTokens: ["token1","token2"] // target token 1266 | } 1267 | 1268 | await FirebaseService.sendMessage(message) 1269 | 1270 | ``` 1271 | 1272 | - Noted 1273 | 1274 | 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 1275 | 1276 | ``` env 1277 | FIREBASE_SERVICE_ACCOUNT_BASE64= # base64 of firebase service account (.json) 1278 | FIREBASE_CLOUD_MESSAGING_SERVER_KEY= #fcm server key 1279 | 1280 | ``` 1281 | 1282 | # Seeder 1283 | 1284 | - Running seeder 1285 | 1286 | Running seeder via cli 1287 | 1288 | ``` 1289 | npx nodemi seed:run 1290 | ``` 1291 | 1292 | You can put your seeder code inside `seeder` function in the `core/seeder/Seeder.js` file 1293 | 1294 | ``` js 1295 | 1296 | const seeder = async () => { 1297 | 1298 | // put code here.. 1299 | 1300 | } 1301 | 1302 | ``` 1303 | 1304 | # Cors 1305 | 1306 | The configuration for Cross-Origin Resource Sharing (CORS) can be found in the `core/config/Cors.js` file. 1307 | --------------------------------------------------------------------------------