new version
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
package net.xintanalabs.rssotto
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
|
||||
@SpringBootApplication
|
||||
class RssottoApplication
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<RssottoApplication>(*args)
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
package net.xintanalabs.rssotto.components.checkers
|
||||
|
||||
import net.xintanalabs.rssotto.components.checker.api.ApiChecker
|
||||
import net.xintanalabs.rssotto.components.checkers.scrape.ScrapeChecker
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class CheckerFactory(
|
||||
private val apiChecker: ApiChecker,
|
||||
private val scrapeChecker: ScrapeChecker
|
||||
) {
|
||||
fun createChecker(type: String): IVersionChecker {
|
||||
val parts = type.split(":")
|
||||
return when (parts[0].lowercase()) {
|
||||
"scrape" -> scrapeChecker
|
||||
"api" -> apiChecker
|
||||
else -> throw IllegalArgumentException("Unknown checker type: $type")
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
package net.xintanalabs.rssotto.components.checkers
|
||||
|
||||
import net.xintanalabs.rssotto.model.Source
|
||||
|
||||
interface IVersionChecker {
|
||||
suspend fun getLatestVersion(paramsDict: Map<String, String>): String
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
package net.xintanalabs.rssotto.components.checker.api
|
||||
|
||||
import kotlinx.serialization.json.*
|
||||
import net.xintanalabs.rssotto.components.checkers.IVersionChecker
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.reactive.function.client.WebClient
|
||||
import org.springframework.web.reactive.function.client.awaitBody
|
||||
import org.springframework.web.reactive.function.client.awaitExchange
|
||||
import kotlin.text.get
|
||||
|
||||
@Component
|
||||
class ApiChecker(private val webClient: WebClient) : IVersionChecker {
|
||||
override suspend fun getLatestVersion(paramsDict: Map<String, String>): String {
|
||||
val url = paramsDict["url"]?.takeIf { it.isNotEmpty() }
|
||||
?: throw IllegalArgumentException("API URL required")
|
||||
val jsonPath = paramsDict["jsonPath"]?.takeIf { it.isNotEmpty() }
|
||||
?: throw IllegalArgumentException("jsonPath required")
|
||||
|
||||
val response = webClient.get()
|
||||
.uri(url)
|
||||
.awaitExchange { it.awaitBody<String>() }
|
||||
?: throw Exception("Empty API response")
|
||||
|
||||
val jsonDoc = Json.parseToJsonElement(response)
|
||||
|
||||
var element: JsonElement = jsonDoc
|
||||
for (key in jsonPath.split('.')) {
|
||||
element = (element as? JsonObject)?.get(key)
|
||||
?: throw Exception("JSON key '$key' not found in response")
|
||||
}
|
||||
|
||||
if (element is JsonPrimitive && element.isString) {
|
||||
return element.content.trim()
|
||||
} else {
|
||||
throw Exception("JSON value for '$jsonPath' is not a string")
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
package net.xintanalabs.rssotto.components.checkers.scrape
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.client.RestTemplate
|
||||
|
||||
@Component("default")
|
||||
class DefaultScrapeFetcher(private val restTemplate: RestTemplate) : IScrapeFetcher{
|
||||
override suspend fun fetch(url: String): String {
|
||||
return restTemplate.getForObject(url, String::class.java)
|
||||
?: throw Exception("Empty response from URL")
|
||||
}
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
package net.xintanalabs.rssotto.components.checkers.scrape
|
||||
|
||||
interface IScrapeFetcher {
|
||||
suspend fun fetch(url: String): String
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
package net.xintanalabs.rssotto.components.checkers.scrape
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
import org.jsoup.Jsoup
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component("jsoup")
|
||||
class JSoupFetcher: IScrapeFetcher {
|
||||
override suspend fun fetch(url: String): String {
|
||||
delay(1000) // 1-second delay to avoid rate limiting
|
||||
return try {
|
||||
Jsoup.connect(url)
|
||||
.userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
|
||||
.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
|
||||
.header("Accept-Language", "en-US,en;q=0.5")
|
||||
.header("Accept-Encoding", "gzip, deflate")
|
||||
//.header("Connection", "keep-alive")
|
||||
.header("Upgrade-Insecure-Requests", "1")
|
||||
.get()
|
||||
.html()
|
||||
} catch (e: Exception) {
|
||||
throw RuntimeException("Error fetching ${url}", e)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,43 @@
|
||||
package net.xintanalabs.rssotto.components.checkers.scrape
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
import net.xintanalabs.rssotto.components.checkers.IVersionChecker
|
||||
import net.xintanalabs.rssotto.model.Source
|
||||
import net.xintanalabs.rssotto.tasks.ScheduledTasks
|
||||
import org.openqa.selenium.chrome.ChromeDriver
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.client.RestTemplate
|
||||
import java.util.regex.Pattern
|
||||
|
||||
@Component
|
||||
class ScrapeChecker(
|
||||
private val restTemplate: RestTemplate,
|
||||
private val chromeDriver: ChromeDriver
|
||||
) : IVersionChecker {
|
||||
|
||||
private val log = LoggerFactory.getLogger(ScrapeChecker::class.java)
|
||||
|
||||
override suspend fun getLatestVersion(paramsDict: Map<String, String>): String {
|
||||
val url = paramsDict["url"]?.takeIf { it.isNotEmpty() }
|
||||
?: throw IllegalArgumentException("URL required")
|
||||
log.info("Url : {}", url)
|
||||
val mode = paramsDict["mode"]
|
||||
log.info("Mode : {}", mode)
|
||||
val fetcher: IScrapeFetcher = when (mode) {
|
||||
"selenium" -> SeleniumFetcher(chromeDriver)
|
||||
"jsoup" -> JSoupFetcher()
|
||||
else -> DefaultScrapeFetcher(restTemplate)
|
||||
}
|
||||
val response = fetcher.fetch(url)
|
||||
val cleanedResponse = response.replace(">\\s+<".toRegex(), "><")
|
||||
|
||||
val regex: String = paramsDict["regex"] as String
|
||||
log.info("Regex : {}", regex)
|
||||
val match = Pattern.compile(regex).matcher(cleanedResponse)
|
||||
if (!match.find() || match.groupCount() < 1) {
|
||||
throw Exception("No match with regex in response")
|
||||
}
|
||||
return match.group(1)
|
||||
}
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
package net.xintanalabs.rssotto.components.checkers.scrape
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
import org.openqa.selenium.chrome.ChromeDriver
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component("selenium")
|
||||
class SeleniumFetcher(private val chromeDriver: ChromeDriver): IScrapeFetcher {
|
||||
override suspend fun fetch(url: String): String {
|
||||
try {
|
||||
|
||||
chromeDriver.get(url)
|
||||
delay(2000) // Consider WebDriverWait for robustness
|
||||
return chromeDriver.pageSource
|
||||
} finally {
|
||||
// Note: Don't quit driver here since it's Spring-managed
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,76 @@
|
||||
package net.xintanalabs.rssotto.config
|
||||
|
||||
import net.xintanalabs.rssotto.repository.UserRepository
|
||||
import net.xintanalabs.rssotto.service.CustomUserDetailsService
|
||||
import org.openqa.selenium.chrome.ChromeDriver
|
||||
import org.openqa.selenium.chrome.ChromeDriverService
|
||||
import org.openqa.selenium.chrome.ChromeOptions
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.security.authentication.AuthenticationManager
|
||||
import org.springframework.security.authentication.AuthenticationProvider
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider
|
||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration
|
||||
import org.springframework.security.core.userdetails.UserDetailsService
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.web.client.RestTemplate
|
||||
import org.springframework.web.reactive.function.client.WebClient
|
||||
import java.io.File
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(JwtProperties::class)
|
||||
class Configuration{
|
||||
@Bean
|
||||
fun userDetailsService(userRepository: UserRepository) : UserDetailsService =
|
||||
CustomUserDetailsService(userRepository)
|
||||
|
||||
@Bean
|
||||
fun encoder(): PasswordEncoder = BCryptPasswordEncoder()
|
||||
|
||||
@Bean
|
||||
fun authenticationProvider(userRepository: UserRepository): AuthenticationProvider =
|
||||
DaoAuthenticationProvider()
|
||||
.also {
|
||||
it.setUserDetailsService(userDetailsService(userRepository))
|
||||
it.setPasswordEncoder(encoder())
|
||||
}
|
||||
@Bean
|
||||
fun authenticationManager(config: AuthenticationConfiguration): AuthenticationManager =
|
||||
config.authenticationManager
|
||||
|
||||
@Bean
|
||||
fun webClient(builder: WebClient.Builder): WebClient {
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun restTemplate(): RestTemplate {
|
||||
return RestTemplate()
|
||||
}
|
||||
|
||||
@Bean(destroyMethod = "quit")
|
||||
fun chromeDriver(): ChromeDriver {
|
||||
// Set log file to suppress console output (optional)
|
||||
val logFile = File("chromedriver.log")
|
||||
val service = ChromeDriverService.Builder()
|
||||
.withLogFile(logFile) // Redirect logs to a file
|
||||
.withSilent(true) // Suppress console output
|
||||
.withVerbose(false) // Explicitly disable verbose logging
|
||||
.build()
|
||||
|
||||
// Optional: Set hideCommandPromptWindow for Windows
|
||||
System.setProperty("webdriver.chrome.hideCommandPromptWindow", "true")
|
||||
|
||||
val options = ChromeOptions().apply {
|
||||
addArguments("--headless")
|
||||
addArguments("--disable-gpu")
|
||||
addArguments("--no-sandbox")
|
||||
addArguments("--disable-dev-shm-usage")
|
||||
addArguments("--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124")
|
||||
}
|
||||
return ChromeDriver(service, options)
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,60 @@
|
||||
package net.xintanalabs.rssotto.config
|
||||
|
||||
import jakarta.servlet.FilterChain
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import net.xintanalabs.rssotto.service.CustomUserDetailsService
|
||||
import net.xintanalabs.rssotto.service.TokenService
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||
import org.springframework.security.core.context.SecurityContextHolder
|
||||
import org.springframework.security.core.userdetails.UserDetails
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.filter.OncePerRequestFilter
|
||||
|
||||
@Component
|
||||
class JwtAuthenticationFilter(
|
||||
private val userDetailService: CustomUserDetailsService,
|
||||
private val tokenService: TokenService
|
||||
) : OncePerRequestFilter() {
|
||||
|
||||
override fun doFilterInternal(
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
filterChain: FilterChain
|
||||
) {
|
||||
val authHeader: String? = request.getHeader("Authorization")
|
||||
|
||||
if (authHeader.doesNotContainBearerToken()) {
|
||||
filterChain.doFilter(request, response)
|
||||
return
|
||||
}
|
||||
|
||||
val jwtToken = authHeader!!.extractTokenValue()
|
||||
val username = tokenService.extractUsername(jwtToken)
|
||||
|
||||
if (username != null && SecurityContextHolder.getContext().authentication == null){
|
||||
val foundUser = userDetailService.loadUserByUsername(username)
|
||||
if (tokenService.isValid(jwtToken, foundUser)) {
|
||||
updateContext(foundUser, request)
|
||||
}
|
||||
filterChain.doFilter(request, response)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateContext(
|
||||
foundUser: UserDetails,
|
||||
request: HttpServletRequest
|
||||
) {
|
||||
val authToken = UsernamePasswordAuthenticationToken(foundUser, null, foundUser.authorities)
|
||||
authToken.details = WebAuthenticationDetailsSource().buildDetails(request)
|
||||
SecurityContextHolder.getContext().authentication = authToken
|
||||
}
|
||||
|
||||
|
||||
private fun String?.doesNotContainBearerToken(): Boolean =
|
||||
this == null || !this.startsWith("Bearer ")
|
||||
|
||||
private fun String.extractTokenValue() =
|
||||
this.substringAfter("Bearer ").trim()
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
package net.xintanalabs.rssotto.config
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
|
||||
@ConfigurationProperties("jwt")
|
||||
data class JwtProperties(
|
||||
val key: String,
|
||||
val accessTokenExpiration: Long,
|
||||
val refreshTokenExpiration: Long
|
||||
)
|
@@ -0,0 +1,43 @@
|
||||
package net.xintanalabs.rssotto.config
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.http.HttpMethod
|
||||
import org.springframework.security.authentication.AuthenticationProvider
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||
import org.springframework.security.config.http.SessionCreationPolicy
|
||||
import org.springframework.security.web.DefaultSecurityFilterChain
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
class SecurityConfiguration(
|
||||
private val authenticationProvider: AuthenticationProvider
|
||||
) {
|
||||
|
||||
@Bean
|
||||
fun securityFilterChain(
|
||||
http: HttpSecurity,
|
||||
jwtAuthenticationFilter: JwtAuthenticationFilter
|
||||
): DefaultSecurityFilterChain =
|
||||
http
|
||||
.csrf { it.disable() }
|
||||
.authorizeHttpRequests {
|
||||
it
|
||||
.requestMatchers("/api/auth", "/api/auth/refresh", "/error")
|
||||
.permitAll()
|
||||
.requestMatchers(HttpMethod.POST, "/api/user")
|
||||
.permitAll()
|
||||
.requestMatchers( "/api/user**")
|
||||
.hasRole("ADMIN")
|
||||
.anyRequest()
|
||||
.fullyAuthenticated()
|
||||
}
|
||||
.sessionManagement {
|
||||
it.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||
}
|
||||
.authenticationProvider(authenticationProvider)
|
||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java)
|
||||
.build()
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
package net.xintanalabs.rssotto.constants
|
||||
|
||||
object Constants {
|
||||
const val API_BASE_PATH: String = "/api"
|
||||
const val COLLECTION_TYPES: String = "types"
|
||||
const val COLLECTION_APPS: String = "apps"
|
||||
const val COLLECTION_SOURCES: String = "sources"
|
||||
const val COLLECTION_CHECKER_TYPES: String = "checkerTypes"
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
package net.xintanalabs.rssotto.controller.article
|
||||
|
||||
import net.xintanalabs.rssotto.constants.Constants
|
||||
import net.xintanalabs.rssotto.model.Article
|
||||
import net.xintanalabs.rssotto.service.ArticleService
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@RestController
|
||||
@RequestMapping("${Constants.API_BASE_PATH}/article")
|
||||
class ArticleController(
|
||||
private val articleService: ArticleService
|
||||
) {
|
||||
|
||||
@GetMapping
|
||||
fun listAll(): List<ArticleResponse> =
|
||||
articleService.findAll()
|
||||
.map { it.toResponse() }
|
||||
|
||||
fun Article.toResponse() =
|
||||
ArticleResponse(
|
||||
id = this.id,
|
||||
title = this.title,
|
||||
content = this.content
|
||||
)
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
package net.xintanalabs.rssotto.controller.article
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
data class ArticleResponse(
|
||||
val id: UUID,
|
||||
val title: String,
|
||||
val content: String
|
||||
)
|
@@ -0,0 +1,28 @@
|
||||
package net.xintanalabs.rssotto.controller.auth
|
||||
|
||||
import net.xintanalabs.rssotto.service.AuthenticationService
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
class AuthController(
|
||||
private val authenticationService: AuthenticationService
|
||||
) {
|
||||
|
||||
@PostMapping
|
||||
suspend fun authenticate(@RequestBody authRequest: AuthenticationRequest) : AuthenticationResponse =
|
||||
authenticationService.authenticate(authRequest)
|
||||
|
||||
@PostMapping("/refresh")
|
||||
suspend fun refresh(@RequestBody request: RefreshTokenRequest) : TokenResponse =
|
||||
authenticationService.refreshAccessToken(request.token)
|
||||
?.mapToTokenResponse()
|
||||
?: throw ResponseStatusException(HttpStatus.FORBIDDEN,"Invalid refresh token")
|
||||
|
||||
private fun String.mapToTokenResponse(): TokenResponse = TokenResponse(this)
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
package net.xintanalabs.rssotto.controller.auth
|
||||
|
||||
data class AuthenticationRequest (
|
||||
val username: String,
|
||||
val password: String
|
||||
)
|
@@ -0,0 +1,6 @@
|
||||
package net.xintanalabs.rssotto.controller.auth
|
||||
|
||||
data class AuthenticationResponse (
|
||||
val accessToken: String,
|
||||
val refreshToken: String
|
||||
)
|
@@ -0,0 +1,5 @@
|
||||
package net.xintanalabs.rssotto.controller.auth
|
||||
|
||||
data class RefreshTokenRequest(
|
||||
val token: String,
|
||||
)
|
@@ -0,0 +1,5 @@
|
||||
package net.xintanalabs.rssotto.controller.auth
|
||||
|
||||
data class TokenResponse (
|
||||
val token: String
|
||||
)
|
@@ -0,0 +1,49 @@
|
||||
package net.xintanalabs.rssotto.controller.source
|
||||
|
||||
import net.xintanalabs.rssotto.constants.Constants
|
||||
import net.xintanalabs.rssotto.model.Source
|
||||
import net.xintanalabs.rssotto.service.SourceService
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
|
||||
@RestController
|
||||
@RequestMapping("${Constants.API_BASE_PATH}/source")
|
||||
class SourceController(private val sourceService: SourceService) {
|
||||
@PostMapping
|
||||
suspend fun create(@RequestBody sourceRequest: SourceRequest): SourceResponse {
|
||||
return try {
|
||||
sourceService.create(sourceRequest.toModel())
|
||||
.toResponse()
|
||||
} catch (e: Exception) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot create source: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
fun getAll(): List<Source> {
|
||||
return try {
|
||||
sourceService.findALl()
|
||||
} catch (e: Exception) {
|
||||
throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Cannot retrieve sources: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Source.toResponse(): SourceResponse = SourceResponse(
|
||||
id = this.id,
|
||||
name = this.name,
|
||||
checkerType = this.checkerType,
|
||||
defaults = this.defaults,
|
||||
)
|
||||
|
||||
private fun SourceRequest.toModel(): Source = Source(
|
||||
id = null,
|
||||
name = this.name,
|
||||
checkerType = this.checkerType,
|
||||
defaults = this.defaults,
|
||||
)
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
package net.xintanalabs.rssotto.controller.source
|
||||
|
||||
data class SourceRequest(
|
||||
val name: String,
|
||||
val checkerType: String,
|
||||
val defaults: Map<String, String> = mapOf()
|
||||
)
|
@@ -0,0 +1,8 @@
|
||||
package net.xintanalabs.rssotto.controller.source
|
||||
|
||||
data class SourceResponse(
|
||||
val id: String?,
|
||||
val name: String,
|
||||
val checkerType: String,
|
||||
val defaults: Map<String, String> = mapOf()
|
||||
)
|
@@ -0,0 +1,62 @@
|
||||
package net.xintanalabs.rssotto.controller.user
|
||||
|
||||
import net.xintanalabs.rssotto.model.Role
|
||||
import net.xintanalabs.rssotto.model.User
|
||||
import net.xintanalabs.rssotto.service.UserService
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.DeleteMapping
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import java.util.UUID
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/user")
|
||||
class UserController(private val userService: UserService) {
|
||||
|
||||
@PostMapping
|
||||
fun create(@RequestBody userRequest: UserRequest): UserResponse? =
|
||||
userService.createUser(userRequest.toModel())
|
||||
?.toResponse()
|
||||
?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot create user")
|
||||
|
||||
@GetMapping
|
||||
fun listAll(): List<UserResponse> = listOf()
|
||||
//userService.findAll().map { it.toResponse() }
|
||||
|
||||
@GetMapping("/{uuid}")
|
||||
fun findById(@PathVariable uuid: UUID): UserResponse {
|
||||
val user = userService.findById(uuid);
|
||||
return user
|
||||
?.toResponse()
|
||||
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "User not found")
|
||||
}
|
||||
|
||||
@DeleteMapping("/{uuid}")
|
||||
fun deleteById(@PathVariable uuid: UUID): ResponseEntity<Boolean> {
|
||||
val success = userService.deleteById(uuid)
|
||||
return if (success)
|
||||
ResponseEntity.noContent().build<Boolean>()
|
||||
else
|
||||
throw ResponseStatusException(HttpStatus.NOT_FOUND, "User not found")
|
||||
}
|
||||
|
||||
|
||||
private fun UserRequest.toModel() = User(
|
||||
id = UUID.randomUUID(),
|
||||
username = this.username,
|
||||
password = this.password,
|
||||
role = Role.USER
|
||||
)
|
||||
|
||||
private fun User.toResponse(): UserResponse = UserResponse(
|
||||
id = this.id,
|
||||
username = this.username
|
||||
)
|
||||
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
package net.xintanalabs.rssotto.controller.user
|
||||
|
||||
data class UserRequest(
|
||||
val username: String,
|
||||
val password: String
|
||||
)
|
@@ -0,0 +1,8 @@
|
||||
package net.xintanalabs.rssotto.controller.user
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
data class UserResponse(
|
||||
val id: UUID,
|
||||
val username: String
|
||||
)
|
@@ -0,0 +1,31 @@
|
||||
package net.xintanalabs.rssotto.db.mongodb
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
|
||||
abstract class MongoDBAbstract<T: Any> (
|
||||
private val mongoDBClient: MongoDBClient
|
||||
) {
|
||||
protected abstract val collection: String
|
||||
protected abstract val entityClass: Class<T>
|
||||
protected val idField: String = "id"
|
||||
|
||||
protected fun create(entity: T): T {
|
||||
return mongoDBClient.insert(collection, entity, entityClass)
|
||||
}
|
||||
|
||||
protected fun getAll(): List<T> {
|
||||
return mongoDBClient.findAll(collection, entityClass)
|
||||
}
|
||||
|
||||
protected fun getById(id: String): T? {
|
||||
return mongoDBClient.findOne(collection, idField, id, entityClass)
|
||||
}
|
||||
|
||||
protected fun delete(id: String): Long {
|
||||
return mongoDBClient.deleteOne(collection, idField, id, entityClass)
|
||||
}
|
||||
|
||||
protected fun update(id: String, updateFields: Map<String, Any>): Long {
|
||||
return mongoDBClient.updateOne(collection, idField, id, updateFields, entityClass)
|
||||
}
|
||||
}
|
@@ -0,0 +1,70 @@
|
||||
package net.xintanalabs.rssotto.db.mongodb
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import org.springframework.data.mongodb.core.MongoTemplate
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation
|
||||
import org.springframework.data.mongodb.core.query.Criteria
|
||||
import org.springframework.data.mongodb.core.query.Query
|
||||
import org.springframework.data.mongodb.core.query.Update
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class MongoDBClient(private val mongoTemplate: MongoTemplate) {
|
||||
|
||||
// Insert a single document
|
||||
fun <T : Any> insert(collectionName: String, document: T, clazz: Class<T>): T {
|
||||
return mongoTemplate.insert(document, collectionName)
|
||||
}
|
||||
|
||||
// Insert multiple documents
|
||||
fun <T : Any> insertMany(collectionName: String, documents: List<T>, clazz: Class<T>): List<T> {
|
||||
return mongoTemplate.insert(documents, collectionName).toList()
|
||||
}
|
||||
|
||||
// Find one document by a field
|
||||
fun <T : Any> findOne(collectionName: String, field: String, value: Any, clazz: Class<T>): T? {
|
||||
val query = Query(Criteria.where(field).`is`(value))
|
||||
return mongoTemplate.findOne(query, clazz, collectionName)
|
||||
}
|
||||
|
||||
// Find all documents in a collection
|
||||
fun <T : Any> findAll(collectionName: String, clazz: Class<T>): List<T> {
|
||||
return mongoTemplate.findAll(clazz, collectionName)
|
||||
}
|
||||
|
||||
// Find documents with a custom filter
|
||||
fun <T : Any> findByFilter(collectionName: String, criteria: Criteria, clazz: Class<T>): List<T> {
|
||||
val query = Query(criteria)
|
||||
return mongoTemplate.find(query, clazz, collectionName)
|
||||
}
|
||||
|
||||
// Update one document
|
||||
fun <T : Any> updateOne(collectionName: String, field: String, value: Any, updateFields: Map<String, Any>, clazz: Class<T>): Long {
|
||||
val query = Query(Criteria.where(field).`is`(value))
|
||||
val update = Update().apply {
|
||||
updateFields.forEach { (k, v) -> set(k, v) }
|
||||
}
|
||||
return mongoTemplate.updateFirst(query, update, clazz, collectionName).modifiedCount
|
||||
}
|
||||
|
||||
// Delete one document
|
||||
fun <T : Any> deleteOne(collectionName: String, field: String, value: Any, clazz: Class<T>): Long {
|
||||
val query = Query(Criteria.where(field).`is`(value))
|
||||
return mongoTemplate.remove(query, clazz, collectionName).deletedCount
|
||||
}
|
||||
|
||||
fun <T: Any, R: Any> aggregate(
|
||||
collectionName: String,
|
||||
aggregation: Aggregation,
|
||||
inputType: Class<T>,
|
||||
outputType: Class<R>
|
||||
): Flow<R> {
|
||||
return try {
|
||||
val results = mongoTemplate.aggregate(aggregation, collectionName, outputType)
|
||||
results.asFlow()
|
||||
} catch (e: Exception) {
|
||||
throw RuntimeException("Aggregation failed: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
}
|
9
src/main/kotlin/net/xintanalabs/rssotto/model/Article.kt
Normal file
9
src/main/kotlin/net/xintanalabs/rssotto/model/Article.kt
Normal file
@@ -0,0 +1,9 @@
|
||||
package net.xintanalabs.rssotto.model
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
data class Article(
|
||||
val id: UUID,
|
||||
val title: String,
|
||||
val content: String
|
||||
)
|
16
src/main/kotlin/net/xintanalabs/rssotto/model/Field.kt
Normal file
16
src/main/kotlin/net/xintanalabs/rssotto/model/Field.kt
Normal file
@@ -0,0 +1,16 @@
|
||||
package net.xintanalabs.rssotto.model
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
import org.springframework.data.annotation.Id
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
data class Field(
|
||||
@Id val id: String? = null,
|
||||
val name: String,
|
||||
val label: String,
|
||||
val description: String? = null,
|
||||
val type: String = "string",
|
||||
val required: Boolean = false,
|
||||
val controlType: String = "text",
|
||||
val defaultValue: String? = null
|
||||
)
|
14
src/main/kotlin/net/xintanalabs/rssotto/model/Source.kt
Normal file
14
src/main/kotlin/net/xintanalabs/rssotto/model/Source.kt
Normal file
@@ -0,0 +1,14 @@
|
||||
package net.xintanalabs.rssotto.model
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
import org.springframework.data.annotation.Id
|
||||
import org.springframework.data.mongodb.core.mapping.Field
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
data class Source(
|
||||
@Id val id: String? = null,
|
||||
val name: String,
|
||||
@Field("type")
|
||||
val checkerType: String,
|
||||
val defaults: Map<String, String> = mapOf()
|
||||
)
|
13
src/main/kotlin/net/xintanalabs/rssotto/model/Type.kt
Normal file
13
src/main/kotlin/net/xintanalabs/rssotto/model/Type.kt
Normal file
@@ -0,0 +1,13 @@
|
||||
package net.xintanalabs.rssotto.model
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
import org.springframework.data.annotation.Id
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
data class Type(
|
||||
@Id val id: String? = null,
|
||||
val shortName: String,
|
||||
val name: String,
|
||||
val configFields: List<Field>,
|
||||
val appFields: List<Field>
|
||||
)
|
15
src/main/kotlin/net/xintanalabs/rssotto/model/User.kt
Normal file
15
src/main/kotlin/net/xintanalabs/rssotto/model/User.kt
Normal file
@@ -0,0 +1,15 @@
|
||||
package net.xintanalabs.rssotto.model
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
data class User(
|
||||
val id: UUID,
|
||||
val username: String,
|
||||
val password: String,
|
||||
val role: Role
|
||||
)
|
||||
|
||||
|
||||
enum class Role {
|
||||
USER, ADMIN
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
package net.xintanalabs.rssotto.repository
|
||||
|
||||
import net.xintanalabs.rssotto.model.Article
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.util.UUID
|
||||
|
||||
@Repository
|
||||
class ArticleRepository {
|
||||
private val articles = mutableListOf<Article>(
|
||||
Article(UUID.randomUUID(), "Title 1", "Content 1"),
|
||||
Article(UUID.randomUUID(), "Title 2", "Content 2"),
|
||||
Article(UUID.randomUUID(), "Title 3", "Content 3")
|
||||
)
|
||||
|
||||
fun findAll(): List<Article> = articles
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
package net.xintanalabs.rssotto.repository
|
||||
|
||||
import org.springframework.security.core.userdetails.UserDetails
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
class RefreshTokenRepository {
|
||||
private val tokens = mutableMapOf<String, UserDetails>()
|
||||
|
||||
fun findUserDetailsByToken(token: String): UserDetails? =
|
||||
tokens[token]
|
||||
|
||||
fun save(token: String, userDetails: UserDetails) {
|
||||
tokens[token] = userDetails
|
||||
}
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
package net.xintanalabs.rssotto.repository
|
||||
|
||||
import net.xintanalabs.rssotto.constants.Constants
|
||||
import net.xintanalabs.rssotto.db.mongodb.MongoDBAbstract
|
||||
import net.xintanalabs.rssotto.db.mongodb.MongoDBClient
|
||||
import net.xintanalabs.rssotto.model.Source
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
class SourceRepository (
|
||||
mongoDBClient: MongoDBClient
|
||||
): MongoDBAbstract<Source>(mongoDBClient) {
|
||||
override val collection: String = Constants.COLLECTION_SOURCES
|
||||
override val entityClass: Class<Source> = Source::class.java
|
||||
|
||||
fun createSource(source: Source): Source {
|
||||
return create(source)
|
||||
}
|
||||
|
||||
fun getAllSources(): List<Source> {
|
||||
return getAll()
|
||||
}
|
||||
|
||||
fun getSourceById(id: String): Source? {
|
||||
return getById(id)
|
||||
}
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
package net.xintanalabs.rssotto.repository
|
||||
|
||||
import net.xintanalabs.rssotto.constants.Constants
|
||||
import net.xintanalabs.rssotto.db.mongodb.MongoDBAbstract
|
||||
import net.xintanalabs.rssotto.db.mongodb.MongoDBClient
|
||||
import net.xintanalabs.rssotto.model.Type
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
class TypeRepository(mongoDBClient: MongoDBClient): MongoDBAbstract<Type>(mongoDBClient) {
|
||||
override val collection: String = Constants.COLLECTION_TYPES
|
||||
override val entityClass: Class<Type> = Type::class.java
|
||||
|
||||
suspend fun createType(type: Type): Type {
|
||||
return create(type)
|
||||
}
|
||||
|
||||
suspend fun getAllTypes(): List<Type> {
|
||||
return getAll()
|
||||
}
|
||||
|
||||
suspend fun getTypeById(id: String): Type? {
|
||||
return getById(id)
|
||||
}
|
||||
}
|
@@ -0,0 +1,48 @@
|
||||
package net.xintanalabs.rssotto.repository
|
||||
|
||||
import net.xintanalabs.rssotto.model.Role
|
||||
import net.xintanalabs.rssotto.model.User
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.util.UUID
|
||||
|
||||
@Repository
|
||||
class UserRepository(
|
||||
private val encoder: PasswordEncoder
|
||||
) {
|
||||
private val users = mutableListOf<User>(
|
||||
User(
|
||||
UUID.randomUUID(),
|
||||
"admin",
|
||||
encoder.encode("admin"),
|
||||
Role.ADMIN),
|
||||
User(
|
||||
UUID.randomUUID(),
|
||||
"user",
|
||||
encoder.encode("user"),
|
||||
Role.USER),
|
||||
User(
|
||||
UUID.randomUUID(),
|
||||
"test",
|
||||
encoder.encode("test"),
|
||||
Role.USER)
|
||||
)
|
||||
|
||||
fun save(user: User): Boolean {
|
||||
val updated = user.copy(password = encoder.encode(user.password))
|
||||
return users.add(updated)
|
||||
}
|
||||
|
||||
fun findByUsername(username: String): User? =
|
||||
users.find { it.username == username }
|
||||
|
||||
fun findAll(): List<User> =
|
||||
users.toList()
|
||||
|
||||
fun findById(id: UUID): User? =
|
||||
users.find { it.id == id }
|
||||
|
||||
fun deleteById(id: UUID): Boolean =
|
||||
users.removeIf { it.id == id }
|
||||
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
package net.xintanalabs.rssotto.service
|
||||
|
||||
import net.xintanalabs.rssotto.model.Article
|
||||
import net.xintanalabs.rssotto.repository.ArticleRepository
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
class ArticleService(
|
||||
private val articleRepository: ArticleRepository
|
||||
) {
|
||||
|
||||
fun findAll(): List<Article> =
|
||||
articleRepository.findAll()
|
||||
}
|
@@ -0,0 +1,61 @@
|
||||
package net.xintanalabs.rssotto.service
|
||||
|
||||
import net.xintanalabs.rssotto.config.JwtProperties
|
||||
import net.xintanalabs.rssotto.controller.auth.AuthenticationRequest
|
||||
import net.xintanalabs.rssotto.controller.auth.AuthenticationResponse
|
||||
import net.xintanalabs.rssotto.repository.RefreshTokenRepository
|
||||
import org.springframework.security.authentication.AuthenticationManager
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||
import org.springframework.security.core.userdetails.UserDetails
|
||||
import org.springframework.stereotype.Service
|
||||
import java.util.Date
|
||||
|
||||
@Service
|
||||
class AuthenticationService(
|
||||
private val authManager: AuthenticationManager,
|
||||
private val userDetailsService: CustomUserDetailsService,
|
||||
private val tokenService: TokenService,
|
||||
private val jwtProperties: JwtProperties,
|
||||
private val refreshTokenRepository: RefreshTokenRepository
|
||||
) {
|
||||
fun authenticate(authRequest: AuthenticationRequest): AuthenticationResponse {
|
||||
authManager.authenticate(
|
||||
UsernamePasswordAuthenticationToken(authRequest.username, authRequest.password)
|
||||
)
|
||||
|
||||
val user = userDetailsService.loadUserByUsername(authRequest.username)
|
||||
val accessToken = generateAccessToken(user)
|
||||
val refreshToken = generateRefreshToken(user)
|
||||
|
||||
refreshTokenRepository.save(refreshToken, user)
|
||||
|
||||
return AuthenticationResponse(
|
||||
accessToken = accessToken,
|
||||
refreshToken = refreshToken
|
||||
)
|
||||
}
|
||||
|
||||
private fun generateRefreshToken(user: UserDetails): String = tokenService.generate(
|
||||
user,
|
||||
Date(System.currentTimeMillis() + jwtProperties.refreshTokenExpiration)
|
||||
)
|
||||
|
||||
private fun generateAccessToken(user: UserDetails): String = tokenService.generate(
|
||||
user,
|
||||
Date(System.currentTimeMillis() + jwtProperties.accessTokenExpiration)
|
||||
)
|
||||
|
||||
fun refreshAccessToken(token: String): String? {
|
||||
val extractedUsername = tokenService.extractUsername(token)
|
||||
return extractedUsername?.let {
|
||||
val currentUserDetails = userDetailsService.loadUserByUsername(it)
|
||||
val refreshTokenUserDetails = refreshTokenRepository.findUserDetailsByToken(token)
|
||||
if (!tokenService.isExpired(token) && currentUserDetails.username == refreshTokenUserDetails?.username) {
|
||||
generateAccessToken(currentUserDetails)
|
||||
} else
|
||||
null
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
package net.xintanalabs.rssotto.service
|
||||
|
||||
|
||||
import net.xintanalabs.rssotto.repository.UserRepository
|
||||
import org.springframework.security.core.userdetails.User
|
||||
import org.springframework.security.core.userdetails.UserDetails
|
||||
import org.springframework.security.core.userdetails.UserDetailsService
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
typealias ApplicationUser = net.xintanalabs.rssotto.model.User
|
||||
|
||||
@Service
|
||||
class CustomUserDetailsService(
|
||||
private val userRepository: UserRepository
|
||||
): UserDetailsService {
|
||||
|
||||
override fun loadUserByUsername(username: String): UserDetails =
|
||||
userRepository.findByUsername(username)
|
||||
?.mapToUserDetails()
|
||||
?: throw UsernameNotFoundException("User not found: $username")
|
||||
|
||||
fun ApplicationUser.mapToUserDetails(): UserDetails =
|
||||
User.builder()
|
||||
.username(this.username)
|
||||
.password(this.password)
|
||||
.roles(this.role.name)
|
||||
.build()
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
package net.xintanalabs.rssotto.service
|
||||
|
||||
import net.xintanalabs.rssotto.model.Source
|
||||
import net.xintanalabs.rssotto.repository.SourceRepository
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
class SourceService(
|
||||
private val sourceRepository: SourceRepository
|
||||
) {
|
||||
fun findALl(): List<Source> {
|
||||
return sourceRepository.getAllSources()
|
||||
}
|
||||
|
||||
fun create(source: Source): Source {
|
||||
return sourceRepository.createSource(source)
|
||||
}
|
||||
|
||||
fun findById(id: String): Source? {
|
||||
return sourceRepository.getSourceById(id)
|
||||
}
|
||||
}
|
@@ -0,0 +1,54 @@
|
||||
package net.xintanalabs.rssotto.service
|
||||
|
||||
import io.jsonwebtoken.Claims
|
||||
import io.jsonwebtoken.Jwts
|
||||
import io.jsonwebtoken.security.Keys
|
||||
import net.xintanalabs.rssotto.config.JwtProperties
|
||||
import org.springframework.security.core.userdetails.UserDetails
|
||||
import org.springframework.stereotype.Service
|
||||
import java.util.Date
|
||||
|
||||
@Service
|
||||
class TokenService(
|
||||
jwtProperties: JwtProperties
|
||||
) {
|
||||
private val secretKey = Keys.hmacShaKeyFor(
|
||||
jwtProperties.key.toByteArray()
|
||||
)
|
||||
|
||||
fun generate(
|
||||
userDetails: UserDetails,
|
||||
expirationDate: Date,
|
||||
additionalClaims: Map<String, Any> = emptyMap()
|
||||
): String {
|
||||
return Jwts.builder()
|
||||
.claims()
|
||||
.subject(userDetails.username)
|
||||
.issuedAt(Date(System.currentTimeMillis()))
|
||||
.expiration(expirationDate)
|
||||
.add(additionalClaims)
|
||||
.and()
|
||||
.signWith(secretKey)
|
||||
.compact()
|
||||
}
|
||||
|
||||
fun extractUsername(token: String): String? {
|
||||
return getAllClaims(token).subject
|
||||
}
|
||||
|
||||
fun isExpired(token: String): Boolean {
|
||||
return getAllClaims(token).expiration.before(Date(System.currentTimeMillis()))
|
||||
}
|
||||
|
||||
fun isValid(token: String, userDetails: UserDetails): Boolean {
|
||||
val username = extractUsername(token)
|
||||
return (username == userDetails.username) && !isExpired(token)
|
||||
}
|
||||
|
||||
private fun getAllClaims(token: String): Claims {
|
||||
val parser = Jwts.parser()
|
||||
.verifyWith(secretKey)
|
||||
.build()
|
||||
return parser.parseSignedClaims(token).payload
|
||||
}
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
package net.xintanalabs.rssotto.service
|
||||
|
||||
import net.xintanalabs.rssotto.model.User
|
||||
import net.xintanalabs.rssotto.repository.UserRepository
|
||||
import org.springframework.stereotype.Service
|
||||
import java.util.UUID
|
||||
|
||||
@Service
|
||||
class UserService(private val userRepository: UserRepository) {
|
||||
fun createUser(user: User): User? {
|
||||
val found = userRepository.findByUsername(user.username)
|
||||
return if (found == null && userRepository.save(user)) user else null
|
||||
}
|
||||
|
||||
fun findById(id: UUID): User? =
|
||||
userRepository.findById(id)
|
||||
|
||||
fun findAll(): List<User> =
|
||||
userRepository.findAll()
|
||||
|
||||
fun deleteById(id: UUID): Boolean =
|
||||
userRepository.deleteById(id)
|
||||
|
||||
fun findByUsername(username: String): User? =
|
||||
userRepository.findByUsername(username)
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
package net.xintanalabs.rssotto.tasks
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.scheduling.annotation.Scheduled
|
||||
import org.springframework.stereotype.Component
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
|
||||
@Component
|
||||
class ScheduledTasks {
|
||||
private val log = LoggerFactory.getLogger(ScheduledTasks::class.java)
|
||||
private val dateFormat = SimpleDateFormat("HH:mm:ss")
|
||||
|
||||
@Scheduled(fixedRate = 500000)
|
||||
fun reportCurrentTime() {
|
||||
log.info("The time is now {}", dateFormat.format(Date()))
|
||||
}
|
||||
}
|
31
src/main/resources/application.yaml
Normal file
31
src/main/resources/application.yaml
Normal file
@@ -0,0 +1,31 @@
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: rssotto
|
||||
data:
|
||||
mongodb:
|
||||
uri: mongodb://root:A%3Ay(nW%3C06Gu%5D*Q8%5DA%40j)@192.168.1.115:27017/
|
||||
database: rssotto
|
||||
|
||||
jwt:
|
||||
key: ${JWT_KEY}
|
||||
# QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm1234==
|
||||
refresh-key: ${JWT_REFRESH_KEY}
|
||||
# ZXCVBNMASDFGHJKLQWERTYUIOPzxcvbnmasdfghjklqwertyuiop5678==
|
||||
access-token-expiration: 3600000 # 1 hour
|
||||
refresh-token-expiration: 604800000 # 7 days
|
||||
|
||||
logging:
|
||||
level:
|
||||
org:
|
||||
springframework:
|
||||
data:
|
||||
mongodb: DEBUG
|
||||
security: DEBUG
|
||||
|
||||
version-checker:
|
||||
interval-minutes: 5
|
||||
|
||||
|
@@ -0,0 +1,13 @@
|
||||
package net.xintanalabs.rssotto
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
|
||||
@SpringBootTest
|
||||
class RssottoApplicationTests {
|
||||
|
||||
@Test
|
||||
fun contextLoads() {
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user