🚀 Let's ride!
This commit is contained in:
commit
372a7a5dee
8
.babelrc
Normal file
8
.babelrc
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"presets": [
|
||||
"@babel/preset-env"
|
||||
],
|
||||
"plugins": [
|
||||
"babel-plugin-root-import"
|
||||
]
|
||||
}
|
||||
19
.env.example
Executable file
19
.env.example
Executable file
@ -0,0 +1,19 @@
|
||||
JWT_SECRET="your-secret-key"
|
||||
|
||||
DEV_DB_DIALECT=mariadb
|
||||
DEV_DB_HOST=localhost
|
||||
DEV_DB_PORT=3306
|
||||
DEV_DB_USERNAME=root
|
||||
DEV_DB_PASSWORD=
|
||||
DEV_DB_NAME=production_db
|
||||
DEV_DB_LOGGING=false
|
||||
|
||||
DB_DIALECT=mariadb
|
||||
DB_HOSTNAME=localhost
|
||||
DB_PORT=3306
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=
|
||||
DB_NAME=development_db
|
||||
DB_LOGGING=false
|
||||
|
||||
VITE_API_URL=http://localhost:3000/api/v1
|
||||
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
.env
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
server/.env
|
||||
61
README.md
Normal file
61
README.md
Normal file
@ -0,0 +1,61 @@
|
||||
# WebService Boilerplate
|
||||
|
||||
A boilerplate for a web service using NodeJS and Express.
|
||||
|
||||
## Installation
|
||||
|
||||
Install dependencies with `yarn`
|
||||
|
||||
```bash
|
||||
yarn
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Build the project with `yarn build`
|
||||
|
||||
```bash
|
||||
yarn build
|
||||
```
|
||||
|
||||
Start the server with `yarn start`
|
||||
|
||||
```bash
|
||||
yarn start
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
Run `yarn dev` to start a development server.
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
## Sequelize CLI
|
||||
|
||||
You can use the [Sequelize CLI](https://sequelize.org/docs/v6/cli/) to manage your database.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The following environment variables are required:
|
||||
|
||||
| Variable | Description |
|
||||
|-------------------|-----------------------------------------------------------|
|
||||
| `DB_USERNAME` | The username for the database |
|
||||
| `DB_PASSWORD` | The password for the database |
|
||||
| `DB_NAME` | The name of the database |
|
||||
| `DB_HOSTNAME` | The hostname of the database |
|
||||
| `DB_PORT` | The port of the database |
|
||||
| `DB_DIALECT` | The dialect of the database |
|
||||
| `DB_LOGGING` | Whether to log the queries to the console (true or false) |
|
||||
| `DEV_DB_USERNAME` | The username for the development database |
|
||||
| `DEV_DB_PASSWORD` | The password for the development database |
|
||||
| `DEV_DB_NAME` | The name of the development database |
|
||||
| `DEV_DB_HOST` | The hostname of the development database |
|
||||
| `DEV_DB_PORT` | The port of the development database |
|
||||
| `DEV_DB_DIALECT` | The dialect of the development database |
|
||||
| `DEV_DB_LOGGING` | Whether to log the queries to the console (true or false) |
|
||||
| `JWT_SECRET` | The secret for the JWT token |
|
||||
| `VITE_API_URL` | The URL of the API |
|
||||
|
||||
87
app.js
Normal file
87
app.js
Normal file
@ -0,0 +1,87 @@
|
||||
/**
|
||||
* @file Main entry point for the application backend.
|
||||
*/
|
||||
|
||||
import express from "express";
|
||||
import path from "path";
|
||||
|
||||
import cors from "cors";
|
||||
import passport from "passport";
|
||||
import BearerStrategy from "passport-http-bearer";
|
||||
|
||||
|
||||
|
||||
import spaRouter from "./routers/spaRouter.js";
|
||||
import api_v1_Router from "./routers/api_v1_Router.js";
|
||||
import expressListRoutes from "express-list-routes";
|
||||
|
||||
import AuthController from "./modules/Auth/AuthController";
|
||||
|
||||
/**
|
||||
* The port the server will listen on.
|
||||
* @type {number}
|
||||
*/
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
/**
|
||||
* The path to the distribution directory.
|
||||
* @type {string}
|
||||
*/
|
||||
const distPath = path.join(path.resolve(), "dist");
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The Express application instance.
|
||||
* @type {express.Application}
|
||||
*/
|
||||
const app = express();
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
/**
|
||||
* Serves static files from the distribution directory in production, or the current working directory in development.
|
||||
*/
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
app.use("/", express.static(distPath));
|
||||
} else {
|
||||
app.use(express.static(path.join(process.cwd())));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Authentication middlewares
|
||||
*/
|
||||
app.use(cors());
|
||||
passport.use(new BearerStrategy(AuthController.auth))
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Mounts the API v1 router at the `/api/v1` endpoint.
|
||||
*/
|
||||
app.use("/api/v1", api_v1_Router);
|
||||
|
||||
|
||||
/**
|
||||
* Handles requests to the `/api` endpoint that don't match any existing routes.
|
||||
* Responds with a 404 error.
|
||||
*/
|
||||
app.use("/api", (req, res) => {
|
||||
res.status(404).json({error: 404});
|
||||
});
|
||||
|
||||
/**
|
||||
* Mounts the SPA router.
|
||||
*/
|
||||
app.use(spaRouter);
|
||||
|
||||
|
||||
/**
|
||||
* Starts the server and listens for incoming requests on the specified port.
|
||||
*/
|
||||
app.listen(port, () => {
|
||||
console.log("Server listening on port", port);
|
||||
});
|
||||
expressListRoutes(app);
|
||||
|
||||
32
config/config.js
Normal file
32
config/config.js
Normal file
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* @file Configuration file for database (Sequelize)
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
module.exports = {
|
||||
development: {
|
||||
username: process.env.DEV_DB_USERNAME,
|
||||
password: process.env.DEV_DB_PASSWORD,
|
||||
database: process.env.DEV_DB_NAME,
|
||||
host: process.env.DEV_DB_HOST,
|
||||
port: process.env.DEV_DB_PORT,
|
||||
dialect: process.env.DEV_DB_DIALECT || 'mysql',
|
||||
dialectOptions: {
|
||||
bigNumberStrings: true,
|
||||
},
|
||||
logging: process.env.DEV_DB_LOGGING === 'true',
|
||||
},
|
||||
production: {
|
||||
username: process.env.DB_USERNAME,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
host: process.env.DB_HOSTNAME,
|
||||
port: process.env.DB_PORT,
|
||||
dialect: process.env.DB_DIALECT,
|
||||
dialectOptions: {
|
||||
bigNumberStrings: true,
|
||||
},
|
||||
logging: process.env.DB_LOGGING === 'true',
|
||||
},
|
||||
};
|
||||
47
migrations/20241228133529-create-user.js
Normal file
47
migrations/20241228133529-create-user.js
Normal file
@ -0,0 +1,47 @@
|
||||
'use strict';
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.createTable('users', {
|
||||
id: {
|
||||
allowNull: false,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
type: Sequelize.INTEGER
|
||||
},
|
||||
username: {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
email: {
|
||||
type: Sequelize.STRING
|
||||
},
|
||||
firstName: {
|
||||
type: Sequelize.STRING
|
||||
},
|
||||
lastName: {
|
||||
type: Sequelize.STRING
|
||||
},
|
||||
password: {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
isActive: {
|
||||
type: Sequelize.BOOLEAN,
|
||||
defaultValue: true,
|
||||
allowNull: false
|
||||
},
|
||||
createdAt: {
|
||||
allowNull: false,
|
||||
type: Sequelize.DATE
|
||||
},
|
||||
updatedAt: {
|
||||
allowNull: false,
|
||||
type: Sequelize.DATE
|
||||
}
|
||||
});
|
||||
},
|
||||
async down(queryInterface, Sequelize) {
|
||||
await queryInterface.dropTable('users');
|
||||
}
|
||||
};
|
||||
87
models/index.js
Normal file
87
models/index.js
Normal file
@ -0,0 +1,87 @@
|
||||
/**
|
||||
* @file Database configuration and model initialization for Sequelize.
|
||||
* Initializes the Sequelize ORM based on environment settings and loads database models.
|
||||
* Provides access to the Sequelize instance and loaded models.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const Sequelize = require('sequelize');
|
||||
const process = require('process');
|
||||
|
||||
/**
|
||||
* Base filename of the current file
|
||||
* @type {string}
|
||||
*/
|
||||
const basename = path.basename(__filename);
|
||||
|
||||
/**
|
||||
* Current environment (development/production/test)
|
||||
* @type {string}
|
||||
*/
|
||||
const env = process.env.NODE_ENV || 'development';
|
||||
|
||||
/**
|
||||
* Database configuration for the current environment
|
||||
* @type {Object}
|
||||
*/
|
||||
const config = require(__dirname + '/../config/config.js')[env];
|
||||
|
||||
/**
|
||||
* Object containing all database models and Sequelize instances
|
||||
* @type {Object}
|
||||
*/
|
||||
const db = {};
|
||||
|
||||
/**
|
||||
* Sequelize instance for database connection
|
||||
* @type {Sequelize}
|
||||
*/
|
||||
let sequelize;
|
||||
|
||||
// Initialize Sequelize with environment config or direct config
|
||||
if (config.use_env_variable) {
|
||||
sequelize = new Sequelize(process.env[config.use_env_variable], config);
|
||||
} else {
|
||||
sequelize = new Sequelize(config.database, config.username, config.password, config);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Load all model files from current directory and initialize them
|
||||
*/
|
||||
fs
|
||||
.readdirSync(__dirname)
|
||||
.filter(file => {
|
||||
return (
|
||||
file.indexOf('.') !== 0 &&
|
||||
file !== basename &&
|
||||
file.slice(-3) === '.js' &&
|
||||
file.indexOf('.test.js') === -1
|
||||
);
|
||||
})
|
||||
.forEach(file => {
|
||||
/**
|
||||
* Initialize model from file
|
||||
* @type {Sequelize.Model}
|
||||
*/
|
||||
const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes);
|
||||
db[model.name] = model;
|
||||
});
|
||||
|
||||
/**
|
||||
* Set up model associations if defined
|
||||
*/
|
||||
Object.keys(db).forEach(modelName => {
|
||||
if (db[modelName].associate) {
|
||||
db[modelName].associate(db);
|
||||
}
|
||||
});
|
||||
|
||||
// Add Sequelize instances to db object
|
||||
db.sequelize = sequelize;
|
||||
db.Sequelize = Sequelize;
|
||||
|
||||
module.exports = db;
|
||||
59
models/user.js
Normal file
59
models/user.js
Normal file
@ -0,0 +1,59 @@
|
||||
'use strict';
|
||||
const {
|
||||
Model
|
||||
} = require('sequelize');
|
||||
|
||||
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
class User extends Model {
|
||||
/**
|
||||
* Helper method for defining associations.
|
||||
* This method is not a part of Sequelize lifecycle.
|
||||
* The `models/index` file will call this method automatically.
|
||||
*/
|
||||
static associate(models) {
|
||||
// define association here
|
||||
}
|
||||
}
|
||||
User.init({
|
||||
// Model attributes are defined here
|
||||
username: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
},
|
||||
email: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
firstName: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
lastName: {
|
||||
type: DataTypes.STRING,
|
||||
// allowNull defaults to true
|
||||
},
|
||||
password: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
isActive: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true,
|
||||
},
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'User',
|
||||
tableName: 'users',
|
||||
defaultScope: {
|
||||
attributes: { exclude: ['password'] },
|
||||
},
|
||||
scopes: {
|
||||
withPassword: {
|
||||
attributes: { },
|
||||
}
|
||||
}
|
||||
});
|
||||
return User;
|
||||
};
|
||||
106
modules/Auth/AuthController.js
Normal file
106
modules/Auth/AuthController.js
Normal file
@ -0,0 +1,106 @@
|
||||
import jwt from "jsonwebtoken";
|
||||
import {User} from "../../models/index";
|
||||
import Controller from "../Controller";
|
||||
import bcrypt from "bcrypt";
|
||||
|
||||
|
||||
class AuthController extends Controller {
|
||||
|
||||
async auth(token, done) {
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
const user = await User.findByPk(decoded.userId);
|
||||
if (!user) {
|
||||
return done(null, false);
|
||||
} else {
|
||||
return done(null, user);
|
||||
}
|
||||
} catch (e) {
|
||||
return done(null, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async login(req, res) {
|
||||
try {
|
||||
// Получаем email и пароль из тела запроса
|
||||
const {username, password} = req.body;
|
||||
|
||||
// Находим пользователя по username
|
||||
const user = await User.scope('withPassword').findOne({where: {username}});
|
||||
|
||||
// Если пользователь не найден, возвращаем ошибку
|
||||
if (!user) {
|
||||
console.log(`User not found ${username}`);
|
||||
return res.status(401).json({message: 'Неверное имя пользователя или пароль'});
|
||||
}
|
||||
|
||||
|
||||
// Сравниваем пароль из запроса с хешированным паролем пользователя
|
||||
const isPasswordValid = await bcrypt.compare(password, user.password);
|
||||
|
||||
// Если пароль неверный, возвращаем ошибку
|
||||
if (!isPasswordValid) {
|
||||
return res.status(401).json({message: 'Неверное имя пользователя или пароль'});
|
||||
}
|
||||
|
||||
// Создаем JWT-токен
|
||||
const token = jwt.sign({userId: user.id}, process.env.JWT_SECRET, {
|
||||
expiresIn: '1h',
|
||||
});
|
||||
|
||||
// Возвращаем токен
|
||||
res.json({token});
|
||||
} catch (error) {
|
||||
console.error('Internal server error', error);
|
||||
res.status(500).json({message: 'Internal server error'});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async user(req, res) {
|
||||
try {
|
||||
return res.json(req.user);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({message: 'Internal server error'});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
async logout(req, res) {
|
||||
try {
|
||||
// Удаляем токен из заголовка авторизации
|
||||
res.clearCookie('token');
|
||||
res.json({message: 'Вы успешно вышли из системы'});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({message: 'Internal server error'});
|
||||
}
|
||||
}
|
||||
|
||||
async generatePasswordHash(req, res) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// Get passwort from get request
|
||||
const password = req.query.password;
|
||||
|
||||
// Hash password
|
||||
bcrypt.hash(password, 10, (err, hash) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return res.status(500).json({message: 'Internal server error'});
|
||||
}
|
||||
console.log(hash)
|
||||
return res.json({hash});
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
export default new AuthController()
|
||||
5
modules/Auth/AuthService.js
Normal file
5
modules/Auth/AuthService.js
Normal file
@ -0,0 +1,5 @@
|
||||
import Service from "../Service";
|
||||
|
||||
class AuthService extends Service {
|
||||
|
||||
}
|
||||
7
modules/Controller.js
Normal file
7
modules/Controller.js
Normal file
@ -0,0 +1,7 @@
|
||||
/**
|
||||
* @desc Class HTTP Controller
|
||||
*/
|
||||
class Controller {
|
||||
|
||||
}
|
||||
export default Controller
|
||||
4
modules/Service.js
Normal file
4
modules/Service.js
Normal file
@ -0,0 +1,4 @@
|
||||
class Service {
|
||||
|
||||
}
|
||||
export default Service
|
||||
49
package.json
Normal file
49
package.json
Normal file
@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "selfhostie",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev:backend": "nodemon --exec babel-node app.js",
|
||||
"dev:frontend": "vite",
|
||||
"dev": "cross-env NODE_ENV=development concurrently 'yarn dev:backend' 'yarn dev:frontend'",
|
||||
"start": "cross-env NODE_ENV=production babel-node app.js",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/register": "^7.25.9",
|
||||
"axios": "^1.7.9",
|
||||
"bcrypt": "^5.1.1",
|
||||
"concurrently": "^9.1.2",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.7",
|
||||
"ejs": "^3.1.10",
|
||||
"express": "^5.0.1",
|
||||
"express-list-routes": "^1.2.4",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mariadb": "^3.4.0",
|
||||
"mysql2": "^3.12.0",
|
||||
"nodemon": "^3.1.9",
|
||||
"passport": "^0.7.0",
|
||||
"passport-http-bearer": "^1.0.1",
|
||||
"pinia": "^3.0.1",
|
||||
"sequelize": "^6.37.5",
|
||||
"vue": "^3.5.13",
|
||||
"vue-auth3": "^1.1.0",
|
||||
"vue-router": "4",
|
||||
"vuetify": "^3.7.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.26.9",
|
||||
"@babel/node": "^7.26.0",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/preset-env": "^7.26.9",
|
||||
"@mdi/font": "^7.4.47",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"babel-plugin-root-import": "^6.6.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"sass-embedded": "^1.85.0",
|
||||
"sequelize-cli": "^6.6.2",
|
||||
"vite": "^6.1.0"
|
||||
}
|
||||
}
|
||||
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
18
routers/api_v1_Router.js
Normal file
18
routers/api_v1_Router.js
Normal file
@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @file API v1 router
|
||||
* Routes all requests to /api/v1/*
|
||||
*/
|
||||
|
||||
import express from "express";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/hello", (_req, res) => {
|
||||
res.json({message: "Hello, world!"});
|
||||
});
|
||||
|
||||
/* Auth routes */
|
||||
require("./v1/auth")(router);
|
||||
|
||||
|
||||
export default router;
|
||||
36
routers/spaRouter.js
Normal file
36
routers/spaRouter.js
Normal file
@ -0,0 +1,36 @@
|
||||
/**
|
||||
* @file SPA router
|
||||
* Routes all requests to index.html view. Index.html is a Vue.js single page application.
|
||||
* If in production, manifest.json is parsed and passed to index.html
|
||||
*/
|
||||
|
||||
import express from "express";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
|
||||
const environment = process.env.NODE_ENV;
|
||||
|
||||
router.get(/.*/, async (req, res) => {
|
||||
const manifest = await parseManifest()
|
||||
|
||||
const data = {
|
||||
environment, manifest
|
||||
}
|
||||
|
||||
res.render("index.html.ejs", data)
|
||||
})
|
||||
|
||||
const parseManifest = async () => {
|
||||
if (environment !== "production") return {}
|
||||
|
||||
const manifestPath = path.join(path.resolve(), "dist", ".vite", "manifest.json");
|
||||
const manifestFile = await fs.readFile(manifestPath);
|
||||
|
||||
return JSON.parse(manifestFile)
|
||||
}
|
||||
|
||||
|
||||
export default router;
|
||||
14
routers/v1/auth.js
Normal file
14
routers/v1/auth.js
Normal file
@ -0,0 +1,14 @@
|
||||
import passport from "passport";
|
||||
import AuthController from "../../modules/Auth/AuthController";
|
||||
|
||||
module.exports = function(router){
|
||||
router.post('/auth/login', AuthController.login)
|
||||
router.get('/auth/user', passport.authenticate('bearer', {session: false}), AuthController.user)
|
||||
router.post('/auth/logout', AuthController.logout)
|
||||
|
||||
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
router.get('/auth/generatePasswordHash', AuthController.generatePasswordHash)
|
||||
}
|
||||
}
|
||||
7
src-frontend/App.vue
Normal file
7
src-frontend/App.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<AppLayout></AppLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AppLayout from "./layouts/AppLayout.vue";
|
||||
</script>
|
||||
1
src-frontend/assets/vue.svg
Normal file
1
src-frontend/assets/vue.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
20
src-frontend/components/TestComponent.vue
Normal file
20
src-frontend/components/TestComponent.vue
Normal file
@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<h2>Server says: {{ serverHello }}</h2>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useAxios} from "../composables/useAxios.js";
|
||||
import {onMounted, ref} from "vue";
|
||||
const {client} = useAxios();
|
||||
|
||||
const serverHello = ref('');
|
||||
|
||||
onMounted(async () => {
|
||||
const response = await client.get('hello')
|
||||
serverHello.value = response.data
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
39
src-frontend/composables/useAxios.js
Normal file
39
src-frontend/composables/useAxios.js
Normal file
@ -0,0 +1,39 @@
|
||||
import axios from 'axios'
|
||||
|
||||
export const useAxios = () => {
|
||||
const client = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
withCredentials: true
|
||||
})
|
||||
|
||||
client.interceptors.request.use((config) => {
|
||||
|
||||
/**
|
||||
* Here you can modify every request
|
||||
*/
|
||||
|
||||
return config
|
||||
});
|
||||
|
||||
client.interceptors.response.use((response) => {
|
||||
|
||||
/**
|
||||
* Here you can modify every response
|
||||
*/
|
||||
|
||||
return response
|
||||
}, (error) => {
|
||||
|
||||
/**
|
||||
* Here you can modify every error
|
||||
*/
|
||||
|
||||
return Promise.reject(error)
|
||||
});
|
||||
|
||||
|
||||
return { client };
|
||||
}
|
||||
33
src-frontend/layouts/AppLayout.vue
Normal file
33
src-frontend/layouts/AppLayout.vue
Normal file
@ -0,0 +1,33 @@
|
||||
<script >
|
||||
import { computed, defineComponent } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AppLayout',
|
||||
setup() {
|
||||
const route = useRoute();
|
||||
/**
|
||||
* This is a computed property that will return the name
|
||||
* of the current route
|
||||
*/
|
||||
const layout = computed(() => {
|
||||
const layout = route?.meta?.layout;
|
||||
|
||||
if (layout) {
|
||||
console.log('layout', layout)
|
||||
return `${layout}Layout`;
|
||||
}
|
||||
return 'div';
|
||||
});
|
||||
return {
|
||||
layout,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="layout">
|
||||
<router-view />
|
||||
</component>
|
||||
</template>
|
||||
61
src-frontend/layouts/AuthLayout.vue
Normal file
61
src-frontend/layouts/AuthLayout.vue
Normal file
@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<v-app id="inspire">
|
||||
<v-app-bar flat>
|
||||
<v-container class="mx-auto d-flex align-center justify-center">
|
||||
<v-avatar
|
||||
class="me-4"
|
||||
color="grey-darken-1"
|
||||
size="32"
|
||||
text="AL"
|
||||
></v-avatar>
|
||||
<v-chip text="AuthLayout"></v-chip>
|
||||
<v-btn
|
||||
v-for="link in links"
|
||||
:key="link"
|
||||
:text="link.text"
|
||||
variant="text"
|
||||
:to="link.to"
|
||||
></v-btn>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-responsive max-width="160">
|
||||
<v-text-field
|
||||
density="compact"
|
||||
label="Поиск"
|
||||
rounded="lg"
|
||||
variant="solo-filled"
|
||||
flat
|
||||
hide-details
|
||||
single-line
|
||||
></v-text-field>
|
||||
</v-responsive>
|
||||
</v-container>
|
||||
</v-app-bar>
|
||||
|
||||
<v-main class="bg-grey-lighten-3">
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-sheet
|
||||
class="pa-5"
|
||||
min-height="70vh"
|
||||
rounded="lg"
|
||||
>
|
||||
<slot></slot>
|
||||
</v-sheet>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-main>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const links = [
|
||||
{text: 'Главная', to: '/'},
|
||||
{text: 'О сервисе', to: '/about'},
|
||||
{text: 'Авторизация', to: '/login'},
|
||||
|
||||
]
|
||||
</script>
|
||||
87
src-frontend/layouts/MainLayout.vue
Normal file
87
src-frontend/layouts/MainLayout.vue
Normal file
@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<v-app id="inspire">
|
||||
<v-app-bar flat>
|
||||
<v-container class="mx-auto d-flex align-center justify-center">
|
||||
<v-avatar
|
||||
class="me-4"
|
||||
color="grey-darken-1"
|
||||
size="32"
|
||||
text="ML"
|
||||
></v-avatar>
|
||||
<v-chip text="MainLayout"></v-chip>
|
||||
<v-btn
|
||||
v-for="link in links"
|
||||
:key="link"
|
||||
:text="link.text"
|
||||
variant="text"
|
||||
:to="link.to"
|
||||
></v-btn>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-responsive max-width="160">
|
||||
<v-text-field
|
||||
density="compact"
|
||||
label="Поиск"
|
||||
rounded="lg"
|
||||
variant="solo-filled"
|
||||
flat
|
||||
hide-details
|
||||
single-line
|
||||
></v-text-field>
|
||||
</v-responsive>
|
||||
</v-container>
|
||||
</v-app-bar>
|
||||
|
||||
<v-main class="bg-grey-lighten-3">
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="2">
|
||||
<v-sheet rounded="lg">
|
||||
<v-list rounded="lg">
|
||||
<v-list-item
|
||||
v-for="n in 5"
|
||||
:key="n"
|
||||
:title="`List Item ${n}`"
|
||||
link
|
||||
></v-list-item>
|
||||
|
||||
<v-divider class="my-2"></v-divider>
|
||||
|
||||
<v-list-item
|
||||
v-if="auth.user()"
|
||||
@click="auth.logout()"
|
||||
color="grey-lighten-4"
|
||||
title="Выход"
|
||||
link
|
||||
></v-list-item>
|
||||
</v-list>
|
||||
</v-sheet>
|
||||
</v-col>
|
||||
|
||||
<v-col>
|
||||
<v-sheet
|
||||
class="pa-5"
|
||||
min-height="70vh"
|
||||
rounded="lg"
|
||||
>
|
||||
<slot></slot>
|
||||
</v-sheet>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-main>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useAuth} from "vue-auth3";
|
||||
|
||||
const auth = useAuth()
|
||||
const links = [
|
||||
{text: 'Главная', to: '/'},
|
||||
{text: 'О сервисе', to: '/about'},
|
||||
{text: 'Авторизация', to: '/login'},
|
||||
|
||||
]
|
||||
</script>
|
||||
107
src-frontend/main.js
Normal file
107
src-frontend/main.js
Normal file
@ -0,0 +1,107 @@
|
||||
import {createApp} from 'vue'
|
||||
import {createPinia} from "pinia";
|
||||
import './style.scss'
|
||||
import router from './router'
|
||||
|
||||
|
||||
import App from './App.vue'
|
||||
|
||||
|
||||
// Vuetify
|
||||
import 'vuetify/styles'
|
||||
import '@mdi/font/css/materialdesignicons.css' // Ensure you are using css-loader
|
||||
|
||||
import {createVuetify} from 'vuetify'
|
||||
import * as components from 'vuetify/components'
|
||||
import * as directives from 'vuetify/directives'
|
||||
|
||||
// Auth
|
||||
import {createAuth} from "vue-auth3";
|
||||
import driverAuthBearerToken from "vue-auth3/dist/drivers/auth/bearer-token.js"
|
||||
import {useAxios} from "./composables/useAxios";
|
||||
|
||||
const {client} = useAxios();
|
||||
|
||||
const vuetify = createVuetify({
|
||||
components,
|
||||
directives,
|
||||
icons: {
|
||||
defaultSet: 'mdi', // This is already the default value - only for display purposes
|
||||
},
|
||||
})
|
||||
|
||||
const pinia = createPinia()
|
||||
const app = createApp(App)
|
||||
app.use(router)
|
||||
app.use(vuetify)
|
||||
app.use(pinia)
|
||||
|
||||
registerLayouts(app);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @description Register all layouts
|
||||
* @param app
|
||||
*/
|
||||
function registerLayouts(app) {
|
||||
const layouts = import.meta.glob('./layouts/*.vue', {eager: true});
|
||||
|
||||
Object.entries(layouts).forEach(([file, layout]) => {
|
||||
// get file name from file
|
||||
const fileName = file.split('/').pop().replace('.vue', '');
|
||||
|
||||
// register layout
|
||||
app.component(fileName, layout?.default);
|
||||
});
|
||||
}
|
||||
|
||||
const auth = createAuth({
|
||||
rolesKey: "type",
|
||||
notFoundRedirect: "/",
|
||||
fetchData: {
|
||||
url: "/auth/user",
|
||||
enabled: true,
|
||||
cache: true,
|
||||
},
|
||||
loginData: {
|
||||
url: "/auth/login",
|
||||
},
|
||||
logoutData: {
|
||||
redirect: "/login",
|
||||
url: "/auth/logout",
|
||||
},
|
||||
refreshToken: {
|
||||
enabled: false,
|
||||
},
|
||||
|
||||
plugins: {
|
||||
router,
|
||||
},
|
||||
drivers: {
|
||||
http: {
|
||||
request: client,
|
||||
},
|
||||
auth: driverAuthBearerToken,
|
||||
},
|
||||
})
|
||||
|
||||
const token = auth.token();
|
||||
if (token) {
|
||||
client.defaults.headers.common['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
|
||||
client.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
if (error.response.status === 401) {
|
||||
auth.logout()
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
app.use(auth)
|
||||
|
||||
app.mount('#app')
|
||||
22
src-frontend/pages/AboutPage.vue
Normal file
22
src-frontend/pages/AboutPage.vue
Normal file
@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<h1>О сервисе</h1>
|
||||
|
||||
<div>
|
||||
<v-progress-circular
|
||||
color="primary"
|
||||
:model-value="testStore.counter"
|
||||
:size="43"
|
||||
:width="9"
|
||||
> {{ testStore.counter }}
|
||||
</v-progress-circular>
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
<v-btn color="primary" @click="testStore.increment()">Увеличить счётчик</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import {useTestStore} from "../stores/test-store.js";
|
||||
|
||||
const testStore = useTestStore()
|
||||
</script>
|
||||
117
src-frontend/pages/AuthPage.vue
Normal file
117
src-frontend/pages/AuthPage.vue
Normal file
@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div>AuthPage</div>
|
||||
|
||||
<v-form v-model="valid" @submit.prevent="login">
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="username"
|
||||
:rules="usernameRules"
|
||||
label="Имя пользователя"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="password"
|
||||
:counter="10"
|
||||
:rules="passwordRules"
|
||||
label="Пароль"
|
||||
type="password"
|
||||
required
|
||||
></v-text-field>
|
||||
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="12"
|
||||
class="text-right"
|
||||
>
|
||||
<v-btn
|
||||
:disabled="!valid"
|
||||
color="success"
|
||||
class="mr-4"
|
||||
type="submit"
|
||||
:loading="loading"
|
||||
>
|
||||
Войти
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from "vue";
|
||||
import {useAxios} from "../composables/useAxios";
|
||||
import {useAuth} from "vue-auth3";
|
||||
|
||||
const {client} = useAxios();
|
||||
const auth = useAuth();
|
||||
|
||||
const valid = ref(false);
|
||||
const loading = ref(false);
|
||||
|
||||
const password = ref('123123123');
|
||||
const passwordRules = [
|
||||
value => {
|
||||
if (value) return true
|
||||
|
||||
return 'Пароль обязателен.'
|
||||
},
|
||||
value => {
|
||||
if (value?.length >= 8) return true
|
||||
|
||||
return 'Пароль должен быть не менее 8 символов.'
|
||||
},
|
||||
];
|
||||
|
||||
const username = ref('test');
|
||||
const usernameRules = [
|
||||
value => {
|
||||
if (value) return true
|
||||
|
||||
return 'Введите имя пользователя.'
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
async function login() {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
|
||||
console.log(auth)
|
||||
|
||||
const { data } = await auth.login({
|
||||
data: {
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
},
|
||||
redirect: { name: "home" },
|
||||
staySignedIn: true,
|
||||
fetchUser: true,
|
||||
})
|
||||
|
||||
client.defaults.headers.common['Authorization'] = `Bearer ${data.token}`
|
||||
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
console.log(import.meta.env);
|
||||
</script>
|
||||
11
src-frontend/pages/Error404Page.vue
Normal file
11
src-frontend/pages/Error404Page.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<v-empty-state
|
||||
headline="Упс, 404"
|
||||
title="Страница не найдена"
|
||||
text="Мы не можем найти страницу, которую вы ищите."
|
||||
image="https://cdn-icons-png.flaticon.com/512/7465/7465751.png"
|
||||
></v-empty-state>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</script>
|
||||
57
src-frontend/pages/HomePage.vue
Normal file
57
src-frontend/pages/HomePage.vue
Normal file
@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<v-card class="mb-6">
|
||||
<v-card-title>Home Page</v-card-title>
|
||||
<v-card-text>Current route: {{ $route.fullPath }}</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-card class="mb-6">
|
||||
<v-card-title>Тест аутентификации</v-card-title>
|
||||
<v-card-text>
|
||||
Текущий пользователь: <pre v-if="auth.user()">{{ auth.user() }}</pre> <pre v-else>null</pre>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
|
||||
<v-card class="mb-6">
|
||||
<v-card-title>Тест ассетов</v-card-title>
|
||||
<v-card-text>
|
||||
<a href="https://vite.dev" target="_blank">
|
||||
<img src="/vite.svg" class="logo" alt="Vite logo" />
|
||||
</a>
|
||||
<a href="https://vuejs.org/" target="_blank">
|
||||
<img src="./../assets/vue.svg" class="logo vue" alt="Vue logo" />
|
||||
</a>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-card class="mb-6">
|
||||
<v-card-title>Тест иконок</v-card-title>
|
||||
<v-card-text>
|
||||
<v-icon color="info" icon="mdi-wifi" size="x-large"></v-icon>
|
||||
<v-icon icon="mdi-home" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-card class="mb-6">
|
||||
<v-card-title>Тест роутера</v-card-title>
|
||||
<v-card-text>
|
||||
<RouterLink to="/">Go to Home</RouterLink><br>
|
||||
<RouterLink to="/about">Go to About</RouterLink>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-card class="mb-6">
|
||||
<v-card-title>Тест компонента</v-card-title>
|
||||
<v-card-text>
|
||||
<TestComponent></TestComponent>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import TestComponent from "../components/TestComponent.vue";
|
||||
import {useAuth} from "vue-auth3";
|
||||
|
||||
const auth = useAuth();
|
||||
</script>
|
||||
21
src-frontend/router/index.js
Normal file
21
src-frontend/router/index.js
Normal file
@ -0,0 +1,21 @@
|
||||
import {createWebHistory, createRouter} from 'vue-router'
|
||||
|
||||
import HomePage from './../pages/HomePage.vue'
|
||||
import AboutPage from './../pages/AboutPage.vue'
|
||||
import Error404Page from "../pages/Error404Page.vue";
|
||||
import AuthPage from "../pages/AuthPage.vue";
|
||||
|
||||
const routes = [
|
||||
{path: '/', component: HomePage, name: 'home', meta: {layout: 'Main'}},
|
||||
{path: '/about', component: AboutPage, meta: {layout: 'Main', auth: true}},
|
||||
{path: '/login', component: AuthPage, meta: {layout: 'Auth', auth: false}},
|
||||
|
||||
{path: '/:pathMatch(.*)*', component: Error404Page}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
})
|
||||
|
||||
export default router;
|
||||
21
src-frontend/stores/test-store.js
Normal file
21
src-frontend/stores/test-store.js
Normal file
@ -0,0 +1,21 @@
|
||||
import { defineStore, acceptHMRUpdate } from 'pinia'
|
||||
|
||||
export const useTestStore = defineStore('test', {
|
||||
state: () => ({
|
||||
counter: 0
|
||||
}),
|
||||
|
||||
getters: {
|
||||
doubleCount: (state) => state.counter * 2
|
||||
},
|
||||
|
||||
actions: {
|
||||
increment() {
|
||||
this.counter++
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(acceptHMRUpdate(useTestStore, import.meta.hot))
|
||||
}
|
||||
0
src-frontend/style.scss
Normal file
0
src-frontend/style.scss
Normal file
23
views/index.html.ejs
Normal file
23
views/index.html.ejs
Normal file
@ -0,0 +1,23 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + Vue</title>
|
||||
<% if (environment === 'production') { %>
|
||||
<% manifest['src-frontend/main.js'].css.forEach(function(cssFile) { %>
|
||||
<link rel="stylesheet" href="<%= cssFile %>">
|
||||
<% }); %>
|
||||
<% } %>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<% if (environment === 'production') { %>
|
||||
<script type="module" src="<%= manifest['src-frontend/main.js'].file %>"></script>
|
||||
<% } else { %>
|
||||
<script type="module" src="http://localhost:5173/@vite/client"></script>
|
||||
<script type="module" src="http://localhost:5173/src-frontend/main.js"></script>
|
||||
<% } %>
|
||||
</body>
|
||||
</html>
|
||||
13
vite.config.js
Normal file
13
vite.config.js
Normal file
@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
build: {
|
||||
manifest: true,
|
||||
rollupOptions: {
|
||||
input: './src-frontend/main.js'
|
||||
}
|
||||
}
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user