Bank gateway integration, rest for getting slots, email sending.
This commit is contained in:
		
							parent
							
								
									e84fa3ce3e
								
							
						
					
					
						commit
						46bf29a5eb
					
				@ -24,7 +24,9 @@ dependencies {
 | 
				
			|||||||
    implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
 | 
					    implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
 | 
				
			||||||
    implementation("org.springframework.boot:spring-boot-starter-security")
 | 
					    implementation("org.springframework.boot:spring-boot-starter-security")
 | 
				
			||||||
    implementation("org.springframework.boot:spring-boot-starter-web")
 | 
					    implementation("org.springframework.boot:spring-boot-starter-web")
 | 
				
			||||||
 | 
					    implementation("org.springframework.boot:spring-boot-starter-mail")
 | 
				
			||||||
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
 | 
					    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
 | 
				
			||||||
 | 
					    implementation("io.nayuki:qrcodegen:1.8.0")
 | 
				
			||||||
    compileOnly("org.projectlombok:lombok")
 | 
					    compileOnly("org.projectlombok:lombok")
 | 
				
			||||||
    annotationProcessor("org.projectlombok:lombok")
 | 
					    annotationProcessor("org.projectlombok:lombok")
 | 
				
			||||||
    implementation("org.jetbrains.kotlin:kotlin-reflect")
 | 
					    implementation("org.jetbrains.kotlin:kotlin-reflect")
 | 
				
			||||||
 | 
				
			|||||||
@ -2,8 +2,12 @@ package ru.vyatsu.qr_access_api
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import org.springframework.boot.autoconfigure.SpringBootApplication
 | 
					import org.springframework.boot.autoconfigure.SpringBootApplication
 | 
				
			||||||
import org.springframework.boot.runApplication
 | 
					import org.springframework.boot.runApplication
 | 
				
			||||||
 | 
					import org.springframework.scheduling.annotation.EnableScheduling
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//TODO: Сделать ретраи везде, где в блок-схемах они указаны
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@SpringBootApplication
 | 
					@SpringBootApplication
 | 
				
			||||||
 | 
					@EnableScheduling
 | 
				
			||||||
class QrAccessApiApplication
 | 
					class QrAccessApiApplication
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fun main(args: Array<String>) {
 | 
					fun main(args: Array<String>) {
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					package ru.vyatsu.qr_access_api.booking.controller
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.springframework.web.bind.annotation.PostMapping
 | 
				
			||||||
 | 
					import org.springframework.web.bind.annotation.RequestBody
 | 
				
			||||||
 | 
					import org.springframework.web.bind.annotation.RestController
 | 
				
			||||||
 | 
					import ru.vyatsu.qr_access_api.booking.request.BookCallbackRequest
 | 
				
			||||||
 | 
					import ru.vyatsu.qr_access_api.booking.service.BookingService
 | 
				
			||||||
 | 
					import ru.vyatsu.qr_access_api.booking.request.BookRequest
 | 
				
			||||||
 | 
					import ru.vyatsu.qr_access_api.booking.request.BookResponse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@RestController
 | 
				
			||||||
 | 
					class BookingController(private val service: BookingService) {
 | 
				
			||||||
 | 
					    // TODO: Убрать /public, так как эти методы должны быть закрыты авторизацией client_credential и корсами
 | 
				
			||||||
 | 
					    // TODO: Для общего процесса бронирования нужно сделать альтернативный путь для шлюза с опросом.
 | 
				
			||||||
 | 
					    @PostMapping("/public/book")
 | 
				
			||||||
 | 
					    fun book(@RequestBody request: BookRequest): BookResponse {
 | 
				
			||||||
 | 
					        return BookResponse(service.book(request))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @PostMapping("/public/book/callback")
 | 
				
			||||||
 | 
					    fun bookCallback(@RequestBody request: BookCallbackRequest) {
 | 
				
			||||||
 | 
					        return service.bookPayed(request.rentId)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					package ru.vyatsu.qr_access_api.booking.job
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.springframework.scheduling.annotation.Scheduled
 | 
				
			||||||
 | 
					import org.springframework.stereotype.Service
 | 
				
			||||||
 | 
					import ru.vyatsu.qr_access_api.booking.repository.BookingRepository
 | 
				
			||||||
 | 
					import ru.vyatsu.qr_access_api.common.logger
 | 
				
			||||||
 | 
					import java.time.LocalDateTime
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Service
 | 
				
			||||||
 | 
					class CleanExpiredNotPayedRentJob(private val repository: BookingRepository) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Scheduled(cron = "0 */5 * * * *")
 | 
				
			||||||
 | 
					    fun clean() {
 | 
				
			||||||
 | 
					        val deletedCount =
 | 
				
			||||||
 | 
					            repository.deleteRentByDateCreatedLessThen(LocalDateTime.now().minusMinutes(10))
 | 
				
			||||||
 | 
					        logger().info("CleanExpiredNotPayedRentJob deleted {} rents", deletedCount)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,106 @@
 | 
				
			|||||||
 | 
					package ru.vyatsu.qr_access_api.booking.repository
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.springframework.dao.EmptyResultDataAccessException
 | 
				
			||||||
 | 
					import org.springframework.jdbc.core.JdbcTemplate
 | 
				
			||||||
 | 
					import org.springframework.jdbc.support.GeneratedKeyHolder
 | 
				
			||||||
 | 
					import org.springframework.stereotype.Repository
 | 
				
			||||||
 | 
					import org.springframework.transaction.annotation.Transactional
 | 
				
			||||||
 | 
					import ru.vyatsu.apis.NotFoundException
 | 
				
			||||||
 | 
					import ru.vyatsu.qr_access_api.booking.repository.entity.RentWithEmail
 | 
				
			||||||
 | 
					import java.sql.Date
 | 
				
			||||||
 | 
					import java.sql.Statement
 | 
				
			||||||
 | 
					import java.sql.Time
 | 
				
			||||||
 | 
					import java.sql.Timestamp
 | 
				
			||||||
 | 
					import java.time.LocalDate
 | 
				
			||||||
 | 
					import java.time.LocalDateTime
 | 
				
			||||||
 | 
					import java.time.LocalTime
 | 
				
			||||||
 | 
					import java.util.*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					private const val ADD_BOOKING_INFO_QUERY =
 | 
				
			||||||
 | 
					    "insert into rents(id, start_time, end_time, client_id, date, door_id, qr_code, payed, date_created) values (?, ? ,?, ?, ?, ?, ?, false, CURRENT_TIMESTAMP) RETURNING id"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					private const val CREATE_NEW_CLIENT =
 | 
				
			||||||
 | 
					    "insert into clients(id, email, email_is_confirmed) values (?, ?, false) RETURNING id"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					private const val FIND_CLIENT_BY_EMAIL = "select id from clients where email = ?"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					private const val MARK_RENT_AS_PAYED_QUERY = "update rents set payed=true where id = ?"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Repository
 | 
				
			||||||
 | 
					class BookingRepository(val template: JdbcTemplate) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Transactional
 | 
				
			||||||
 | 
					    fun book(doorId: String, date: LocalDate, startTime: LocalTime, endTime: LocalTime, clientEmail: String): String {
 | 
				
			||||||
 | 
					        val clientIdHolder = GeneratedKeyHolder()
 | 
				
			||||||
 | 
					        val rentIdHolder = GeneratedKeyHolder()
 | 
				
			||||||
 | 
					        var clientId: String? = null
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            clientId = template.queryForObject(
 | 
				
			||||||
 | 
					                FIND_CLIENT_BY_EMAIL,
 | 
				
			||||||
 | 
					                { rs, _ -> rs.getString("id") },
 | 
				
			||||||
 | 
					                clientEmail
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        } catch (_: EmptyResultDataAccessException) {
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (clientId == null) {
 | 
				
			||||||
 | 
					            template.update({
 | 
				
			||||||
 | 
					                val stmt = it.prepareStatement(CREATE_NEW_CLIENT, Statement.RETURN_GENERATED_KEYS)
 | 
				
			||||||
 | 
					                stmt.setString(1, UUID.randomUUID().toString())
 | 
				
			||||||
 | 
					                stmt.setString(2, clientEmail)
 | 
				
			||||||
 | 
					                stmt
 | 
				
			||||||
 | 
					            }, clientIdHolder)
 | 
				
			||||||
 | 
					            clientId = clientIdHolder.getKeyAs(String::class.java)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (clientId == null) throw RuntimeException("clientId is null even after insert, booking cannot be continued")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val insertedRows = template.update({
 | 
				
			||||||
 | 
					            val stmt = it.prepareStatement(ADD_BOOKING_INFO_QUERY, Statement.RETURN_GENERATED_KEYS)
 | 
				
			||||||
 | 
					            stmt.setString(1, UUID.randomUUID().toString())
 | 
				
			||||||
 | 
					            stmt.setTime(2, Time.valueOf(startTime))
 | 
				
			||||||
 | 
					            stmt.setTime(3, Time.valueOf(endTime))
 | 
				
			||||||
 | 
					            stmt.setString(4, clientId)
 | 
				
			||||||
 | 
					            stmt.setDate(5, Date.valueOf(date))
 | 
				
			||||||
 | 
					            stmt.setString(6, doorId)
 | 
				
			||||||
 | 
					            stmt.setString(7, UUID.randomUUID().toString())
 | 
				
			||||||
 | 
					            stmt
 | 
				
			||||||
 | 
					        }, rentIdHolder)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val insertedRentId = rentIdHolder.getKeyAs(String::class.java)
 | 
				
			||||||
 | 
					        if (insertedRows <= 0 || insertedRentId == null) throw RuntimeException(
 | 
				
			||||||
 | 
					            "Inserted rows number is invalid: %d. Have to be more then 0".format(
 | 
				
			||||||
 | 
					                insertedRows
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return insertedRentId
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fun findRentWithClientById(id: String): RentWithEmail {
 | 
				
			||||||
 | 
					        return template.queryForObject(
 | 
				
			||||||
 | 
					            "select r.qr_code, c.email from rents r join clients c on (c.id = r.client_id) where r.id = ?",
 | 
				
			||||||
 | 
					            { rs, _ -> RentWithEmail(rs.getString("qr_code"), rs.getString("email")) },
 | 
				
			||||||
 | 
					            id
 | 
				
			||||||
 | 
					        ) ?: throw NotFoundException("Cannot find rent with id %s".format(id))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fun markBookingPayed(rentId: String) {
 | 
				
			||||||
 | 
					        val updatedAmount = template.update {
 | 
				
			||||||
 | 
					            val stmt = it.prepareStatement(MARK_RENT_AS_PAYED_QUERY)
 | 
				
			||||||
 | 
					            stmt.setString(1, rentId)
 | 
				
			||||||
 | 
					            stmt
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (updatedAmount == 0)
 | 
				
			||||||
 | 
					            throw NotFoundException("Cannot find rent with id %s".format(rentId))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fun deleteRentByDateCreatedLessThen(dateTime: LocalDateTime): Int {
 | 
				
			||||||
 | 
					        return template.update {
 | 
				
			||||||
 | 
					            val stmt = it.prepareStatement("delete from rents where date_created <= ? and payed = false")
 | 
				
			||||||
 | 
					            stmt.setTimestamp(1, Timestamp.valueOf(dateTime))
 | 
				
			||||||
 | 
					            stmt
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					package ru.vyatsu.qr_access_api.booking.repository.entity
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					data class RentWithEmail(val qrCode: String, val email: String)
 | 
				
			||||||
@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					package ru.vyatsu.qr_access_api.booking.request
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					data class BookCallbackRequest(val rentId: String)
 | 
				
			||||||
@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					package ru.vyatsu.qr_access_api.booking.request
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.time.LocalDateTime
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					data class BookRequest(
 | 
				
			||||||
 | 
					    val startDateTime: LocalDateTime,
 | 
				
			||||||
 | 
					    val endDateTime: LocalDateTime,
 | 
				
			||||||
 | 
					    val doorId: String,
 | 
				
			||||||
 | 
					    val clientEmail: String
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					package ru.vyatsu.qr_access_api.booking.request
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					data class BookResponse(val rentId: String)
 | 
				
			||||||
@ -0,0 +1,37 @@
 | 
				
			|||||||
 | 
					package ru.vyatsu.qr_access_api.booking.service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.fasterxml.jackson.databind.node.JsonNodeFactory
 | 
				
			||||||
 | 
					import com.fasterxml.jackson.databind.node.ObjectNode
 | 
				
			||||||
 | 
					import org.springframework.stereotype.Service
 | 
				
			||||||
 | 
					import org.springframework.transaction.annotation.Transactional
 | 
				
			||||||
 | 
					import ru.vyatsu.qr_access_api.booking.repository.BookingRepository
 | 
				
			||||||
 | 
					import ru.vyatsu.qr_access_api.booking.request.BookRequest
 | 
				
			||||||
 | 
					import ru.vyatsu.qr_access_api.common.exception.ValidationException
 | 
				
			||||||
 | 
					import ru.vyatsu.qr_access_api.email.repository.EmailRepository
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Service
 | 
				
			||||||
 | 
					class BookingService(val bookingRepository: BookingRepository, val emailRepository: EmailRepository) {
 | 
				
			||||||
 | 
					    fun book(request: BookRequest): String {
 | 
				
			||||||
 | 
					        // TODO: Произвести еще валидации, если нужны
 | 
				
			||||||
 | 
					        val date = request.startDateTime.toLocalDate()
 | 
				
			||||||
 | 
					        if (date != request.endDateTime.toLocalDate()) {
 | 
				
			||||||
 | 
					            throw ValidationException(
 | 
				
			||||||
 | 
					                "startDateTime, endDateTime",
 | 
				
			||||||
 | 
					                "startDateTime and endDateTime have to have the same day"
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        val startTime = request.startDateTime.toLocalTime()
 | 
				
			||||||
 | 
					        val endTime = request.endDateTime.toLocalTime()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return bookingRepository.book(request.doorId, date, startTime, endTime, request.clientEmail)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Transactional
 | 
				
			||||||
 | 
					    fun bookPayed(bookId: String) {
 | 
				
			||||||
 | 
					        bookingRepository.markBookingPayed(bookId)
 | 
				
			||||||
 | 
					        val (qrCode, email) = bookingRepository.findRentWithClientById(bookId)
 | 
				
			||||||
 | 
					        val additionalData = JsonNodeFactory.instance.objectNode()
 | 
				
			||||||
 | 
					        additionalData.set<ObjectNode>("qr_code", JsonNodeFactory.instance.textNode(qrCode))
 | 
				
			||||||
 | 
					        emailRepository.createEmailOutboxRecord(email, "qr_code", additionalData)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										8
									
								
								src/main/kotlin/ru/vyatsu/qr_access_api/common/Log.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/main/kotlin/ru/vyatsu/qr_access_api/common/Log.kt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					package ru.vyatsu.qr_access_api.common
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.slf4j.Logger
 | 
				
			||||||
 | 
					import org.slf4j.LoggerFactory
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					inline fun <reified T> T.logger(): Logger {
 | 
				
			||||||
 | 
					    return LoggerFactory.getLogger(T::class.java)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					package ru.vyatsu.qr_access_api.common.config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.springframework.context.annotation.Bean
 | 
				
			||||||
 | 
					import org.springframework.context.annotation.Configuration
 | 
				
			||||||
 | 
					import org.springframework.mail.javamail.JavaMailSender
 | 
				
			||||||
 | 
					import org.springframework.mail.javamail.JavaMailSenderImpl
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Configuration
 | 
				
			||||||
 | 
					class EmailConfig {
 | 
				
			||||||
 | 
					    @Bean
 | 
				
			||||||
 | 
					    fun mailSender(): JavaMailSender {
 | 
				
			||||||
 | 
					        val mailSender = JavaMailSenderImpl()
 | 
				
			||||||
 | 
					        mailSender.host = "smtp.yandex.ru"
 | 
				
			||||||
 | 
					        mailSender.port = 587
 | 
				
			||||||
 | 
					        mailSender.username = "kashiuno@yandex.ru"
 | 
				
			||||||
 | 
					        mailSender.password = "qmpEMP262049!!!?EEWChaosMeteor"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val props = mailSender.javaMailProperties
 | 
				
			||||||
 | 
					        props["mail.transport.protocol"] = "smtp"
 | 
				
			||||||
 | 
					        props["mail.smtp.auth"] = "true"
 | 
				
			||||||
 | 
					        props["mail.smtp.starttls.enable"] = "true"
 | 
				
			||||||
 | 
					        props["mail.debug"] = "true"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return mailSender
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					package ru.vyatsu.qr_access_api.common.exception
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ValidationException(val fieldName: String, override val message: String) :
 | 
				
			||||||
 | 
					    RuntimeException("fieldName: %s -- message: %s".format(fieldName, message))
 | 
				
			||||||
@ -2,22 +2,40 @@ package ru.vyatsu.qr_access_api.config
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
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.HttpHeaders
 | 
				
			||||||
 | 
					import org.springframework.http.HttpMethod
 | 
				
			||||||
import org.springframework.security.config.Customizer
 | 
					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.web.SecurityFilterChain
 | 
					import org.springframework.security.web.SecurityFilterChain
 | 
				
			||||||
 | 
					import org.springframework.web.cors.CorsConfiguration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Configuration
 | 
					@Configuration
 | 
				
			||||||
@EnableWebSecurity
 | 
					@EnableWebSecurity(debug = true)
 | 
				
			||||||
class SecurityConfig {
 | 
					class SecurityConfig {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Bean
 | 
					    @Bean
 | 
				
			||||||
    fun defaultSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
 | 
					    fun defaultSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
 | 
				
			||||||
        return http.authorizeHttpRequests {
 | 
					        return http.authorizeHttpRequests {
 | 
				
			||||||
            it.requestMatchers("/public/**").permitAll()
 | 
					            it.requestMatchers("/public/**", "/error").permitAll()
 | 
				
			||||||
                .anyRequest().authenticated()
 | 
					                .anyRequest().authenticated()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
            .oauth2ResourceServer { it.jwt(Customizer.withDefaults()) }
 | 
					            .oauth2ResourceServer { it.jwt(Customizer.withDefaults()) }
 | 
				
			||||||
 | 
					            .cors { c ->
 | 
				
			||||||
 | 
					                c.configurationSource {
 | 
				
			||||||
 | 
					                    val config = CorsConfiguration()
 | 
				
			||||||
 | 
					                    config.addAllowedOrigin("http://localhost:3000")
 | 
				
			||||||
 | 
					                    config.allowedMethods = listOf(
 | 
				
			||||||
 | 
					                        HttpMethod.GET.name(),
 | 
				
			||||||
 | 
					                        HttpMethod.POST.name()
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    config.allowedHeaders = listOf(
 | 
				
			||||||
 | 
					                        HttpHeaders.CONTENT_TYPE
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    config
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            .csrf { c -> c.disable() }
 | 
				
			||||||
            .build()
 | 
					            .build()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -0,0 +1,71 @@
 | 
				
			|||||||
 | 
					package ru.vyatsu.qr_access_api.email.job
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import io.nayuki.qrcodegen.QrCode
 | 
				
			||||||
 | 
					import org.springframework.mail.javamail.JavaMailSender
 | 
				
			||||||
 | 
					import org.springframework.mail.javamail.MimeMessageHelper
 | 
				
			||||||
 | 
					import org.springframework.scheduling.annotation.Scheduled
 | 
				
			||||||
 | 
					import org.springframework.stereotype.Service
 | 
				
			||||||
 | 
					import ru.vyatsu.qr_access_api.common.logger
 | 
				
			||||||
 | 
					import ru.vyatsu.qr_access_api.email.repository.EmailRepository
 | 
				
			||||||
 | 
					import java.awt.image.BufferedImage
 | 
				
			||||||
 | 
					import java.io.ByteArrayInputStream
 | 
				
			||||||
 | 
					import java.io.ByteArrayOutputStream
 | 
				
			||||||
 | 
					import java.util.*
 | 
				
			||||||
 | 
					import javax.imageio.ImageIO
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Service
 | 
				
			||||||
 | 
					class EmailOutboxSendJob(val repository: EmailRepository, val mailSender: JavaMailSender) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Scheduled(cron = "*/5 * * * * *")
 | 
				
			||||||
 | 
					    fun sendEmails() {
 | 
				
			||||||
 | 
					        val messagesToSend = repository.findRecordsToSendWithBlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        messagesToSend.forEach {
 | 
				
			||||||
 | 
					            val msg = mailSender.createMimeMessage()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            val helper = MimeMessageHelper(msg, true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            helper.setFrom("kashiuno@yandex.ru")
 | 
				
			||||||
 | 
					            helper.setTo(it.email)
 | 
				
			||||||
 | 
					            helper.setSubject("Приобретение qr-кода")
 | 
				
			||||||
 | 
					            helper.setText("Вы приобрели проход в коворкинг на нашем сайте. qr-код во вложении. QR-код нужно приложить к сканеру соответствующей двери и она откроется")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            val qrCode = it.additionalData.get("qr_code")
 | 
				
			||||||
 | 
					            val code = QrCode.encodeText(qrCode.asText(), QrCode.Ecc.MEDIUM)
 | 
				
			||||||
 | 
					            val image = toImage(code, 3, 2)
 | 
				
			||||||
 | 
					            val os = ByteArrayOutputStream()
 | 
				
			||||||
 | 
					            ImageIO.write(image, "png", os)
 | 
				
			||||||
 | 
					            val imageIS = ByteArrayInputStream(os.toByteArray())
 | 
				
			||||||
 | 
					            helper.addAttachment("qr_code.png") { imageIS }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val recordsWasDeletedCount = repository.deleteRecordsToSend(messagesToSend.map { it.id })
 | 
				
			||||||
 | 
					        logger().info("Emails sent {}", recordsWasDeletedCount)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    companion object {
 | 
				
			||||||
 | 
					        private fun toImage(qr: QrCode, scale: Int, border: Int, lightColor: Int, darkColor: Int): BufferedImage {
 | 
				
			||||||
 | 
					            Objects.requireNonNull(qr)
 | 
				
			||||||
 | 
					            require(!(scale <= 0 || border < 0)) { "Value out of range" }
 | 
				
			||||||
 | 
					            require(!(border > Int.MAX_VALUE / 2 || qr.size + border * 2L > Int.MAX_VALUE / scale)) { "Scale or border too large" }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            val result = BufferedImage(
 | 
				
			||||||
 | 
					                (qr.size + border * 2) * scale,
 | 
				
			||||||
 | 
					                (qr.size + border * 2) * scale,
 | 
				
			||||||
 | 
					                BufferedImage.TYPE_INT_RGB
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            for (y in 0 until result.height) {
 | 
				
			||||||
 | 
					                for (x in 0 until result.width) {
 | 
				
			||||||
 | 
					                    val color = qr.getModule(x / scale - border, y / scale - border)
 | 
				
			||||||
 | 
					                    result.setRGB(x, y, if (color) darkColor else lightColor)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return result
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        fun toImage(qr: QrCode?, scale: Int, border: Int): BufferedImage {
 | 
				
			||||||
 | 
					            return toImage(qr!!, scale, border, 0xFFFFFF, 0x000000)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,54 @@
 | 
				
			|||||||
 | 
					package ru.vyatsu.qr_access_api.email.repository
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.fasterxml.jackson.databind.JsonNode
 | 
				
			||||||
 | 
					import com.fasterxml.jackson.databind.ObjectMapper
 | 
				
			||||||
 | 
					import org.springframework.jdbc.core.BatchPreparedStatementSetter
 | 
				
			||||||
 | 
					import org.springframework.jdbc.core.JdbcTemplate
 | 
				
			||||||
 | 
					import org.springframework.stereotype.Repository
 | 
				
			||||||
 | 
					import ru.vyatsu.qr_access_api.email.repository.entity.EmailOutbox
 | 
				
			||||||
 | 
					import java.sql.PreparedStatement
 | 
				
			||||||
 | 
					import java.util.*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					private const val INSERT_EMAIL_OUTBOX_RECORD =
 | 
				
			||||||
 | 
					    "insert into email_outbox(id, email, template, additional_info) values (?, ?, ?, (to_json(?::json)))"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Repository
 | 
				
			||||||
 | 
					class EmailRepository(private val template: JdbcTemplate, private val om: ObjectMapper) {
 | 
				
			||||||
 | 
					    fun createEmailOutboxRecord(email: String, msgTemplate: String, additionalInfo: JsonNode) {
 | 
				
			||||||
 | 
					        val insertedCount = template.update {
 | 
				
			||||||
 | 
					            val stmt = it.prepareStatement(INSERT_EMAIL_OUTBOX_RECORD)
 | 
				
			||||||
 | 
					            stmt.setString(1, UUID.randomUUID().toString())
 | 
				
			||||||
 | 
					            stmt.setString(2, email)
 | 
				
			||||||
 | 
					            stmt.setString(3, msgTemplate)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            stmt.setObject(4, om.writeValueAsString(additionalInfo))
 | 
				
			||||||
 | 
					            stmt
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (insertedCount != 1)
 | 
				
			||||||
 | 
					            throw RuntimeException("Inserted rows should be equals to 1")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fun findRecordsToSendWithBlock(): List<EmailOutbox> {
 | 
				
			||||||
 | 
					        return template.query("select id, email, template, additional_info from email_outbox for update", { rs, _ ->
 | 
				
			||||||
 | 
					            EmailOutbox(
 | 
				
			||||||
 | 
					                rs.getString("id"),
 | 
				
			||||||
 | 
					                rs.getString("email"),
 | 
				
			||||||
 | 
					                rs.getString("template"),
 | 
				
			||||||
 | 
					                om.readTree(rs.getString("additional_info"))
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fun deleteRecordsToSend(ids: List<String>): Int {
 | 
				
			||||||
 | 
					        val query = "delete from email_outbox where id = ?"
 | 
				
			||||||
 | 
					        return template.batchUpdate(query, object : BatchPreparedStatementSetter {
 | 
				
			||||||
 | 
					            override fun setValues(ps: PreparedStatement, i: Int) {
 | 
				
			||||||
 | 
					                ps.setString(1, ids[i])
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            override fun getBatchSize(): Int = ids.size
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					            .sum()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					package ru.vyatsu.qr_access_api.email.repository.entity
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.fasterxml.jackson.databind.JsonNode
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					data class EmailOutbox(val id: String, val email: String, val template: String, val additionalData: JsonNode)
 | 
				
			||||||
@ -1,10 +1,10 @@
 | 
				
			|||||||
package ru.vyatsu.qr_access_api.controller
 | 
					package ru.vyatsu.qr_access_api.qr.controller
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import org.springframework.http.ResponseEntity
 | 
					import org.springframework.http.ResponseEntity
 | 
				
			||||||
import org.springframework.web.bind.annotation.RestController
 | 
					import org.springframework.web.bind.annotation.RestController
 | 
				
			||||||
import ru.vyatsu.apis.QrApi
 | 
					import ru.vyatsu.apis.QrApi
 | 
				
			||||||
import ru.vyatsu.models.QrCodesResponse
 | 
					import ru.vyatsu.models.QrCodesResponse
 | 
				
			||||||
import ru.vyatsu.qr_access_api.service.QrSyncService
 | 
					import ru.vyatsu.qr_access_api.qr.service.QrSyncService
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@RestController
 | 
					@RestController
 | 
				
			||||||
class QrSyncController(val syncService: QrSyncService) : QrApi {
 | 
					class QrSyncController(val syncService: QrSyncService) : QrApi {
 | 
				
			||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
package ru.vyatsu.qr_access_api.controller
 | 
					package ru.vyatsu.qr_access_api.qr.controller
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import org.springframework.http.HttpStatusCode
 | 
					import org.springframework.http.HttpStatusCode
 | 
				
			||||||
import org.springframework.http.ResponseEntity
 | 
					import org.springframework.http.ResponseEntity
 | 
				
			||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
package ru.vyatsu.qr_access_api.repository
 | 
					package ru.vyatsu.qr_access_api.qr.repository
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import org.springframework.jdbc.core.JdbcTemplate
 | 
					import org.springframework.jdbc.core.JdbcTemplate
 | 
				
			||||||
import org.springframework.stereotype.Repository
 | 
					import org.springframework.stereotype.Repository
 | 
				
			||||||
@ -1,9 +1,9 @@
 | 
				
			|||||||
package ru.vyatsu.qr_access_api.service
 | 
					package ru.vyatsu.qr_access_api.qr.service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import org.springframework.security.core.context.SecurityContextHolder
 | 
					import org.springframework.security.core.context.SecurityContextHolder
 | 
				
			||||||
import org.springframework.stereotype.Service
 | 
					import org.springframework.stereotype.Service
 | 
				
			||||||
import ru.vyatsu.models.QrCode
 | 
					import ru.vyatsu.models.QrCode
 | 
				
			||||||
import ru.vyatsu.qr_access_api.repository.QrRepository
 | 
					import ru.vyatsu.qr_access_api.qr.repository.QrRepository
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Service
 | 
					@Service
 | 
				
			||||||
class QrSyncService(val qrRepository: QrRepository) {
 | 
					class QrSyncService(val qrRepository: QrRepository) {
 | 
				
			||||||
@ -1,15 +1,12 @@
 | 
				
			|||||||
package ru.vyatsu.qr_access_api.slots.controller
 | 
					package ru.vyatsu.qr_access_api.slots.controller
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import org.springframework.web.bind.annotation.GetMapping
 | 
					import org.springframework.web.bind.annotation.*
 | 
				
			||||||
import org.springframework.web.bind.annotation.PathVariable
 | 
					 | 
				
			||||||
import org.springframework.web.bind.annotation.RequestMapping
 | 
					 | 
				
			||||||
import org.springframework.web.bind.annotation.RestController
 | 
					 | 
				
			||||||
import ru.vyatsu.qr_access_api.slots.response.SlotResponse
 | 
					import ru.vyatsu.qr_access_api.slots.response.SlotResponse
 | 
				
			||||||
import ru.vyatsu.qr_access_api.slots.service.SlotService
 | 
					import ru.vyatsu.qr_access_api.slots.service.SlotService
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@RestController
 | 
					@RestController
 | 
				
			||||||
@RequestMapping("public")
 | 
					@RequestMapping("public")
 | 
				
			||||||
class PublicSlotsController(val service: SlotService) {
 | 
					class SlotsController(val service: SlotService) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @GetMapping("/slots/{partnerId}")
 | 
					    @GetMapping("/slots/{partnerId}")
 | 
				
			||||||
    fun getSlots(@PathVariable partnerId: String): SlotResponse {
 | 
					    fun getSlots(@PathVariable partnerId: String): SlotResponse {
 | 
				
			||||||
@ -6,12 +6,11 @@ import ru.vyatsu.qr_access_api.slots.repository.entity.*
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const val FIND_DOORS_SCHEDULE_BY_PARTNER_ID =
 | 
					const val FIND_DOORS_SCHEDULE_BY_PARTNER_ID =
 | 
				
			||||||
    "select d.id, d.description, d.count, s.date, s.start_time, s.end_time from oauth2_registered_client u join doors d on (d.unit_id = u.client_id) join schedule s on (d.id = s.door_id) where u.partner_id = ?"
 | 
					    "select d.id, d.description, d.count, s.date, s.start_time, s.end_time from oauth2_registered_client u join doors d on (d.unit_id = u.client_id) join schedule s on (d.id = s.door_id) where u.partner_id = ?"
 | 
				
			||||||
const val FIND_RENT_DOORS_BY_DOOR_IDS =
 | 
					const val FIND_RENT_DOORS_BY_PARTNER_ID =
 | 
				
			||||||
    "select d.id, r.date, r.start_time, r.end_time from oauth2_registered_client u join doors d on (d.unit_id = u.client_id) join rent r on (r.door_id = d.id) where u.partner_id = ? order by r.start_time asc"
 | 
					    "select d.id, r.date, r.start_time, r.end_time from oauth2_registered_client u join doors d on (d.unit_id = u.client_id) join rents r on (r.door_id = d.id) where u.partner_id = ? order by r.start_time asc"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Repository
 | 
					@Repository
 | 
				
			||||||
class SlotRepository(private val jdbc: JdbcTemplate) {
 | 
					class SlotRepository(private val jdbc: JdbcTemplate) {
 | 
				
			||||||
 | 
					 | 
				
			||||||
    fun findAllDoorsWithScheduleByPartnerId(partnerId: String): Collection<DoorWithSchedule> {
 | 
					    fun findAllDoorsWithScheduleByPartnerId(partnerId: String): Collection<DoorWithSchedule> {
 | 
				
			||||||
        val doors: MutableMap<String, DoorWithSchedule> = mutableMapOf()
 | 
					        val doors: MutableMap<String, DoorWithSchedule> = mutableMapOf()
 | 
				
			||||||
        jdbc.query({
 | 
					        jdbc.query({
 | 
				
			||||||
@ -43,10 +42,10 @@ class SlotRepository(private val jdbc: JdbcTemplate) {
 | 
				
			|||||||
        return doors.values
 | 
					        return doors.values
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fun findRentDateTimesByDoorsId(partnerId: String): DoorRent {
 | 
					    fun findRentDateTimesByPartnerId(partnerId: String): DoorRent {
 | 
				
			||||||
        val doors = DoorRent(mutableMapOf())
 | 
					        val doors = DoorRent(mutableMapOf())
 | 
				
			||||||
        jdbc.query({
 | 
					        jdbc.query({
 | 
				
			||||||
            val stmt = it.prepareStatement(FIND_RENT_DOORS_BY_DOOR_IDS)
 | 
					            val stmt = it.prepareStatement(FIND_RENT_DOORS_BY_PARTNER_ID)
 | 
				
			||||||
            stmt.setString(1, partnerId)
 | 
					            stmt.setString(1, partnerId)
 | 
				
			||||||
            stmt
 | 
					            stmt
 | 
				
			||||||
        }, { rs ->
 | 
					        }, { rs ->
 | 
				
			||||||
 | 
				
			|||||||
@ -14,7 +14,7 @@ class SlotService(private val repository: SlotRepository) {
 | 
				
			|||||||
    fun getAllSlotsByPartner(partnerId: String): SlotResponse {
 | 
					    fun getAllSlotsByPartner(partnerId: String): SlotResponse {
 | 
				
			||||||
        // TODO: Учитывать количество мест и переписать логику
 | 
					        // TODO: Учитывать количество мест и переписать логику
 | 
				
			||||||
        val doorsWithSchedule = repository.findAllDoorsWithScheduleByPartnerId(partnerId)
 | 
					        val doorsWithSchedule = repository.findAllDoorsWithScheduleByPartnerId(partnerId)
 | 
				
			||||||
        val doorRents = repository.findRentDateTimesByDoorsId(partnerId)
 | 
					        val doorRents = repository.findRentDateTimesByPartnerId(partnerId)
 | 
				
			||||||
        val doors: MutableList<Door> = mutableListOf()
 | 
					        val doors: MutableList<Door> = mutableListOf()
 | 
				
			||||||
        doorsWithSchedule.forEach { d ->
 | 
					        doorsWithSchedule.forEach { d ->
 | 
				
			||||||
            val slots: MutableList<Slot> = mutableListOf()
 | 
					            val slots: MutableList<Slot> = mutableListOf()
 | 
				
			||||||
 | 
				
			|||||||
@ -132,6 +132,11 @@ databaseChangeLog:
 | 
				
			|||||||
                  type: TEXT
 | 
					                  type: TEXT
 | 
				
			||||||
                  constraints:
 | 
					                  constraints:
 | 
				
			||||||
                    nullable: true
 | 
					                    nullable: true
 | 
				
			||||||
 | 
					              - column:
 | 
				
			||||||
 | 
					                  name: price
 | 
				
			||||||
 | 
					                  type: DECIMAL(12, 2)
 | 
				
			||||||
 | 
					                  constraints:
 | 
				
			||||||
 | 
					                    nullable: false
 | 
				
			||||||
        - createTable:
 | 
					        - createTable:
 | 
				
			||||||
            tableName: clients
 | 
					            tableName: clients
 | 
				
			||||||
            columns:
 | 
					            columns:
 | 
				
			||||||
@ -145,6 +150,7 @@ databaseChangeLog:
 | 
				
			|||||||
              - column:
 | 
					              - column:
 | 
				
			||||||
                  constraints:
 | 
					                  constraints:
 | 
				
			||||||
                    nullable: false
 | 
					                    nullable: false
 | 
				
			||||||
 | 
					                    unique: true
 | 
				
			||||||
                  name: email
 | 
					                  name: email
 | 
				
			||||||
                  type: TEXT
 | 
					                  type: TEXT
 | 
				
			||||||
              - column:
 | 
					              - column:
 | 
				
			||||||
@ -153,7 +159,7 @@ databaseChangeLog:
 | 
				
			|||||||
                  name: email_is_confirmed
 | 
					                  name: email_is_confirmed
 | 
				
			||||||
                  type: BOOLEAN
 | 
					                  type: BOOLEAN
 | 
				
			||||||
        - createTable:
 | 
					        - createTable:
 | 
				
			||||||
            tableName: rent
 | 
					            tableName: rents
 | 
				
			||||||
            columns:
 | 
					            columns:
 | 
				
			||||||
              - column:
 | 
					              - column:
 | 
				
			||||||
                  constraints:
 | 
					                  constraints:
 | 
				
			||||||
 | 
				
			|||||||
@ -7,6 +7,7 @@ import org.springframework.context.annotation.Bean
 | 
				
			|||||||
import org.springframework.context.annotation.Import
 | 
					import org.springframework.context.annotation.Import
 | 
				
			||||||
import org.springframework.jdbc.core.JdbcTemplate
 | 
					import org.springframework.jdbc.core.JdbcTemplate
 | 
				
			||||||
import ru.vyatsu.qr_access_api.database.utils.InsertDatabaseHelper
 | 
					import ru.vyatsu.qr_access_api.database.utils.InsertDatabaseHelper
 | 
				
			||||||
 | 
					import ru.vyatsu.qr_access_api.qr.repository.QrRepository
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@JdbcTest
 | 
					@JdbcTest
 | 
				
			||||||
@Import(RepositoryTest.Configuration::class)
 | 
					@Import(RepositoryTest.Configuration::class)
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user