Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
Rubén Ramírez
/
MangAffinity
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Settings
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
3c7dca7a
authored
Feb 21, 2025
by
Rubén Ramírez
Browse files
Options
_('Browse Files')
Download
Email Patches
Plain Diff
fix: [*]: Corregidos unos bugs detectados
parent
3c4635e0
Show whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
34 additions
and
57 deletions
src/main/java/com/ujaen/tfg/mangaffinity/excepciones/NombreUsuarioYaCogido.java
src/main/java/com/ujaen/tfg/mangaffinity/repositorios/RepositorioUsuario.java
src/main/java/com/ujaen/tfg/mangaffinity/rest/DTO/DTOLoginRespuesta.java
src/main/java/com/ujaen/tfg/mangaffinity/rest/DTO/DTOUsuario.java
src/main/java/com/ujaen/tfg/mangaffinity/seguridad/JwtUtil.java
src/main/java/com/ujaen/tfg/mangaffinity/seguridad/ServicioSeguridad.java
src/main/java/com/ujaen/tfg/mangaffinity/servicios/ServicioUsuarios.java
src/test/java/com/ujaen/tfg/mangaffinity/rest/TestUsuariosController.java
src/test/java/com/ujaen/tfg/mangaffinity/servicios/TestServicioUsuarios.java
src/main/java/com/ujaen/tfg/mangaffinity/excepciones/NombreUsuarioYaCogido.java
0 → 100644
View file @
3c7dca7a
package
com
.
ujaen
.
tfg
.
mangaffinity
.
excepciones
;
public
class
NombreUsuarioYaCogido
extends
RuntimeException
{
}
src/main/java/com/ujaen/tfg/mangaffinity/repositorios/RepositorioUsuario.java
View file @
3c7dca7a
package
com
.
ujaen
.
tfg
.
mangaffinity
.
repositorios
;
package
com
.
ujaen
.
tfg
.
mangaffinity
.
repositorios
;
import
com.ujaen.tfg.mangaffinity.entidades.Usuario
;
import
com.ujaen.tfg.mangaffinity.entidades.Usuario
;
import
com.ujaen.tfg.mangaffinity.excepciones.NombreUsuarioYaCogido
;
import
com.ujaen.tfg.mangaffinity.excepciones.UsuarioYaRegistrado
;
import
com.ujaen.tfg.mangaffinity.excepciones.UsuarioYaRegistrado
;
import
jakarta.persistence.EntityManager
;
import
jakarta.persistence.EntityManager
;
import
jakarta.persistence.PersistenceContext
;
import
jakarta.persistence.PersistenceContext
;
...
@@ -18,13 +19,26 @@ public class RepositorioUsuario {
...
@@ -18,13 +19,26 @@ public class RepositorioUsuario {
public
void
guardar
(
Usuario
usuario
)
{
public
void
guardar
(
Usuario
usuario
)
{
if
(!
em
.
createQuery
(
"select u from Usuario u where u.email = ?1"
,
Usuario
.
class
)
// Email no registrados
.
setParameter
(
1
,
usuario
.
getEmail
())
boolean
emailExistente
=
!
em
.
createQuery
(
"SELECT u FROM Usuario u WHERE u.email = :email"
,
Usuario
.
class
)
.
setParameter
(
"email"
,
usuario
.
getEmail
())
.
getResultList
()
.
getResultList
()
.
isEmpty
())
{
.
isEmpty
();
if
(
emailExistente
)
{
throw
new
UsuarioYaRegistrado
();
throw
new
UsuarioYaRegistrado
();
}
}
//Nombre de usuario no utilizado
boolean
nombreUsuarioExistente
=
!
em
.
createQuery
(
"SELECT u FROM Usuario u WHERE u.nombreUsuario = :nombreUsuario"
,
Usuario
.
class
)
.
setParameter
(
"nombreUsuario"
,
usuario
.
getNombreUsuario
())
.
getResultList
()
.
isEmpty
();
if
(
nombreUsuarioExistente
)
{
throw
new
NombreUsuarioYaCogido
();
}
em
.
persist
(
usuario
);
em
.
persist
(
usuario
);
}
}
...
...
src/main/java/com/ujaen/tfg/mangaffinity/rest/DTO/DTOLoginRespuesta.java
View file @
3c7dca7a
...
@@ -6,7 +6,6 @@ import lombok.Getter;
...
@@ -6,7 +6,6 @@ import lombok.Getter;
@Getter
@Getter
@AllArgsConstructor
@AllArgsConstructor
public
class
DTOLoginRespuesta
{
public
class
DTOLoginRespuesta
{
private
String
token
;
private
String
token
;
private
String
email
;
private
String
email
;
private
String
nombreUsuario
;
private
String
nombreUsuario
;
...
...
src/main/java/com/ujaen/tfg/mangaffinity/rest/DTO/DTOUsuario.java
View file @
3c7dca7a
...
@@ -13,9 +13,7 @@ import lombok.Setter;
...
@@ -13,9 +13,7 @@ import lombok.Setter;
@NoArgsConstructor
@NoArgsConstructor
@AllArgsConstructor
@AllArgsConstructor
public
class
DTOUsuario
{
public
class
DTOUsuario
{
private
Long
id
=
null
;
// Permito que sea nulo al crear
private
Long
id
=
null
;
// Permitir que sea nulo al crear
@Email
@Email
private
String
email
;
private
String
email
;
...
...
src/main/java/com/ujaen/tfg/mangaffinity/seguridad/JwtUtil.java
View file @
3c7dca7a
...
@@ -10,12 +10,12 @@ import javax.crypto.SecretKey;
...
@@ -10,12 +10,12 @@ import javax.crypto.SecretKey;
@Component
@Component
public
class
JwtUtil
{
public
class
JwtUtil
{
// Clave secreta para firmar y verificar el token (debe ser la misma en todo el proyecto)
private
static
final
SecretKey
SECRET_KEY
=
Keys
.
secretKeyFor
(
SignatureAlgorithm
.
HS256
);
private
static
final
SecretKey
SECRET_KEY
=
Keys
.
secretKeyFor
(
SignatureAlgorithm
.
HS256
);
private
static
final
long
EXPIRATION_TIME
=
86400000
;
// 1 día en milisegundos
private
static
final
long
EXPIRATION_TIME
=
86400000
;
// 1 día en milisegundos
// Genera un token JWT con los datos
proporcionados
// Genera un token JWT con los datos
public
String
generateToken
(
Map
<
String
,
Object
>
claims
,
String
subject
)
{
public
String
generateToken
(
Map
<
String
,
Object
>
claims
,
String
subject
)
{
return
Jwts
.
builder
()
return
Jwts
.
builder
()
.
setClaims
(
claims
)
.
setClaims
(
claims
)
...
@@ -26,7 +26,7 @@ public class JwtUtil {
...
@@ -26,7 +26,7 @@ public class JwtUtil {
.
compact
();
.
compact
();
}
}
// Decodifica el JWT
utilizando la misma clave secreta
// Decodifica el JWT
public
Claims
decodeJWT
(
String
token
)
{
public
Claims
decodeJWT
(
String
token
)
{
return
Jwts
.
parserBuilder
()
return
Jwts
.
parserBuilder
()
.
setSigningKey
(
SECRET_KEY
)
// Usamos la misma clave para decodificar
.
setSigningKey
(
SECRET_KEY
)
// Usamos la misma clave para decodificar
...
@@ -35,23 +35,22 @@ public class JwtUtil {
...
@@ -35,23 +35,22 @@ public class JwtUtil {
.
getBody
();
.
getBody
();
}
}
// Extrae el nombre de usuario
(subject) desde el token JWT
// Extrae el nombre de usuario
public
String
extractUsername
(
String
token
)
{
public
String
extractUsername
(
String
token
)
{
return
extractClaim
(
token
,
Claims:
:
getSubject
);
return
extractClaim
(
token
,
Claims:
:
getSubject
);
}
}
// Extrae la fecha de expiración
del token
// Extrae la fecha de expiración
public
Date
extractExpiration
(
String
token
)
{
public
Date
extractExpiration
(
String
token
)
{
return
extractClaim
(
token
,
Claims:
:
getExpiration
);
return
extractClaim
(
token
,
Claims:
:
getExpiration
);
}
}
// Extrae un claim específico
del token
// Extrae un claim específico
public
<
T
>
T
extractClaim
(
String
token
,
java
.
util
.
function
.
Function
<
Claims
,
T
>
claimsResolver
)
{
public
<
T
>
T
extractClaim
(
String
token
,
java
.
util
.
function
.
Function
<
Claims
,
T
>
claimsResolver
)
{
final
Claims
claims
=
extractAllClaims
(
token
);
final
Claims
claims
=
extractAllClaims
(
token
);
return
claimsResolver
.
apply
(
claims
);
return
claimsResolver
.
apply
(
claims
);
}
}
// Validar si un token es correcto y no ha expirado
public
boolean
validateToken
(
String
token
,
String
username
)
{
public
boolean
validateToken
(
String
token
,
String
username
)
{
final
String
extractedUsername
=
extractUsername
(
token
);
final
String
extractedUsername
=
extractUsername
(
token
);
return
(
extractedUsername
.
equals
(
username
)
&&
!
isTokenExpired
(
token
));
return
(
extractedUsername
.
equals
(
username
)
&&
!
isTokenExpired
(
token
));
...
...
src/main/java/com/ujaen/tfg/mangaffinity/seguridad/ServicioSeguridad.java
View file @
3c7dca7a
...
@@ -3,13 +3,12 @@ package com.ujaen.tfg.mangaffinity.seguridad;
...
@@ -3,13 +3,12 @@ package com.ujaen.tfg.mangaffinity.seguridad;
import
org.springframework.context.annotation.Bean
;
import
org.springframework.context.annotation.Bean
;
import
org.springframework.context.annotation.Configuration
;
import
org.springframework.context.annotation.Configuration
;
import
org.springframework.http.HttpMethod
;
import
org.springframework.http.HttpMethod
;
import
org.springframework.security.config.Customizer
;
import
org.springframework.security.config.annotation.web.builders.HttpSecurity
;
import
org.springframework.security.config.annotation.web.builders.HttpSecurity
;
import
org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
;
import
org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
;
import
org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
;
import
org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
;
import
org.springframework.security.crypto.password.PasswordEncoder
;
import
org.springframework.security.crypto.password.PasswordEncoder
;
import
org.springframework.security.web.SecurityFilterChain
;
import
org.springframework.security.web.SecurityFilterChain
;
import
org.springframework.security.web.access.expression.WebExpressionAuthorizationManager
;
@Configuration
@Configuration
@EnableWebSecurity
@EnableWebSecurity
...
...
src/main/java/com/ujaen/tfg/mangaffinity/servicios/ServicioUsuarios.java
View file @
3c7dca7a
...
@@ -18,15 +18,12 @@ import java.util.Optional;
...
@@ -18,15 +18,12 @@ import java.util.Optional;
@Service
@Service
public
class
ServicioUsuarios
{
public
class
ServicioUsuarios
{
@Autowired
@Autowired
RepositorioUsuario
repositorioUsuario
;
RepositorioUsuario
repositorioUsuario
;
@Autowired
@Autowired
private
PasswordEncoder
passwordEncoder
;
private
PasswordEncoder
passwordEncoder
;
@Autowired
@Autowired
private
JwtUtil
jwtUtil
;
private
JwtUtil
jwtUtil
;
...
@@ -38,7 +35,6 @@ public class ServicioUsuarios {
...
@@ -38,7 +35,6 @@ public class ServicioUsuarios {
this
.
admin
.
setNombreUsuario
(
"admin"
);
this
.
admin
.
setNombreUsuario
(
"admin"
);
}
}
/**
/**
* Función para crear un Socio en la estructura
* Función para crear un Socio en la estructura
* @param usuario usuario que se va a añadir
* @param usuario usuario que se va a añadir
...
@@ -55,36 +51,23 @@ public class ServicioUsuarios {
...
@@ -55,36 +51,23 @@ public class ServicioUsuarios {
* @return DTOLoginRespuesta con el token si es válido, o null si falla
* @return DTOLoginRespuesta con el token si es válido, o null si falla
*/
*/
public
DTOLoginRespuesta
autenticarUsuario
(
String
email
,
String
contrasenia
)
{
public
DTOLoginRespuesta
autenticarUsuario
(
String
email
,
String
contrasenia
)
{
System
.
out
.
println
(
"🔍 Intentando autenticar usuario: "
+
email
);
Optional
<
Usuario
>
usuario
=
repositorioUsuario
.
findByEmail
(
email
);
Optional
<
Usuario
>
usuario
=
repositorioUsuario
.
findByEmail
(
email
);
if
(
usuario
.
isEmpty
())
{
if
(
usuario
.
isEmpty
())
{
System
.
out
.
println
(
"❌ Usuario no encontrado: "
+
email
);
return
null
;
return
null
;
}
}
System
.
out
.
println
(
"✅ Usuario encontrado: "
+
usuario
.
get
().
getEmail
());
System
.
out
.
println
(
"Contraseña almacenada (encriptada): "
+
usuario
.
get
().
getContrasenia
());
System
.
out
.
println
(
"Contraseña ingresada: "
+
contrasenia
);
System
.
out
.
println
(
"¿Coincide? "
+
passwordEncoder
.
matches
(
contrasenia
,
usuario
.
get
().
getContrasenia
()));
if
(!
passwordEncoder
.
matches
(
contrasenia
,
usuario
.
get
().
getContrasenia
()))
{
if
(!
passwordEncoder
.
matches
(
contrasenia
,
usuario
.
get
().
getContrasenia
()))
{
System
.
out
.
println
(
"❌ Contraseña incorrecta para: "
+
email
);
return
null
;
return
null
;
}
}
String
rol
=
"USUARIO_REGISTRADO"
;
String
rol
=
"USUARIO_REGISTRADO"
;
if
(
email
.
equals
(
admin
.
getEmail
()))
{
if
(
email
.
equals
(
admin
.
getEmail
()))
{
if
(!
passwordEncoder
.
matches
(
contrasenia
,
passwordEncoder
.
encode
(
"adminpassword"
)))
{
if
(!
passwordEncoder
.
matches
(
contrasenia
,
passwordEncoder
.
encode
(
"adminpassword"
)))
{
System
.
out
.
println
(
"❌ Contraseña incorrecta para admin"
);
return
null
;
return
null
;
}
}
rol
=
"ADMIN"
;
rol
=
"ADMIN"
;
}
}
// Datos adicionales en el token (claims)
// Datos adicionales en el token (claims)
Map
<
String
,
Object
>
claims
=
new
HashMap
<>();
Map
<
String
,
Object
>
claims
=
new
HashMap
<>();
claims
.
put
(
"email"
,
usuario
.
get
().
getEmail
());
claims
.
put
(
"email"
,
usuario
.
get
().
getEmail
());
...
@@ -93,9 +76,6 @@ public class ServicioUsuarios {
...
@@ -93,9 +76,6 @@ public class ServicioUsuarios {
// Generamos el token JWT
// Generamos el token JWT
String
token
=
jwtUtil
.
generateToken
(
claims
,
usuario
.
get
().
getEmail
());
String
token
=
jwtUtil
.
generateToken
(
claims
,
usuario
.
get
().
getEmail
());
System
.
out
.
println
(
"✅ Usuario autenticado exitosamente. Token generado."
);
return
new
DTOLoginRespuesta
(
token
,
usuario
.
get
().
getEmail
(),
usuario
.
get
().
getContrasenia
());
return
new
DTOLoginRespuesta
(
token
,
usuario
.
get
().
getEmail
(),
usuario
.
get
().
getContrasenia
());
}
}
...
...
src/test/java/com/ujaen/tfg/mangaffinity/rest/TestUsuariosController.java
View file @
3c7dca7a
...
@@ -69,7 +69,7 @@ public class TestUsuariosController {
...
@@ -69,7 +69,7 @@ public class TestUsuariosController {
Assertions
.
assertThat
(
usuarioCreado
.
getId
()).
isNotNull
();
Assertions
.
assertThat
(
usuarioCreado
.
getId
()).
isNotNull
();
// Caso 3: Intentar registrar un usuario con el mismo email
// Caso 3: Intentar registrar un usuario con el mismo email
var
usuarioDuplicado
=
new
DTOUsuario
(
null
,
"
carlitos@gmail.com"
,
"Carlitos"
,
"password123
"
);
var
usuarioDuplicado
=
new
DTOUsuario
(
null
,
"
pedro@gmail.com"
,
"Pedro"
,
"pedrito
"
);
var
respuestaDuplicado
=
restTemplateUsuarios
.
postForEntity
(
var
respuestaDuplicado
=
restTemplateUsuarios
.
postForEntity
(
"/"
,
"/"
,
usuarioDuplicado
,
usuarioDuplicado
,
...
...
src/test/java/com/ujaen/tfg/mangaffinity/servicios/TestServicioUsuarios.java
View file @
3c7dca7a
...
@@ -7,26 +7,19 @@ import com.ujaen.tfg.mangaffinity.excepciones.UsuarioYaRegistrado;
...
@@ -7,26 +7,19 @@ import com.ujaen.tfg.mangaffinity.excepciones.UsuarioYaRegistrado;
import
com.ujaen.tfg.mangaffinity.rest.DTO.DTOLoginRespuesta
;
import
com.ujaen.tfg.mangaffinity.rest.DTO.DTOLoginRespuesta
;
import
com.ujaen.tfg.mangaffinity.seguridad.JwtUtil
;
import
com.ujaen.tfg.mangaffinity.seguridad.JwtUtil
;
import
io.jsonwebtoken.Claims
;
import
io.jsonwebtoken.Claims
;
import
io.jsonwebtoken.Jwts
;
import
io.jsonwebtoken.SignatureAlgorithm
;
import
io.jsonwebtoken.security.Keys
;
import
org.assertj.core.api.Assertions
;
import
org.junit.jupiter.api.Test
;
import
org.junit.jupiter.api.Test
;
import
org.slf4j.Logger
;
import
org.slf4j.Logger
;
import
org.slf4j.LoggerFactory
;
import
org.slf4j.LoggerFactory
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.boot.test.context.SpringBootTest
;
import
org.springframework.boot.test.context.SpringBootTest
;
import
org.springframework.security.crypto.password.PasswordEncoder
;
import
org.springframework.test.annotation.DirtiesContext
;
import
org.springframework.test.annotation.DirtiesContext
;
import
org.springframework.test.context.ActiveProfiles
;
import
org.springframework.test.context.ActiveProfiles
;
import
javax.crypto.SecretKey
;
import
static
org
.
assertj
.
core
.
api
.
Assertions
.
assertThatThrownBy
;
import
static
org
.
assertj
.
core
.
api
.
Assertions
.
assertThatThrownBy
;
import
static
org
.
assertj
.
core
.
api
.
AssertionsForClassTypes
.
assertThat
;
import
static
org
.
assertj
.
core
.
api
.
AssertionsForClassTypes
.
assertThat
;
@SpringBootTest
(
classes
=
{
MangAffinityApplication
.
class
,
JpaTestConfig
.
class
})
@SpringBootTest
(
classes
=
{
MangAffinityApplication
.
class
,
JpaTestConfig
.
class
})
@ActiveProfiles
(
"test"
)
@ActiveProfiles
(
"test"
)
@DirtiesContext
(
classMode
=
DirtiesContext
.
ClassMode
.
AFTER_EACH_TEST_METHOD
)
public
class
TestServicioUsuarios
{
public
class
TestServicioUsuarios
{
@Autowired
@Autowired
...
@@ -48,37 +41,29 @@ public class TestServicioUsuarios {
...
@@ -48,37 +41,29 @@ public class TestServicioUsuarios {
@Test
@Test
@DirtiesContext
@DirtiesContext
void
testAutenticarUsuario
()
{
void
testAutenticarUsuario
()
{
logger
.
info
(
"🔍 Iniciando test de autenticación"
);
// Caso 1: Usuario con email incorrecto
// Caso 1: Usuario con email incorrecto
String
emailInexistente
=
"nonexistent@example.com"
;
String
emailInexistente
=
"nonexistent@example.com"
;
String
contraseniaValida
=
"validpassword"
;
String
contraseniaValida
=
"validpassword"
;
assertThat
(
servicioUsuarios
.
autenticarUsuario
(
emailInexistente
,
contraseniaValida
)).
isNull
();
assertThat
(
servicioUsuarios
.
autenticarUsuario
(
emailInexistente
,
contraseniaValida
)).
isNull
();
logger
.
info
(
"✅ Caso 1 completado: Usuario inexistente no autenticado"
);
// Caso 2: Contraseña incorrecta
// Caso 2: Contraseña incorrecta
String
emailExistente
=
"test@example.com"
;
String
emailExistente
=
"test@example.com"
;
String
contraseniaIncorrecta
=
"wrongpassword"
;
String
contraseniaIncorrecta
=
"wrongpassword"
;
assertThat
(
servicioUsuarios
.
autenticarUsuario
(
emailExistente
,
contraseniaIncorrecta
)).
isNull
();
assertThat
(
servicioUsuarios
.
autenticarUsuario
(
emailExistente
,
contraseniaIncorrecta
)).
isNull
();
logger
.
info
(
"✅ Caso 2 completado: Contraseña incorrecta no autenticada"
);
// Caso 3: Usuario con email y contraseña correctos
// Caso 3: Usuario con email y contraseña correctos
var
usuario
1
=
new
Usuario
(
"pedro@gmail.com"
,
"Pedro
"
,
"pedrito"
);
// Se pasa en texto plano
var
usuario
2
=
new
Usuario
(
"pedra@gmail.com"
,
"Pedra
"
,
"pedrito"
);
// Se pasa en texto plano
servicioUsuarios
.
crearUsuario
(
usuario
1
);
servicioUsuarios
.
crearUsuario
(
usuario
2
);
Usuario
usuarioGuardado
=
servicioUsuarios
.
buscaUsuario
(
"pedro@gmail.com"
);
Usuario
usuarioGuardado
=
servicioUsuarios
.
buscaUsuario
(
"pedro@gmail.com"
);
assertThat
(
usuarioGuardado
).
isNotNull
();
assertThat
(
usuarioGuardado
).
isNotNull
();
logger
.
info
(
"✅ Usuario guardado en BD: {}"
,
usuarioGuardado
.
getEmail
());
logger
.
info
(
"Contraseña en BD (encriptada): {}"
,
usuarioGuardado
.
getContrasenia
());
DTOLoginRespuesta
respuestaValida
=
servicioUsuarios
.
autenticarUsuario
(
usuario
1
.
getEmail
(),
"pedrito"
);
DTOLoginRespuesta
respuestaValida
=
servicioUsuarios
.
autenticarUsuario
(
usuario
2
.
getEmail
(),
"pedrito"
);
assertThat
(
respuestaValida
).
isNotNull
();
assertThat
(
respuestaValida
).
isNotNull
();
assertThat
(
respuestaValida
.
getToken
()).
isNotNull
();
assertThat
(
respuestaValida
.
getToken
()).
isNotNull
();
logger
.
info
(
"✅ Caso 3 completado: Usuario autenticado correctamente"
);
// Comprobamos que el rol de usuario registrado esté incluido en el token
// Comprobamos que el rol de usuario registrado esté incluido en el token
Claims
claims
=
jwtUtil
.
decodeJWT
(
respuestaValida
.
getToken
());
// Usamos la misma clave secreta para decodificar
Claims
claims
=
jwtUtil
.
decodeJWT
(
respuestaValida
.
getToken
());
// Usamos la misma clave secreta para decodificar
assertThat
(
claims
.
get
(
"rol"
)).
isEqualTo
(
"USUARIO_REGISTRADO"
);
assertThat
(
claims
.
get
(
"rol"
)).
isEqualTo
(
"USUARIO_REGISTRADO"
);
logger
.
info
(
"✅ El rol de usuario registrado está presente en el token"
);
}
}
}
}
\ No newline at end of file
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment