Modifiche
This commit is contained in:
@@ -6,3 +6,5 @@
|
||||
/frontend-angular/.angular/
|
||||
/backend-node/node_modules/
|
||||
/backend-node/.env
|
||||
/frontend-angular/node_modules/
|
||||
/.claude/
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
@@ -1,11 +1,28 @@
|
||||
module.exports = (sequelize) => {
|
||||
// eslint-disable-next-line import/no-import-module-exports
|
||||
import { DataTypes } from 'sequelize';
|
||||
|
||||
module.exports = sequelize => {
|
||||
const { User, Group, FriendRequest, Friendship } = sequelize.models;
|
||||
|
||||
// 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' });
|
||||
// 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',
|
||||
});
|
||||
|
||||
// Friendship is a direct link between two users.
|
||||
// user1Id will always be less than user2Id to ensure uniqueness.
|
||||
@@ -14,8 +31,16 @@ module.exports = (sequelize) => {
|
||||
|
||||
// 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' });
|
||||
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' });
|
||||
};
|
||||
|
||||
@@ -1,21 +1,34 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
const FriendRequest = sequelize.define('FriendRequest', {
|
||||
module.exports = sequelize => {
|
||||
const FriendRequest = sequelize.define(
|
||||
'FriendRequest',
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
primaryKey: true,
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: 'PENDING',
|
||||
allowNull: false
|
||||
}
|
||||
allowNull: false,
|
||||
},
|
||||
// Foreign keys 'fromUserId' and 'toUserId' are added via associations
|
||||
}, {
|
||||
},
|
||||
{
|
||||
// Uses default `createdAt` and `updatedAt` timestamps from Sequelize
|
||||
tableName: 'friend_requests'
|
||||
});
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
const Group = sequelize.define('Group', {
|
||||
module.exports = sequelize => {
|
||||
const Group = sequelize.define(
|
||||
'Group',
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
primaryKey: true,
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
}
|
||||
}, {
|
||||
allowNull: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: false,
|
||||
tableName: 'groups'
|
||||
});
|
||||
tableName: 'groups',
|
||||
}
|
||||
);
|
||||
|
||||
Group.prototype.toJSON = function () {
|
||||
const values = { ...this.get() };
|
||||
if (values.id !== undefined) values.id = String(values.id);
|
||||
return values;
|
||||
};
|
||||
|
||||
return Group;
|
||||
};
|
||||
|
||||
@@ -1,35 +1,51 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
const User = sequelize.define('User', {
|
||||
module.exports = sequelize => {
|
||||
const User = sequelize.define(
|
||||
'User',
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
primaryKey: true,
|
||||
},
|
||||
keycloakId: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
unique: true,
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
},
|
||||
email: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
validate: {
|
||||
isEmail: true
|
||||
}
|
||||
isEmail: true,
|
||||
},
|
||||
},
|
||||
avatarUrl: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
},
|
||||
score: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0
|
||||
defaultValue: 0,
|
||||
},
|
||||
}, {
|
||||
},
|
||||
{
|
||||
timestamps: false,
|
||||
tableName: 'users'
|
||||
});
|
||||
tableName: 'users',
|
||||
}
|
||||
);
|
||||
|
||||
User.prototype.toJSON = function () {
|
||||
const values = { ...this.get() };
|
||||
if (values.id !== undefined) values.id = String(values.id);
|
||||
return values;
|
||||
};
|
||||
|
||||
return User;
|
||||
};
|
||||
|
||||
Generated
+205
@@ -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",
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const express = require('express');
|
||||
|
||||
// Il router è ora una factory function che riceve l'istanza di sequelize
|
||||
module.exports = (sequelize) => {
|
||||
module.exports = sequelize => {
|
||||
const router = express.Router();
|
||||
|
||||
// --- Middleware per simulare un utente loggato ---
|
||||
@@ -21,7 +21,6 @@ module.exports = (sequelize) => {
|
||||
});
|
||||
|
||||
// --- 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));
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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,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;
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+57
-1
@@ -42,6 +42,57 @@ services:
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# Keycloak Database
|
||||
keycloak-db:
|
||||
image: postgres:15-alpine
|
||||
container_name: scommetti-keycloak-db
|
||||
environment:
|
||||
POSTGRES_DB: keycloak
|
||||
POSTGRES_USER: keycloak
|
||||
POSTGRES_PASSWORD: keycloak
|
||||
volumes:
|
||||
- keycloak_db_data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- scommetti-network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U keycloak -d keycloak"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# Keycloak Auth Server
|
||||
keycloak:
|
||||
image: quay.io/keycloak/keycloak:23.0.0
|
||||
container_name: scommetti-keycloak
|
||||
command: start-dev --import-realm
|
||||
environment:
|
||||
KC_DB: postgres
|
||||
KC_DB_URL: jdbc:postgresql://keycloak-db:5432/keycloak
|
||||
KC_DB_USERNAME: keycloak
|
||||
KC_DB_PASSWORD: keycloak
|
||||
KEYCLOAK_ADMIN: admin
|
||||
KEYCLOAK_ADMIN_PASSWORD: admin
|
||||
KC_HEALTH_ENABLED: "true"
|
||||
KC_METRICS_ENABLED: "true"
|
||||
KC_HOSTNAME_STRICT: "false"
|
||||
KC_HTTP_ENABLED: "true"
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./keycloak/realm-export.json:/opt/keycloak/data/import/realm.json:ro
|
||||
- ./keycloak/themes/scommettiapp:/opt/keycloak/themes/scommettiapp:ro
|
||||
depends_on:
|
||||
keycloak-db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- scommetti-network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "exec 3<>/dev/tcp/localhost/8080 && printf 'GET /health/ready HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n' >&3 && timeout 5 cat <&3 | grep -q 'UP'"]
|
||||
interval: 15s
|
||||
timeout: 15s
|
||||
retries: 10
|
||||
start_period: 90s
|
||||
|
||||
# Backend Node.js
|
||||
backend:
|
||||
build:
|
||||
@@ -53,6 +104,7 @@ services:
|
||||
PORT: 3000
|
||||
POSTGRES_URI: postgres://admin:admin@postgres:5432/amici_scommesse
|
||||
MONGO_URI: mongodb://mongodb:27017/amici-scommesse
|
||||
KEYCLOAK_JWKS_URI: http://keycloak:8080/realms/scommettiapp/protocol/openid-connect/certs
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
@@ -63,6 +115,8 @@ services:
|
||||
condition: service_healthy
|
||||
mongodb:
|
||||
condition: service_healthy
|
||||
keycloak:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- scommetti-network
|
||||
command: npm start
|
||||
@@ -84,9 +138,9 @@ services:
|
||||
- /app/.angular
|
||||
depends_on:
|
||||
- backend
|
||||
- keycloak
|
||||
networks:
|
||||
- scommetti-network
|
||||
command: npm run dev
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
@@ -94,6 +148,8 @@ volumes:
|
||||
driver: local
|
||||
mongodb_data:
|
||||
driver: local
|
||||
keycloak_db_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
scommetti-network:
|
||||
|
||||
@@ -8,7 +8,7 @@ WORKDIR /app
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci
|
||||
RUN npm install
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"serve": {
|
||||
"builder": "@angular/build:dev-server",
|
||||
"options": {
|
||||
"port": 3000
|
||||
"port": 4200
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
|
||||
@@ -1,15 +1,36 @@
|
||||
import '@angular/compiler';
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { provideZonelessChangeDetection } from '@angular/core';
|
||||
import { provideHttpClient, withFetch } from '@angular/common/http';
|
||||
import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
|
||||
import Keycloak from 'keycloak-js';
|
||||
|
||||
import { AppComponent } from './src/app.component';
|
||||
import { KEYCLOAK_INSTANCE } from './src/core/tokens/keycloak.token';
|
||||
import { authInterceptor } from './src/core/interceptors/auth.interceptor';
|
||||
|
||||
bootstrapApplication(AppComponent, {
|
||||
const keycloak = new Keycloak({
|
||||
url: 'http://localhost:8080',
|
||||
realm: 'scommettiapp',
|
||||
clientId: 'scommetti-frontend',
|
||||
});
|
||||
|
||||
keycloak.init({
|
||||
onLoad: 'login-required',
|
||||
checkLoginIframe: false,
|
||||
pkceMethod: 'S256',
|
||||
}).then(authenticated => {
|
||||
if (!authenticated) {
|
||||
keycloak.login();
|
||||
return;
|
||||
}
|
||||
|
||||
bootstrapApplication(AppComponent, {
|
||||
providers: [
|
||||
provideZonelessChangeDetection(),
|
||||
provideHttpClient(withFetch()),
|
||||
provideHttpClient(withFetch(), withInterceptors([authInterceptor])),
|
||||
{ provide: KEYCLOAK_INSTANCE, useValue: keycloak },
|
||||
],
|
||||
}).catch(err => console.error(err));
|
||||
|
||||
// AI Studio always uses an `index.tsx` file for all project types.
|
||||
}).catch(err => console.error(err));
|
||||
}).catch(err => {
|
||||
console.error('Keycloak initialization failed:', err);
|
||||
});
|
||||
|
||||
Generated
+26
@@ -15,6 +15,7 @@
|
||||
"@angular/compiler-cli": "^20.3.0",
|
||||
"@angular/core": "^20.3.0",
|
||||
"@angular/platform-browser": "^20.3.0",
|
||||
"keycloak-js": "^24.0.0",
|
||||
"rxjs": "^7.8.2",
|
||||
"tailwindcss": "latest"
|
||||
},
|
||||
@@ -4931,6 +4932,12 @@
|
||||
"@pkgjs/parseargs": "^0.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/js-sha256": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.11.1.tgz",
|
||||
"integrity": "sha512-o6WSo/LUvY2uC4j7mO50a2ms7E/EAdbP0swigLV+nzHKTTaYnaLIWJ02VdXrsJX0vGedDESQnLsOekr94ryfjg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -4991,6 +4998,25 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jwt-decode": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
|
||||
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/keycloak-js": {
|
||||
"version": "24.0.5",
|
||||
"resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-24.0.5.tgz",
|
||||
"integrity": "sha512-VQOSn3j13DPB6OuavKAq+sRjDERhIKrXgBzekoHRstifPuyULILguugX6yxRUYFSpn3OMYUXmSX++tkdCupOjA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"js-sha256": "^0.11.0",
|
||||
"jwt-decode": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/listr2": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.1.tgz",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"@angular/build": "^20.3.0",
|
||||
"@angular/cli": "^20.3.0",
|
||||
"@angular/compiler-cli": "^20.3.0",
|
||||
"keycloak-js": "^24.0.0",
|
||||
"tailwindcss": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
|
||||
import { Component, ChangeDetectionStrategy, signal, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { HomeComponent } from './features/home/home.component';
|
||||
import { DiscoverComponent } from './features/discover/discover.component';
|
||||
import { GroupsComponent } from './features/groups/groups.component';
|
||||
import { ProfileComponent } from './features/profile/profile.component';
|
||||
import { CreateBetComponent } from './features/bets/create-bet.component';
|
||||
import { DataService } from './core/services/data.service';
|
||||
|
||||
type View = 'home' | 'discover' | 'groups' | 'profile' | 'create-bet';
|
||||
|
||||
@@ -23,9 +24,15 @@ type View = 'home' | 'discover' | 'groups' | 'profile' | 'create-bet';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppComponent {
|
||||
private dataService = inject(DataService);
|
||||
|
||||
currentView = signal<View>('home');
|
||||
private previousView = signal<View>('home');
|
||||
|
||||
constructor() {
|
||||
this.dataService.loadCurrentUser();
|
||||
}
|
||||
|
||||
setView(view: View) {
|
||||
if (view === 'create-bet') {
|
||||
this.previousView.set(this.currentView());
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { HttpInterceptorFn } from '@angular/common/http';
|
||||
import { inject } from '@angular/core';
|
||||
import { from, switchMap } from 'rxjs';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
|
||||
export const authInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
const authService = inject(AuthService);
|
||||
|
||||
return from(authService.getValidToken()).pipe(
|
||||
switchMap(token => {
|
||||
if (!token) return next(req);
|
||||
const authReq = req.clone({
|
||||
headers: req.headers.set('Authorization', `Bearer ${token}`)
|
||||
});
|
||||
return next(authReq);
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -1,8 +1,3 @@
|
||||
import { User } from './user.model';
|
||||
import { Group } from './group.model';
|
||||
import { Bet } from './bet.model';
|
||||
import { FriendRequest } from './friend-request.model';
|
||||
|
||||
export interface SuccessResponse<T = any> {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
@@ -21,16 +16,3 @@ export interface ErrorResponse {
|
||||
errors?: ErrorDetail[];
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface InitialData {
|
||||
currentUser: User;
|
||||
users: User[];
|
||||
friends: User[];
|
||||
friendRequests: FriendRequest[];
|
||||
receivedFriendRequests?: FriendRequest[]; // Legacy or convenience
|
||||
sentFriendRequests?: FriendRequest[]; // Legacy or convenience
|
||||
groups: Group[];
|
||||
userBets: Bet[];
|
||||
bets?: Bet[]; // Compatibility with data.service
|
||||
publicBets: Bet[];
|
||||
}
|
||||
|
||||
@@ -21,6 +21,18 @@ export class BetApiService {
|
||||
private http = inject(HttpClient);
|
||||
private readonly apiUrl = 'http://localhost:3000/api/v1/bets';
|
||||
|
||||
getUserBets(): Observable<Bet[]> {
|
||||
return this.http.get<SuccessResponse<Bet[]>>(`${this.apiUrl}/user`).pipe(
|
||||
map(res => res.data.map(mapBet))
|
||||
);
|
||||
}
|
||||
|
||||
getPublicBets(): Observable<Bet[]> {
|
||||
return this.http.get<SuccessResponse<Bet[]>>(`${this.apiUrl}/public`).pipe(
|
||||
map(res => res.data.map(mapBet))
|
||||
);
|
||||
}
|
||||
|
||||
getBets(): Observable<Bet[]> {
|
||||
return this.http.get<SuccessResponse<Bet[]>>(this.apiUrl).pipe(
|
||||
map(res => res.data.map(mapBet))
|
||||
|
||||
@@ -13,6 +13,18 @@ export class FriendRequestApiService {
|
||||
private http = inject(HttpClient);
|
||||
private readonly apiUrl = 'http://localhost:3000/api/v1/friend-requests';
|
||||
|
||||
getFriends(): Observable<User[]> {
|
||||
return this.http.get<SuccessResponse<User[]>>(this.apiUrl).pipe(
|
||||
map(res => res.data)
|
||||
);
|
||||
}
|
||||
|
||||
getPendingRequests(): Observable<FriendRequest[]> {
|
||||
return this.http.get<SuccessResponse<FriendRequest[]>>(`${this.apiUrl}/pending`).pipe(
|
||||
map(res => res.data)
|
||||
);
|
||||
}
|
||||
|
||||
sendRequest(request: SendFriendRequestRequest): Observable<FriendRequest> {
|
||||
return this.http.post<SuccessResponse<FriendRequest>>(this.apiUrl, request).pipe(
|
||||
map(res => res.data)
|
||||
|
||||
@@ -2,24 +2,8 @@ import { inject, Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { User, UpdateUserRequest } from '../../models/user.model';
|
||||
import { SuccessResponse, InitialData } from '../../models/api-responses.model';
|
||||
import { SuccessResponse } from '../../models/api-responses.model';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { toDate } from '../../utils/type-guards';
|
||||
|
||||
function mapInitialData(data: InitialData): InitialData {
|
||||
const mapBet = (bet: any) => ({
|
||||
...bet,
|
||||
startDate: toDate(bet.startDate)!,
|
||||
endDate: toDate(bet.endDate)
|
||||
});
|
||||
|
||||
return {
|
||||
...data,
|
||||
userBets: (data.userBets || []).map(mapBet),
|
||||
publicBets: (data.publicBets || []).map(mapBet),
|
||||
bets: (data.bets || []).map(mapBet)
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@@ -28,9 +12,9 @@ export class UserApiService {
|
||||
private http = inject(HttpClient);
|
||||
private readonly apiUrl = 'http://localhost:3000/api/v1';
|
||||
|
||||
getInitialData(): Observable<InitialData> {
|
||||
return this.http.get<SuccessResponse<InitialData>>(`${this.apiUrl}/initial-data`).pipe(
|
||||
map(res => mapInitialData(res.data))
|
||||
getCurrentUser(): Observable<User> {
|
||||
return this.http.get<SuccessResponse<User>>(`${this.apiUrl}/users/current`).pipe(
|
||||
map(res => res.data)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,6 +23,4 @@ export class UserApiService {
|
||||
map(res => res.data)
|
||||
);
|
||||
}
|
||||
|
||||
// Potential future: getProfile(id), etc.
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { KEYCLOAK_INSTANCE } from '../tokens/keycloak.token';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthService {
|
||||
private keycloak = inject(KEYCLOAK_INSTANCE);
|
||||
|
||||
async getValidToken(): Promise<string> {
|
||||
try {
|
||||
await this.keycloak.updateToken(30);
|
||||
} catch {
|
||||
this.keycloak.login();
|
||||
return '';
|
||||
}
|
||||
return this.keycloak.token!;
|
||||
}
|
||||
|
||||
get tokenParsed() {
|
||||
return this.keycloak.tokenParsed;
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.keycloak.logout({ redirectUri: window.location.origin });
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@ export class DataService {
|
||||
private userApiService = inject(UserApiService);
|
||||
private friendRequestApiService = inject(FriendRequestApiService);
|
||||
|
||||
private readonly _users = signal<User[]>([]);
|
||||
private readonly _bets = signal<Bet[]>([]);
|
||||
private readonly _groups = signal<Group[]>([]);
|
||||
private readonly _friends = signal<User[]>([]);
|
||||
@@ -47,25 +46,63 @@ export class DataService {
|
||||
|
||||
readonly getPublicBets = computed(() => this._publicBets());
|
||||
|
||||
constructor() {
|
||||
this._initializeData();
|
||||
// --- Load methods: chiamati da ogni componente al momento della visualizzazione ---
|
||||
|
||||
async loadCurrentUser(): Promise<void> {
|
||||
try {
|
||||
const user = await firstValueFrom(this.userApiService.getCurrentUser());
|
||||
this.currentUser.set(user);
|
||||
} catch (error) {
|
||||
console.error('Failed to load current user', error);
|
||||
}
|
||||
}
|
||||
|
||||
private _initializeData() {
|
||||
this.userApiService.getInitialData().subscribe({
|
||||
next: data => {
|
||||
this.currentUser.set(data.currentUser);
|
||||
this._users.set(data.users);
|
||||
this._friends.set(data.friends);
|
||||
// Combine received and sent requests if they come separated, or use the array if unified
|
||||
this._friendRequests.set(data.friendRequests || []);
|
||||
this._bets.set(data.userBets || data.bets || []);
|
||||
this._publicBets.set(data.publicBets);
|
||||
this._groups.set(data.groups);
|
||||
},
|
||||
error: err => console.error("Failed to load initial data", err)
|
||||
});
|
||||
async loadUserBets(): Promise<void> {
|
||||
try {
|
||||
const bets = await firstValueFrom(this.betApiService.getUserBets());
|
||||
this._bets.set(bets);
|
||||
} catch (error) {
|
||||
console.error('Failed to load user bets', error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadPublicBets(): Promise<void> {
|
||||
try {
|
||||
const bets = await firstValueFrom(this.betApiService.getPublicBets());
|
||||
this._publicBets.set(bets);
|
||||
} catch (error) {
|
||||
console.error('Failed to load public bets', error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadFriends(): Promise<void> {
|
||||
try {
|
||||
const friends = await firstValueFrom(this.friendRequestApiService.getFriends());
|
||||
this._friends.set(friends);
|
||||
} catch (error) {
|
||||
console.error('Failed to load friends', error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadFriendRequests(): Promise<void> {
|
||||
try {
|
||||
const requests = await firstValueFrom(this.friendRequestApiService.getPendingRequests());
|
||||
this._friendRequests.set(requests);
|
||||
} catch (error) {
|
||||
console.error('Failed to load friend requests', error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadGroups(): Promise<void> {
|
||||
try {
|
||||
const groups = await firstValueFrom(this.groupApiService.getGroups());
|
||||
this._groups.set(groups);
|
||||
} catch (error) {
|
||||
console.error('Failed to load groups', error);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Utility ---
|
||||
|
||||
getGroupById(id: string): Group | undefined {
|
||||
return this._groups().find(g => g.id === id);
|
||||
@@ -75,6 +112,8 @@ export class DataService {
|
||||
return this._bets().filter(b => b.groupId === groupId);
|
||||
}
|
||||
|
||||
// --- Mutations ---
|
||||
|
||||
async sendFriendRequest(identifier: string): Promise<{ success: boolean, message: string }> {
|
||||
try {
|
||||
const newRequest = await firstValueFrom(this.friendRequestApiService.sendRequest({ identifier }));
|
||||
@@ -90,7 +129,7 @@ export class DataService {
|
||||
await firstValueFrom(this.friendRequestApiService.cancelRequest(requestId));
|
||||
this._friendRequests.update(reqs => reqs.filter(r => r.id !== requestId));
|
||||
} catch (error) {
|
||||
console.error("Failed to cancel friend request", error);
|
||||
console.error('Failed to cancel friend request', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +139,7 @@ export class DataService {
|
||||
this._friends.update(f => [...f, newFriend]);
|
||||
this._friendRequests.update(reqs => reqs.filter(r => r.id !== request.id));
|
||||
} catch (error) {
|
||||
console.error("Failed to accept friend request", error);
|
||||
console.error('Failed to accept friend request', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +148,7 @@ export class DataService {
|
||||
await firstValueFrom(this.friendRequestApiService.declineRequest(requestId));
|
||||
this._friendRequests.update(reqs => reqs.filter(r => r.id !== requestId));
|
||||
} catch (error) {
|
||||
console.error("Failed to decline friend request", error);
|
||||
console.error('Failed to decline friend request', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +158,7 @@ export class DataService {
|
||||
const newGroup = await firstValueFrom(this.groupApiService.createGroup({ name, members: memberIds }));
|
||||
this._groups.update(groups => [...groups, newGroup]);
|
||||
} catch (error) {
|
||||
console.error("Failed to add group", error);
|
||||
console.error('Failed to add group', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +167,7 @@ export class DataService {
|
||||
const updatedGroup = await firstValueFrom(this.groupApiService.removeMember(groupId, userId));
|
||||
this._groups.update(groups => groups.map(g => g.id === groupId ? updatedGroup : g));
|
||||
} catch (error) {
|
||||
console.error("Failed to remove user from group", error);
|
||||
console.error('Failed to remove user from group', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +177,7 @@ export class DataService {
|
||||
const updatedGroup = await firstValueFrom(this.groupApiService.addMembers(groupId, { userIds }));
|
||||
this._groups.update(groups => groups.map(g => g.id === groupId ? updatedGroup : g));
|
||||
} catch (error) {
|
||||
console.error("Failed to add users to group", error);
|
||||
console.error('Failed to add users to group', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,9 +185,8 @@ export class DataService {
|
||||
try {
|
||||
const updatedUser = await firstValueFrom(this.userApiService.updateCurrentUser(data));
|
||||
this.currentUser.set(updatedUser);
|
||||
this._users.update(users => users.map(u => u.id === updatedUser.id ? updatedUser : u));
|
||||
} catch (error) {
|
||||
console.error("Failed to update user", error);
|
||||
console.error('Failed to update user', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,7 +218,7 @@ export class DataService {
|
||||
}));
|
||||
this._bets.update(bets => [newBet, ...bets]);
|
||||
} catch (error) {
|
||||
console.error("Failed to add bet", error);
|
||||
console.error('Failed to add bet', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import { InjectionToken } from '@angular/core';
|
||||
import Keycloak from 'keycloak-js';
|
||||
|
||||
export const KEYCLOAK_INSTANCE = new InjectionToken<Keycloak>('KeycloakInstance');
|
||||
@@ -2,7 +2,6 @@ import { Component, ChangeDetectionStrategy, output, inject, signal, computed, O
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { DataService } from '../../core/services/data.service';
|
||||
import { BetParticipant, BetRole } from '../../core/models/bet.model';
|
||||
import { Group } from '../../core/models/group.model';
|
||||
import { User } from '../../core/models/user.model';
|
||||
import { getUserId } from '../../core/utils/type-guards';
|
||||
|
||||
@@ -51,6 +50,9 @@ export class CreateBetComponent implements OnInit {
|
||||
});
|
||||
|
||||
ngOnInit() {
|
||||
this.dataService.loadFriends();
|
||||
this.dataService.loadGroups();
|
||||
|
||||
const user = this.currentUser();
|
||||
if (user) {
|
||||
this.participants.set([{ user, role: BetRole.Participant }]);
|
||||
@@ -59,13 +61,13 @@ export class CreateBetComponent implements OnInit {
|
||||
|
||||
addParticipant(friend: User) {
|
||||
this.participants.update(p => [...p, { user: friend, role: BetRole.Participant }]);
|
||||
this.groupId.set(undefined); // Reset group if participants are manually changed
|
||||
this.groupId.set(undefined);
|
||||
this.showFriendSelector.set(false);
|
||||
}
|
||||
|
||||
removeParticipant(userId: string) {
|
||||
this.participants.update(p => p.filter(participant => getUserId(participant.user) !== userId));
|
||||
this.groupId.set(undefined); // Reset group if participants are manually changed
|
||||
this.groupId.set(undefined);
|
||||
}
|
||||
|
||||
updateParticipantRole(userId: string, newRole: BetRole) {
|
||||
@@ -94,7 +96,6 @@ export class CreateBetComponent implements OnInit {
|
||||
this.participants.set(groupParticipants);
|
||||
}
|
||||
} else {
|
||||
// Reset to just the current user if "Nessun gruppo" is selected
|
||||
const user = this.currentUser();
|
||||
if (user) {
|
||||
this.participants.set([{ user, role: BetRole.Participant }]);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
|
||||
import { Component, ChangeDetectionStrategy, inject, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { DataService } from '../../core/services/data.service';
|
||||
import { BetCardComponent } from '../../shared/components/bet-card/bet-card.component';
|
||||
@@ -10,7 +10,11 @@ import { BetCardComponent } from '../../shared/components/bet-card/bet-card.comp
|
||||
templateUrl: './discover.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class DiscoverComponent {
|
||||
export class DiscoverComponent implements OnInit {
|
||||
private dataService = inject(DataService);
|
||||
publicBets = this.dataService.getPublicBets;
|
||||
readonly publicBets = this.dataService.getPublicBets;
|
||||
|
||||
ngOnInit() {
|
||||
this.dataService.loadPublicBets();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Component, ChangeDetectionStrategy, input, output, inject, signal, computed, effect } from '@angular/core';
|
||||
import { Component, ChangeDetectionStrategy, input, output, inject, signal, computed } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { DataService } from '../../../core/services/data.service';
|
||||
import { Bet, BetStatus } from '../../../core/models/bet.model';
|
||||
import { Group } from '../../../core/models/group.model';
|
||||
import { User } from '../../../core/models/user.model';
|
||||
import { BetCardComponent } from '../../../shared/components/bet-card/bet-card.component';
|
||||
import { DataService } from '@/src/core/services/data.service';
|
||||
import { Bet, BetStatus } from '@/src/core/models/bet.model';
|
||||
import { BetCardComponent } from '@/src/shared/components/bet-card/bet-card.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-group-detail',
|
||||
@@ -22,30 +20,21 @@ export class GroupDetailComponent {
|
||||
private allGroups = this.dataService.userGroups;
|
||||
group = computed(() => this.allGroups().find(g => g.id === this.groupId()) ?? null);
|
||||
|
||||
groupBets = signal<Bet[]>([]);
|
||||
readonly groupBets = computed(() => this.dataService.getBetsByGroupId(this.groupId()));
|
||||
|
||||
isAddingMembers = signal(false);
|
||||
selectedFriendIds = signal<{ [key: string]: boolean }>({});
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
const id = this.groupId();
|
||||
if (id) {
|
||||
this.groupBets.set(this.dataService.getBetsByGroupId(id));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
sortedMembers = computed(() => {
|
||||
return this.group()?.members.slice().sort((a, b) => b.score - a.score) ?? [];
|
||||
});
|
||||
|
||||
activeBets = computed(() => {
|
||||
return this.groupBets().filter(b => b.status === BetStatus.Active || b.status === BetStatus.AwaitingValidation);
|
||||
return this.groupBets().filter((b: Bet) => b.status === BetStatus.Active || b.status === BetStatus.AwaitingValidation);
|
||||
});
|
||||
|
||||
pastBets = computed(() => {
|
||||
return this.groupBets().filter(b => b.status === BetStatus.Completed || b.status === BetStatus.Expired);
|
||||
return this.groupBets().filter((b: Bet) => b.status === BetStatus.Completed || b.status === BetStatus.Expired);
|
||||
});
|
||||
|
||||
availableFriends = computed(() => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Component, ChangeDetectionStrategy, inject, signal, computed } from '@angular/core';
|
||||
import { Component, ChangeDetectionStrategy, inject, signal, computed, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { DataService } from '../../core/services/data.service';
|
||||
import { User } from '../../core/models/user.model';
|
||||
import { GroupDetailComponent } from './group-detail/group-detail.component';
|
||||
|
||||
@Component({
|
||||
@@ -11,7 +10,7 @@ import { GroupDetailComponent } from './group-detail/group-detail.component';
|
||||
templateUrl: './groups.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class GroupsComponent {
|
||||
export class GroupsComponent implements OnInit {
|
||||
private dataService = inject(DataService);
|
||||
|
||||
groups = this.dataService.userGroups;
|
||||
@@ -27,6 +26,12 @@ export class GroupsComponent {
|
||||
return this.newGroupName().trim().length > 0 && Object.keys(this.selectedFriendIds()).length > 0;
|
||||
});
|
||||
|
||||
ngOnInit() {
|
||||
this.dataService.loadGroups();
|
||||
this.dataService.loadFriends();
|
||||
this.dataService.loadUserBets();
|
||||
}
|
||||
|
||||
selectGroup(groupId: string) {
|
||||
this.selectedGroupId.set(groupId);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Component, ChangeDetectionStrategy, inject, signal, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { DataService } from '../../core/services/data.service';
|
||||
import { Bet } from '../../core/models/bet.model';
|
||||
import { BetsByTimeComponent } from './bets-by-time/bets-by-time.component';
|
||||
import { BetsByGroupComponent } from './bets-by-group/bets-by-group.component';
|
||||
|
||||
@@ -16,12 +15,10 @@ export class HomeComponent implements OnInit {
|
||||
private dataService = inject(DataService);
|
||||
|
||||
viewMode = signal<'time' | 'group'>('time');
|
||||
allBets = signal<Bet[]>([]);
|
||||
readonly allBets = this.dataService.getActiveBets;
|
||||
|
||||
ngOnInit() {
|
||||
// FIX: getActiveBets is a computed signal which returns a value when called once.
|
||||
// The extra parentheses caused an error because it tried to execute an array.
|
||||
this.allBets.set(this.dataService.getActiveBets());
|
||||
this.dataService.loadUserBets();
|
||||
}
|
||||
|
||||
setViewMode(mode: 'time' | 'group') {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- Header with Logout -->
|
||||
<header class="flex justify-between items-center mb-4">
|
||||
<h1 class="text-3xl font-bold">Profilo</h1>
|
||||
<button title="Logout" class="p-2 text-slate-400 hover:text-white rounded-full hover:bg-slate-700 transition-colors">
|
||||
<button (click)="logout()" title="Logout" class="p-2 text-slate-400 hover:text-white rounded-full hover:bg-slate-700 transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||
</svg>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Component, ChangeDetectionStrategy, inject, signal } from '@angular/core';
|
||||
import { Component, ChangeDetectionStrategy, inject, signal, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { DataService } from '../../core/services/data.service';
|
||||
import { AuthService } from '../../core/services/auth.service';
|
||||
import { EditProfileComponent } from './edit-profile/edit-profile.component';
|
||||
import { FriendRequest } from '../../core/models/friend-request.model';
|
||||
|
||||
@@ -11,8 +12,9 @@ import { FriendRequest } from '../../core/models/friend-request.model';
|
||||
templateUrl: './profile.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ProfileComponent {
|
||||
export class ProfileComponent implements OnInit {
|
||||
private dataService = inject(DataService);
|
||||
private authService = inject(AuthService);
|
||||
|
||||
currentUser = this.dataService.currentUser;
|
||||
friends = this.dataService.friends;
|
||||
@@ -26,6 +28,12 @@ export class ProfileComponent {
|
||||
addFriendError = signal<string | null>(null);
|
||||
addFriendSuccess = signal<string | null>(null);
|
||||
|
||||
ngOnInit() {
|
||||
this.dataService.loadCurrentUser();
|
||||
this.dataService.loadFriends();
|
||||
this.dataService.loadFriendRequests();
|
||||
}
|
||||
|
||||
toggleEditMode() {
|
||||
this.isEditing.update(val => !val);
|
||||
}
|
||||
@@ -74,4 +82,8 @@ export class ProfileComponent {
|
||||
declineRequest(requestId: string) {
|
||||
this.dataService.declineFriendRequest(requestId);
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.authService.logout();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"id": "scommettiapp",
|
||||
"realm": "scommettiapp",
|
||||
"enabled": true,
|
||||
"loginTheme": "scommettiapp",
|
||||
"registrationAllowed": true,
|
||||
"loginWithEmailAllowed": true,
|
||||
"duplicateEmailsAllowed": false,
|
||||
"resetPasswordAllowed": true,
|
||||
"editUsernameAllowed": false,
|
||||
"clients": [
|
||||
{
|
||||
"clientId": "scommetti-frontend",
|
||||
"enabled": true,
|
||||
"publicClient": true,
|
||||
"protocol": "openid-connect",
|
||||
"standardFlowEnabled": true,
|
||||
"implicitFlowEnabled": false,
|
||||
"directAccessGrantsEnabled": false,
|
||||
"redirectUris": [
|
||||
"http://localhost:4200/*"
|
||||
],
|
||||
"webOrigins": [
|
||||
"http://localhost:4200"
|
||||
],
|
||||
"attributes": {
|
||||
"pkce.code.challenge.method": "S256"
|
||||
}
|
||||
},
|
||||
{
|
||||
"clientId": "scommetti-backend",
|
||||
"enabled": true,
|
||||
"publicClient": false,
|
||||
"secret": "your-backend-client-secret-123",
|
||||
"protocol": "openid-connect",
|
||||
"standardFlowEnabled": false,
|
||||
"serviceAccountsEnabled": true
|
||||
}
|
||||
],
|
||||
"roles": {
|
||||
"realm": [
|
||||
{
|
||||
"name": "user",
|
||||
"description": "Base user role"
|
||||
},
|
||||
{
|
||||
"name": "admin",
|
||||
"description": "Administrator role"
|
||||
}
|
||||
]
|
||||
},
|
||||
"defaultRoles": ["user"]
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ScommettiApp - Accedi</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus {
|
||||
-webkit-text-fill-color: #f1f5f9;
|
||||
-webkit-box-shadow: 0 0 0px 1000px #0f172a inset;
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
}
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-[#0f172a] min-h-screen flex items-center justify-center p-4">
|
||||
<div class="w-full max-w-md fade-in">
|
||||
|
||||
<!-- Logo / Header -->
|
||||
<div class="text-center mb-8">
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 bg-cyan-500 rounded-2xl mb-4 shadow-lg shadow-cyan-500/20">
|
||||
<svg class="w-9 h-9 text-[#0f172a]" viewBox="0 0 24 24" fill="currentColor">
|
||||
<rect x="2" y="2" width="9" height="9" rx="2"/>
|
||||
<rect x="13" y="2" width="9" height="9" rx="2"/>
|
||||
<rect x="2" y="13" width="9" height="9" rx="2"/>
|
||||
<rect x="13" y="13" width="9" height="9" rx="2"/>
|
||||
<circle cx="6.5" cy="6.5" r="1.2" fill="#06b6d4"/>
|
||||
<circle cx="17.5" cy="6.5" r="1.2" fill="#06b6d4"/>
|
||||
<circle cx="6.5" cy="17.5" r="1.2" fill="#06b6d4"/>
|
||||
<circle cx="17.5" cy="17.5" r="1.2" fill="#06b6d4"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="text-2xl font-bold text-white tracking-tight">ScommettiApp</h1>
|
||||
<p class="text-[#94a3b8] text-sm mt-1">Sfida i tuoi amici</p>
|
||||
</div>
|
||||
|
||||
<!-- Card -->
|
||||
<div class="bg-[#1e293b] border border-[#334155] rounded-2xl p-8 shadow-xl">
|
||||
<h2 class="text-lg font-semibold text-white mb-6">Accedi al tuo account</h2>
|
||||
|
||||
<!-- Messaggio globale (errore / info) -->
|
||||
<#if message?has_content>
|
||||
<div class="rounded-xl p-4 mb-6 text-sm
|
||||
<#if message.type == 'error'>bg-red-500/10 border border-red-500/30 text-red-400
|
||||
<#elseif message.type == 'success'>bg-green-500/10 border border-green-500/30 text-green-400
|
||||
<#elseif message.type == 'warning'>bg-yellow-500/10 border border-yellow-500/30 text-yellow-400
|
||||
<#else>bg-blue-500/10 border border-blue-500/30 text-blue-400</#if>">
|
||||
${message.summary?no_esc}
|
||||
</div>
|
||||
</#if>
|
||||
|
||||
<form action="${url.loginAction}" method="post" novalidate>
|
||||
|
||||
<!-- Campo username / email -->
|
||||
<div class="mb-5">
|
||||
<label for="username" class="block text-[#cbd5e1] text-sm font-medium mb-2">
|
||||
<#if realm.loginWithEmailAllowed && !realm.registrationEmailAsUsername>
|
||||
Email o Username
|
||||
<#elseif realm.loginWithEmailAllowed>
|
||||
Email
|
||||
<#else>
|
||||
Username
|
||||
</#if>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
value="${(login.username!'')}"
|
||||
class="w-full bg-[#0f172a] border border-[#334155] rounded-xl px-4 py-3 text-[#f1f5f9] placeholder-[#475569] focus:outline-none focus:border-cyan-500 transition-colors text-sm"
|
||||
placeholder="<#if realm.loginWithEmailAllowed>nome@esempio.it<#else>il tuo username</#if>"
|
||||
autocomplete="username"
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Campo password -->
|
||||
<div class="mb-5">
|
||||
<label for="password" class="block text-[#cbd5e1] text-sm font-medium mb-2">
|
||||
Password
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
class="w-full bg-[#0f172a] border border-[#334155] rounded-xl px-4 py-3 pr-12 text-[#f1f5f9] placeholder-[#475569] focus:outline-none focus:border-cyan-500 transition-colors text-sm"
|
||||
placeholder="La tua password"
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onclick="togglePassword('password', this)"
|
||||
tabindex="-1"
|
||||
class="absolute right-3 top-1/2 -translate-y-1/2 text-[#475569] hover:text-[#94a3b8] transition-colors p-1"
|
||||
>
|
||||
<svg id="eye-icon-password" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ricordami + Password dimenticata -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<#if realm.rememberMe>
|
||||
<label class="flex items-center gap-2 cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="rememberMe"
|
||||
name="rememberMe"
|
||||
<#if login.rememberMe??>checked</#if>
|
||||
class="w-4 h-4 rounded border-[#334155] bg-[#0f172a] accent-cyan-500 cursor-pointer"
|
||||
/>
|
||||
<span class="text-[#94a3b8] text-sm">Ricordami</span>
|
||||
</label>
|
||||
<#else>
|
||||
<span></span>
|
||||
</#if>
|
||||
|
||||
<#if realm.resetPasswordAllowed>
|
||||
<a href="${url.loginResetCredentialsUrl}"
|
||||
class="text-cyan-400 text-sm hover:text-cyan-300 transition-colors">
|
||||
Password dimenticata?
|
||||
</a>
|
||||
</#if>
|
||||
</div>
|
||||
|
||||
<!-- Bottone accedi -->
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full bg-cyan-500 hover:bg-cyan-400 active:bg-cyan-600 text-[#0f172a] font-semibold py-3 rounded-xl transition-colors text-sm"
|
||||
>
|
||||
Accedi
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Link registrazione -->
|
||||
<#if realm.registrationAllowed>
|
||||
<p class="text-center text-[#94a3b8] mt-6 text-sm">
|
||||
Non hai un account?
|
||||
<a href="${url.registrationUrl}"
|
||||
class="text-cyan-400 hover:text-cyan-300 transition-colors font-medium ml-1">
|
||||
Registrati
|
||||
</a>
|
||||
</p>
|
||||
</#if>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function togglePassword(inputId, btn) {
|
||||
const input = document.getElementById(inputId);
|
||||
const isHidden = input.type === 'password';
|
||||
input.type = isHidden ? 'text' : 'password';
|
||||
const svg = btn.querySelector('svg');
|
||||
if (isHidden) {
|
||||
svg.innerHTML = `
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7
|
||||
a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243
|
||||
M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29
|
||||
M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7
|
||||
a10.025 10.025 0 01-4.132 5.411m0 0L21 21"/>`;
|
||||
} else {
|
||||
svg.innerHTML = `
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7
|
||||
-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,264 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ScommettiApp - Registrati</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus {
|
||||
-webkit-text-fill-color: #f1f5f9;
|
||||
-webkit-box-shadow: 0 0 0px 1000px #0f172a inset;
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
}
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.field-error {
|
||||
border-color: #f87171 !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-[#0f172a] min-h-screen flex items-center justify-center p-4 py-10">
|
||||
<div class="w-full max-w-md fade-in">
|
||||
|
||||
<!-- Logo / Header -->
|
||||
<div class="text-center mb-8">
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 bg-cyan-500 rounded-2xl mb-4 shadow-lg shadow-cyan-500/20">
|
||||
<svg class="w-9 h-9 text-[#0f172a]" viewBox="0 0 24 24" fill="currentColor">
|
||||
<rect x="2" y="2" width="9" height="9" rx="2"/>
|
||||
<rect x="13" y="2" width="9" height="9" rx="2"/>
|
||||
<rect x="2" y="13" width="9" height="9" rx="2"/>
|
||||
<rect x="13" y="13" width="9" height="9" rx="2"/>
|
||||
<circle cx="6.5" cy="6.5" r="1.2" fill="#06b6d4"/>
|
||||
<circle cx="17.5" cy="6.5" r="1.2" fill="#06b6d4"/>
|
||||
<circle cx="6.5" cy="17.5" r="1.2" fill="#06b6d4"/>
|
||||
<circle cx="17.5" cy="17.5" r="1.2" fill="#06b6d4"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="text-2xl font-bold text-white tracking-tight">ScommettiApp</h1>
|
||||
<p class="text-[#94a3b8] text-sm mt-1">Crea il tuo account</p>
|
||||
</div>
|
||||
|
||||
<!-- Card -->
|
||||
<div class="bg-[#1e293b] border border-[#334155] rounded-2xl p-8 shadow-xl">
|
||||
<h2 class="text-lg font-semibold text-white mb-6">Registrazione</h2>
|
||||
|
||||
<!-- Messaggio globale -->
|
||||
<#if message?has_content>
|
||||
<div class="rounded-xl p-4 mb-6 text-sm
|
||||
<#if message.type == 'error'>bg-red-500/10 border border-red-500/30 text-red-400
|
||||
<#elseif message.type == 'success'>bg-green-500/10 border border-green-500/30 text-green-400
|
||||
<#elseif message.type == 'warning'>bg-yellow-500/10 border border-yellow-500/30 text-yellow-400
|
||||
<#else>bg-blue-500/10 border border-blue-500/30 text-blue-400</#if>">
|
||||
${message.summary?no_esc}
|
||||
</div>
|
||||
</#if>
|
||||
|
||||
<form action="${url.registrationAction}" method="post" novalidate>
|
||||
|
||||
<!-- Nome e Cognome affiancati -->
|
||||
<div class="grid grid-cols-2 gap-4 mb-5">
|
||||
<div>
|
||||
<label for="firstName" class="block text-[#cbd5e1] text-sm font-medium mb-2">
|
||||
Nome
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="firstName"
|
||||
name="firstName"
|
||||
value="${(register.firstName!'')}"
|
||||
class="w-full bg-[#0f172a] border rounded-xl px-4 py-3 text-[#f1f5f9] placeholder-[#475569] focus:outline-none focus:border-cyan-500 transition-colors text-sm
|
||||
<#if messagesPerField.existsError('firstName')>field-error border-red-400<#else>border-[#334155]</#if>"
|
||||
placeholder="Mario"
|
||||
autocomplete="given-name"
|
||||
/>
|
||||
<#if messagesPerField.existsError('firstName')>
|
||||
<p class="text-red-400 text-xs mt-1">${messagesPerField.get('firstName')?no_esc}</p>
|
||||
</#if>
|
||||
</div>
|
||||
<div>
|
||||
<label for="lastName" class="block text-[#cbd5e1] text-sm font-medium mb-2">
|
||||
Cognome
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="lastName"
|
||||
name="lastName"
|
||||
value="${(register.lastName!'')}"
|
||||
class="w-full bg-[#0f172a] border rounded-xl px-4 py-3 text-[#f1f5f9] placeholder-[#475569] focus:outline-none focus:border-cyan-500 transition-colors text-sm
|
||||
<#if messagesPerField.existsError('lastName')>field-error border-red-400<#else>border-[#334155]</#if>"
|
||||
placeholder="Rossi"
|
||||
autocomplete="family-name"
|
||||
/>
|
||||
<#if messagesPerField.existsError('lastName')>
|
||||
<p class="text-red-400 text-xs mt-1">${messagesPerField.get('lastName')?no_esc}</p>
|
||||
</#if>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="mb-5">
|
||||
<label for="email" class="block text-[#cbd5e1] text-sm font-medium mb-2">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
value="${(register.email!'')}"
|
||||
class="w-full bg-[#0f172a] border rounded-xl px-4 py-3 text-[#f1f5f9] placeholder-[#475569] focus:outline-none focus:border-cyan-500 transition-colors text-sm
|
||||
<#if messagesPerField.existsError('email')>field-error border-red-400<#else>border-[#334155]</#if>"
|
||||
placeholder="nome@esempio.it"
|
||||
autocomplete="email"
|
||||
/>
|
||||
<#if messagesPerField.existsError('email')>
|
||||
<p class="text-red-400 text-xs mt-1">${messagesPerField.get('email')?no_esc}</p>
|
||||
</#if>
|
||||
</div>
|
||||
|
||||
<!-- Username (solo se non usa email come username) -->
|
||||
<#if !realm.registrationEmailAsUsername>
|
||||
<div class="mb-5">
|
||||
<label for="username" class="block text-[#cbd5e1] text-sm font-medium mb-2">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
value="${(register.username!'')}"
|
||||
class="w-full bg-[#0f172a] border rounded-xl px-4 py-3 text-[#f1f5f9] placeholder-[#475569] focus:outline-none focus:border-cyan-500 transition-colors text-sm
|
||||
<#if messagesPerField.existsError('username')>field-error border-red-400<#else>border-[#334155]</#if>"
|
||||
placeholder="il tuo username"
|
||||
autocomplete="username"
|
||||
/>
|
||||
<#if messagesPerField.existsError('username')>
|
||||
<p class="text-red-400 text-xs mt-1">${messagesPerField.get('username')?no_esc}</p>
|
||||
</#if>
|
||||
</div>
|
||||
</#if>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="mb-5">
|
||||
<label for="password" class="block text-[#cbd5e1] text-sm font-medium mb-2">
|
||||
Password
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
class="w-full bg-[#0f172a] border rounded-xl px-4 py-3 pr-12 text-[#f1f5f9] placeholder-[#475569] focus:outline-none focus:border-cyan-500 transition-colors text-sm
|
||||
<#if messagesPerField.existsError('password')>field-error border-red-400<#else>border-[#334155]</#if>"
|
||||
placeholder="Almeno 8 caratteri"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onclick="togglePassword('password', this)"
|
||||
tabindex="-1"
|
||||
class="absolute right-3 top-1/2 -translate-y-1/2 text-[#475569] hover:text-[#94a3b8] transition-colors p-1"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7
|
||||
-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<#if messagesPerField.existsError('password')>
|
||||
<p class="text-red-400 text-xs mt-1">${messagesPerField.get('password')?no_esc}</p>
|
||||
</#if>
|
||||
</div>
|
||||
|
||||
<!-- Conferma password -->
|
||||
<div class="mb-7">
|
||||
<label for="password-confirm" class="block text-[#cbd5e1] text-sm font-medium mb-2">
|
||||
Conferma password
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="password"
|
||||
id="password-confirm"
|
||||
name="password-confirm"
|
||||
class="w-full bg-[#0f172a] border rounded-xl px-4 py-3 pr-12 text-[#f1f5f9] placeholder-[#475569] focus:outline-none focus:border-cyan-500 transition-colors text-sm
|
||||
<#if messagesPerField.existsError('password-confirm')>field-error border-red-400<#else>border-[#334155]</#if>"
|
||||
placeholder="Ripeti la password"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onclick="togglePassword('password-confirm', this)"
|
||||
tabindex="-1"
|
||||
class="absolute right-3 top-1/2 -translate-y-1/2 text-[#475569] hover:text-[#94a3b8] transition-colors p-1"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7
|
||||
-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<#if messagesPerField.existsError('password-confirm')>
|
||||
<p class="text-red-400 text-xs mt-1">${messagesPerField.get('password-confirm')?no_esc}</p>
|
||||
</#if>
|
||||
</div>
|
||||
|
||||
<!-- Bottone registrati -->
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full bg-cyan-500 hover:bg-cyan-400 active:bg-cyan-600 text-[#0f172a] font-semibold py-3 rounded-xl transition-colors text-sm"
|
||||
>
|
||||
Crea account
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Link accedi -->
|
||||
<p class="text-center text-[#94a3b8] mt-6 text-sm">
|
||||
Hai già un account?
|
||||
<a href="${url.loginUrl}"
|
||||
class="text-cyan-400 hover:text-cyan-300 transition-colors font-medium ml-1">
|
||||
Accedi
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function togglePassword(inputId, btn) {
|
||||
const input = document.getElementById(inputId);
|
||||
const isHidden = input.type === 'password';
|
||||
input.type = isHidden ? 'text' : 'password';
|
||||
const svg = btn.querySelector('svg');
|
||||
if (isHidden) {
|
||||
svg.innerHTML = `
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7
|
||||
a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243
|
||||
M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29
|
||||
M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7
|
||||
a10.025 10.025 0 01-4.132 5.411m0 0L21 21"/>`;
|
||||
} else {
|
||||
svg.innerHTML = `
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7
|
||||
-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,2 @@
|
||||
parent=keycloak
|
||||
import=common/keycloak
|
||||
Reference in New Issue
Block a user