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 |
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 |
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 |
--------------------------------------------------------------------------------