integration of authentication
This commit is contained in:
parent
0c7fc6fdf7
commit
ee932a2339
|
|
@ -1,4 +1,4 @@
|
||||||
module.exports = {
|
const config = {
|
||||||
db: {
|
db: {
|
||||||
connectionString: "mongodb://db:27017",
|
connectionString: "mongodb://db:27017",
|
||||||
dbName: "jucundus",
|
dbName: "jucundus",
|
||||||
|
|
@ -19,5 +19,11 @@ module.exports = {
|
||||||
jwtOptions: {
|
jwtOptions: {
|
||||||
issuer: "jucundus.com",
|
issuer: "jucundus.com",
|
||||||
audience: "yoursite",
|
audience: "yoursite",
|
||||||
|
},
|
||||||
|
agent :{
|
||||||
|
ApiAgentURL: 'http://host.docker.internal:3020/api',
|
||||||
|
token: '861v48gr4YTHJTUre0reg40g8e6r8r64eggv1r4e6g4r81PKREVJ8g6reg46r8eg416reST6ger84g14er86e',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
module.exports = { config };
|
||||||
|
|
@ -185,6 +185,16 @@ exports.current = asyncHandler(async (req, res, next) => {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
exports.agentConnected = asyncHandler(async (req, res, next) => {
|
||||||
|
try{
|
||||||
|
res.status(200).send("OK");
|
||||||
|
}catch(err){
|
||||||
|
console.log(err);
|
||||||
|
return res.status(500).send(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
exports.getAllUsers = asyncHandler(async (req, res, next) => {
|
exports.getAllUsers = asyncHandler(async (req, res, next) => {
|
||||||
try{
|
try{
|
||||||
const userDb = await UserDb.init();
|
const userDb = await UserDb.init();
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,53 @@
|
||||||
|
const passport = require('passport');
|
||||||
|
|
||||||
|
function Admited(acceptedRoles){
|
||||||
|
return (req, res, next) => {
|
||||||
|
|
||||||
|
// if Unconnected accepted
|
||||||
|
if (acceptedRoles.includes('Unconnected') || acceptedRoles.length == 0){
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
function checkIsConcernedUserOrAdmin(req, res, next) {
|
passport.authenticate('jwt', { session: false }, (err, user, info) => {
|
||||||
const user = req.user; // User is set by Passport
|
|
||||||
const userIdParam = req.params.id;
|
if (err) {
|
||||||
|
// An error occurred, return a JSON error response
|
||||||
|
return res.status(500).json({ error: "An error occurred" });
|
||||||
|
}
|
||||||
|
if (!user) {
|
||||||
|
// User not authenticated, return a JSON error response
|
||||||
|
console.log('User not authenticated');
|
||||||
|
return res.status(401).json({ error: "User not authenticated" });
|
||||||
|
}
|
||||||
|
// User authenticated, attach user to request and proceed
|
||||||
|
req.user = user;
|
||||||
|
|
||||||
if (user.isAdmin === true || user._id === userIdParam) {
|
// Determine the user's role
|
||||||
next();
|
let userRoles = [];
|
||||||
} else {
|
if (user.isAdmin) {
|
||||||
res.status(403).json({ error: 'Forbidden' });
|
userRoles.push('Admin');
|
||||||
|
}
|
||||||
|
if (user.isAgent) {
|
||||||
|
userRoles.push('Agent')
|
||||||
|
}
|
||||||
|
if (user._id.toString() === req.params.id) {
|
||||||
|
userRoles.push('ConcernedUser')
|
||||||
|
}
|
||||||
|
if (user) {
|
||||||
|
userRoles.push('NormalUser')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any of the user's roles are in the list of accepted roles
|
||||||
|
const isAuthorized = userRoles.some(role => acceptedRoles.includes(role));
|
||||||
|
|
||||||
|
// Check if the user's role is in the list of accepted roles
|
||||||
|
if (isAuthorized) {
|
||||||
|
next(); // User's role is accepted, proceed to the next middleware/controller
|
||||||
|
} else {
|
||||||
|
res.status(403).json({ error: 'Forbidden' }); // User's role is not accepted, return 403 Forbidden
|
||||||
|
}
|
||||||
|
})(req, res, next);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
module.exports = { Admited };
|
||||||
function checkIsAdmin(req, res, next) {
|
|
||||||
const user = req.user; // User is set by Passport
|
|
||||||
|
|
||||||
if (user.isAdmin === true) {
|
|
||||||
next();
|
|
||||||
} else {
|
|
||||||
res.status(403).json({ error: 'Forbidden' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkIsAgent(req, res, next) {
|
|
||||||
const user = req.user; // User is set by Passport
|
|
||||||
|
|
||||||
if (user.isAgent === true) {
|
|
||||||
next();
|
|
||||||
} else {
|
|
||||||
res.status(403).json({ error: 'Forbidden' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
module.exports = { checkIsConcernedUserOrAdmin, checkIsAgent, checkIsAdmin };
|
|
||||||
|
|
@ -2,7 +2,7 @@ var passport = require('passport');
|
||||||
var LocalStrategy = require('passport-local');
|
var LocalStrategy = require('passport-local');
|
||||||
var crypto = require('crypto');
|
var crypto = require('crypto');
|
||||||
const router = require('express').Router()
|
const router = require('express').Router()
|
||||||
const config = require("../config.js");
|
const {config} = require("../config.js");
|
||||||
const keys = require("../.Keys.js");
|
const keys = require("../.Keys.js");
|
||||||
|
|
||||||
/* Configure password authentication strategy.
|
/* Configure password authentication strategy.
|
||||||
|
|
@ -98,13 +98,17 @@ router.post('/authenticate', async function(req, res) {
|
||||||
if (err) { return res.status(500).json({message: err.message}); }
|
if (err) { return res.status(500).json({message: err.message}); }
|
||||||
if (!user) { return res.status(401).json({message: 'Incorrect email or password.'}); }
|
if (!user) { return res.status(401).json({message: 'Incorrect email or password.'}); }
|
||||||
|
|
||||||
|
console.log('/authenticate: '+user.username);
|
||||||
|
|
||||||
// User found, generate a JWT for the user
|
// User found, generate a JWT for the user
|
||||||
var token = jwt.sign({ sub: user._id, email: user.email }, opts.secretOrKey, {
|
var token = jwt.sign({ sub: user._id, email: user.email }, opts.secretOrKey, {
|
||||||
issuer: opts.issuer,
|
issuer: opts.issuer,
|
||||||
audience: opts.audience,
|
audience: opts.audience,
|
||||||
expiresIn: 86400 * 30 // 30 days
|
expiresIn: 86400 * 30 // 30 days
|
||||||
});
|
});
|
||||||
|
console.log('Token: '+token);
|
||||||
res.json({ token: token });
|
res.json({ token: token });
|
||||||
|
|
||||||
//return res.status(500).json({message: 'Incorrect email or password.'})
|
//return res.status(500).json({message: 'Incorrect email or password.'})
|
||||||
|
|
||||||
})(req, res);
|
})(req, res);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
const controllers = require('../controllers/favorite')
|
const controllers = require('../controllers/favorite')
|
||||||
const router = require('express').Router()
|
const router = require('express').Router()
|
||||||
const passport = require('passport');
|
const { Admited } = require('../middleware/authMiddleware')
|
||||||
const { checkIsConcernedUserOrAdmin } = require('../middleware/authMiddleware')
|
|
||||||
|
|
||||||
router.post('/save/', passport.authenticate('jwt', { session: false }), controllers.save)
|
router.post('/save/', Admited(['NormalUser']), controllers.save)
|
||||||
router.get('/getAll/', passport.authenticate('jwt', { session: false }), controllers.getAll)
|
router.get('/getAll/', Admited(['NormalUser']), controllers.getAll)
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
@ -1,21 +1,20 @@
|
||||||
const controllers = require('../controllers/lot')
|
const controllers = require('../controllers/lot')
|
||||||
const router = require('express').Router()
|
const router = require('express').Router()
|
||||||
const passport = require('passport');
|
const { Admited } = require('../middleware/authMiddleware')
|
||||||
const { checkIsAgent, checkIsAdmin } = require('../middleware/authMiddleware')
|
|
||||||
|
|
||||||
router.get('/getInfos/:url', passport.authenticate('jwt', { session: false }), controllers.getInfos)
|
router.get('/getInfos/:url', Admited(['NormalUser']), controllers.getInfos)
|
||||||
router.get('/getPictures/:url', passport.authenticate('jwt', { session: false }), controllers.getPictures)
|
router.get('/getPictures/:url', Admited(['NormalUser']), controllers.getPictures)
|
||||||
router.get('/getLotsBySale/:id', passport.authenticate('jwt', { session: false }), controllers.getLotsBySale)
|
router.get('/getLotsBySale/:id', Admited(['NormalUser']), controllers.getLotsBySale)
|
||||||
|
|
||||||
// DB
|
// DB
|
||||||
router.get('/lot/:id', passport.authenticate('jwt', { session: false }), checkIsAdmin, controllers.get)
|
router.get('/lot/:id', Admited(['Admin']), controllers.get)
|
||||||
router.post('/lot/', passport.authenticate('jwt', { session: false }), checkIsAdmin, controllers.post)
|
router.post('/lot/', Admited(['Admin']), controllers.post)
|
||||||
router.put('/lot/:id', passport.authenticate('jwt', { session: false }), checkIsAdmin, controllers.put)
|
router.put('/lot/:id', Admited(['Admin']), controllers.put)
|
||||||
router.delete('/lot/:id', passport.authenticate('jwt', { session: false }), checkIsAdmin, controllers.delete)
|
router.delete('/lot/:id', Admited(['Admin']), controllers.delete)
|
||||||
|
|
||||||
// Live Data
|
// Live Data
|
||||||
router.post('/NextItem/', checkIsAgent, controllers.NextItem)
|
router.post('/NextItem/', Admited(['Agent', 'Admin']), controllers.NextItem)
|
||||||
router.post('/AuctionedItem/', checkIsAgent, controllers.AuctionedItem)
|
router.post('/AuctionedItem/', Admited(['Agent', 'Admin']), controllers.AuctionedItem)
|
||||||
router.post('/Bid/', checkIsAgent, controllers.Bid)
|
router.post('/Bid/', Admited(['Agent', 'Admin']), controllers.Bid)
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
@ -1,24 +1,24 @@
|
||||||
const controllers = require('../controllers/sale')
|
const controllers = require('../controllers/sale')
|
||||||
const passport = require('passport');
|
|
||||||
const router = require('express').Router()
|
const router = require('express').Router()
|
||||||
|
const { Admited } = require('../middleware/authMiddleware')
|
||||||
|
|
||||||
// AuctionAgent
|
// AuctionAgent
|
||||||
router.get('/getSaleInfos/:url', controllers.getSaleInfos)
|
router.get('/getSaleInfos/:url', Admited(['NormalUser']), controllers.getSaleInfos)
|
||||||
router.get('/prepareSale/:id', controllers.prepareSale)
|
router.get('/prepareSale/:id', Admited(['NormalUser']), controllers.prepareSale)
|
||||||
router.get('/followSale/:id', controllers.followSale)
|
router.get('/followSale/:id', Admited(['NormalUser']), controllers.followSale)
|
||||||
|
|
||||||
|
|
||||||
// DB
|
// DB
|
||||||
router.get('/sale/:id', controllers.get)
|
router.get('/sale/:id', Admited(['Admin']), controllers.get)
|
||||||
router.post('/sale/', controllers.post)
|
router.post('/sale/', Admited(['Admin']), controllers.post)
|
||||||
router.put('/sale/:id', controllers.put)
|
router.put('/sale/:id', Admited(['Admin', 'Agent']), controllers.put)
|
||||||
router.delete('/sale/:id', controllers.delete)
|
router.delete('/sale/:id', Admited(['Admin']), controllers.delete)
|
||||||
|
|
||||||
//router.get('/getAll/', controllers.getAll)
|
|
||||||
router.get('/getAll/', passport.authenticate('jwt', { session: false }), controllers.getAll);
|
router.get('/getAll/', Admited(['Admin']), controllers.getAll);
|
||||||
router.get('/getByUrl/:url', controllers.getByUrl)
|
router.get('/getByUrl/:url', Admited(['Agent', 'Admin']), controllers.getByUrl)
|
||||||
router.get('/postProcessing/:id', controllers.postProcessing)
|
router.get('/postProcessing/:id', Admited(['Admin']), controllers.postProcessing)
|
||||||
router.get('/SaleStatXsl/:id', controllers.SaleStatXsl)
|
router.get('/SaleStatXsl/:id', Admited(['Admin']), controllers.SaleStatXsl)
|
||||||
|
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
const controllers = require('../controllers/user')
|
const controllers = require('../controllers/user')
|
||||||
const passport = require('passport');
|
|
||||||
const router = require('express').Router()
|
const router = require('express').Router()
|
||||||
const { checkIsConcernedUserOrAdmin, checkIsAdmin } = require('../middleware/authMiddleware')
|
const { Admited } = require('../middleware/authMiddleware')
|
||||||
|
|
||||||
// DB
|
// DB
|
||||||
router.get('/user/:id', passport.authenticate('jwt', { session: false }), checkIsConcernedUserOrAdmin, controllers.get);
|
router.get('/user/:id', Admited(['Admin', 'ConcernedUser']), controllers.get);
|
||||||
router.post('/user/', controllers.post)
|
router.post('/user/', Admited(['Unconnected']), controllers.post)
|
||||||
router.put('/user/:id', passport.authenticate('jwt', { session: false }), checkIsConcernedUserOrAdmin, controllers.put)
|
router.put('/user/:id', Admited(['Admin', 'ConcernedUser']), controllers.put)
|
||||||
router.delete('/user/:id', passport.authenticate('jwt', { session: false }), checkIsConcernedUserOrAdmin, controllers.delete)
|
router.delete('/user/:id', Admited(['Admin', 'ConcernedUser']), controllers.delete)
|
||||||
|
|
||||||
router.get('/current', passport.authenticate('jwt', { session: false }), controllers.current)
|
router.get('/current', Admited(['NormalUser']), controllers.current)
|
||||||
router.get('/getAllUsers', passport.authenticate('jwt', { session: false }), checkIsAdmin, controllers.getAllUsers)
|
router.get('/agentConnected', Admited(['Agent']), controllers.agentConnected)
|
||||||
|
router.get('/getAllUsers', Admited(['Admin']), controllers.getAllUsers)
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
@ -4,20 +4,39 @@ const lotDb = new LotDb();
|
||||||
const { SaleDb } = require("./saleDb");
|
const { SaleDb } = require("./saleDb");
|
||||||
const saleDb = new SaleDb();
|
const saleDb = new SaleDb();
|
||||||
const moment = require('moment-timezone');
|
const moment = require('moment-timezone');
|
||||||
|
const { config } = require("../config.js");
|
||||||
|
|
||||||
const Agent = class
|
const Agent = class
|
||||||
{
|
{
|
||||||
constructor()
|
constructor()
|
||||||
{
|
{
|
||||||
this.ApiAgentURL = "http://host.docker.internal:3020/api";
|
this.ApiAgentURL = config.agent.ApiAgentURL;
|
||||||
|
this.token = config.agent.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
async request(url, method){
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fetch(url,{
|
||||||
|
method: method,
|
||||||
|
headers: {
|
||||||
|
'authorization': this.token
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
resolve(data);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSaleInfos(url)
|
async getSaleInfos(url)
|
||||||
{
|
{
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
url = encodeURIComponent(url);
|
url = encodeURIComponent(url);
|
||||||
fetch(this.ApiAgentURL+'/sale/getSaleInfos/'+url)
|
this.request(this.ApiAgentURL+'/sale/getSaleInfos/'+url, 'GET')
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
.then(data => {
|
||||||
resolve(data);
|
resolve(data);
|
||||||
})
|
})
|
||||||
|
|
@ -40,9 +59,7 @@ const Agent = class
|
||||||
|
|
||||||
let url = Sale.url
|
let url = Sale.url
|
||||||
url = encodeURIComponent(url);
|
url = encodeURIComponent(url);
|
||||||
|
this.request(this.ApiAgentURL+'/sale/getLotList/'+url, 'GET')
|
||||||
fetch(this.ApiAgentURL+'/sale/getLotList/'+url)
|
|
||||||
.then(response => response.json())
|
|
||||||
.then( async data => {
|
.then( async data => {
|
||||||
|
|
||||||
for (let lot of data) {
|
for (let lot of data) {
|
||||||
|
|
@ -68,9 +85,7 @@ const Agent = class
|
||||||
|
|
||||||
let url = Sale.url
|
let url = Sale.url
|
||||||
url = encodeURIComponent(url);
|
url = encodeURIComponent(url);
|
||||||
|
this.request(this.ApiAgentURL+'/sale/followSale/'+url, 'GET')
|
||||||
fetch(this.ApiAgentURL+'/sale/followSale/'+url)
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(async data => {
|
.then(async data => {
|
||||||
|
|
||||||
// set the Sale status to following
|
// set the Sale status to following
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
const MongoClient = require("mongodb").MongoClient;
|
const MongoClient = require("mongodb").MongoClient;
|
||||||
const config = require("../config.js");
|
const {config} = require("../config.js");
|
||||||
const connectionString = config.db.connectionString;
|
const connectionString = config.db.connectionString;
|
||||||
const client = new MongoClient(connectionString);
|
const client = new MongoClient(connectionString);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,13 @@
|
||||||
<ng-container matColumnDef="plateform">
|
<ng-container matColumnDef="plateform">
|
||||||
<th mat-header-cell *matHeaderCellDef> Plateforme </th>
|
<th mat-header-cell *matHeaderCellDef> Plateforme </th>
|
||||||
<td mat-cell *matCellDef="let element"> {{element.platform}} </td>
|
<td mat-cell *matCellDef="let element"> {{element.platform}} </td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Status Column -->
|
||||||
|
<ng-container matColumnDef="status">
|
||||||
|
<th mat-header-cell *matHeaderCellDef> Status </th>
|
||||||
|
<td mat-cell *matCellDef="let element"> {{element.status}} </td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<!-- Action Column -->
|
<!-- Action Column -->
|
||||||
<ng-container matColumnDef="action">
|
<ng-container matColumnDef="action">
|
||||||
|
|
@ -80,7 +86,7 @@
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
<tr mat-row *matRowDef="let row; columns: displayedColumns; trackBy: trackByElementId"></tr>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -120,6 +126,12 @@
|
||||||
<td mat-cell *matCellDef="let element"> {{element.platform}} </td>
|
<td mat-cell *matCellDef="let element"> {{element.platform}} </td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Status Column -->
|
||||||
|
<ng-container matColumnDef="status">
|
||||||
|
<th mat-header-cell *matHeaderCellDef> Status </th>
|
||||||
|
<td mat-cell *matCellDef="let element"> {{element.status}} </td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<!-- PostProcessing Column -->
|
<!-- PostProcessing Column -->
|
||||||
<ng-container matColumnDef="postProcessing">
|
<ng-container matColumnDef="postProcessing">
|
||||||
<th mat-header-cell *matHeaderCellDef> Post Processing </th>
|
<th mat-header-cell *matHeaderCellDef> Post Processing </th>
|
||||||
|
|
@ -137,7 +149,7 @@
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<tr mat-header-row *matHeaderRowDef="displayedColumnsOld"></tr>
|
<tr mat-header-row *matHeaderRowDef="displayedColumnsOld"></tr>
|
||||||
<tr mat-row *matRowDef="let row; columns: displayedColumnsOld;"></tr>
|
<tr mat-row *matRowDef="let row; columns: displayedColumnsOld; trackBy: trackByElementId"></tr>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,8 @@ export class SalesPageComponent implements OnInit,OnDestroy {
|
||||||
|
|
||||||
url: string = '';
|
url: string = '';
|
||||||
refreshSalesId: any;
|
refreshSalesId: any;
|
||||||
displayedColumns: string[] = ['title', 'date', 'house', 'plateform', 'action'];
|
displayedColumns: string[] = ['title', 'date', 'house', 'plateform', 'status', 'action'];
|
||||||
displayedColumnsOld: string[] = ['title', 'date', 'house', 'plateform', 'postProcessing', 'delete'];
|
displayedColumnsOld: string[] = ['title', 'date', 'house', 'plateform', 'status', 'postProcessing', 'delete'];
|
||||||
|
|
||||||
futureSales = []
|
futureSales = []
|
||||||
pastSales = []
|
pastSales = []
|
||||||
|
|
@ -62,6 +62,10 @@ export class SalesPageComponent implements OnInit,OnDestroy {
|
||||||
.sort((a: any, b: any) => moment(a.date).isAfter(b.date) ? 1 : -1);
|
.sort((a: any, b: any) => moment(a.date).isAfter(b.date) ? 1 : -1);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trackByElementId(index: number, element: any): string {
|
||||||
|
return element._id;
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.titleService.setTitle('Jucundus - Sales');
|
this.titleService.setTitle('Jucundus - Sales');
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ services:
|
||||||
- ./backend:/backend
|
- ./backend:/backend
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
- "9229:9229"
|
- "9228:9229"
|
||||||
command: ["npm", "run", "dev"]
|
command: ["npm", "run", "dev"]
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
Loading…
Reference in New Issue