Compare commits

...

10 Commits

Author SHA1 Message Date
311d1fbd3b Merge pull request '0002350-endpoint-movimenti' (#2) from 0002350-endpoint-movimenti into dev
Reviewed-on: #2
2025-03-05 08:50:08 +00:00
dc29e2c6b1 /movimenti
implementazione endpoint per il recupero dei movimenti legati ad una tessera
2025-02-21 17:37:59 +01:00
e0d64f91d1 Merge pull request '0002343-endpoint-tessere' (#1) from 0002343-endpoint-tessere into dev
Reviewed-on: #1
2025-02-21 16:36:09 +00:00
af2b7cea6d modifica tipi ID
da integer a long
2025-02-21 17:32:39 +01:00
488cab4ac7 Merge remote-tracking branch 'upstream/dev' into 0002343-endpoint-tessere 2025-02-18 16:07:53 +01:00
cd597a52c2 implementazione metodo di verifica JWT
secret in file di configurazione application.config
2025-02-18 16:06:15 +01:00
8e417f02f2 /tessere
uso del metodo authenticate per la verifica della validità del JWT
2025-02-18 16:02:41 +01:00
728eca6dd6 Merge remote-tracking branch 'upstream/dev' into 0002343-endpoint-tessere 2025-02-18 12:55:00 +01:00
b8ee25b762 modifiche uso JWT
utilizzo dipendenze come da documentazione Ktor per JWT
recupero secret da file di configurazione (e non da database)
2025-02-18 12:50:02 +01:00
e3fc1f73b7 prima implementazione
mapping e repository
2025-02-18 09:22:59 +01:00
16 changed files with 359 additions and 71 deletions

View File

@ -35,9 +35,8 @@ dependencies {
implementation("org.jetbrains.exposed:exposed-dao:$exposed_version")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1")
implementation ("org.jetbrains.exposed:exposed-java-time:$exposed_version")
implementation("io.jsonwebtoken:jjwt-api:0.11.5")
implementation("io.jsonwebtoken:jjwt-impl:0.11.5")
implementation("io.jsonwebtoken:jjwt-jackson:0.11.5")
implementation("io.ktor:ktor-server-auth:$ktor_version")
implementation("io.ktor:ktor-server-auth-jwt:$ktor_version")
testImplementation("io.ktor:ktor-server-test-host-jvm")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
}

View File

@ -1,5 +1,5 @@
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0"
}
rootProject.name = "Backend_API"
rootProject.name = "Backend_API_DSU"

View File

@ -1,7 +1,6 @@
package eu.maiora
import eu.maiora.plugins.configureDatabases
//import eu.maiora.model.LogScriptRepositoryImpl
import eu.maiora.plugins.*
import io.ktor.http.*
import io.ktor.server.application.*
@ -18,14 +17,15 @@ fun main() {
fun Application.module() {
val config = ApplicationConfig("application.conf")
val dbUrl = config.property("ktor.database.url").getString();
val username = config.property("ktor.database.username").getString();
val password = config.property("ktor.database.password").getString();
//val repository = LogScriptRepositoryImpl()
val dbUrl = config.property("ktor.database.url").getString()
val username = config.property("ktor.database.username").getString()
val password = config.property("ktor.database.password").getString()
val secret = config.property("ktor.jwt.secret").getString()
configureDatabases(dbUrl, username, password)
//configureRouting(dbUrl, username, password, repository)
configureSecurity(secret)
configureRouting(dbUrl, username, password)
configureSerialization()
install(CallLogging)
install(CORS){
anyHost()

View File

@ -1,15 +1,22 @@
package eu.maiora.db
import com.fasterxml.jackson.databind.deser.impl.CreatorCandidate.Param
import eu.maiora.model.Accounts
import eu.maiora.model.Movimenti
import eu.maiora.model.Parametri
import eu.maiora.model.Tessere
import kotlinx.coroutines.Dispatchers
import org.jetbrains.exposed.dao.IntEntity
import org.jetbrains.exposed.dao.IntEntityClass
import org.jetbrains.exposed.dao.LongEntity
import org.jetbrains.exposed.dao.LongEntityClass
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.dao.id.IdTable
import org.jetbrains.exposed.sql.Transaction
import org.jetbrains.exposed.sql.javatime.datetime
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
import java.time.format.DateTimeFormatter
object AccountsTable : IdTable<Int>("accounts"){
override val id = integer("id").entityId()
@ -27,6 +34,34 @@ object ParametriTable : IdTable<Int>("parametri"){
override val primaryKey = PrimaryKey(id)
}
object TessereTable : IdTable<Long>("view_tessere_api"){
override val id = long("id").entityId()
val idUtente = long("id_utente")
val codiceFiscale = varchar("codice_fiscale", 255)
val numero = varchar("numero", 255)
val saldo = double("saldo")
val punti = integer("punti")
override val primaryKey = PrimaryKey(id)
}
object MovimentiTable : IdTable<Long>("view_movimenti_api"){
override val id = long("id").entityId()
val idTessera = long("id_tessera")
val numero = varchar("numero_tessera", 255)
val operazione = varchar("operazione", 255)
val dataMovimento = datetime("data_movimento")
val transazione = varchar("transazione", 255)
val dispositivo = varchar("dispositivo", 255)
val riferimento= varchar("riferimento", 255)
val saldoPre = double("saldo_pre")
val importo = double("importo")
val saldoPost = double("saldo_post")
val puntiPre = integer("punti_pre")
val punti = integer("punti")
val puntiPost = integer("punti_post")
}
class AccountsDAO(id: EntityID<Int>) :IntEntity(id) {
companion object : IntEntityClass<AccountsDAO>(AccountsTable)
@ -41,6 +76,34 @@ class ParametriDAO(id: EntityID<Int>) :IntEntity(id) {
var valore by ParametriTable.valore
}
class TessereDao(id: EntityID<Long>) :LongEntity(id) {
companion object : LongEntityClass<TessereDao>(TessereTable)
var idUtente by TessereTable.idUtente
var codiceFiscale by TessereTable.codiceFiscale
var numero by TessereTable.numero
var saldo by TessereTable.saldo
var punti by TessereTable.punti
}
class MovimentiDao(id: EntityID<Long>) :LongEntity(id) {
companion object : LongEntityClass<MovimentiDao>(MovimentiTable)
var idTessera by MovimentiTable.idTessera
var numero by MovimentiTable.numero
var operazione by MovimentiTable.operazione
var dataMovimento by MovimentiTable.dataMovimento
var transazione by MovimentiTable.transazione
var dispositivo by MovimentiTable.dispositivo
var riferimento by MovimentiTable.riferimento
var saldoPre by MovimentiTable.saldoPre
var importo by MovimentiTable.importo
var saldoPost by MovimentiTable.saldoPost
var puntiPre by MovimentiTable.puntiPre
var punti by MovimentiTable.punti
var puntiPost by MovimentiTable.puntiPost
}
fun accountsDaoToModel(dao: AccountsDAO) = Accounts(
dao.id.value,
@ -54,6 +117,40 @@ fun parametriDaoToModel(dao: ParametriDAO) = Parametri(
dao.valore
)
fun tessereDaoToModel(dao: TessereDao) = Tessere(
dao.id.value,
dao.idUtente,
dao.codiceFiscale,
dao.numero,
dao.saldo,
dao.punti
)
fun movimentiDaoToModel(dao: MovimentiDao) :Movimenti{
val formatter = DateTimeFormatter.ofPattern("ddMMyyyy HH:mm")
val formattedDate = dao.dataMovimento.format(formatter)
return Movimenti(
dao.id.value,
dao.idTessera,
dao.numero,
dao.operazione,
formattedDate,
dao.transazione,
dao.dispositivo,
dao.riferimento,
dao.saldoPre,
dao.importo,
dao.saldoPost,
dao.puntiPre,
dao.punti,
dao.puntiPost
)
}
suspend fun <T> suspendTransaction(block: Transaction.() -> T): T =
newSuspendedTransaction(Dispatchers.IO, statement = block)

View File

@ -0,0 +1,22 @@
package eu.maiora.model
import kotlinx.serialization.Serializable
@Serializable
data class Movimenti(
val id: Long,
val idTessera: Long,
val numero: String,
val operazione: String,
val dataMovimento: String,
val transazione: String,
val dispositivo: String,
val riferimento: String,
val saldoPre: Double,
val importo: Double,
val saldoPost: Double,
val puntiPre: Int,
val punti: Int,
val puntiPost: Int
)

View File

@ -0,0 +1,5 @@
package eu.maiora.model
interface MovimentiRepository {
suspend fun movimentiByIdTessera(idTessera : Long): List<Movimenti>?
}

View File

@ -0,0 +1,15 @@
package eu.maiora.model
import eu.maiora.db.*
import org.jetbrains.exposed.sql.SortOrder
class MovimentiRepositoryImpl : MovimentiRepository {
override suspend fun movimentiByIdTessera(idTessera : Long): List<Movimenti> = suspendTransaction {
// Cerca la lista di movimenti
MovimentiDao.find { MovimentiTable.idTessera eq idTessera }
.orderBy(MovimentiTable.dataMovimento to SortOrder.DESC)
.toList() // Restituisce la lista dei movimenti
.map { movimentiDaoToModel(it) } // Converte il DAO in un oggetto Movimenti
}
}

View File

@ -0,0 +1,13 @@
package eu.maiora.model
import kotlinx.serialization.Serializable
@Serializable
data class Tessere(
val id: Long,
val idUtente : Long,
val codiceFiscale : String,
val numero : String,
val saldo : Double,
val punti : Int
)

View File

@ -0,0 +1,5 @@
package eu.maiora.model
interface TessereRepository {
suspend fun tesseraByCodiceFiscale(cf : String): Tessere?
}

View File

@ -0,0 +1,13 @@
package eu.maiora.model
import eu.maiora.db.*
class TessereRepositoryImpl : TessereRepository {
override suspend fun tesseraByCodiceFiscale(cf: String): Tessere? = suspendTransaction {
// Cerca tessere in base al codice fiscale
TessereDao.find { TessereTable.codiceFiscale eq cf }
.singleOrNull() // Restituisce un singolo risultato o null se non trovato
?.let { tessereDaoToModel(it) } // Converte il DAO in un oggetto Tessere
}
}

View File

@ -5,8 +5,12 @@ package eu.maiora.plugins
//import eu.maiora.routes.eseguiScriptSQLRoute
//import eu.maiora.routes.logScriptRouting
import eu.maiora.model.AccountsRepositoryImpl
import eu.maiora.model.MovimentiRepositoryImpl
import eu.maiora.model.ParametriRepositoryImpl
import eu.maiora.model.TessereRepositoryImpl
import eu.maiora.routes.auth
import eu.maiora.routes.movimenti
import eu.maiora.routes.tessere
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
@ -18,9 +22,8 @@ fun Application.configureRouting(dbUrl : String, username : String, password : S
call.respondText("Hello World!")
}
auth(AccountsRepositoryImpl(), ParametriRepositoryImpl())
//analizzaURLRoute()
//eseguiScriptSQLRoute(dbUrl, username, password)
//logScriptRouting(repository)
auth(AccountsRepositoryImpl())
tessere(TessereRepositoryImpl())
movimenti(MovimentiRepositoryImpl())
}
}

View File

@ -0,0 +1,35 @@
package eu.maiora.plugins
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import io.ktor.server.response.*
fun Application.configureSecurity(secret: String) {
install(Authentication) {
jwt ("auth-jwt"){
verifier(
JWT
.require(Algorithm.HMAC256(secret))
.build())
validate { credential ->
val expiresAt = credential.payload.expiresAt?.time ?: 0
val now = System.currentTimeMillis()
// Verifica se il token ? scaduto
if (expiresAt >= now) {
JWTPrincipal(credential.payload)
}
else {
null
}
}
challenge { defaultScheme, realm ->
call.respond(HttpStatusCode.Unauthorized, "Token non valido o scaduto")
}
}
}
}

View File

@ -1,13 +1,11 @@
package eu.maiora.routes
import com.fasterxml.jackson.databind.ser.Serializers.Base
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import eu.maiora.model.AccountsRepositoryImpl
import eu.maiora.model.ParametriRepositoryImpl
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.SignatureAlgorithm
import io.jsonwebtoken.security.Keys
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.config.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
@ -17,70 +15,77 @@ import org.slf4j.LoggerFactory
import java.util.*
fun Route.auth(accountsRepository: AccountsRepositoryImpl, parametriRepository: ParametriRepositoryImpl){
post("/auth"){
// Riceve il body della richiesta e lo deserializza in ReceivedResponse
val receivedResponse = try {
call.receive<ReceivedResponse>()
} catch (e: Exception) {
return@post call.respondText("Body mancante " + e.stackTraceToString(), status = HttpStatusCode.BadRequest)
}
val logger = LoggerFactory.getLogger("AuthRoute")
logger.info("param: " +
receivedResponse.param);
// Decodifica la stringa da Base64 a oggetto Credentials
val decodedBytes = Base64.getDecoder().decode(receivedResponse.param)
val decodedString = String(decodedBytes)
val credentials = Json.decodeFromString<Credentials>(decodedString)
//verifica credenziali (recupero account dal database)
val account = accountsRepository.accountByUsername(credentials.username)
//se le credenziali sono valide, creare il JWT
if (account != null) {
val passwordPlain = String(
Base64.getDecoder().decode(
StringBuffer(
String(
Base64.getDecoder().decode(account.password.toByteArray())
)
).reverse().toString().toByteArray()
fun Route.auth(accountsRepository: AccountsRepositoryImpl) {
route("/auth") {
post() {
// Riceve il body della richiesta e lo deserializza in ReceivedResponse
val receivedResponse = try {
call.receive<ReceivedResponse>()
} catch (e: Exception) {
return@post call.respondText(
"Body mancante " + e.stackTraceToString(),
status = HttpStatusCode.BadRequest
)
)
if(passwordPlain.equals(credentials.password)){
val parametro = parametriRepository.parametroByChiave("jwt_secret")
if (parametro != null) {
}
val logger = LoggerFactory.getLogger("AuthRoute")
logger.info(
"param: " +
receivedResponse.param
);
// Decodifica la stringa da Base64 a oggetto Credentials
val decodedBytes = Base64.getDecoder().decode(receivedResponse.param)
val decodedString = String(decodedBytes)
val credentials = try {
Json.decodeFromString<Credentials>(decodedString)
} catch (e: Exception) {
return@post call.respondText(
"Errore nel param. Verificare la codifica. \n" + e.stackTraceToString(),
status = HttpStatusCode.BadRequest
)
}
//verifica credenziali (recupero account dal database)
val account = accountsRepository.accountByUsername(credentials.username)
//se le credenziali sono valide, creare il JWT
if (account != null) {
val passwordPlain = String(
Base64.getDecoder().decode(
StringBuffer(
String(
Base64.getDecoder().decode(account.password.toByteArray())
)
).reverse().toString().toByteArray()
)
)
if (passwordPlain.equals(credentials.password)) {
val config = ApplicationConfig("application.conf")
val secret = config.property("ktor.jwt.secret").getString()
val key = Keys.hmacShaKeyFor(parametro.valore.toByteArray())
val now = System.currentTimeMillis()
val expiration = now + 86400000 // Scadenza tra 1 giorno (24 ore)
val token = Jwts.builder()
.setSubject(account.username) // Soggetto del JWT
.setIssuedAt(Date(now)) // Data di emissione
.setExpiration(Date(expiration)) // Data di scadenza
.signWith(key, SignatureAlgorithm.HS256) // Firma con una chiave segreta
.compact()
val token = JWT.create()
.withSubject(account.username)
.withIssuedAt(Date(now))
.withExpiresAt(Date(expiration))
.sign(Algorithm.HMAC256(secret))
// Risponde con la stringa decodificata
call.respond(HttpStatusCode.OK, token)
}
}
else {
} else {
call.respond(HttpStatusCode.Unauthorized)
}
} else {
call.respond(HttpStatusCode.Unauthorized)
}
}
else {
call.respond(HttpStatusCode.Unauthorized)
}
}
}
}
@Serializable

View File

@ -0,0 +1,36 @@
package eu.maiora.routes
import eu.maiora.model.MovimentiRepositoryImpl
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Route.movimenti(movimentiRepository: MovimentiRepositoryImpl){
route("/api/movimenti"){
authenticate("auth-jwt") {
get("{idTessera}"){
// Ottieni l'id della tessera dal percorso
val idTessera = call.parameters["idTessera"]
if (idTessera == null) {
call.respondText("ID tessera non valido", status = HttpStatusCode.BadRequest)
return@get
}
// Cerca la tessera per codice fiscale
val listaMovimenti = movimentiRepository.movimentiByIdTessera(idTessera.toLong())
if (listaMovimenti != null) {
call.respond(listaMovimenti)
} else {
call.respondText("Movimenti non trovati", status = HttpStatusCode.NotFound)
}
}
}
}
}

View File

@ -0,0 +1,35 @@
package eu.maiora.routes
import eu.maiora.model.TessereRepositoryImpl
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Route.tessere(tessereRepository: TessereRepositoryImpl){
route("/api/tessere"){
authenticate("auth-jwt") {
get("{cf}"){
// Ottieni il codice fiscale dal percorso
val cf = call.parameters["cf"]
if (cf == null) {
call.respondText("Codice fiscale non valido", status = HttpStatusCode.BadRequest)
return@get
}
// Cerca la tessera per codice fiscale
val tessera = tessereRepository.tesseraByCodiceFiscale(cf)
if (tessera != null) {
call.respond(tessera)
} else {
call.respondText("Tessera non trovata", status = HttpStatusCode.NotFound)
}
}
}
}
}

View File

@ -13,4 +13,9 @@ ktor {
;username = "EP_DONORIONE"
;password = "ep_donorione"
}
jwt {
# secret per JWT generato partendo dalla stringa '?Backend_API*06022025!' codificato in Base64
secret = "P0JhY2tlbmRfQVBJKjA2MDIyMDI1IQ=="
}
}