🚀 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