Init commit

This commit is contained in:
Evgenii Saenko
2025-10-15 12:01:54 -07:00
commit c470f766e0
25 changed files with 886 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
package cc.essaenko
import io.ktor.server.application.*
fun main(args: Array<String>) {
io.ktor.server.netty.EngineMain.main(args)
}
fun Application.module() {
configureSerialization()
configureSecurity()
configureHTTP()
configureRouting()
configureDatabase()
}

View File

@@ -0,0 +1,38 @@
package cc.essaenko
import io.github.cdimascio.dotenv.dotenv
import io.ktor.server.application.Application
import org.ktorm.database.Database
import io.ktor.server.application.*
import io.ktor.util.AttributeKey
val DatabaseKey = AttributeKey<Database>("Database")
val Application.database: Database
get() = attributes[DatabaseKey]
val ApplicationCall.database: Database
get() = application.database
fun Application.configureDatabase() {
val env = dotenv {
ignoreIfMissing = true
}
val host = env["DB_HOST"] ?: "localhost"
val port = env["DB_PORT"] ?: "5432"
val name = env["DB_NAME"] ?: "db"
val user = env["DB_USER"] ?: "user"
val password = env["DB_PASSWORD"] ?: "password"
val url = "jdbc:postgresql://$host:$port/$name"
val database = Database.connect(
url = url,
driver = "org.postgresql.Driver",
user = user,
password = password
)
attributes.put(DatabaseKey, database)
}

45
src/main/kotlin/HTTP.kt Normal file
View File

@@ -0,0 +1,45 @@
package cc.essaenko
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import io.ktor.server.plugins.cachingheaders.*
import io.ktor.server.plugins.compression.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.plugins.cors.routing.*
import io.ktor.server.plugins.defaultheaders.*
import io.ktor.server.plugins.swagger.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Application.configureHTTP() {
routing {
swaggerUI(path = "openapi")
}
install(DefaultHeaders) {
header("X-Engine", "Ktor") // will send this header with each response
}
install(CORS) {
allowMethod(HttpMethod.Options)
allowMethod(HttpMethod.Put)
allowMethod(HttpMethod.Delete)
allowMethod(HttpMethod.Patch)
allowHeader(HttpHeaders.Authorization)
allowHeader("MyCustomHeader")
anyHost() // @TODO: Don't do this in production if possible. Try to limit it.
}
install(Compression)
install(CachingHeaders) {
options { call, outgoingContent ->
when (outgoingContent.contentType?.withoutParameters()) {
ContentType.Text.CSS -> CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 24 * 60 * 60))
else -> null
}
}
}
}

View File

@@ -0,0 +1,14 @@
package cc.essaenko
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Application.configureRouting() {
routing {
get("/") {
call.respondText("Hello World!")
}
}
}

View File

@@ -0,0 +1,41 @@
package cc.essaenko
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import io.ktor.server.plugins.cachingheaders.*
import io.ktor.server.plugins.compression.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.plugins.cors.routing.*
import io.ktor.server.plugins.defaultheaders.*
import io.ktor.server.plugins.swagger.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Application.configureSecurity() {
// Please read the jwt property from the config file if you are using EngineMain
val jwtAudience = "jwt-audience"
val jwtDomain = "https://jwt-provider-domain/"
val jwtRealm = "ktor sample app"
val jwtSecret = "secret"
authentication {
jwt {
realm = jwtRealm
verifier(
JWT
.require(Algorithm.HMAC256(jwtSecret))
.withAudience(jwtAudience)
.withIssuer(jwtDomain)
.build()
)
validate { credential ->
if (credential.payload.audience.contains(jwtAudience)) JWTPrincipal(credential.payload) else null
}
}
}
}

View File

@@ -0,0 +1,29 @@
package cc.essaenko
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import io.ktor.server.plugins.cachingheaders.*
import io.ktor.server.plugins.compression.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.plugins.cors.routing.*
import io.ktor.server.plugins.defaultheaders.*
import io.ktor.server.plugins.swagger.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Application.configureSerialization() {
install(ContentNegotiation) {
json()
}
routing {
get("/json/kotlinx-serialization") {
call.respond(mapOf("hello" to "world"))
}
}
}

View File

@@ -0,0 +1,27 @@
package cc.essaenko.model
import org.ktorm.database.Database
import org.ktorm.entity.Entity
import org.ktorm.entity.sequenceOf
import java.time.LocalDateTime
import org.ktorm.schema.*
interface Admin : Entity<Admin> {
companion object : Entity.Factory<Admin>()
var id: Long
var username: String
var passwordHash: String
var createdAt: LocalDateTime
var lastLoginAt: LocalDateTime?
}
object Admins : Table<Admin>("t_admins") {
val id = long("id").primaryKey().bindTo { it.id }
val username = varchar("username").bindTo { it.username }
val passwordHash = varchar("password_hash").bindTo { it.passwordHash }
val createdAt = datetime("created_at").bindTo { it.createdAt }
val lastLoginAt = datetime("last_login_at").bindTo { it.lastLoginAt }
}
val Database.admins get() = this.sequenceOf(Admins)

View File

@@ -0,0 +1,36 @@
package cc.essaenko.model
import org.ktorm.database.Database
import org.ktorm.entity.Entity
import org.ktorm.entity.sequenceOf
import java.time.LocalDateTime
import org.ktorm.schema.*
interface News : Entity<News> {
companion object : Entity.Factory<News>()
var id: Long
var title: String
var slug: String
var summary: String
var content: String
var status: String // "DRAFT" | "PUBLISHED" | "ARCHIVED"
var publishedAt: LocalDateTime?
var imageUrl: String?
var createdAt: LocalDateTime
var updatedAt: LocalDateTime
}
object NewsT : Table<News>("t_news") {
val id = long("id").primaryKey().bindTo { it.id }
val title = varchar("title").bindTo { it.title }
val slug = varchar("slug").bindTo { it.slug }
val summary = varchar("summary").bindTo { it.summary }
val content = text("content").bindTo { it.content }
val status = varchar("status").bindTo { it.status }
val publishedAt = datetime("published_at").bindTo { it.publishedAt }
val imageUrl = varchar("image_url").bindTo { it.imageUrl }
val createdAt = datetime("created_at").bindTo { it.createdAt }
val updatedAt = datetime("updated_at").bindTo { it.updatedAt }
}
val Database.news get() = this.sequenceOf(NewsT)

View File

@@ -0,0 +1,38 @@
package cc.essaenko.model
import org.ktorm.database.Database
import org.ktorm.entity.Entity
import org.ktorm.entity.sequenceOf
import java.time.LocalDateTime
import java.math.BigDecimal
import org.ktorm.schema.*
interface Service : Entity<Service> {
companion object : Entity.Factory<Service>()
var id: Long
var title: String
var slug: String
var description: String
var priceFrom: BigDecimal?
var imageUrl: String?
var status: String // "PUBLISHED" | "DRAFT" | "ARCHIVED"
var categoryId: Long? // связь по FK
var createdAt: LocalDateTime
var updatedAt: LocalDateTime
}
object Services : Table<Service>("t_services") {
val id = long("id").primaryKey().bindTo { it.id }
val title = varchar("title").bindTo { it.title }
val slug = varchar("slug").bindTo { it.slug }
val description = text("description").bindTo { it.description }
val priceFrom = decimal("price_from").bindTo { it.priceFrom }
val imageUrl = varchar("image_url").bindTo { it.imageUrl }
val status = varchar("status").bindTo { it.status }
val categoryId = long("category_id").bindTo { it.categoryId }
val createdAt = datetime("created_at").bindTo { it.createdAt }
val updatedAt = datetime("updated_at").bindTo { it.updatedAt }
}
val Database.service get() = this.sequenceOf(Services)

View File

@@ -0,0 +1,25 @@
package cc.essaenko.model
import org.ktorm.database.Database
import org.ktorm.entity.Entity
import org.ktorm.entity.sequenceOf
import org.ktorm.schema.*
interface ServiceCategory : Entity<ServiceCategory> {
companion object : Entity.Factory<ServiceCategory>()
var id: Long
var name: String
var slug: String
var sortOrder: Int
}
object ServiceCategories : Table<ServiceCategory>("t_service_categories") {
val id = long("id").primaryKey().bindTo { it.id }
val name = varchar("name").bindTo { it.name }
val slug = varchar("slug").bindTo { it.slug }
val sortOrder = int("sort_order").bindTo { it.sortOrder }
}
val Database.serviceCategories get() = this.sequenceOf(ServiceCategories)

View File

@@ -0,0 +1,31 @@
package cc.essaenko.model
import org.ktorm.database.Database
import org.ktorm.entity.*
import java.time.LocalDateTime
import org.ktorm.schema.*
// Холодная база (все заявки)
interface User : Entity<User> {
companion object : Entity.Factory<User>()
var id: Long
var fullName: String
var email: String
var phone: String?
var message: String
var sourcePage: String?
var createdAt: LocalDateTime
}
object Users : Table<User>("t_users") {
val id = long("id").primaryKey().bindTo { it.id }
val fullName = varchar("full_name").bindTo { it.fullName }
val email = varchar("email").bindTo { it.email }
val phone = varchar("phone").bindTo { it.phone }
val message = text("message").bindTo { it.message }
val sourcePage = varchar("source_page").bindTo { it.sourcePage }
val createdAt = datetime("created_at").bindTo { it.createdAt }
}
val Database.users get() = this.sequenceOf(Users);

View File

@@ -0,0 +1,10 @@
ktor:
application:
modules:
- cc.essaenko.ApplicationKt.module
deployment:
port: 8080
jwt:
domain: "https://jwt-provider-domain/"
audience: "jwt-audience"
realm: "ktor sample app"

View File

@@ -0,0 +1,12 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
<logger name="org.eclipse.jetty" level="INFO"/>
<logger name="io.netty" level="INFO"/>
</configuration>

View File

@@ -0,0 +1,23 @@
openapi: "3.0.3"
info:
title: "Application API"
description: "Application API"
version: "1.0.0"
servers:
- url: "http://0.0.0.0:8080"
paths:
/:
get:
description: "Hello World!"
responses:
"200":
description: "OK"
content:
text/plain:
schema:
type: "string"
examples:
Example#1:
value: "Hello World!"
components:
schemas: {}

View File

@@ -0,0 +1,21 @@
package cc.essaenko
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.server.testing.*
import kotlin.test.Test
import kotlin.test.assertEquals
class ApplicationTest {
@Test
fun testRoot() = testApplication {
application {
module()
}
client.get("/").apply {
assertEquals(HttpStatusCode.OK, status)
}
}
}