Modifiche
This commit is contained in:
@@ -6,3 +6,5 @@
|
|||||||
/frontend-angular/.angular/
|
/frontend-angular/.angular/
|
||||||
/backend-node/node_modules/
|
/backend-node/node_modules/
|
||||||
/backend-node/.env
|
/backend-node/.env
|
||||||
|
/frontend-angular/node_modules/
|
||||||
|
/.claude/
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ COPY package*.json ./
|
|||||||
FROM base AS dependencies
|
FROM base AS dependencies
|
||||||
|
|
||||||
# Install ALL dependencies (including devDependencies for build)
|
# Install ALL dependencies (including devDependencies for build)
|
||||||
RUN npm ci
|
RUN npm install
|
||||||
|
|
||||||
# ========================================
|
# ========================================
|
||||||
# Stage 3: Development
|
# Stage 3: Development
|
||||||
@@ -50,7 +50,7 @@ CMD ["npm", "start"]
|
|||||||
FROM base AS production-dependencies
|
FROM base AS production-dependencies
|
||||||
|
|
||||||
# Install only production dependencies
|
# Install only production dependencies
|
||||||
RUN npm ci --only=production && \
|
RUN npm install --only=production && \
|
||||||
npm cache clean --force
|
npm cache clean --force
|
||||||
|
|
||||||
# ========================================
|
# ========================================
|
||||||
|
|||||||
@@ -5,6 +5,24 @@ class BetsController {
|
|||||||
this.betsService = betsService;
|
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) {
|
async getAllBets(req, res, next) {
|
||||||
try {
|
try {
|
||||||
const bets = await this.betsService.getAllBets();
|
const bets = await this.betsService.getAllBets();
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ class GroupsController {
|
|||||||
|
|
||||||
async getAllGroups(req, res, next) {
|
async getAllGroups(req, res, next) {
|
||||||
try {
|
try {
|
||||||
const groups = await this.groupsService.getAllGroups();
|
const groups = await this.groupsService.getAllGroups(req.userId);
|
||||||
return ResponseHandler.success(res, groups, 'Groups retrieved successfully');
|
return ResponseHandler.success(res, groups, 'Groups retrieved successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
@@ -52,10 +52,7 @@ class GroupsController {
|
|||||||
|
|
||||||
async addMembers(req, res, next) {
|
async addMembers(req, res, next) {
|
||||||
try {
|
try {
|
||||||
const group = await this.groupsService.addMembersToGroup(
|
const group = await this.groupsService.addMembersToGroup(req.params.id, req.body.userIds);
|
||||||
req.params.id,
|
|
||||||
req.body.userIds
|
|
||||||
);
|
|
||||||
return ResponseHandler.success(res, group, 'Members added successfully');
|
return ResponseHandler.success(res, group, 'Members added successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(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;
|
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) {
|
async updateCurrentUser(req, res, next) {
|
||||||
try {
|
try {
|
||||||
const user = await this.usersService.updateUserProfile(req.userId, req.body);
|
const user = await this.usersService.updateUserProfile(req.userId, req.body);
|
||||||
|
|||||||
@@ -18,13 +18,13 @@ function setupContainer() {
|
|||||||
const modelsDir = path.join(__dirname, '../models');
|
const modelsDir = path.join(__dirname, '../models');
|
||||||
fs.readdirSync(modelsDir)
|
fs.readdirSync(modelsDir)
|
||||||
.filter(
|
.filter(
|
||||||
(file) =>
|
file =>
|
||||||
file.indexOf('.') !== 0 &&
|
file.indexOf('.') !== 0 &&
|
||||||
file.slice(-3) === '.js' &&
|
file.slice(-3) === '.js' &&
|
||||||
file !== 'bet.model.js' &&
|
file !== 'bet.model.js' &&
|
||||||
file !== 'associations.js'
|
file !== 'associations.js'
|
||||||
)
|
)
|
||||||
.forEach((file) => {
|
.forEach(file => {
|
||||||
require(path.join(modelsDir, file))(sequelize);
|
require(path.join(modelsDir, file))(sequelize);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -38,27 +38,19 @@ function setupContainer() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
container.register({
|
container.register({
|
||||||
usersRepository: asClass(
|
usersRepository: asClass(require('../repositories/postgres/users.repository')).scoped(),
|
||||||
require('../repositories/postgres/users.repository')
|
groupsRepository: asClass(require('../repositories/postgres/groups.repository')).scoped(),
|
||||||
).scoped(),
|
|
||||||
groupsRepository: asClass(
|
|
||||||
require('../repositories/postgres/groups.repository')
|
|
||||||
).scoped(),
|
|
||||||
friendshipsRepository: asClass(
|
friendshipsRepository: asClass(
|
||||||
require('../repositories/postgres/friendships.repository')
|
require('../repositories/postgres/friendships.repository')
|
||||||
).scoped(),
|
).scoped(),
|
||||||
friendRequestsRepository: asClass(
|
friendRequestsRepository: asClass(
|
||||||
require('../repositories/postgres/friendRequests.repository')
|
require('../repositories/postgres/friendRequests.repository')
|
||||||
).scoped(),
|
).scoped(),
|
||||||
betsRepository: asClass(
|
betsRepository: asClass(require('../repositories/mongo/bets.repository')).scoped(),
|
||||||
require('../repositories/mongo/bets.repository')
|
|
||||||
).scoped(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
container.register({
|
container.register({
|
||||||
dataStitchingService: asClass(
|
dataStitchingService: asClass(require('../services/data-stitching.service')).scoped(),
|
||||||
require('../services/data-stitching.service')
|
|
||||||
).scoped(),
|
|
||||||
usersService: asClass(require('../services/users.service')).scoped(),
|
usersService: asClass(require('../services/users.service')).scoped(),
|
||||||
groupsService: asClass(require('../services/groups.service')).scoped(),
|
groupsService: asClass(require('../services/groups.service')).scoped(),
|
||||||
friendsService: asClass(require('../services/friends.service')).scoped(),
|
friendsService: asClass(require('../services/friends.service')).scoped(),
|
||||||
@@ -70,9 +62,6 @@ function setupContainer() {
|
|||||||
groupsController: asClass(require('../controllers/groups.controller')).scoped(),
|
groupsController: asClass(require('../controllers/groups.controller')).scoped(),
|
||||||
usersController: asClass(require('../controllers/users.controller')).scoped(),
|
usersController: asClass(require('../controllers/users.controller')).scoped(),
|
||||||
friendsController: asClass(require('../controllers/friends.controller')).scoped(),
|
friendsController: asClass(require('../controllers/friends.controller')).scoped(),
|
||||||
initialDataController: asClass(
|
|
||||||
require('../controllers/initialData.controller')
|
|
||||||
).scoped(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return container;
|
return container;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ DROP TABLE IF EXISTS users CASCADE;
|
|||||||
-- users table
|
-- users table
|
||||||
CREATE TABLE users (
|
CREATE TABLE users (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
|
"keycloakId" VARCHAR(255) UNIQUE,
|
||||||
name VARCHAR(255) NOT NULL,
|
name VARCHAR(255) NOT NULL,
|
||||||
"email" VARCHAR(255) NOT NULL UNIQUE,
|
"email" VARCHAR(255) NOT NULL UNIQUE,
|
||||||
"avatarUrl" VARCHAR(255) NOT NULL,
|
"avatarUrl" VARCHAR(255) NOT NULL,
|
||||||
@@ -63,6 +64,7 @@ CREATE TABLE friend_requests (
|
|||||||
|
|
||||||
-- User <-> Group many-to-many join table
|
-- User <-> Group many-to-many join table
|
||||||
CREATE TABLE "UserGroups" (
|
CREATE TABLE "UserGroups" (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
"userId" INTEGER NOT NULL,
|
"userId" INTEGER NOT NULL,
|
||||||
"groupId" INTEGER NOT NULL,
|
"groupId" INTEGER NOT NULL,
|
||||||
CONSTRAINT usergroups_user_fk FOREIGN KEY ("userId") REFERENCES users(id) ON DELETE CASCADE,
|
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;
|
const { User, Group, FriendRequest, Friendship } = sequelize.models;
|
||||||
|
|
||||||
// User <-> Group: Many-to-Many
|
// User <-> Group: Many-to-Many
|
||||||
// A User can be in many Groups, and a Group can have many Users (members).
|
// Il modello dichiara esplicitamente la colonna id per evitare che sync() la cerchi
|
||||||
const UserGroup = sequelize.define('UserGroup', {}, { timestamps: false });
|
// sulla tabella UserGroups senza trovarla (causando errori su RETURNING "id").
|
||||||
User.belongsToMany(Group, { through: UserGroup, foreignKey: 'userId' });
|
const UserGroup = sequelize.define(
|
||||||
Group.belongsToMany(User, { through: UserGroup, as: 'members', foreignKey: 'groupId' });
|
'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.
|
// Friendship is a direct link between two users.
|
||||||
// user1Id will always be less than user2Id to ensure uniqueness.
|
// user1Id will always be less than user2Id to ensure uniqueness.
|
||||||
@@ -14,8 +31,16 @@ module.exports = (sequelize) => {
|
|||||||
|
|
||||||
// User <-> FriendRequest: One-to-Many
|
// User <-> FriendRequest: One-to-Many
|
||||||
// A User can send many friend requests and receive many friend requests.
|
// A User can send many friend requests and receive many friend requests.
|
||||||
User.hasMany(FriendRequest, { as: 'sentRequests', foreignKey: 'fromUserId', onDelete: 'CASCADE' });
|
User.hasMany(FriendRequest, {
|
||||||
User.hasMany(FriendRequest, { as: 'receivedRequests', foreignKey: 'toUserId', onDelete: 'CASCADE' });
|
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: 'fromUser', foreignKey: 'fromUserId' });
|
||||||
FriendRequest.belongsTo(User, { as: 'toUser', foreignKey: 'toUserId' });
|
FriendRequest.belongsTo(User, { as: 'toUser', foreignKey: 'toUserId' });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,21 +1,34 @@
|
|||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require('sequelize');
|
||||||
|
|
||||||
module.exports = (sequelize) => {
|
module.exports = sequelize => {
|
||||||
const FriendRequest = sequelize.define('FriendRequest', {
|
const FriendRequest = sequelize.define(
|
||||||
|
'FriendRequest',
|
||||||
|
{
|
||||||
id: {
|
id: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
autoIncrement: true,
|
autoIncrement: true,
|
||||||
primaryKey: true
|
primaryKey: true,
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
defaultValue: 'PENDING',
|
defaultValue: 'PENDING',
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
}
|
},
|
||||||
// Foreign keys 'fromUserId' and 'toUserId' are added via associations
|
// Foreign keys 'fromUserId' and 'toUserId' are added via associations
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
// Uses default `createdAt` and `updatedAt` timestamps from Sequelize
|
// 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;
|
return FriendRequest;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,19 +1,30 @@
|
|||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require('sequelize');
|
||||||
|
|
||||||
module.exports = (sequelize) => {
|
module.exports = sequelize => {
|
||||||
const Group = sequelize.define('Group', {
|
const Group = sequelize.define(
|
||||||
|
'Group',
|
||||||
|
{
|
||||||
id: {
|
id: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
autoIncrement: true,
|
autoIncrement: true,
|
||||||
primaryKey: true
|
primaryKey: true,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
}
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
timestamps: 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;
|
return Group;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,35 +1,51 @@
|
|||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require('sequelize');
|
||||||
|
|
||||||
module.exports = (sequelize) => {
|
module.exports = sequelize => {
|
||||||
const User = sequelize.define('User', {
|
const User = sequelize.define(
|
||||||
|
'User',
|
||||||
|
{
|
||||||
id: {
|
id: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
autoIncrement: true,
|
autoIncrement: true,
|
||||||
primaryKey: true
|
primaryKey: true,
|
||||||
|
},
|
||||||
|
keycloakId: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
unique: true,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true,
|
unique: true,
|
||||||
validate: {
|
validate: {
|
||||||
isEmail: true
|
isEmail: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
avatarUrl: {
|
avatarUrl: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
score: {
|
score: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
defaultValue: 0
|
defaultValue: 0,
|
||||||
},
|
},
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
timestamps: false,
|
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;
|
return User;
|
||||||
};
|
};
|
||||||
|
|||||||
Generated
+205
@@ -13,6 +13,8 @@
|
|||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"joi": "^17.12.1",
|
"joi": "^17.12.1",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"jwks-rsa": "^3.1.0",
|
||||||
"mongoose": "^8.4.0",
|
"mongoose": "^8.4.0",
|
||||||
"pg": "^8.11.5",
|
"pg": "^8.11.5",
|
||||||
"pg-hstore": "^2.3.4",
|
"pg-hstore": "^2.3.4",
|
||||||
@@ -294,6 +296,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/ms": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
||||||
@@ -653,6 +665,12 @@
|
|||||||
"node": ">=16.20.1"
|
"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": {
|
"node_modules/bytes": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||||
@@ -1003,6 +1021,15 @@
|
|||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/ee-first": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
@@ -2640,6 +2667,15 @@
|
|||||||
"@sideway/pinpoint": "^2.0.0"
|
"@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": {
|
"node_modules/js-yaml": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||||
@@ -2687,6 +2723,94 @@
|
|||||||
"json5": "lib/cli.js"
|
"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": {
|
"node_modules/kareem": {
|
||||||
"version": "2.6.3",
|
"version": "2.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz",
|
||||||
@@ -2720,6 +2844,11 @@
|
|||||||
"node": ">= 0.8.0"
|
"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": {
|
"node_modules/locate-path": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||||
@@ -2742,6 +2871,48 @@
|
|||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
@@ -2749,6 +2920,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/lower-case": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
|
||||||
@@ -2758,6 +2935,28 @@
|
|||||||
"tslib": "^2.0.3"
|
"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": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@@ -4763,6 +4962,12 @@
|
|||||||
"node": ">=0.4"
|
"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": {
|
"node_modules/yocto-queue": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||||
|
|||||||
@@ -30,6 +30,8 @@
|
|||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"joi": "^17.12.1",
|
"joi": "^17.12.1",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"jwks-rsa": "^3.1.0",
|
||||||
"mongoose": "^8.4.0",
|
"mongoose": "^8.4.0",
|
||||||
"pg": "^8.11.5",
|
"pg": "^8.11.5",
|
||||||
"pg-hstore": "^2.3.4",
|
"pg-hstore": "^2.3.4",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const { NotFoundError, DatabaseError } = require('../../shared/errors');
|
|
||||||
const { Op } = require('sequelize');
|
const { Op } = require('sequelize');
|
||||||
|
const { NotFoundError, DatabaseError } = require('../../shared/errors');
|
||||||
|
|
||||||
class FriendRequestsRepository {
|
class FriendRequestsRepository {
|
||||||
constructor({ sequelize }) {
|
constructor({ sequelize }) {
|
||||||
@@ -30,7 +30,7 @@ class FriendRequestsRepository {
|
|||||||
try {
|
try {
|
||||||
return await this.FriendRequest.findAll({
|
return await this.FriendRequest.findAll({
|
||||||
where: {
|
where: {
|
||||||
toUserId: userId,
|
[Op.or]: [{ toUserId: userId }, { fromUserId: userId }],
|
||||||
status: 'PENDING',
|
status: 'PENDING',
|
||||||
},
|
},
|
||||||
include: [
|
include: [
|
||||||
@@ -39,10 +39,7 @@ class FriendRequestsRepository {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError(
|
throw new DatabaseError(`Failed to fetch pending requests for user ${userId}`, error);
|
||||||
`Failed to fetch pending requests for user ${userId}`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,11 +61,12 @@ class FriendRequestsRepository {
|
|||||||
|
|
||||||
async create(fromUserId, toUserId) {
|
async create(fromUserId, toUserId) {
|
||||||
try {
|
try {
|
||||||
return await this.FriendRequest.create({
|
const created = await this.FriendRequest.create({
|
||||||
fromUserId,
|
fromUserId,
|
||||||
toUserId,
|
toUserId,
|
||||||
status: 'PENDING',
|
status: 'PENDING',
|
||||||
});
|
});
|
||||||
|
return await this.findById(created.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError('Failed to create friend request', error);
|
throw new DatabaseError('Failed to create friend request', error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const { Op } = require('sequelize');
|
||||||
const { NotFoundError, DatabaseError } = require('../../shared/errors');
|
const { NotFoundError, DatabaseError } = require('../../shared/errors');
|
||||||
|
|
||||||
class GroupsRepository {
|
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) {
|
async findById(id, includeMembers = false) {
|
||||||
try {
|
try {
|
||||||
const options = {};
|
const options = {};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
|
||||||
// Il router è ora una factory function che riceve l'istanza di sequelize
|
// Il router è ora una factory function che riceve l'istanza di sequelize
|
||||||
module.exports = (sequelize) => {
|
module.exports = sequelize => {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// --- Middleware per simulare un utente loggato ---
|
// --- Middleware per simulare un utente loggato ---
|
||||||
@@ -21,7 +21,6 @@ module.exports = (sequelize) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// --- Monta i router da file separati ---
|
// --- Monta i router da file separati ---
|
||||||
router.use(require('./initial-data.routes')(sequelize));
|
|
||||||
router.use('/bets', require('./bets.routes')(sequelize));
|
router.use('/bets', require('./bets.routes')(sequelize));
|
||||||
router.use('/groups', require('./groups.routes')(sequelize));
|
router.use('/groups', require('./groups.routes')(sequelize));
|
||||||
router.use('/friend-requests', require('./friends.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 express = require('express');
|
||||||
const { validate } = require('../../middleware/validation');
|
const { validate } = require('../../middleware/validation');
|
||||||
const {
|
const { createBetSchema, updateBetSchema } = require('../../shared/validators/schemas/bet.schema');
|
||||||
createBetSchema,
|
|
||||||
updateBetSchema,
|
|
||||||
} = require('../../shared/validators/schemas/bet.schema');
|
|
||||||
|
|
||||||
module.exports = ({ betsController }) => {
|
module.exports = ({ betsController }) => {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const controller = {
|
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),
|
getAllBets: (req, res, next) => betsController.getAllBets(req, res, next),
|
||||||
getBetById: (req, res, next) => betsController.getBetById(req, res, next),
|
getBetById: (req, res, next) => betsController.getBetById(req, res, next),
|
||||||
createBet: (req, res, next) => betsController.createBet(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),
|
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('/', controller.getAllBets);
|
||||||
router.get('/:id', controller.getBetById);
|
router.get('/:id', controller.getBetById);
|
||||||
router.post('/', validate(createBetSchema), controller.createBet);
|
router.post('/', validate(createBetSchema), controller.createBet);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
|
||||||
module.exports = (container) => {
|
module.exports = container => {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const controllers = {
|
const controllers = {
|
||||||
@@ -8,14 +8,12 @@ module.exports = (container) => {
|
|||||||
betsController: container.resolve('betsController'),
|
betsController: container.resolve('betsController'),
|
||||||
usersController: container.resolve('usersController'),
|
usersController: container.resolve('usersController'),
|
||||||
friendsController: container.resolve('friendsController'),
|
friendsController: container.resolve('friendsController'),
|
||||||
initialDataController: container.resolve('initialDataController'),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
router.use('/groups', require('./groups.routes')(controllers));
|
router.use('/groups', require('./groups.routes')(controllers));
|
||||||
router.use('/bets', require('./bets.routes')(controllers));
|
router.use('/bets', require('./bets.routes')(controllers));
|
||||||
router.use('/users', require('./users.routes')(controllers));
|
router.use('/users', require('./users.routes')(controllers));
|
||||||
router.use('/friend-requests', require('./friends.routes')(controllers));
|
router.use('/friend-requests', require('./friends.routes')(controllers));
|
||||||
router.use('/initial-data', require('./initialData.routes')(controllers));
|
|
||||||
|
|
||||||
return router;
|
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 router = express.Router();
|
||||||
|
|
||||||
const controller = {
|
const controller = {
|
||||||
|
getCurrentUser: (req, res, next) => usersController.getCurrentUser(req, res, next),
|
||||||
updateCurrentUser: (req, res, next) => usersController.updateCurrentUser(req, res, next),
|
updateCurrentUser: (req, res, next) => usersController.updateCurrentUser(req, res, next),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
router.get('/current', controller.getCurrentUser);
|
||||||
router.put('/current', validate(updateUserSchema), controller.updateCurrentUser);
|
router.put('/current', validate(updateUserSchema), controller.updateCurrentUser);
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ const cors = require('cors');
|
|||||||
const config = require('./config');
|
const config = require('./config');
|
||||||
const setupContainer = require('./di/container');
|
const setupContainer = require('./di/container');
|
||||||
const errorHandler = require('./middleware/errorHandler');
|
const errorHandler = require('./middleware/errorHandler');
|
||||||
const mockAuthMiddleware = require('./middleware/auth.mock');
|
const keycloakAuthMiddleware = require('./middleware/auth.keycloak');
|
||||||
const { seedDatabase } = require('./seed');
|
|
||||||
|
|
||||||
const legacyApiRoutes = require('./routes/api');
|
const legacyApiRoutes = require('./routes/api');
|
||||||
|
|
||||||
@@ -21,20 +20,22 @@ const sequelize = container.resolve('sequelize');
|
|||||||
mongoose
|
mongoose
|
||||||
.connect(config.database.mongo.uri, config.database.mongo.options)
|
.connect(config.database.mongo.uri, config.database.mongo.options)
|
||||||
.then(() => console.log('Connected to MongoDB'))
|
.then(() => console.log('Connected to MongoDB'))
|
||||||
.catch((err) => {
|
.catch(err => {
|
||||||
console.error('MongoDB connection error:', err);
|
console.error('MongoDB connection error:', err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use('/api', mockAuthMiddleware(sequelize));
|
app.use('/api', keycloakAuthMiddleware(sequelize));
|
||||||
|
|
||||||
app.use('/api/legacy', legacyApiRoutes(sequelize));
|
app.use('/api/legacy', legacyApiRoutes(sequelize));
|
||||||
|
|
||||||
const v1Routes = require('./routes/v1');
|
const v1Routes = require('./routes/v1');
|
||||||
|
|
||||||
app.use('/api/v1', v1Routes(container));
|
app.use('/api/v1', v1Routes(container));
|
||||||
app.use('/api', v1Routes(container));
|
app.use('/api', v1Routes(container));
|
||||||
|
|
||||||
const healthRoutes = require('./routes/health.routes');
|
const healthRoutes = require('./routes/health.routes');
|
||||||
|
|
||||||
app.use('/', healthRoutes({ sequelize, mongoose }));
|
app.use('/', healthRoutes({ sequelize, mongoose }));
|
||||||
|
|
||||||
app.use(errorHandler);
|
app.use(errorHandler);
|
||||||
@@ -43,11 +44,10 @@ sequelize
|
|||||||
.authenticate()
|
.authenticate()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log('Connected to PostgreSQL');
|
console.log('Connected to PostgreSQL');
|
||||||
return sequelize.sync({ force: true });
|
return sequelize.sync({ alter: true });
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log('PostgreSQL models synchronized');
|
console.log('PostgreSQL models synchronized');
|
||||||
return seedDatabase(sequelize);
|
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
app.listen(config.port, () => {
|
app.listen(config.port, () => {
|
||||||
@@ -58,7 +58,7 @@ sequelize
|
|||||||
console.log(`Health check available at /health`);
|
console.log(`Health check available at /health`);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch(err => {
|
||||||
console.error('PostgreSQL connection or sync error:', err);
|
console.error('PostgreSQL connection or sync error:', err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
const { ValidationError, ConflictError } = require('../shared/errors');
|
|
||||||
const { Op } = require('sequelize');
|
const { Op } = require('sequelize');
|
||||||
|
const { ValidationError, ConflictError } = require('../shared/errors');
|
||||||
|
|
||||||
class FriendsService {
|
class FriendsService {
|
||||||
constructor({
|
constructor({ friendshipsRepository, friendRequestsRepository, usersRepository, sequelize }) {
|
||||||
friendshipsRepository,
|
|
||||||
friendRequestsRepository,
|
|
||||||
usersRepository,
|
|
||||||
sequelize,
|
|
||||||
}) {
|
|
||||||
this.friendshipsRepository = friendshipsRepository;
|
this.friendshipsRepository = friendshipsRepository;
|
||||||
this.friendRequestsRepository = friendRequestsRepository;
|
this.friendRequestsRepository = friendRequestsRepository;
|
||||||
this.usersRepository = usersRepository;
|
this.usersRepository = usersRepository;
|
||||||
@@ -33,9 +28,7 @@ class FriendsService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!toUser) {
|
if (!toUser) {
|
||||||
throw new ValidationError(
|
throw new ValidationError('User not found with provided name or email');
|
||||||
'User not found with provided name or email'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,18 +36,12 @@ class FriendsService {
|
|||||||
throw new ValidationError('Cannot send friend request to yourself');
|
throw new ValidationError('Cannot send friend request to yourself');
|
||||||
}
|
}
|
||||||
|
|
||||||
const friendshipExists = await this.friendshipsRepository.exists(
|
const friendshipExists = await this.friendshipsRepository.exists(fromUserId, toUser.id);
|
||||||
fromUserId,
|
|
||||||
toUser.id
|
|
||||||
);
|
|
||||||
if (friendshipExists) {
|
if (friendshipExists) {
|
||||||
throw new ConflictError('You are already friends with this user');
|
throw new ConflictError('You are already friends with this user');
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingRequest = await this.friendRequestsRepository.findExisting(
|
const existingRequest = await this.friendRequestsRepository.findExisting(fromUserId, toUser.id);
|
||||||
fromUserId,
|
|
||||||
toUser.id
|
|
||||||
);
|
|
||||||
if (existingRequest) {
|
if (existingRequest) {
|
||||||
throw new ConflictError('Friend request already exists');
|
throw new ConflictError('Friend request already exists');
|
||||||
}
|
}
|
||||||
@@ -73,6 +60,8 @@ class FriendsService {
|
|||||||
throw new ValidationError('This request has already been processed');
|
throw new ValidationError('This request has already been processed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newFriend = request.fromUser;
|
||||||
|
|
||||||
const t = await this.sequelize.transaction();
|
const t = await this.sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
await this.friendRequestsRepository.updateStatus(requestId, 'ACCEPTED');
|
await this.friendRequestsRepository.updateStatus(requestId, 'ACCEPTED');
|
||||||
@@ -80,7 +69,7 @@ class FriendsService {
|
|||||||
await this.friendRequestsRepository.delete(requestId);
|
await this.friendRequestsRepository.delete(requestId);
|
||||||
await t.commit();
|
await t.commit();
|
||||||
|
|
||||||
return { message: 'Friend request accepted successfully' };
|
return { message: 'Friend request accepted successfully', newFriend };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await t.rollback();
|
await t.rollback();
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -6,16 +6,8 @@ class GroupsService {
|
|||||||
this.usersRepository = usersRepository;
|
this.usersRepository = usersRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllGroups() {
|
async getAllGroups(userId) {
|
||||||
return await this.groupsRepository.findAll({
|
return await this.groupsRepository.findByUserId(userId);
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: this.usersRepository.User,
|
|
||||||
as: 'members',
|
|
||||||
through: { attributes: [] },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getGroupById(groupId) {
|
async getGroupById(groupId) {
|
||||||
|
|||||||
+57
-1
@@ -42,6 +42,57 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
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 Node.js
|
||||||
backend:
|
backend:
|
||||||
build:
|
build:
|
||||||
@@ -53,6 +104,7 @@ services:
|
|||||||
PORT: 3000
|
PORT: 3000
|
||||||
POSTGRES_URI: postgres://admin:admin@postgres:5432/amici_scommesse
|
POSTGRES_URI: postgres://admin:admin@postgres:5432/amici_scommesse
|
||||||
MONGO_URI: mongodb://mongodb:27017/amici-scommesse
|
MONGO_URI: mongodb://mongodb:27017/amici-scommesse
|
||||||
|
KEYCLOAK_JWKS_URI: http://keycloak:8080/realms/scommettiapp/protocol/openid-connect/certs
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
volumes:
|
volumes:
|
||||||
@@ -63,6 +115,8 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
mongodb:
|
mongodb:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
keycloak:
|
||||||
|
condition: service_healthy
|
||||||
networks:
|
networks:
|
||||||
- scommetti-network
|
- scommetti-network
|
||||||
command: npm start
|
command: npm start
|
||||||
@@ -84,9 +138,9 @@ services:
|
|||||||
- /app/.angular
|
- /app/.angular
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
|
- keycloak
|
||||||
networks:
|
networks:
|
||||||
- scommetti-network
|
- scommetti-network
|
||||||
command: npm run dev
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
@@ -94,6 +148,8 @@ volumes:
|
|||||||
driver: local
|
driver: local
|
||||||
mongodb_data:
|
mongodb_data:
|
||||||
driver: local
|
driver: local
|
||||||
|
keycloak_db_data:
|
||||||
|
driver: local
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
scommetti-network:
|
scommetti-network:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ WORKDIR /app
|
|||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN npm ci
|
RUN npm install
|
||||||
|
|
||||||
# Copy application code
|
# Copy application code
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
"serve": {
|
"serve": {
|
||||||
"builder": "@angular/build:dev-server",
|
"builder": "@angular/build:dev-server",
|
||||||
"options": {
|
"options": {
|
||||||
"port": 3000
|
"port": 4200
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
|
|||||||
@@ -1,15 +1,36 @@
|
|||||||
import '@angular/compiler';
|
import '@angular/compiler';
|
||||||
import { bootstrapApplication } from '@angular/platform-browser';
|
import { bootstrapApplication } from '@angular/platform-browser';
|
||||||
import { provideZonelessChangeDetection } from '@angular/core';
|
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 { AppComponent } from './src/app.component';
|
||||||
|
import { KEYCLOAK_INSTANCE } from './src/core/tokens/keycloak.token';
|
||||||
|
import { authInterceptor } from './src/core/interceptors/auth.interceptor';
|
||||||
|
|
||||||
|
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, {
|
bootstrapApplication(AppComponent, {
|
||||||
providers: [
|
providers: [
|
||||||
provideZonelessChangeDetection(),
|
provideZonelessChangeDetection(),
|
||||||
provideHttpClient(withFetch()),
|
provideHttpClient(withFetch(), withInterceptors([authInterceptor])),
|
||||||
|
{ provide: KEYCLOAK_INSTANCE, useValue: keycloak },
|
||||||
],
|
],
|
||||||
}).catch(err => console.error(err));
|
}).catch(err => console.error(err));
|
||||||
|
}).catch(err => {
|
||||||
// AI Studio always uses an `index.tsx` file for all project types.
|
console.error('Keycloak initialization failed:', err);
|
||||||
|
});
|
||||||
|
|||||||
Generated
+26
@@ -15,6 +15,7 @@
|
|||||||
"@angular/compiler-cli": "^20.3.0",
|
"@angular/compiler-cli": "^20.3.0",
|
||||||
"@angular/core": "^20.3.0",
|
"@angular/core": "^20.3.0",
|
||||||
"@angular/platform-browser": "^20.3.0",
|
"@angular/platform-browser": "^20.3.0",
|
||||||
|
"keycloak-js": "^24.0.0",
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"tailwindcss": "latest"
|
"tailwindcss": "latest"
|
||||||
},
|
},
|
||||||
@@ -4931,6 +4932,12 @@
|
|||||||
"@pkgjs/parseargs": "^0.11.0"
|
"@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": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -4991,6 +4998,25 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/listr2": {
|
||||||
"version": "9.0.1",
|
"version": "9.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.1.tgz",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"@angular/build": "^20.3.0",
|
"@angular/build": "^20.3.0",
|
||||||
"@angular/cli": "^20.3.0",
|
"@angular/cli": "^20.3.0",
|
||||||
"@angular/compiler-cli": "^20.3.0",
|
"@angular/compiler-cli": "^20.3.0",
|
||||||
|
"keycloak-js": "^24.0.0",
|
||||||
"tailwindcss": "latest"
|
"tailwindcss": "latest"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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 { CommonModule } from '@angular/common';
|
||||||
import { HomeComponent } from './features/home/home.component';
|
import { HomeComponent } from './features/home/home.component';
|
||||||
import { DiscoverComponent } from './features/discover/discover.component';
|
import { DiscoverComponent } from './features/discover/discover.component';
|
||||||
import { GroupsComponent } from './features/groups/groups.component';
|
import { GroupsComponent } from './features/groups/groups.component';
|
||||||
import { ProfileComponent } from './features/profile/profile.component';
|
import { ProfileComponent } from './features/profile/profile.component';
|
||||||
import { CreateBetComponent } from './features/bets/create-bet.component';
|
import { CreateBetComponent } from './features/bets/create-bet.component';
|
||||||
|
import { DataService } from './core/services/data.service';
|
||||||
|
|
||||||
type View = 'home' | 'discover' | 'groups' | 'profile' | 'create-bet';
|
type View = 'home' | 'discover' | 'groups' | 'profile' | 'create-bet';
|
||||||
|
|
||||||
@@ -23,9 +24,15 @@ type View = 'home' | 'discover' | 'groups' | 'profile' | 'create-bet';
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
|
private dataService = inject(DataService);
|
||||||
|
|
||||||
currentView = signal<View>('home');
|
currentView = signal<View>('home');
|
||||||
private previousView = signal<View>('home');
|
private previousView = signal<View>('home');
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.dataService.loadCurrentUser();
|
||||||
|
}
|
||||||
|
|
||||||
setView(view: View) {
|
setView(view: View) {
|
||||||
if (view === 'create-bet') {
|
if (view === 'create-bet') {
|
||||||
this.previousView.set(this.currentView());
|
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> {
|
export interface SuccessResponse<T = any> {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
message?: string;
|
message?: string;
|
||||||
@@ -21,16 +16,3 @@ export interface ErrorResponse {
|
|||||||
errors?: ErrorDetail[];
|
errors?: ErrorDetail[];
|
||||||
timestamp: string;
|
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 http = inject(HttpClient);
|
||||||
private readonly apiUrl = 'http://localhost:3000/api/v1/bets';
|
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[]> {
|
getBets(): Observable<Bet[]> {
|
||||||
return this.http.get<SuccessResponse<Bet[]>>(this.apiUrl).pipe(
|
return this.http.get<SuccessResponse<Bet[]>>(this.apiUrl).pipe(
|
||||||
map(res => res.data.map(mapBet))
|
map(res => res.data.map(mapBet))
|
||||||
|
|||||||
@@ -13,6 +13,18 @@ export class FriendRequestApiService {
|
|||||||
private http = inject(HttpClient);
|
private http = inject(HttpClient);
|
||||||
private readonly apiUrl = 'http://localhost:3000/api/v1/friend-requests';
|
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> {
|
sendRequest(request: SendFriendRequestRequest): Observable<FriendRequest> {
|
||||||
return this.http.post<SuccessResponse<FriendRequest>>(this.apiUrl, request).pipe(
|
return this.http.post<SuccessResponse<FriendRequest>>(this.apiUrl, request).pipe(
|
||||||
map(res => res.data)
|
map(res => res.data)
|
||||||
|
|||||||
@@ -2,24 +2,8 @@ import { inject, Injectable } from '@angular/core';
|
|||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { User, UpdateUserRequest } from '../../models/user.model';
|
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 { 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({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@@ -28,9 +12,9 @@ export class UserApiService {
|
|||||||
private http = inject(HttpClient);
|
private http = inject(HttpClient);
|
||||||
private readonly apiUrl = 'http://localhost:3000/api/v1';
|
private readonly apiUrl = 'http://localhost:3000/api/v1';
|
||||||
|
|
||||||
getInitialData(): Observable<InitialData> {
|
getCurrentUser(): Observable<User> {
|
||||||
return this.http.get<SuccessResponse<InitialData>>(`${this.apiUrl}/initial-data`).pipe(
|
return this.http.get<SuccessResponse<User>>(`${this.apiUrl}/users/current`).pipe(
|
||||||
map(res => mapInitialData(res.data))
|
map(res => res.data)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +23,4 @@ export class UserApiService {
|
|||||||
map(res => res.data)
|
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 userApiService = inject(UserApiService);
|
||||||
private friendRequestApiService = inject(FriendRequestApiService);
|
private friendRequestApiService = inject(FriendRequestApiService);
|
||||||
|
|
||||||
private readonly _users = signal<User[]>([]);
|
|
||||||
private readonly _bets = signal<Bet[]>([]);
|
private readonly _bets = signal<Bet[]>([]);
|
||||||
private readonly _groups = signal<Group[]>([]);
|
private readonly _groups = signal<Group[]>([]);
|
||||||
private readonly _friends = signal<User[]>([]);
|
private readonly _friends = signal<User[]>([]);
|
||||||
@@ -47,25 +46,63 @@ export class DataService {
|
|||||||
|
|
||||||
readonly getPublicBets = computed(() => this._publicBets());
|
readonly getPublicBets = computed(() => this._publicBets());
|
||||||
|
|
||||||
constructor() {
|
// --- Load methods: chiamati da ogni componente al momento della visualizzazione ---
|
||||||
this._initializeData();
|
|
||||||
|
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() {
|
async loadUserBets(): Promise<void> {
|
||||||
this.userApiService.getInitialData().subscribe({
|
try {
|
||||||
next: data => {
|
const bets = await firstValueFrom(this.betApiService.getUserBets());
|
||||||
this.currentUser.set(data.currentUser);
|
this._bets.set(bets);
|
||||||
this._users.set(data.users);
|
} catch (error) {
|
||||||
this._friends.set(data.friends);
|
console.error('Failed to load user bets', error);
|
||||||
// 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 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 {
|
getGroupById(id: string): Group | undefined {
|
||||||
return this._groups().find(g => g.id === id);
|
return this._groups().find(g => g.id === id);
|
||||||
@@ -75,6 +112,8 @@ export class DataService {
|
|||||||
return this._bets().filter(b => b.groupId === groupId);
|
return this._bets().filter(b => b.groupId === groupId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Mutations ---
|
||||||
|
|
||||||
async sendFriendRequest(identifier: string): Promise<{ success: boolean, message: string }> {
|
async sendFriendRequest(identifier: string): Promise<{ success: boolean, message: string }> {
|
||||||
try {
|
try {
|
||||||
const newRequest = await firstValueFrom(this.friendRequestApiService.sendRequest({ identifier }));
|
const newRequest = await firstValueFrom(this.friendRequestApiService.sendRequest({ identifier }));
|
||||||
@@ -90,7 +129,7 @@ export class DataService {
|
|||||||
await firstValueFrom(this.friendRequestApiService.cancelRequest(requestId));
|
await firstValueFrom(this.friendRequestApiService.cancelRequest(requestId));
|
||||||
this._friendRequests.update(reqs => reqs.filter(r => r.id !== requestId));
|
this._friendRequests.update(reqs => reqs.filter(r => r.id !== requestId));
|
||||||
} catch (error) {
|
} 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._friends.update(f => [...f, newFriend]);
|
||||||
this._friendRequests.update(reqs => reqs.filter(r => r.id !== request.id));
|
this._friendRequests.update(reqs => reqs.filter(r => r.id !== request.id));
|
||||||
} catch (error) {
|
} 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));
|
await firstValueFrom(this.friendRequestApiService.declineRequest(requestId));
|
||||||
this._friendRequests.update(reqs => reqs.filter(r => r.id !== requestId));
|
this._friendRequests.update(reqs => reqs.filter(r => r.id !== requestId));
|
||||||
} catch (error) {
|
} 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 }));
|
const newGroup = await firstValueFrom(this.groupApiService.createGroup({ name, members: memberIds }));
|
||||||
this._groups.update(groups => [...groups, newGroup]);
|
this._groups.update(groups => [...groups, newGroup]);
|
||||||
} catch (error) {
|
} 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));
|
const updatedGroup = await firstValueFrom(this.groupApiService.removeMember(groupId, userId));
|
||||||
this._groups.update(groups => groups.map(g => g.id === groupId ? updatedGroup : g));
|
this._groups.update(groups => groups.map(g => g.id === groupId ? updatedGroup : g));
|
||||||
} catch (error) {
|
} 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 }));
|
const updatedGroup = await firstValueFrom(this.groupApiService.addMembers(groupId, { userIds }));
|
||||||
this._groups.update(groups => groups.map(g => g.id === groupId ? updatedGroup : g));
|
this._groups.update(groups => groups.map(g => g.id === groupId ? updatedGroup : g));
|
||||||
} catch (error) {
|
} 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 {
|
try {
|
||||||
const updatedUser = await firstValueFrom(this.userApiService.updateCurrentUser(data));
|
const updatedUser = await firstValueFrom(this.userApiService.updateCurrentUser(data));
|
||||||
this.currentUser.set(updatedUser);
|
this.currentUser.set(updatedUser);
|
||||||
this._users.update(users => users.map(u => u.id === updatedUser.id ? updatedUser : u));
|
|
||||||
} catch (error) {
|
} 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]);
|
this._bets.update(bets => [newBet, ...bets]);
|
||||||
} catch (error) {
|
} 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 { CommonModule } from '@angular/common';
|
||||||
import { DataService } from '../../core/services/data.service';
|
import { DataService } from '../../core/services/data.service';
|
||||||
import { BetParticipant, BetRole } from '../../core/models/bet.model';
|
import { BetParticipant, BetRole } from '../../core/models/bet.model';
|
||||||
import { Group } from '../../core/models/group.model';
|
|
||||||
import { User } from '../../core/models/user.model';
|
import { User } from '../../core/models/user.model';
|
||||||
import { getUserId } from '../../core/utils/type-guards';
|
import { getUserId } from '../../core/utils/type-guards';
|
||||||
|
|
||||||
@@ -51,6 +50,9 @@ export class CreateBetComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
this.dataService.loadFriends();
|
||||||
|
this.dataService.loadGroups();
|
||||||
|
|
||||||
const user = this.currentUser();
|
const user = this.currentUser();
|
||||||
if (user) {
|
if (user) {
|
||||||
this.participants.set([{ user, role: BetRole.Participant }]);
|
this.participants.set([{ user, role: BetRole.Participant }]);
|
||||||
@@ -59,13 +61,13 @@ export class CreateBetComponent implements OnInit {
|
|||||||
|
|
||||||
addParticipant(friend: User) {
|
addParticipant(friend: User) {
|
||||||
this.participants.update(p => [...p, { user: friend, role: BetRole.Participant }]);
|
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);
|
this.showFriendSelector.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeParticipant(userId: string) {
|
removeParticipant(userId: string) {
|
||||||
this.participants.update(p => p.filter(participant => getUserId(participant.user) !== userId));
|
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) {
|
updateParticipantRole(userId: string, newRole: BetRole) {
|
||||||
@@ -94,7 +96,6 @@ export class CreateBetComponent implements OnInit {
|
|||||||
this.participants.set(groupParticipants);
|
this.participants.set(groupParticipants);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Reset to just the current user if "Nessun gruppo" is selected
|
|
||||||
const user = this.currentUser();
|
const user = this.currentUser();
|
||||||
if (user) {
|
if (user) {
|
||||||
this.participants.set([{ user, role: BetRole.Participant }]);
|
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 { CommonModule } from '@angular/common';
|
||||||
import { DataService } from '../../core/services/data.service';
|
import { DataService } from '../../core/services/data.service';
|
||||||
import { BetCardComponent } from '../../shared/components/bet-card/bet-card.component';
|
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',
|
templateUrl: './discover.component.html',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class DiscoverComponent {
|
export class DiscoverComponent implements OnInit {
|
||||||
private dataService = inject(DataService);
|
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 { CommonModule } from '@angular/common';
|
||||||
import { DataService } from '../../../core/services/data.service';
|
import { DataService } from '@/src/core/services/data.service';
|
||||||
import { Bet, BetStatus } from '../../../core/models/bet.model';
|
import { Bet, BetStatus } from '@/src/core/models/bet.model';
|
||||||
import { Group } from '../../../core/models/group.model';
|
import { BetCardComponent } from '@/src/shared/components/bet-card/bet-card.component';
|
||||||
import { User } from '../../../core/models/user.model';
|
|
||||||
import { BetCardComponent } from '../../../shared/components/bet-card/bet-card.component';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-group-detail',
|
selector: 'app-group-detail',
|
||||||
@@ -22,30 +20,21 @@ export class GroupDetailComponent {
|
|||||||
private allGroups = this.dataService.userGroups;
|
private allGroups = this.dataService.userGroups;
|
||||||
group = computed(() => this.allGroups().find(g => g.id === this.groupId()) ?? null);
|
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);
|
isAddingMembers = signal(false);
|
||||||
selectedFriendIds = signal<{ [key: string]: boolean }>({});
|
selectedFriendIds = signal<{ [key: string]: boolean }>({});
|
||||||
|
|
||||||
constructor() {
|
|
||||||
effect(() => {
|
|
||||||
const id = this.groupId();
|
|
||||||
if (id) {
|
|
||||||
this.groupBets.set(this.dataService.getBetsByGroupId(id));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
sortedMembers = computed(() => {
|
sortedMembers = computed(() => {
|
||||||
return this.group()?.members.slice().sort((a, b) => b.score - a.score) ?? [];
|
return this.group()?.members.slice().sort((a, b) => b.score - a.score) ?? [];
|
||||||
});
|
});
|
||||||
|
|
||||||
activeBets = computed(() => {
|
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(() => {
|
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(() => {
|
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 { CommonModule } from '@angular/common';
|
||||||
import { DataService } from '../../core/services/data.service';
|
import { DataService } from '../../core/services/data.service';
|
||||||
import { User } from '../../core/models/user.model';
|
|
||||||
import { GroupDetailComponent } from './group-detail/group-detail.component';
|
import { GroupDetailComponent } from './group-detail/group-detail.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -11,7 +10,7 @@ import { GroupDetailComponent } from './group-detail/group-detail.component';
|
|||||||
templateUrl: './groups.component.html',
|
templateUrl: './groups.component.html',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class GroupsComponent {
|
export class GroupsComponent implements OnInit {
|
||||||
private dataService = inject(DataService);
|
private dataService = inject(DataService);
|
||||||
|
|
||||||
groups = this.dataService.userGroups;
|
groups = this.dataService.userGroups;
|
||||||
@@ -27,6 +26,12 @@ export class GroupsComponent {
|
|||||||
return this.newGroupName().trim().length > 0 && Object.keys(this.selectedFriendIds()).length > 0;
|
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) {
|
selectGroup(groupId: string) {
|
||||||
this.selectedGroupId.set(groupId);
|
this.selectedGroupId.set(groupId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Component, ChangeDetectionStrategy, inject, signal, OnInit } from '@angular/core';
|
import { Component, ChangeDetectionStrategy, inject, signal, OnInit } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { DataService } from '../../core/services/data.service';
|
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 { BetsByTimeComponent } from './bets-by-time/bets-by-time.component';
|
||||||
import { BetsByGroupComponent } from './bets-by-group/bets-by-group.component';
|
import { BetsByGroupComponent } from './bets-by-group/bets-by-group.component';
|
||||||
|
|
||||||
@@ -16,12 +15,10 @@ export class HomeComponent implements OnInit {
|
|||||||
private dataService = inject(DataService);
|
private dataService = inject(DataService);
|
||||||
|
|
||||||
viewMode = signal<'time' | 'group'>('time');
|
viewMode = signal<'time' | 'group'>('time');
|
||||||
allBets = signal<Bet[]>([]);
|
readonly allBets = this.dataService.getActiveBets;
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
// FIX: getActiveBets is a computed signal which returns a value when called once.
|
this.dataService.loadUserBets();
|
||||||
// The extra parentheses caused an error because it tried to execute an array.
|
|
||||||
this.allBets.set(this.dataService.getActiveBets());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setViewMode(mode: 'time' | 'group') {
|
setViewMode(mode: 'time' | 'group') {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<!-- Header with Logout -->
|
<!-- Header with Logout -->
|
||||||
<header class="flex justify-between items-center mb-4">
|
<header class="flex justify-between items-center mb-4">
|
||||||
<h1 class="text-3xl font-bold">Profilo</h1>
|
<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">
|
<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" />
|
<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>
|
</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 { CommonModule } from '@angular/common';
|
||||||
import { DataService } from '../../core/services/data.service';
|
import { DataService } from '../../core/services/data.service';
|
||||||
|
import { AuthService } from '../../core/services/auth.service';
|
||||||
import { EditProfileComponent } from './edit-profile/edit-profile.component';
|
import { EditProfileComponent } from './edit-profile/edit-profile.component';
|
||||||
import { FriendRequest } from '../../core/models/friend-request.model';
|
import { FriendRequest } from '../../core/models/friend-request.model';
|
||||||
|
|
||||||
@@ -11,8 +12,9 @@ import { FriendRequest } from '../../core/models/friend-request.model';
|
|||||||
templateUrl: './profile.component.html',
|
templateUrl: './profile.component.html',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class ProfileComponent {
|
export class ProfileComponent implements OnInit {
|
||||||
private dataService = inject(DataService);
|
private dataService = inject(DataService);
|
||||||
|
private authService = inject(AuthService);
|
||||||
|
|
||||||
currentUser = this.dataService.currentUser;
|
currentUser = this.dataService.currentUser;
|
||||||
friends = this.dataService.friends;
|
friends = this.dataService.friends;
|
||||||
@@ -26,6 +28,12 @@ export class ProfileComponent {
|
|||||||
addFriendError = signal<string | null>(null);
|
addFriendError = signal<string | null>(null);
|
||||||
addFriendSuccess = signal<string | null>(null);
|
addFriendSuccess = signal<string | null>(null);
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.dataService.loadCurrentUser();
|
||||||
|
this.dataService.loadFriends();
|
||||||
|
this.dataService.loadFriendRequests();
|
||||||
|
}
|
||||||
|
|
||||||
toggleEditMode() {
|
toggleEditMode() {
|
||||||
this.isEditing.update(val => !val);
|
this.isEditing.update(val => !val);
|
||||||
}
|
}
|
||||||
@@ -74,4 +82,8 @@ export class ProfileComponent {
|
|||||||
declineRequest(requestId: string) {
|
declineRequest(requestId: string) {
|
||||||
this.dataService.declineFriendRequest(requestId);
|
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