diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts new file mode 100644 index 0000000..6a6783b --- /dev/null +++ b/backend/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + kotlin("multiplatform") + kotlin("plugin.spring") version "1.9.25" + id("org.springframework.boot") version "3.3.3" + id("io.spring.dependency-management") version "1.1.6" + kotlin("plugin.jpa") version "1.9.25" +} + +kotlin { + jvm() + sourceSets { + jvmMain.dependencies { + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation("org.springframework.boot:spring-boot-starter-oauth2-client") + // implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.springframework.boot:spring-boot-starter-validation") + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("io.jsonwebtoken:jjwt-api:0.11.5") + implementation("com.h2database:h2:2.3.232") + runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5") + runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.5") + } + + jvmToolchain { + languageVersion.set(JavaLanguageVersion.of(21)) + } + } + + tasks.withType { + useJUnitPlatform() + } +} diff --git a/backend/src/jvmMain/kotlin/ru/posidata/backend/BackendApplication.kt b/backend/src/jvmMain/kotlin/ru/posidata/backend/BackendApplication.kt new file mode 100644 index 0000000..9c789f4 --- /dev/null +++ b/backend/src/jvmMain/kotlin/ru/posidata/backend/BackendApplication.kt @@ -0,0 +1,13 @@ +package ru.posidata.backend + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication +import org.springframework.context.annotation.ComponentScan + +@SpringBootApplication +@ComponentScan(basePackages = ["ru.posidata"]) +class BackendApplication + +fun main(args: Array) { + runApplication(*args) +} diff --git a/backend/src/jvmMain/kotlin/ru/posidata/backend/controller/TelegramAuthController.kt b/backend/src/jvmMain/kotlin/ru/posidata/backend/controller/TelegramAuthController.kt new file mode 100644 index 0000000..e36ef0e --- /dev/null +++ b/backend/src/jvmMain/kotlin/ru/posidata/backend/controller/TelegramAuthController.kt @@ -0,0 +1,36 @@ +package ru.posidata.backend.controller + +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.security.core.Authentication +import org.springframework.web.bind.annotation.* +import ru.posidata.backend.service.TelegramAuthService + + +@RestController +@RequestMapping(path = ["/api/v1/"]) +class TelegramAuthController(private val telegramAuthService: TelegramAuthService) { + @GetMapping("/user-info") + fun getSelfUserInfo(authentication: Authentication?): String { + return authentication?.principal.toString() + } + + @PostMapping("/test") + fun test(@RequestBody request: Map): String { + return "$request" + } + + @PostMapping("auth/telegram") + fun telegramAuth(@RequestBody request: Map): ResponseEntity { + return try { + val authResult = telegramAuthService.authenticate(request) + if (authResult.isAuthenticated) { + ResponseEntity.ok(authResult.token) + } else { + ResponseEntity.status(HttpStatus.FORBIDDEN).body("Login info hash mismatch") + } + } catch (e: Exception) { + ResponseEntity.status(HttpStatus.FORBIDDEN).body("Server error while authenticating") + } + } +} diff --git a/backend/src/jvmMain/kotlin/ru/posidata/backend/entity/User.kt b/backend/src/jvmMain/kotlin/ru/posidata/backend/entity/User.kt new file mode 100644 index 0000000..4cdabaa --- /dev/null +++ b/backend/src/jvmMain/kotlin/ru/posidata/backend/entity/User.kt @@ -0,0 +1,15 @@ +package ru.posidata.backend.entity + +import jakarta.persistence.* + +@Entity +@Table(name = "users") +data class User( + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0, + @Column(unique = true) + val telegramId: Long, + val firstName: String, + val lastName: String?, + val username: String? +) diff --git a/backend/src/jvmMain/kotlin/ru/posidata/backend/repository/UserRepository.kt b/backend/src/jvmMain/kotlin/ru/posidata/backend/repository/UserRepository.kt new file mode 100644 index 0000000..459ad78 --- /dev/null +++ b/backend/src/jvmMain/kotlin/ru/posidata/backend/repository/UserRepository.kt @@ -0,0 +1,10 @@ +package ru.posidata.backend.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import ru.posidata.backend.entity.User + +@Repository +interface UserRepository : JpaRepository { + fun findByTelegramId(telegramId: Long): User? +} \ No newline at end of file diff --git a/backend/src/jvmMain/kotlin/ru/posidata/backend/service/TelegramAuthService.kt b/backend/src/jvmMain/kotlin/ru/posidata/backend/service/TelegramAuthService.kt new file mode 100644 index 0000000..41c5eee --- /dev/null +++ b/backend/src/jvmMain/kotlin/ru/posidata/backend/service/TelegramAuthService.kt @@ -0,0 +1,43 @@ +package ru.posidata.backend.service + +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service +import java.security.MessageDigest +import java.util.* +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec + +@Service +class TelegramAuthService( + @Value("\${telegram.bot.token}") private val telegramToken: String, + private val userService: UserService +) { + fun authenticate(request: Map): AuthResult { + val hash = request["hash"] as String + val sortedRequest = request.toMutableMap().apply { remove("hash") } + + val dataCheckString = sortedRequest.entries + .sortedBy { it.key.lowercase(Locale.getDefault()) } + .joinToString("\n") { "${it.key}=${it.value}" } + + val secretKey = SecretKeySpec( + MessageDigest.getInstance("SHA-256").digest(telegramToken.toByteArray(Charsets.UTF_8)), + "HmacSHA256" + ) + val mac = Mac.getInstance("HmacSHA256") + mac.init(secretKey) + + val result = mac.doFinal(dataCheckString.toByteArray(Charsets.UTF_8)) + val calculatedHash = result.joinToString("") { "%02x".format(it) } + + return if (hash.equals(calculatedHash, ignoreCase = true)) { + val user = userService.findOrCreateUser(request) + println(user) + AuthResult(true, "token") + } else { + AuthResult(false, null) + } + } +} + +data class AuthResult(val isAuthenticated: Boolean, val token: String?) \ No newline at end of file diff --git a/backend/src/jvmMain/kotlin/ru/posidata/backend/service/UserService.kt b/backend/src/jvmMain/kotlin/ru/posidata/backend/service/UserService.kt new file mode 100644 index 0000000..f89f7d6 --- /dev/null +++ b/backend/src/jvmMain/kotlin/ru/posidata/backend/service/UserService.kt @@ -0,0 +1,23 @@ +package ru.posidata.backend.service + +import org.springframework.stereotype.Service +import ru.posidata.backend.entity.User +import ru.posidata.backend.repository.UserRepository + +@Service +class UserService(private val userRepository: UserRepository) { + fun findOrCreateUser(telegramData: Map): User { + val telegramId = telegramData["id"] as Long + return userRepository.findByTelegramId(telegramId) ?: createUser(telegramData) + } + + private fun createUser(telegramData: Map): User { + val user = User( + telegramId = telegramData["id"] as Long, + firstName = telegramData["first_name"] as String, + lastName = telegramData["last_name"] as? String, + username = telegramData["username"] as? String + ) + return userRepository.save(user) + } +} \ No newline at end of file diff --git a/backend/src/jvmMain/resources/application.yml b/backend/src/jvmMain/resources/application.yml new file mode 100644 index 0000000..4a78e13 --- /dev/null +++ b/backend/src/jvmMain/resources/application.yml @@ -0,0 +1,8 @@ +spring: + application: + name: + backend + +telegram: + bot: + token: diff --git a/backend/src/test/kotlin/ru/posidata/backend/BackendApplicationTests.kt b/backend/src/test/kotlin/ru/posidata/backend/BackendApplicationTests.kt new file mode 100644 index 0000000..62c5351 --- /dev/null +++ b/backend/src/test/kotlin/ru/posidata/backend/BackendApplicationTests.kt @@ -0,0 +1,13 @@ +package ru.posidata.backend + +import org.junit.jupiter.api.Test +import org.springframework.boot.test.context.SpringBootTest + +@SpringBootTest +class BackendApplicationTests { + + @Test + fun contextLoads() { + } + +} diff --git a/build.gradle.kts b/build.gradle.kts index b2c6cf8..300669e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - java + kotlin("multiplatform") version("2.0.0") apply(false) } group = "ru.posidata" diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 9cb80dc..435f979 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - kotlin("multiplatform") version("2.0.0") + kotlin("multiplatform") } kotlin { diff --git a/frontend/build.gradle.kts b/frontend/build.gradle.kts index 8337eaf..fff0c79 100644 --- a/frontend/build.gradle.kts +++ b/frontend/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - kotlin("multiplatform") version ("2.0.0") + kotlin("multiplatform") } kotlin { @@ -59,7 +59,6 @@ kotlin { implementation(npm("react-dom", "^18.0.0")) implementation(npm("react-modal", "^3.0.0")) implementation(npm("@popperjs/core", "2.11.8")) - implementation(npm("animate.css", "^4.1.1")) // ====== font awesome ====== implementation(npm("@fortawesome/fontawesome-svg-core", "6.5.2")) implementation(npm("@fortawesome/free-solid-svg-icons", "6.5.2")) @@ -68,7 +67,7 @@ kotlin { implementation(npm("@fortawesome/fontawesome-free", "6.5.2")) implementation(npm("@fortawesome/react-fontawesome", "0.2.2")) // ====== cookies ====== - implementation(npm("js-cookie", "^3.0.5")) + // implementation(npm("js-cookie", "^3.0.5")) // ====== animation ========= implementation(npm("animate.css", "^4.1.1")) implementation(npm("react-tsparticles", "1.42.1")) diff --git a/frontend/src/jsMain/kotlin/ru/posidata/views/components/LogoButtons.kt b/frontend/src/jsMain/kotlin/ru/posidata/views/components/LogoButtons.kt index 5a20a83..5e29505 100644 --- a/frontend/src/jsMain/kotlin/ru/posidata/views/components/LogoButtons.kt +++ b/frontend/src/jsMain/kotlin/ru/posidata/views/components/LogoButtons.kt @@ -9,7 +9,9 @@ import react.dom.html.ReactHTML.button import react.dom.html.ReactHTML.span import web.cssom.* - +/** + * Wrapper for css style for a glowing neon text, see .glow.scss + */ fun ChildrenBuilder.neonLightingText(input: String) { button { className = ClassName("glowing-btn") @@ -19,4 +21,3 @@ fun ChildrenBuilder.neonLightingText(input: String) { } } } - diff --git a/frontend/src/jsMain/kotlin/ru/posidata/views/main/MainView.kt b/frontend/src/jsMain/kotlin/ru/posidata/views/main/MainView.kt index 2d0c459..0dd29e9 100644 --- a/frontend/src/jsMain/kotlin/ru/posidata/views/main/MainView.kt +++ b/frontend/src/jsMain/kotlin/ru/posidata/views/main/MainView.kt @@ -3,6 +3,8 @@ package ru.posidata.views.main import js.objects.jso import react.FC import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.h1 +import react.dom.html.ReactHTML.script import react.react import react.useState import ru.posidata.views.utils.internals.Answer.NONE @@ -26,12 +28,15 @@ val mainView = FC { val (uniqueRandom, setUniqueRandom) = useState>(listOf()) div { + className = ClassName("full-width-container") div { className = ClassName("row justify-content-center align-items-center") style = jso { minHeight = "100vh".unsafeCast() } + + div { id = "back" className = ClassName("card col-xl-4 col-lg-5 col-md-7 col-sm-8 col-12") @@ -56,6 +61,7 @@ val mainView = FC { this.setSelection = setSelection } } + QUESTION -> questionCard { this.counter = counter this.setCounter = setCounter diff --git a/frontend/src/jsMain/kotlin/ru/posidata/views/utils/externals/cookie/Cookie.kt b/frontend/src/jsMain/kotlin/ru/posidata/views/utils/externals/cookie/Cookie.kt deleted file mode 100644 index 267b6a9..0000000 --- a/frontend/src/jsMain/kotlin/ru/posidata/views/utils/externals/cookie/Cookie.kt +++ /dev/null @@ -1,134 +0,0 @@ -@file:Suppress("HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE") - -package ru.posidata.views.utils.externals.cookie - -import js.objects.jso -import kotlinext.js.require - -/** - * Object that manages cookies - */ -val cookie: Cookie = require("js-cookie") - -/** - * Interface that encapsulates all cookies interactions - */ -external interface Cookie { - /** - * Get cookie by [key] - * - * @param key key to get cookie - * @param cookieAttribute [CookieAttribute] - * @return cookie as [String] - */ - fun get(key: String, cookieAttribute: CookieAttribute = definedExternally): String - - /** - * Get all cookies - * - * @return [Set] of cookies as [String]s - */ - fun get(): Set - - /** - * Set cookie - * - * @param key key to set cookie - * @param value cookie value as [String] - * @param cookieAttribute [CookieAttribute] - */ - fun set(key: String, value: String, cookieAttribute: CookieAttribute = definedExternally) - - /** - * Remove cookie - * - * @param key cookie key - * @param cookieAttribute [CookieAttribute] - */ - fun remove(key: String, cookieAttribute: CookieAttribute = definedExternally) -} - -/** - * Cookie attributes that can be passed in [Cookie.remove], [Cookie.set] or [Cookie.get] methods - * - * @see Documentation on GitHub - */ -external interface CookieAttribute { - /** - * A [String] indicating the path where the cookie is visible. - */ - var path: String? - - /** - * A [String] indicating a valid domain where the cookie should be visible. - * The cookie will also be visible to all subdomains. - */ - var domain: String? - - /** - * Define when the cookie will be removed. - * Value must be an [Int] which will be interpreted as days from time of creation. - * - * If omitted, the cookie becomes a session cookie. - */ - var expires: Int? - - /** - * Either true or false, indicating if the cookie transmission requires a secure protocol (https). - */ - var secure: Boolean? -} - -/** - * Class that encapsulates the cookie information - * - * @property key [String] cookie name - * @property expires amount of days before a cookie is considered to be expired, [DEFAULT_EXPIRES] by default - */ -sealed class CookieKeys(val key: String, val expires: Int = DEFAULT_EXPIRES) { - /** - * Cookie that stores preferred platform language - */ - object PreferredLanguage : CookieKeys("language") - companion object { - /** - * Default value for [CookieKeys.expires] - */ - const val DEFAULT_EXPIRES = 365 - } -} - -/** - * @param key key as [CookieKeys] - * @param value value to set - * @see Cookie.set - */ -fun Cookie.set(key: CookieKeys, value: String) = set(key.key, value, jso { expires = key.expires }) - -/** - * @param key key as [CookieKeys] - * @return cookie as [String] by [CookieKeys.key] of [key] - * @see Cookie.get - */ -fun Cookie.get(key: CookieKeys): String = get(key.key) - -/** - * @param key key as [CookieKeys] - * @see Cookie.remove - */ -fun Cookie.remove(key: CookieKeys) = remove(key.key) - -/** - * Get preferred platform language code - * - * @return preferred platform language code as [String] - */ -fun Cookie.getLanguageCode() = get(CookieKeys.PreferredLanguage) - -/** - * Save preferred platform language code - * - * @param languageCode preferred platform language code as [String] - * @return [Unit] - */ -fun Cookie.saveLanguageCode(languageCode: String) = set(CookieKeys.PreferredLanguage, languageCode) diff --git a/frontend/src/jsMain/resources/index.html b/frontend/src/jsMain/resources/index.html index 003f52b..39b665f 100644 --- a/frontend/src/jsMain/resources/index.html +++ b/frontend/src/jsMain/resources/index.html @@ -1,5 +1,6 @@ + diff --git a/frontend/src/jsMain/resources/scss/_background.scss b/frontend/src/jsMain/resources/scss/_background.scss index 9d9519d..d167cf4 100644 --- a/frontend/src/jsMain/resources/scss/_background.scss +++ b/frontend/src/jsMain/resources/scss/_background.scss @@ -1,5 +1,3 @@ -$font-family-sans-serif: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", 'Noto Color Emoji' !default; - #back { background: -webkit-linear-gradient(0deg, rgb(0, 85, 102), rgb(0, 55, 71)); /* For Safari 5.1 to 6.0 */ background: -o-linear-gradient(0deg, rgb(0, 85, 102), rgb(0, 55, 71)); /* For Opera 11.1 to 12.0 */ @@ -35,4 +33,4 @@ body { .full-width-container .row { margin: 0; -} \ No newline at end of file +} diff --git a/frontend/src/jsMain/resources/scss/_glow.scss b/frontend/src/jsMain/resources/scss/_glow.scss index dd8bb58..6caca57 100644 --- a/frontend/src/jsMain/resources/scss/_glow.scss +++ b/frontend/src/jsMain/resources/scss/_glow.scss @@ -10,6 +10,58 @@ box-sizing: border-box; } +// ======= Just a glowing button and text === +@keyframes text-flicker { + 0% { + opacity: 0.1; + } + + 2% { + opacity: 1; + } + + 8% { + opacity: 0.1; + } + + 9% { + opacity: 1; + } + + 12% { + opacity: 0.1; + } + 20% { + opacity: 1; + } + 25% { + opacity: 0.3; + } + 30% { + opacity: 1; + } + + 70% { + opacity: 0.7; + } + 72% { + opacity: 0.2; + } + + 77% { + opacity: 0.9; + } + 100% { + opacity: 0.9; + } +} + +@media only screen and (max-width: 250px) { + .glowing-btn{ + font-size: 1em; + } +} + .glowing-btn { cursor: default; position: relative; @@ -73,6 +125,8 @@ transition: opacity 100ms linear; } +// ======= Style of buttons for selection === + .logo-main:hover .glowing-btn { color: rgba(0, 0, 0, 0.8); text-shadow: none; @@ -83,8 +137,6 @@ animation: none; } - - .logo-main:hover .glowing-btn:before{ filter: blur(1.5em); opacity: 1; @@ -95,62 +147,7 @@ } -@keyframes text-flicker { - 0% { - opacity: 0.1; - } - - 2% { - opacity: 1; - } - - 8% { - opacity: 0.1; - } - - 9% { - opacity: 1; - } - - 12% { - opacity: 0.1; - } - 20% { - opacity: 1; - } - 25% { - opacity: 0.3; - } - 30% { - opacity: 1; - } - - 70% { - opacity: 0.7; - } - 72% { - opacity: 0.2; - } - - 77% { - opacity: 0.9; - } - 100% { - opacity: 0.9; - } -} - -@media only screen and (max-width: 250px) { - .glowing-btn{ - font-size: 1em; - } -} - // ============= this is a greyscale effect for pictures -a:hover { - text-decoration: none; -} - .img-glow1 { cursor: pointer; -webkit-filter: brightness(80%); @@ -182,4 +179,4 @@ a:hover { .img-glow3:hover { -webkit-filter: brightness(110%); transition: 0.7s -} \ No newline at end of file +} diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock index 30a7d10..274f833 100644 --- a/kotlin-js-store/yarn.lock +++ b/kotlin-js-store/yarn.lock @@ -2071,11 +2071,6 @@ jest-worker@^27.4.5: merge-stream "^2.0.0" supports-color "^8.0.0" -js-cookie@^3.0.5: - version "3.0.5" - resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc" - integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw== - "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" diff --git a/release.sh b/release.sh new file mode 100644 index 0000000..362fe57 --- /dev/null +++ b/release.sh @@ -0,0 +1,12 @@ +rm -rf posidata.ru +rm -rf PokemonOrBigData +mkdir posidata.ru +git clone https://github.com/orchestr7/PokemonOrBigData.git +cd PokemonOrBigData +git checkout origin/main +cd ../ +cp -r PokemonOrBigData/frontend/src/jsMain/resources/* posidata.ru/ +cd PokemonOrBigData +git checkout origin/gh-pages +cd ../ +cp -r PokemonOrBigData/* posidata.ru/ diff --git a/settings.gradle.kts b/settings.gradle.kts index 8171111..92e238d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -19,5 +19,5 @@ plugins { } include("frontend") +include("backend") include("common") -