Modifiche

This commit is contained in:
Lorenzo Sanesi
2026-04-01 23:49:19 +02:00
parent 8752acd2c4
commit e2fe8b264f
52 changed files with 1351 additions and 400 deletions
+2 -2
View File
@@ -22,7 +22,7 @@ COPY package*.json ./
FROM base AS dependencies
# Install ALL dependencies (including devDependencies for build)
RUN npm ci
RUN npm install
# ========================================
# Stage 3: Development
@@ -50,7 +50,7 @@ CMD ["npm", "start"]
FROM base AS production-dependencies
# Install only production dependencies
RUN npm ci --only=production && \
RUN npm install --only=production && \
npm cache clean --force
# ========================================
@@ -5,6 +5,24 @@ class BetsController {
this.betsService = betsService;
}
async getUserBets(req, res, next) {
try {
const bets = await this.betsService.getUserBets(req.userId);
return ResponseHandler.success(res, bets, 'User bets retrieved successfully');
} catch (error) {
next(error);
}
}
async getPublicBets(req, res, next) {
try {
const bets = await this.betsService.getPublicBetsExcludingUser(req.userId);
return ResponseHandler.success(res, bets, 'Public bets retrieved successfully');
} catch (error) {
next(error);
}
}
async getAllBets(req, res, next) {
try {
const bets = await this.betsService.getAllBets();
@@ -7,7 +7,7 @@ class GroupsController {
async getAllGroups(req, res, next) {
try {
const groups = await this.groupsService.getAllGroups();
const groups = await this.groupsService.getAllGroups(req.userId);
return ResponseHandler.success(res, groups, 'Groups retrieved successfully');
} catch (error) {
next(error);
@@ -52,10 +52,7 @@ class GroupsController {
async addMembers(req, res, next) {
try {
const group = await this.groupsService.addMembersToGroup(
req.params.id,
req.body.userIds
);
const group = await this.groupsService.addMembersToGroup(req.params.id, req.body.userIds);
return ResponseHandler.success(res, group, 'Members added successfully');
} catch (error) {
next(error);
@@ -1,38 +0,0 @@
const ResponseHandler = require('../shared/response.handler');
class InitialDataController {
constructor({ usersService, friendsService, groupsService, betsService }) {
this.usersService = usersService;
this.friendsService = friendsService;
this.groupsService = groupsService;
this.betsService = betsService;
}
async getInitialData(req, res, next) {
try {
const currentUser = await this.usersService.getUserById(req.userId);
const users = await this.usersService.getAllUsers();
const friends = await this.friendsService.getFriendsByUserId(req.userId);
const friendRequests = await this.friendsService.getPendingRequests(req.userId);
const groups = await this.groupsService.getAllGroups();
const userBets = await this.betsService.getUserBets(req.userId);
const publicBets = await this.betsService.getPublicBetsExcludingUser(req.userId);
const data = {
currentUser,
users,
friends,
friendRequests,
groups,
userBets,
publicBets,
};
return ResponseHandler.success(res, data, 'Initial data retrieved successfully');
} catch (error) {
next(error);
}
}
}
module.exports = InitialDataController;
@@ -5,6 +5,15 @@ class UsersController {
this.usersService = usersService;
}
async getCurrentUser(req, res, next) {
try {
const user = await this.usersService.getUserById(req.userId);
return ResponseHandler.success(res, user, 'User retrieved successfully');
} catch (error) {
next(error);
}
}
async updateCurrentUser(req, res, next) {
try {
const user = await this.usersService.updateUserProfile(req.userId, req.body);
+6 -17
View File
@@ -18,13 +18,13 @@ function setupContainer() {
const modelsDir = path.join(__dirname, '../models');
fs.readdirSync(modelsDir)
.filter(
(file) =>
file =>
file.indexOf('.') !== 0 &&
file.slice(-3) === '.js' &&
file !== 'bet.model.js' &&
file !== 'associations.js'
)
.forEach((file) => {
.forEach(file => {
require(path.join(modelsDir, file))(sequelize);
});
@@ -38,27 +38,19 @@ function setupContainer() {
});
container.register({
usersRepository: asClass(
require('../repositories/postgres/users.repository')
).scoped(),
groupsRepository: asClass(
require('../repositories/postgres/groups.repository')
).scoped(),
usersRepository: asClass(require('../repositories/postgres/users.repository')).scoped(),
groupsRepository: asClass(require('../repositories/postgres/groups.repository')).scoped(),
friendshipsRepository: asClass(
require('../repositories/postgres/friendships.repository')
).scoped(),
friendRequestsRepository: asClass(
require('../repositories/postgres/friendRequests.repository')
).scoped(),
betsRepository: asClass(
require('../repositories/mongo/bets.repository')
).scoped(),
betsRepository: asClass(require('../repositories/mongo/bets.repository')).scoped(),
});
container.register({
dataStitchingService: asClass(
require('../services/data-stitching.service')
).scoped(),
dataStitchingService: asClass(require('../services/data-stitching.service')).scoped(),
usersService: asClass(require('../services/users.service')).scoped(),
groupsService: asClass(require('../services/groups.service')).scoped(),
friendsService: asClass(require('../services/friends.service')).scoped(),
@@ -70,9 +62,6 @@ function setupContainer() {
groupsController: asClass(require('../controllers/groups.controller')).scoped(),
usersController: asClass(require('../controllers/users.controller')).scoped(),
friendsController: asClass(require('../controllers/friends.controller')).scoped(),
initialDataController: asClass(
require('../controllers/initialData.controller')
).scoped(),
});
return container;
+2
View File
@@ -11,6 +11,7 @@ DROP TABLE IF EXISTS users CASCADE;
-- users table
CREATE TABLE users (
id SERIAL PRIMARY KEY,
"keycloakId" VARCHAR(255) UNIQUE,
name VARCHAR(255) NOT NULL,
"email" VARCHAR(255) NOT NULL UNIQUE,
"avatarUrl" VARCHAR(255) NOT NULL,
@@ -63,6 +64,7 @@ CREATE TABLE friend_requests (
-- User <-> Group many-to-many join table
CREATE TABLE "UserGroups" (
id SERIAL PRIMARY KEY,
"userId" INTEGER NOT NULL,
"groupId" INTEGER NOT NULL,
CONSTRAINT usergroups_user_fk FOREIGN KEY ("userId") REFERENCES users(id) ON DELETE CASCADE,
+67
View File
@@ -0,0 +1,67 @@
const jwksClient = require('jwks-rsa');
const jwt = require('jsonwebtoken');
const client = jwksClient({
jwksUri:
process.env.KEYCLOAK_JWKS_URI ||
'http://localhost:8080/realms/scommettiapp/protocol/openid-connect/certs',
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
});
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
if (err) return callback(err);
callback(null, key.getPublicKey());
});
}
module.exports = sequelize => {
const { User } = sequelize.models;
return (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ success: false, message: 'Token mancante o non valido' });
}
const token = authHeader.slice(7);
jwt.verify(token, getKey, { algorithms: ['RS256'] }, async (err, decoded) => {
if (err) {
return res.status(401).json({ success: false, message: 'Token non valido o scaduto' });
}
try {
const keycloakId = decoded.sub;
const name =
decoded.name ||
`${decoded.given_name || ''} ${decoded.family_name || ''}`.trim() ||
decoded.preferred_username ||
'Utente';
const email = decoded.email || `${keycloakId.slice(0, 8)}@scommetti.app`;
const avatarUrl = `https://picsum.photos/seed/${keycloakId.slice(0, 8)}/200/200`;
// Prima cerca per keycloakId (caso normale dopo il primo login)
let user = await User.findOne({ where: { keycloakId } });
if (!user) {
// Migrazione: l'utente potrebbe esistere già per email
// (creato prima che keycloakId fosse aggiunto al modello)
user = await User.findOne({ where: { email } });
if (user) {
await user.update({ keycloakId });
} else {
user = await User.create({ keycloakId, name, email, avatarUrl, score: 0 });
}
}
req.userId = user.id;
next();
} catch (error) {
next(error);
}
});
};
};
+42 -17
View File
@@ -1,21 +1,46 @@
module.exports = (sequelize) => {
const { User, Group, FriendRequest, Friendship } = sequelize.models;
// eslint-disable-next-line import/no-import-module-exports
import { DataTypes } from 'sequelize';
// User <-> Group: Many-to-Many
// A User can be in many Groups, and a Group can have many Users (members).
const UserGroup = sequelize.define('UserGroup', {}, { timestamps: false });
User.belongsToMany(Group, { through: UserGroup, foreignKey: 'userId' });
Group.belongsToMany(User, { through: UserGroup, as: 'members', foreignKey: 'groupId' });
module.exports = sequelize => {
const { User, Group, FriendRequest, Friendship } = sequelize.models;
// Friendship is a direct link between two users.
// user1Id will always be less than user2Id to ensure uniqueness.
Friendship.belongsTo(User, { as: 'user1', foreignKey: 'user1Id' });
Friendship.belongsTo(User, { as: 'user2', foreignKey: 'user2Id' });
// User <-> Group: Many-to-Many
// Il modello dichiara esplicitamente la colonna id per evitare che sync() la cerchi
// sulla tabella UserGroups senza trovarla (causando errori su RETURNING "id").
const UserGroup = sequelize.define(
'UserGroup',
{
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
userId: { type: DataTypes.INTEGER, allowNull: false },
groupId: { type: DataTypes.INTEGER, allowNull: false },
},
{ timestamps: false }
);
User.belongsToMany(Group, { through: UserGroup, foreignKey: 'userId', otherKey: 'groupId' });
Group.belongsToMany(User, {
through: UserGroup,
as: 'members',
foreignKey: 'groupId',
otherKey: 'userId',
});
// User <-> FriendRequest: One-to-Many
// A User can send many friend requests and receive many friend requests.
User.hasMany(FriendRequest, { as: 'sentRequests', foreignKey: 'fromUserId', onDelete: 'CASCADE' });
User.hasMany(FriendRequest, { as: 'receivedRequests', foreignKey: 'toUserId', onDelete: 'CASCADE' });
FriendRequest.belongsTo(User, { as: 'fromUser', foreignKey: 'fromUserId' });
FriendRequest.belongsTo(User, { as: 'toUser', foreignKey: 'toUserId' });
// Friendship is a direct link between two users.
// user1Id will always be less than user2Id to ensure uniqueness.
Friendship.belongsTo(User, { as: 'user1', foreignKey: 'user1Id' });
Friendship.belongsTo(User, { as: 'user2', foreignKey: 'user2Id' });
// User <-> FriendRequest: One-to-Many
// A User can send many friend requests and receive many friend requests.
User.hasMany(FriendRequest, {
as: 'sentRequests',
foreignKey: 'fromUserId',
onDelete: 'CASCADE',
});
User.hasMany(FriendRequest, {
as: 'receivedRequests',
foreignKey: 'toUserId',
onDelete: 'CASCADE',
});
FriendRequest.belongsTo(User, { as: 'fromUser', foreignKey: 'fromUserId' });
FriendRequest.belongsTo(User, { as: 'toUser', foreignKey: 'toUserId' });
};
+28 -15
View File
@@ -1,21 +1,34 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const FriendRequest = sequelize.define('FriendRequest', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
module.exports = sequelize => {
const FriendRequest = sequelize.define(
'FriendRequest',
{
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
},
status: {
type: DataTypes.STRING,
defaultValue: 'PENDING',
allowNull: false,
},
// Foreign keys 'fromUserId' and 'toUserId' are added via associations
},
status: {
type: DataTypes.STRING,
defaultValue: 'PENDING',
allowNull: false
{
// Uses default `createdAt` and `updatedAt` timestamps from Sequelize
tableName: 'friend_requests',
}
// Foreign keys 'fromUserId' and 'toUserId' are added via associations
}, {
// Uses default `createdAt` and `updatedAt` timestamps from Sequelize
tableName: 'friend_requests'
});
);
FriendRequest.prototype.toJSON = function () {
const values = { ...this.get() };
if (values.id !== undefined) values.id = String(values.id);
if (values.fromUserId !== undefined) values.fromUserId = String(values.fromUserId);
if (values.toUserId !== undefined) values.toUserId = String(values.toUserId);
return values;
};
return FriendRequest;
};
+24 -13
View File
@@ -1,19 +1,30 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const Group = sequelize.define('Group', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
module.exports = sequelize => {
const Group = sequelize.define(
'Group',
{
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
},
name: {
type: DataTypes.STRING,
allowNull: false
{
timestamps: false,
tableName: 'groups',
}
}, {
timestamps: false,
tableName: 'groups'
});
);
Group.prototype.toJSON = function () {
const values = { ...this.get() };
if (values.id !== undefined) values.id = String(values.id);
return values;
};
return Group;
};
+46 -30
View File
@@ -1,35 +1,51 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const User = sequelize.define('User', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
module.exports = sequelize => {
const User = sequelize.define(
'User',
{
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
},
keycloakId: {
type: DataTypes.STRING,
allowNull: true,
unique: true,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
validate: {
isEmail: true,
},
},
avatarUrl: {
type: DataTypes.STRING,
allowNull: false,
},
score: {
type: DataTypes.INTEGER,
defaultValue: 0,
},
},
name: {
type: DataTypes.STRING,
allowNull: false
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
validate: {
isEmail: true
}
},
avatarUrl: {
type: DataTypes.STRING,
allowNull: false
},
score: {
type: DataTypes.INTEGER,
defaultValue: 0
},
}, {
timestamps: false,
tableName: 'users'
});
{
timestamps: false,
tableName: 'users',
}
);
User.prototype.toJSON = function () {
const values = { ...this.get() };
if (values.id !== undefined) values.id = String(values.id);
return values;
};
return User;
};
+205
View File
@@ -13,6 +13,8 @@
"cors": "^2.8.5",
"express": "^4.19.2",
"joi": "^17.12.1",
"jsonwebtoken": "^9.0.2",
"jwks-rsa": "^3.1.0",
"mongoose": "^8.4.0",
"pg": "^8.11.5",
"pg-hstore": "^2.3.4",
@@ -294,6 +296,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/jsonwebtoken": {
"version": "9.0.10",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
"integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
"license": "MIT",
"dependencies": {
"@types/ms": "*",
"@types/node": "*"
}
},
"node_modules/@types/ms": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
@@ -653,6 +665,12 @@
"node": ">=16.20.1"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -1003,6 +1021,15 @@
"node": ">= 0.4"
}
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -2640,6 +2667,15 @@
"@sideway/pinpoint": "^2.0.0"
}
},
"node_modules/jose": {
"version": "4.15.9",
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
"integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/js-yaml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
@@ -2687,6 +2723,94 @@
"json5": "lib/cli.js"
}
},
"node_modules/jsonwebtoken": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
"license": "MIT",
"dependencies": {
"jws": "^4.0.1",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/jsonwebtoken/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/jwa": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jwks-rsa": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.2.2.tgz",
"integrity": "sha512-BqTyEDV+lS8F2trk3A+qJnxV5Q9EqKCBJOPti3W97r7qTympCZjb7h2X6f2kc+0K3rsSTY1/6YG2eaXKoj497w==",
"license": "MIT",
"dependencies": {
"@types/jsonwebtoken": "^9.0.4",
"debug": "^4.3.4",
"jose": "^4.15.4",
"limiter": "^1.1.5",
"lru-memoizer": "^2.2.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/jwks-rsa/node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/jwks-rsa/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/jws": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
"license": "MIT",
"dependencies": {
"jwa": "^2.0.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/kareem": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz",
@@ -2720,6 +2844,11 @@
"node": ">= 0.8.0"
}
},
"node_modules/limiter": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
"integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA=="
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -2742,6 +2871,48 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
"license": "MIT"
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
"license": "MIT"
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
"license": "MIT"
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
"license": "MIT"
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
"license": "MIT"
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"license": "MIT"
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
"license": "MIT"
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -2749,6 +2920,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
"node_modules/lower-case": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
@@ -2758,6 +2935,28 @@
"tslib": "^2.0.3"
}
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/lru-memoizer": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz",
"integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==",
"license": "MIT",
"dependencies": {
"lodash.clonedeep": "^4.5.0",
"lru-cache": "6.0.0"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -4763,6 +4962,12 @@
"node": ">=0.4"
}
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC"
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+2
View File
@@ -30,6 +30,8 @@
"cors": "^2.8.5",
"express": "^4.19.2",
"joi": "^17.12.1",
"jsonwebtoken": "^9.0.2",
"jwks-rsa": "^3.1.0",
"mongoose": "^8.4.0",
"pg": "^8.11.5",
"pg-hstore": "^2.3.4",
@@ -1,5 +1,5 @@
const { NotFoundError, DatabaseError } = require('../../shared/errors');
const { Op } = require('sequelize');
const { NotFoundError, DatabaseError } = require('../../shared/errors');
class FriendRequestsRepository {
constructor({ sequelize }) {
@@ -30,7 +30,7 @@ class FriendRequestsRepository {
try {
return await this.FriendRequest.findAll({
where: {
toUserId: userId,
[Op.or]: [{ toUserId: userId }, { fromUserId: userId }],
status: 'PENDING',
},
include: [
@@ -39,10 +39,7 @@ class FriendRequestsRepository {
],
});
} catch (error) {
throw new DatabaseError(
`Failed to fetch pending requests for user ${userId}`,
error
);
throw new DatabaseError(`Failed to fetch pending requests for user ${userId}`, error);
}
}
@@ -64,11 +61,12 @@ class FriendRequestsRepository {
async create(fromUserId, toUserId) {
try {
return await this.FriendRequest.create({
const created = await this.FriendRequest.create({
fromUserId,
toUserId,
status: 'PENDING',
});
return await this.findById(created.id);
} catch (error) {
throw new DatabaseError('Failed to create friend request', error);
}
@@ -1,3 +1,4 @@
const { Op } = require('sequelize');
const { NotFoundError, DatabaseError } = require('../../shared/errors');
class GroupsRepository {
@@ -15,6 +16,29 @@ class GroupsRepository {
}
}
async findByUserId(userId) {
try {
return await this.Group.findAll({
include: [
{
model: this.User,
as: 'members',
through: { attributes: [] },
},
],
where: {
id: {
[Op.in]: this.sequelize.literal(
`(SELECT "groupId" FROM "UserGroups" WHERE "userId" = ${parseInt(userId, 10)})`
),
},
},
});
} catch (error) {
throw new DatabaseError('Failed to fetch groups for user', error);
}
}
async findById(id, includeMembers = false) {
try {
const options = {};
+24 -25
View File
@@ -1,31 +1,30 @@
const express = require('express');
// Il router è ora una factory function che riceve l'istanza di sequelize
module.exports = (sequelize) => {
const router = express.Router();
// --- Middleware per simulare un utente loggato ---
const { User } = sequelize.models;
const MOCK_USER_NAME = 'Mario Rossi';
let MOCK_USER_ID = null;
module.exports = sequelize => {
const router = express.Router();
router.use(async (req, res, next) => {
if (!MOCK_USER_ID) {
const user = await User.findOne({ where: { name: MOCK_USER_NAME } });
if (user) {
MOCK_USER_ID = user.id;
}
}
req.userId = MOCK_USER_ID;
next();
});
// --- Middleware per simulare un utente loggato ---
const { User } = sequelize.models;
const MOCK_USER_NAME = 'Mario Rossi';
let MOCK_USER_ID = null;
// --- Monta i router da file separati ---
router.use(require('./initial-data.routes')(sequelize));
router.use('/bets', require('./bets.routes')(sequelize));
router.use('/groups', require('./groups.routes')(sequelize));
router.use('/friend-requests', require('./friends.routes')(sequelize));
router.use('/users', require('./users.routes')(sequelize));
return router;
router.use(async (req, res, next) => {
if (!MOCK_USER_ID) {
const user = await User.findOne({ where: { name: MOCK_USER_NAME } });
if (user) {
MOCK_USER_ID = user.id;
}
}
req.userId = MOCK_USER_ID;
next();
});
// --- Monta i router da file separati ---
router.use('/bets', require('./bets.routes')(sequelize));
router.use('/groups', require('./groups.routes')(sequelize));
router.use('/friend-requests', require('./friends.routes')(sequelize));
router.use('/users', require('./users.routes')(sequelize));
return router;
};
@@ -1,59 +0,0 @@
const express = require('express');
const router = express.Router();
const { Op } = require('sequelize');
// Modello Mongoose per le Scommesse
const Bet = require('../models/bet.model');
module.exports = (sequelize) => {
const { User, Friendship, FriendRequest } = sequelize.models;
const { populateBetsWithUsers } = require('../utils/populateBets')(sequelize);
// GET /api/initial-data
router.get('/initial-data', async (req, res) => {
try {
// --- Query PostgreSQL ---
const currentUser = await User.findByPk(req.userId);
const users = await User.findAll();
const friendships = await Friendship.findAll({
where: { [Op.or]: [{ user1Id: req.userId }, { user2Id: req.userId }] },
include: [{ model: User, as: 'user1' }, { model: User, as: 'user2' }]
});
const friends = friendships.map(f => (f.user1Id === req.userId ? f.user2 : f.user1));
const receivedFriendRequests = await FriendRequest.findAll({
where: { toUserId: req.userId, status: 'PENDING' },
include: [{ model: User, as: 'fromUser' }, { model: User, as: 'toUser' }]
});
const sentFriendRequests = await FriendRequest.findAll({
where: { fromUserId: req.userId, status: 'PENDING' },
include: [{ model: User, as: 'fromUser' }, { model: User, as: 'toUser' }]
});
const userGroups = await currentUser.getGroups({
include: [{ model: User, as: 'members', through: { attributes: [] } }]
});
// --- Query MongoDB ---
const mongoBets = await Bet.find({ 'participants.user': req.userId }).sort({ startDate: -1 });
const mongoPublicBets = await Bet.find({ isPublic: true, 'participants.user': { $ne: req.userId } });
// --- Data Stitching ---
const bets = await populateBetsWithUsers(mongoBets);
const publicBets = await populateBetsWithUsers(mongoPublicBets);
res.json({
currentUser, users, friends, receivedFriendRequests, sentFriendRequests,
bets, publicBets, groups: userGroups
});
} catch (error) {
console.error('Error fetching initial data:', error);
res.status(500).json({ message: 'Error fetching initial data', error: error.message });
}
});
return router;
};
+5 -4
View File
@@ -1,14 +1,13 @@
const express = require('express');
const { validate } = require('../../middleware/validation');
const {
createBetSchema,
updateBetSchema,
} = require('../../shared/validators/schemas/bet.schema');
const { createBetSchema, updateBetSchema } = require('../../shared/validators/schemas/bet.schema');
module.exports = ({ betsController }) => {
const router = express.Router();
const controller = {
getUserBets: (req, res, next) => betsController.getUserBets(req, res, next),
getPublicBets: (req, res, next) => betsController.getPublicBets(req, res, next),
getAllBets: (req, res, next) => betsController.getAllBets(req, res, next),
getBetById: (req, res, next) => betsController.getBetById(req, res, next),
createBet: (req, res, next) => betsController.createBet(req, res, next),
@@ -16,6 +15,8 @@ module.exports = ({ betsController }) => {
deleteBet: (req, res, next) => betsController.deleteBet(req, res, next),
};
router.get('/user', controller.getUserBets);
router.get('/public', controller.getPublicBets);
router.get('/', controller.getAllBets);
router.get('/:id', controller.getBetById);
router.post('/', validate(createBetSchema), controller.createBet);
+1 -3
View File
@@ -1,6 +1,6 @@
const express = require('express');
module.exports = (container) => {
module.exports = container => {
const router = express.Router();
const controllers = {
@@ -8,14 +8,12 @@ module.exports = (container) => {
betsController: container.resolve('betsController'),
usersController: container.resolve('usersController'),
friendsController: container.resolve('friendsController'),
initialDataController: container.resolve('initialDataController'),
};
router.use('/groups', require('./groups.routes')(controllers));
router.use('/bets', require('./bets.routes')(controllers));
router.use('/users', require('./users.routes')(controllers));
router.use('/friend-requests', require('./friends.routes')(controllers));
router.use('/initial-data', require('./initialData.routes')(controllers));
return router;
};
@@ -1,13 +0,0 @@
const express = require('express');
module.exports = ({ initialDataController }) => {
const router = express.Router();
const controller = {
getInitialData: (req, res, next) => initialDataController.getInitialData(req, res, next),
};
router.get('/', controller.getInitialData);
return router;
};
+2
View File
@@ -6,9 +6,11 @@ module.exports = ({ usersController }) => {
const router = express.Router();
const controller = {
getCurrentUser: (req, res, next) => usersController.getCurrentUser(req, res, next),
updateCurrentUser: (req, res, next) => usersController.updateCurrentUser(req, res, next),
};
router.get('/current', controller.getCurrentUser);
router.put('/current', validate(updateUserSchema), controller.updateCurrentUser);
return router;
+7 -7
View File
@@ -4,8 +4,7 @@ const cors = require('cors');
const config = require('./config');
const setupContainer = require('./di/container');
const errorHandler = require('./middleware/errorHandler');
const mockAuthMiddleware = require('./middleware/auth.mock');
const { seedDatabase } = require('./seed');
const keycloakAuthMiddleware = require('./middleware/auth.keycloak');
const legacyApiRoutes = require('./routes/api');
@@ -21,20 +20,22 @@ const sequelize = container.resolve('sequelize');
mongoose
.connect(config.database.mongo.uri, config.database.mongo.options)
.then(() => console.log('Connected to MongoDB'))
.catch((err) => {
.catch(err => {
console.error('MongoDB connection error:', err);
process.exit(1);
});
app.use('/api', mockAuthMiddleware(sequelize));
app.use('/api', keycloakAuthMiddleware(sequelize));
app.use('/api/legacy', legacyApiRoutes(sequelize));
const v1Routes = require('./routes/v1');
app.use('/api/v1', v1Routes(container));
app.use('/api', v1Routes(container));
const healthRoutes = require('./routes/health.routes');
app.use('/', healthRoutes({ sequelize, mongoose }));
app.use(errorHandler);
@@ -43,11 +44,10 @@ sequelize
.authenticate()
.then(() => {
console.log('Connected to PostgreSQL');
return sequelize.sync({ force: true });
return sequelize.sync({ alter: true });
})
.then(() => {
console.log('PostgreSQL models synchronized');
return seedDatabase(sequelize);
})
.then(() => {
app.listen(config.port, () => {
@@ -58,7 +58,7 @@ sequelize
console.log(`Health check available at /health`);
});
})
.catch((err) => {
.catch(err => {
console.error('PostgreSQL connection or sync error:', err);
process.exit(1);
});
+8 -19
View File
@@ -1,13 +1,8 @@
const { ValidationError, ConflictError } = require('../shared/errors');
const { Op } = require('sequelize');
const { ValidationError, ConflictError } = require('../shared/errors');
class FriendsService {
constructor({
friendshipsRepository,
friendRequestsRepository,
usersRepository,
sequelize,
}) {
constructor({ friendshipsRepository, friendRequestsRepository, usersRepository, sequelize }) {
this.friendshipsRepository = friendshipsRepository;
this.friendRequestsRepository = friendRequestsRepository;
this.usersRepository = usersRepository;
@@ -33,9 +28,7 @@ class FriendsService {
});
if (!toUser) {
throw new ValidationError(
'User not found with provided name or email'
);
throw new ValidationError('User not found with provided name or email');
}
}
@@ -43,18 +36,12 @@ class FriendsService {
throw new ValidationError('Cannot send friend request to yourself');
}
const friendshipExists = await this.friendshipsRepository.exists(
fromUserId,
toUser.id
);
const friendshipExists = await this.friendshipsRepository.exists(fromUserId, toUser.id);
if (friendshipExists) {
throw new ConflictError('You are already friends with this user');
}
const existingRequest = await this.friendRequestsRepository.findExisting(
fromUserId,
toUser.id
);
const existingRequest = await this.friendRequestsRepository.findExisting(fromUserId, toUser.id);
if (existingRequest) {
throw new ConflictError('Friend request already exists');
}
@@ -73,6 +60,8 @@ class FriendsService {
throw new ValidationError('This request has already been processed');
}
const newFriend = request.fromUser;
const t = await this.sequelize.transaction();
try {
await this.friendRequestsRepository.updateStatus(requestId, 'ACCEPTED');
@@ -80,7 +69,7 @@ class FriendsService {
await this.friendRequestsRepository.delete(requestId);
await t.commit();
return { message: 'Friend request accepted successfully' };
return { message: 'Friend request accepted successfully', newFriend };
} catch (error) {
await t.rollback();
throw error;
+2 -10
View File
@@ -6,16 +6,8 @@ class GroupsService {
this.usersRepository = usersRepository;
}
async getAllGroups() {
return await this.groupsRepository.findAll({
include: [
{
model: this.usersRepository.User,
as: 'members',
through: { attributes: [] },
},
],
});
async getAllGroups(userId) {
return await this.groupsRepository.findByUserId(userId);
}
async getGroupById(groupId) {