forked from github/szkolny
[Vulcan/Hebe] Add hebe API login implementation.
This commit is contained in:
parent
73ff09052c
commit
2fcff33bd6
@ -58,6 +58,7 @@ android {
|
||||
dataBinding = true
|
||||
}
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
sourceCompatibility '1.8'
|
||||
targetCompatibility '1.8'
|
||||
}
|
||||
@ -105,6 +106,8 @@ tasks.whenTaskAdded { task ->
|
||||
dependencies {
|
||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
|
||||
|
||||
kapt "androidx.room:room-compiler:${versions.room}"
|
||||
debugImplementation "com.amitshekhar.android:debug-db:1.0.5"
|
||||
|
||||
@ -181,6 +184,7 @@ dependencies {
|
||||
//implementation "org.redundent:kotlin-xml-builder:1.5.3"
|
||||
|
||||
implementation "io.github.wulkanowy:signer-android:0.1.1"
|
||||
implementation 'com.github.wulkanowy.uonet-request-signer:hebe-jvm:a99ca50a31'
|
||||
|
||||
implementation "androidx.work:work-runtime-ktx:${versions.work}"
|
||||
|
||||
|
@ -294,6 +294,19 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
|
||||
"Vulcan"
|
||||
)
|
||||
|
||||
val pushVulcanHebeApp = FirebaseApp.initializeApp(
|
||||
this@App,
|
||||
FirebaseOptions.Builder()
|
||||
.setProjectId("dzienniczekplus")
|
||||
.setStorageBucket("dzienniczekplus.appspot.com")
|
||||
.setDatabaseUrl("https://dzienniczekplus.firebaseio.com")
|
||||
.setGcmSenderId("987828170337")
|
||||
.setApiKey("AIzaSyDW8MUtanHy64_I0oCpY6cOxB3jrvJd_iA")
|
||||
.setApplicationId("1:987828170337:android:7e16404b9e5deaaa")
|
||||
.build(),
|
||||
"VulcanHebe"
|
||||
)
|
||||
|
||||
try {
|
||||
FirebaseInstanceId.getInstance().instanceId.addOnSuccessListener { instanceIdResult ->
|
||||
val token = instanceIdResult.token
|
||||
@ -324,6 +337,14 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
|
||||
config.sync.tokenVulcanList = listOf()
|
||||
}
|
||||
}
|
||||
FirebaseInstanceId.getInstance(pushVulcanHebeApp).instanceId.addOnSuccessListener { instanceIdResult ->
|
||||
val token = instanceIdResult.token
|
||||
d("Firebase", "Got VulcanHebe token: $token")
|
||||
if (token != config.sync.tokenVulcanHebe) {
|
||||
config.sync.tokenVulcanHebe = token
|
||||
config.sync.tokenVulcanHebeList = listOf()
|
||||
}
|
||||
}
|
||||
FirebaseMessaging.getInstance().subscribeToTopic(packageName)
|
||||
} catch (e: IllegalStateException) {
|
||||
e.printStackTrace()
|
||||
|
@ -99,6 +99,10 @@ class ConfigSync(private val config: Config) {
|
||||
var tokenVulcan: String?
|
||||
get() { mTokenVulcan = mTokenVulcan ?: config.values.get("tokenVulcan", null as String?); return mTokenVulcan }
|
||||
set(value) { config.set("tokenVulcan", value); mTokenVulcan = value }
|
||||
private var mTokenVulcanHebe: String? = null
|
||||
var tokenVulcanHebe: String?
|
||||
get() { mTokenVulcanHebe = mTokenVulcanHebe ?: config.values.get("tokenVulcanHebe", null as String?); return mTokenVulcanHebe }
|
||||
set(value) { config.set("tokenVulcanHebe", value); mTokenVulcanHebe = value }
|
||||
|
||||
private var mTokenMobidziennikList: List<Int>? = null
|
||||
var tokenMobidziennikList: List<Int>
|
||||
@ -112,6 +116,10 @@ class ConfigSync(private val config: Config) {
|
||||
var tokenVulcanList: List<Int>
|
||||
get() { mTokenVulcanList = mTokenVulcanList ?: config.values.getIntList("tokenVulcanList", listOf()); return mTokenVulcanList ?: listOf() }
|
||||
set(value) { config.set("tokenVulcanList", value); mTokenVulcanList = value }
|
||||
private var mTokenVulcanHebeList: List<Int>? = null
|
||||
var tokenVulcanHebeList: List<Int>
|
||||
get() { mTokenVulcanHebeList = mTokenVulcanHebeList ?: config.values.getIntList("tokenVulcanHebeList", listOf()); return mTokenVulcanHebeList ?: listOf() }
|
||||
set(value) { config.set("tokenVulcanHebeList", value); mTokenVulcanHebeList = value }
|
||||
|
||||
private var mRegisterAvailability: Map<String, RegisterAvailabilityStatus>? = null
|
||||
var registerAvailability: Map<String, RegisterAvailabilityStatus>
|
||||
|
@ -95,7 +95,16 @@ const val VULCAN_API_APP_NAME = "VULCAN-Android-ModulUcznia"
|
||||
const val VULCAN_API_APP_VERSION = "20.5.1.470"
|
||||
const val VULCAN_API_PASSWORD = "CE75EA598C7743AD9B0B7328DED85B06"
|
||||
const val VULCAN_API_PASSWORD_FAKELOG = "012345678901234567890123456789AB"
|
||||
val VULCAN_API_DEVICE_NAME = "Szkolny.eu ${Build.MODEL}"
|
||||
const val VULCAN_HEBE_USER_AGENT = "Dart/2.10 (dart:io)"
|
||||
const val VULCAN_HEBE_APP_NAME = "DzienniczekPlus 2.0"
|
||||
const val VULCAN_HEBE_APP_VERSION = "21.02.09 (G)"
|
||||
private const val VULCAN_API_DEVICE_NAME_PREFIX = "Szkolny.eu "
|
||||
private const val VULCAN_API_DEVICE_NAME_SUFFIX = " - nie usuwać"
|
||||
val VULCAN_API_DEVICE_NAME by lazy {
|
||||
val base = "$VULCAN_API_DEVICE_NAME_PREFIX${Build.MODEL}"
|
||||
val baseMaxLength = 50 - VULCAN_API_DEVICE_NAME_SUFFIX.length
|
||||
base.take(baseMaxLength) + VULCAN_API_DEVICE_NAME_SUFFIX
|
||||
}
|
||||
|
||||
const val VULCAN_API_ENDPOINT_CERTIFICATE = "mobile-api/Uczen.v3.UczenStart/Certyfikat"
|
||||
const val VULCAN_API_ENDPOINT_STUDENT_LIST = "mobile-api/Uczen.v3.UczenStart/ListaUczniow"
|
||||
@ -116,6 +125,8 @@ const val VULCAN_API_ENDPOINT_MESSAGES_ATTACHMENTS = "mobile-api/Uczen.v3.Uczen/
|
||||
const val VULCAN_API_ENDPOINT_HOMEWORK_ATTACHMENTS = "mobile-api/Uczen.v3.Uczen/ZadaniaDomoweZalacznik"
|
||||
const val VULCAN_WEB_ENDPOINT_LUCKY_NUMBER = "Start.mvc/GetKidsLuckyNumbers"
|
||||
const val VULCAN_WEB_ENDPOINT_REGISTER_DEVICE = "RejestracjaUrzadzeniaToken.mvc/Get"
|
||||
const val VULCAN_HEBE_ENDPOINT_REGISTER_NEW = "api/mobile/register/new"
|
||||
const val VULCAN_HEBE_ENDPOINT_MAIN = "api/mobile/register/hebe"
|
||||
|
||||
const val EDUDZIENNIK_USER_AGENT = "Szkolny.eu/${BuildConfig.VERSION_NAME}"
|
||||
|
||||
|
@ -170,6 +170,7 @@ const val ERROR_VULCAN_WEB_LOGGED_OUT = 350
|
||||
const val ERROR_VULCAN_WEB_CERTIFICATE_POST_FAILED = 351
|
||||
const val ERROR_VULCAN_WEB_GRADUATE_ACCOUNT = 352
|
||||
const val ERROR_VULCAN_WEB_NO_SCHOOLS = 353
|
||||
const val ERROR_VULCAN_HEBE_OTHER = 354
|
||||
|
||||
const val ERROR_LOGIN_IDZIENNIK_WEB_INVALID_LOGIN = 401
|
||||
const val ERROR_LOGIN_IDZIENNIK_WEB_INVALID_SCHOOL_NAME = 402
|
||||
@ -229,5 +230,6 @@ const val ERROR_ONEDRIVE_DOWNLOAD = 930
|
||||
const val EXCEPTION_VULCAN_WEB_LOGIN = 931
|
||||
const val EXCEPTION_VULCAN_WEB_REQUEST = 932
|
||||
const val EXCEPTION_PODLASIE_API_REQUEST = 940
|
||||
const val EXCEPTION_VULCAN_HEBE_REQUEST = 950
|
||||
|
||||
const val LOGIN_NO_ARGUMENTS = 1201
|
||||
|
@ -17,6 +17,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.login.PodlasieLogi
|
||||
import pl.szczodrzynski.edziennik.data.api.edziennik.template.login.TemplateLoginApi
|
||||
import pl.szczodrzynski.edziennik.data.api.edziennik.template.login.TemplateLoginWeb
|
||||
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginApi
|
||||
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginHebe
|
||||
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginWebMain
|
||||
import pl.szczodrzynski.edziennik.data.api.models.LoginMethod
|
||||
|
||||
@ -98,11 +99,13 @@ val mobidziennikLoginMethods = listOf(
|
||||
const val LOGIN_TYPE_VULCAN = 4
|
||||
const val LOGIN_MODE_VULCAN_API = 0
|
||||
const val LOGIN_MODE_VULCAN_WEB = 1
|
||||
const val LOGIN_MODE_VULCAN_HEBE = 2
|
||||
const val LOGIN_METHOD_VULCAN_WEB_MAIN = 100
|
||||
const val LOGIN_METHOD_VULCAN_WEB_NEW = 200
|
||||
const val LOGIN_METHOD_VULCAN_WEB_OLD = 300
|
||||
const val LOGIN_METHOD_VULCAN_WEB_MESSAGES = 400
|
||||
const val LOGIN_METHOD_VULCAN_API = 500
|
||||
const val LOGIN_METHOD_VULCAN_HEBE = 600
|
||||
val vulcanLoginMethods = listOf(
|
||||
LoginMethod(LOGIN_TYPE_VULCAN, LOGIN_METHOD_VULCAN_WEB_MAIN, VulcanLoginWebMain::class.java)
|
||||
.withIsPossible { _, loginStore -> loginStore.hasLoginData("webHost") }
|
||||
@ -117,7 +120,17 @@ val vulcanLoginMethods = listOf(
|
||||
.withRequiredLoginMethod { _, _ -> LOGIN_METHOD_VULCAN_WEB_MAIN },*/
|
||||
|
||||
LoginMethod(LOGIN_TYPE_VULCAN, LOGIN_METHOD_VULCAN_API, VulcanLoginApi::class.java)
|
||||
.withIsPossible { _, _ -> true }
|
||||
.withIsPossible { _, loginStore ->
|
||||
loginStore.mode == LOGIN_MODE_VULCAN_API
|
||||
}
|
||||
.withRequiredLoginMethod { _, loginStore ->
|
||||
if (loginStore.mode == LOGIN_MODE_VULCAN_WEB) LOGIN_METHOD_VULCAN_WEB_MAIN else LOGIN_METHOD_NOT_NEEDED
|
||||
},
|
||||
|
||||
LoginMethod(LOGIN_TYPE_VULCAN, LOGIN_METHOD_VULCAN_HEBE, VulcanLoginHebe::class.java)
|
||||
.withIsPossible { _, loginStore ->
|
||||
loginStore.mode == LOGIN_MODE_VULCAN_HEBE
|
||||
}
|
||||
.withRequiredLoginMethod { _, loginStore ->
|
||||
if (loginStore.mode == LOGIN_MODE_VULCAN_WEB) LOGIN_METHOD_VULCAN_WEB_MAIN else LOGIN_METHOD_NOT_NEEDED
|
||||
}
|
||||
|
@ -4,16 +4,15 @@
|
||||
|
||||
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan
|
||||
|
||||
import pl.szczodrzynski.edziennik.App
|
||||
import pl.szczodrzynski.edziennik.currentTimeUnix
|
||||
import pl.szczodrzynski.edziennik.*
|
||||
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_API
|
||||
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_HEBE
|
||||
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_WEB_MAIN
|
||||
import pl.szczodrzynski.edziennik.data.api.models.Data
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Profile
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Team
|
||||
import pl.szczodrzynski.edziennik.isNotNullNorEmpty
|
||||
import pl.szczodrzynski.edziennik.utils.Utils
|
||||
import pl.szczodrzynski.edziennik.values
|
||||
|
||||
class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app, profile, loginStore) {
|
||||
|
||||
@ -26,12 +25,21 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
|
||||
&& apiFingerprint[symbol].isNotNullNorEmpty()
|
||||
&& apiPrivateKey[symbol].isNotNullNorEmpty()
|
||||
&& symbol.isNotNullNorEmpty()
|
||||
fun isHebeLoginValid() = hebePublicKey.isNotNullNorEmpty()
|
||||
&& hebePrivateKey.isNotNullNorEmpty()
|
||||
&& symbol.isNotNullNorEmpty()
|
||||
|
||||
override fun satisfyLoginMethods() {
|
||||
loginMethods.clear()
|
||||
if (isWebMainLoginValid()) {
|
||||
loginMethods += LOGIN_METHOD_VULCAN_WEB_MAIN
|
||||
}
|
||||
if (isApiLoginValid()) {
|
||||
loginMethods += LOGIN_METHOD_VULCAN_API
|
||||
}
|
||||
if (isHebeLoginValid()) {
|
||||
loginMethods += LOGIN_METHOD_VULCAN_HEBE
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
@ -55,6 +63,17 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
|
||||
|
||||
override fun generateUserCode() = "$schoolCode:$studentId"
|
||||
|
||||
fun buildDeviceId(): String {
|
||||
val deviceId = app.deviceId.padStart(16, '0')
|
||||
val loginStoreId = loginStore.id.toString(16).padStart(4, '0')
|
||||
val symbol = symbol?.crc16()?.toString(16)?.take(2) ?: "00"
|
||||
return deviceId.substring(0..7) +
|
||||
"-" + deviceId.substring(8..11) +
|
||||
"-" + deviceId.substring(12..15) +
|
||||
"-" + loginStoreId +
|
||||
"-" + symbol + "6f72616e7a"
|
||||
}
|
||||
|
||||
/**
|
||||
* A UONET+ client symbol.
|
||||
*
|
||||
@ -203,6 +222,27 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
|
||||
get() { mApiPrivateKey = mApiPrivateKey ?: loginStore.getLoginData("apiPrivateKey", null)?.let { app.gson.fromJson(it, field.toMutableMap()::class.java) }; return mApiPrivateKey ?: mapOf() }
|
||||
set(value) { loginStore.putLoginData("apiPrivateKey", app.gson.toJson(value)); mApiPrivateKey = value }
|
||||
|
||||
/* _ _ _ _____ _____
|
||||
| | | | | | /\ | __ \_ _|
|
||||
| |__| | ___| |__ ___ / \ | |__) || |
|
||||
| __ |/ _ \ '_ \ / _ \ / /\ \ | ___/ | |
|
||||
| | | | __/ |_) | __/ / ____ \| | _| |_
|
||||
|_| |_|\___|_.__/ \___| /_/ \_\_| |____*/
|
||||
private var mHebePublicKey: String? = null
|
||||
var hebePublicKey: String?
|
||||
get() { mHebePublicKey = mHebePublicKey ?: loginStore.getLoginData("hebePublicKey", null); return mHebePublicKey }
|
||||
set(value) { loginStore.putLoginData("hebePublicKey", value); mHebePublicKey = value }
|
||||
|
||||
private var mHebePrivateKey: String? = null
|
||||
var hebePrivateKey: String?
|
||||
get() { mHebePrivateKey = mHebePrivateKey ?: loginStore.getLoginData("hebePrivateKey", null); return mHebePrivateKey }
|
||||
set(value) { loginStore.putLoginData("hebePrivateKey", value); mHebePrivateKey = value }
|
||||
|
||||
private var mHebePublicHash: String? = null
|
||||
var hebePublicHash: String?
|
||||
get() { mHebePublicHash = mHebePublicHash ?: loginStore.getLoginData("hebePublicHash", null); return mHebePublicHash }
|
||||
set(value) { loginStore.putLoginData("hebePublicHash", value); mHebePublicHash = value }
|
||||
|
||||
val apiUrl: String?
|
||||
get() {
|
||||
val url = when (apiToken[symbol]?.substring(0, 3)) {
|
||||
@ -227,7 +267,7 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
|
||||
return if (url != null) "$url/$symbol/" else loginStore.getLoginData("apiUrl", null)
|
||||
}
|
||||
|
||||
val fullApiUrl: String?
|
||||
val fullApiUrl: String
|
||||
get() {
|
||||
return "$apiUrl$schoolSymbol/"
|
||||
}
|
||||
|
@ -0,0 +1,187 @@
|
||||
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data
|
||||
|
||||
import android.os.Build
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonObject
|
||||
import im.wangchao.mhttp.Request
|
||||
import im.wangchao.mhttp.Response
|
||||
import im.wangchao.mhttp.body.MediaTypeUtils
|
||||
import im.wangchao.mhttp.callback.JsonCallbackHandler
|
||||
import io.github.wulkanowy.signer.hebe.getSignatureHeaders
|
||||
import pl.szczodrzynski.edziennik.*
|
||||
import pl.szczodrzynski.edziennik.data.api.*
|
||||
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
|
||||
import pl.szczodrzynski.edziennik.data.api.models.ApiError
|
||||
import pl.szczodrzynski.edziennik.utils.Utils.d
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URLEncoder
|
||||
import java.time.ZoneId
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.*
|
||||
|
||||
open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) {
|
||||
companion object {
|
||||
const val TAG = "VulcanHebe"
|
||||
}
|
||||
|
||||
val profileId
|
||||
get() = data.profile?.id ?: -1
|
||||
|
||||
val profile
|
||||
get() = data.profile
|
||||
|
||||
inline fun <reified T> apiRequest(
|
||||
tag: String,
|
||||
endpoint: String,
|
||||
method: Int = GET,
|
||||
payload: JsonObject? = null,
|
||||
baseUrl: Boolean = false,
|
||||
crossinline onSuccess: (json: T, response: Response?) -> Unit
|
||||
) {
|
||||
val url = "${if (baseUrl) data.apiUrl else data.fullApiUrl}$endpoint"
|
||||
|
||||
d(tag, "Request: Vulcan/Hebe - $url")
|
||||
|
||||
val privateKey = data.hebePrivateKey
|
||||
val publicHash = data.hebePublicHash
|
||||
|
||||
if (privateKey == null || publicHash == null) {
|
||||
data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING))
|
||||
return
|
||||
}
|
||||
|
||||
val timestamp = ZonedDateTime.now(ZoneId.of("GMT"))
|
||||
val timestampMillis = timestamp.toInstant().toEpochMilli()
|
||||
val timestampIso = timestamp.format(DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss"))
|
||||
|
||||
val finalPayload = if (payload != null) {
|
||||
JsonObject(
|
||||
"AppName" to VULCAN_HEBE_APP_NAME,
|
||||
"AppVersion" to VULCAN_HEBE_APP_VERSION,
|
||||
"CertificateId" to publicHash,
|
||||
"Envelope" to payload,
|
||||
"FirebaseToken" to data.app.config.sync.tokenVulcanHebe,
|
||||
"API" to 1,
|
||||
"RequestId" to UUID.randomUUID().toString(),
|
||||
"Timestamp" to timestampMillis,
|
||||
"TimestampFormatted" to timestampIso
|
||||
)
|
||||
} else null
|
||||
val jsonString = finalPayload?.toString()
|
||||
|
||||
val headers = getSignatureHeaders(
|
||||
publicHash,
|
||||
privateKey,
|
||||
jsonString,
|
||||
endpoint,
|
||||
timestamp
|
||||
)
|
||||
|
||||
val callback = object : JsonCallbackHandler() {
|
||||
override fun onSuccess(json: JsonObject?, response: Response?) {
|
||||
if (json == null) {
|
||||
data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY)
|
||||
.withResponse(response)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val status = json.getJsonObject("Status")
|
||||
if (status?.getInt("Code") != 0) {
|
||||
data.error(ApiError(tag, ERROR_VULCAN_HEBE_OTHER)
|
||||
.withResponse(response)
|
||||
.withApiResponse(json.toString()))
|
||||
}
|
||||
|
||||
val envelope = when (T::class.java) {
|
||||
JsonObject::class.java -> json.getJsonObject("Envelope")
|
||||
JsonArray::class.java -> json.getJsonArray("Envelope")
|
||||
else -> {
|
||||
data.error(ApiError(tag, ERROR_RESPONSE_EMPTY)
|
||||
.withResponse(response)
|
||||
.withApiResponse(json)
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
onSuccess(envelope as T, response)
|
||||
} catch (e: Exception) {
|
||||
data.error(ApiError(tag, EXCEPTION_VULCAN_HEBE_REQUEST)
|
||||
.withResponse(response)
|
||||
.withThrowable(e)
|
||||
.withApiResponse(json)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(response: Response?, throwable: Throwable?) {
|
||||
data.error(ApiError(tag, ERROR_REQUEST_FAILURE)
|
||||
.withResponse(response)
|
||||
.withThrowable(throwable)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Request.builder()
|
||||
.url(url)
|
||||
.userAgent(VULCAN_HEBE_USER_AGENT)
|
||||
.addHeader("vOS", "Android")
|
||||
.addHeader("vDeviceModel", Build.MODEL)
|
||||
.addHeader("vAPI", "1")
|
||||
.apply {
|
||||
headers.forEach {
|
||||
addHeader(it.key, it.value)
|
||||
}
|
||||
when (method) {
|
||||
GET -> get()
|
||||
POST -> {
|
||||
post()
|
||||
setTextBody(jsonString, MediaTypeUtils.APPLICATION_JSON)
|
||||
}
|
||||
}
|
||||
}
|
||||
.allowErrorCode(HttpURLConnection.HTTP_BAD_REQUEST)
|
||||
.allowErrorCode(HttpURLConnection.HTTP_FORBIDDEN)
|
||||
.allowErrorCode(HttpURLConnection.HTTP_UNAUTHORIZED)
|
||||
.allowErrorCode(HttpURLConnection.HTTP_UNAVAILABLE)
|
||||
.callback(callback)
|
||||
.build()
|
||||
.enqueue()
|
||||
}
|
||||
|
||||
inline fun <reified T> apiGet(
|
||||
tag: String,
|
||||
endpoint: String,
|
||||
query: Map<String, String> = mapOf(),
|
||||
baseUrl: Boolean = false,
|
||||
crossinline onSuccess: (json: T, response: Response?) -> Unit
|
||||
) {
|
||||
val queryPath = query.map { it.key + "=" + URLEncoder.encode(it.value, "UTF-8") }.join("&")
|
||||
apiRequest(
|
||||
tag,
|
||||
if (query.isNotEmpty()) "$endpoint?$queryPath" else endpoint,
|
||||
baseUrl = baseUrl,
|
||||
onSuccess = onSuccess
|
||||
)
|
||||
}
|
||||
|
||||
inline fun <reified T> apiPost(
|
||||
tag: String,
|
||||
endpoint: String,
|
||||
payload: JsonObject,
|
||||
baseUrl: Boolean = false,
|
||||
crossinline onSuccess: (json: T, response: Response?) -> Unit
|
||||
) {
|
||||
apiRequest(
|
||||
tag,
|
||||
endpoint,
|
||||
method = POST,
|
||||
payload,
|
||||
baseUrl = baseUrl,
|
||||
onSuccess = onSuccess
|
||||
)
|
||||
}
|
||||
}
|
@ -4,14 +4,17 @@
|
||||
|
||||
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.firstlogin
|
||||
|
||||
import com.google.gson.JsonArray
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import pl.szczodrzynski.edziennik.*
|
||||
import pl.szczodrzynski.edziennik.data.api.*
|
||||
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
|
||||
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanApi
|
||||
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
|
||||
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanWebMain
|
||||
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.CufsCertificate
|
||||
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginApi
|
||||
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginHebe
|
||||
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginWebMain
|
||||
import pl.szczodrzynski.edziennik.data.api.events.FirstLoginFinishedEvent
|
||||
import pl.szczodrzynski.edziennik.data.api.models.ApiError
|
||||
@ -25,6 +28,7 @@ class VulcanFirstLogin(val data: DataVulcan, val onSuccess: () -> Unit) {
|
||||
|
||||
private val api = VulcanApi(data, null)
|
||||
private val web = VulcanWebMain(data, null)
|
||||
private val hebe = VulcanHebe(data, null)
|
||||
private val profileList = mutableListOf<Profile>()
|
||||
private val loginStoreId = data.loginStore.id
|
||||
private var firstProfileId = loginStoreId
|
||||
@ -50,12 +54,18 @@ class VulcanFirstLogin(val data: DataVulcan, val onSuccess: () -> Unit) {
|
||||
checkSymbol(certificate)
|
||||
}
|
||||
}
|
||||
else {
|
||||
else if (data.loginStore.mode == LOGIN_MODE_VULCAN_API) {
|
||||
registerDevice {
|
||||
EventBus.getDefault().postSticky(FirstLoginFinishedEvent(profileList, data.loginStore))
|
||||
onSuccess()
|
||||
}
|
||||
}
|
||||
else {
|
||||
registerDeviceHebe {
|
||||
EventBus.getDefault().postSticky(FirstLoginFinishedEvent(profileList, data.loginStore))
|
||||
onSuccess()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkSymbol(certificate: CufsCertificate) {
|
||||
@ -103,7 +113,7 @@ class VulcanFirstLogin(val data: DataVulcan, val onSuccess: () -> Unit) {
|
||||
data.apiPin = data.apiPin.toMutableMap().also {
|
||||
it[symbol] = json.getString("PIN")
|
||||
}
|
||||
registerDevice(onSuccess)
|
||||
registerDeviceHebe(onSuccess)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -197,4 +207,113 @@ class VulcanFirstLogin(val data: DataVulcan, val onSuccess: () -> Unit) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerDeviceHebe(onSuccess: () -> Unit) {
|
||||
VulcanLoginHebe(data) {
|
||||
hebe.apiGet(
|
||||
TAG,
|
||||
VULCAN_HEBE_ENDPOINT_MAIN,
|
||||
query = mapOf("lastSyncDate" to "null"),
|
||||
baseUrl = true
|
||||
) { students: JsonArray, _ ->
|
||||
if (students.isEmpty()) {
|
||||
EventBus.getDefault().postSticky(FirstLoginFinishedEvent(listOf(), data.loginStore))
|
||||
onSuccess()
|
||||
return@apiGet
|
||||
}
|
||||
|
||||
students.forEach { studentEl ->
|
||||
val student = studentEl.asJsonObject
|
||||
|
||||
val unit = student.getJsonObject("Unit")
|
||||
//val constituentUnit = student.getJsonObject("ConstituentUnit")
|
||||
val pupil = student.getJsonObject("Pupil")
|
||||
val login = student.getJsonObject("Login")
|
||||
val periods = student.getJsonArray("Periods")?.map {
|
||||
it.asJsonObject
|
||||
} ?: listOf()
|
||||
|
||||
val period = periods.firstOrNull {
|
||||
it.getBoolean("Current", false)
|
||||
} ?: return@forEach
|
||||
|
||||
val periodLevel = period.getInt("Level") ?: return@forEach
|
||||
val semester1 = periods.firstOrNull {
|
||||
it.getInt("Level") == periodLevel && it.getInt("Number") == 1
|
||||
}
|
||||
val semester2 = periods.firstOrNull {
|
||||
it.getInt("Level") == periodLevel && it.getInt("Number") == 2
|
||||
}
|
||||
|
||||
val schoolSymbol = unit.getString("Symbol") ?: return@forEach
|
||||
val schoolShort = unit.getString("Short") ?: return@forEach
|
||||
val schoolCode = "${data.symbol}_$schoolSymbol"
|
||||
val studentId = pupil.getInt("Id") ?: return@forEach
|
||||
val studentLoginId = login.getInt("Id") ?: return@forEach
|
||||
//val studentClassId = student.getInt("IdOddzial") ?: return@forEach
|
||||
val studentClassName = student.getString("ClassDisplay") ?: return@forEach
|
||||
val studentFirstName = pupil.getString("FirstName") ?: ""
|
||||
val studentLastName = pupil.getString("Surname") ?: ""
|
||||
val studentNameLong = "$studentFirstName $studentLastName".fixName()
|
||||
val studentNameShort = "$studentFirstName ${studentLastName[0]}.".fixName()
|
||||
val userLogin = login.getString("Value") ?: ""
|
||||
|
||||
val studentSemesterId = period.getInt("Id") ?: return@forEach
|
||||
val studentSemesterNumber = period.getInt("Number") ?: return@forEach
|
||||
|
||||
val isParent = login.getString("LoginRole").equals("opiekun", ignoreCase = true)
|
||||
val accountName = if (isParent)
|
||||
login.getString("DisplayName")?.fixName()
|
||||
else null
|
||||
|
||||
val dateSemester1Start = semester1
|
||||
?.getJsonObject("Start")
|
||||
?.getString("Date")
|
||||
?.let { Date.fromY_m_d(it) }
|
||||
val dateSemester2Start = semester2
|
||||
?.getJsonObject("Start")
|
||||
?.getString("Date")
|
||||
?.let { Date.fromY_m_d(it) }
|
||||
val dateYearEnd = semester2
|
||||
?.getJsonObject("End")
|
||||
?.getString("Date")
|
||||
?.let { Date.fromY_m_d(it) }
|
||||
|
||||
val profile = Profile(
|
||||
firstProfileId++,
|
||||
loginStoreId,
|
||||
LOGIN_TYPE_VULCAN,
|
||||
studentNameLong,
|
||||
userLogin,
|
||||
studentNameLong,
|
||||
studentNameShort,
|
||||
accountName
|
||||
).apply {
|
||||
this.studentClassName = studentClassName
|
||||
studentData["symbol"] = data.symbol
|
||||
|
||||
studentData["studentId"] = studentId
|
||||
studentData["studentLoginId"] = studentLoginId
|
||||
studentData["studentSemesterId"] = studentSemesterId
|
||||
studentData["studentSemesterNumber"] = studentSemesterNumber
|
||||
studentData["semester1Id"] = semester1?.getInt("Id") ?: 0
|
||||
studentData["semester2Id"] = semester2?.getInt("Id") ?: 0
|
||||
studentData["schoolSymbol"] = schoolSymbol
|
||||
studentData["schoolShort"] = schoolShort
|
||||
studentData["schoolName"] = schoolCode
|
||||
}
|
||||
dateSemester1Start?.let {
|
||||
profile.dateSemester1Start = it
|
||||
profile.studentSchoolYearStart = it.year
|
||||
}
|
||||
dateSemester2Start?.let { profile.dateSemester2Start = it }
|
||||
dateYearEnd?.let { profile.dateYearEnd = it }
|
||||
|
||||
profileList.add(profile)
|
||||
}
|
||||
|
||||
onSuccess()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login
|
||||
|
||||
import pl.szczodrzynski.edziennik.R
|
||||
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_API
|
||||
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_HEBE
|
||||
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_WEB_MAIN
|
||||
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
|
||||
import pl.szczodrzynski.edziennik.utils.Utils
|
||||
@ -54,6 +55,10 @@ class VulcanLogin(val data: DataVulcan, val onSuccess: () -> Unit) {
|
||||
data.startProgress(R.string.edziennik_progress_login_vulcan_api)
|
||||
VulcanLoginApi(data) { onSuccess(loginMethodId) }
|
||||
}
|
||||
LOGIN_METHOD_VULCAN_HEBE -> {
|
||||
data.startProgress(R.string.edziennik_progress_login_vulcan_api)
|
||||
VulcanLoginHebe(data) { onSuccess(loginMethodId) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -191,18 +191,6 @@ class VulcanLoginApi(val data: DataVulcan, val onSuccess: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
val deviceId = data.app.deviceId.padStart(16, '0')
|
||||
val loginStoreId = data.loginStore.id.toString(16).padStart(4, '0')
|
||||
val symbol = data.symbol?.crc16()?.toString(16)?.take(2) ?: "00"
|
||||
val uuid =
|
||||
deviceId.substring(0..7) +
|
||||
"-" + deviceId.substring(8..11) +
|
||||
"-" + deviceId.substring(12..15) +
|
||||
"-" + loginStoreId +
|
||||
"-" + symbol + "6f72616e7a"
|
||||
|
||||
val deviceNameSuffix = " - nie usuwać"
|
||||
|
||||
val szkolnyApi = SzkolnyApi(data.app)
|
||||
val firebaseToken = szkolnyApi.runCatching({
|
||||
getFirebaseToken("vulcan")
|
||||
@ -216,8 +204,8 @@ class VulcanLoginApi(val data: DataVulcan, val onSuccess: () -> Unit) {
|
||||
.addHeader("RequestMobileType", "RegisterDevice")
|
||||
.addParameter("PIN", data.apiPin[data.symbol])
|
||||
.addParameter("TokenKey", data.apiToken[data.symbol])
|
||||
.addParameter("DeviceId", uuid)
|
||||
.addParameter("DeviceName", VULCAN_API_DEVICE_NAME.take(50 - deviceNameSuffix.length) + deviceNameSuffix)
|
||||
.addParameter("DeviceId", data.buildDeviceId())
|
||||
.addParameter("DeviceName", VULCAN_API_DEVICE_NAME)
|
||||
.addParameter("DeviceNameUser", "")
|
||||
.addParameter("DeviceDescription", "")
|
||||
.addParameter("DeviceSystemType", "Android")
|
||||
|
@ -0,0 +1,105 @@
|
||||
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login
|
||||
|
||||
import com.google.gson.JsonObject
|
||||
import io.github.wulkanowy.signer.hebe.generateKeyPair
|
||||
import pl.szczodrzynski.edziennik.JsonObject
|
||||
import pl.szczodrzynski.edziennik.data.api.ERROR_LOGIN_DATA_MISSING
|
||||
import pl.szczodrzynski.edziennik.data.api.VULCAN_API_DEVICE_NAME
|
||||
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_REGISTER_NEW
|
||||
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
|
||||
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
|
||||
import pl.szczodrzynski.edziennik.data.api.models.ApiError
|
||||
import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi
|
||||
import pl.szczodrzynski.edziennik.getString
|
||||
import pl.szczodrzynski.edziennik.isNotNullNorEmpty
|
||||
|
||||
class VulcanLoginHebe(val data: DataVulcan, val onSuccess: () -> Unit) {
|
||||
companion object {
|
||||
private const val TAG = "VulcanLoginHebe"
|
||||
}
|
||||
|
||||
init { run {
|
||||
// i'm sure this does something useful
|
||||
// not quite sure what, though
|
||||
if (data.studentSemesterNumber == 1 && data.semester1Id == 0)
|
||||
data.semester1Id = data.studentSemesterNumber
|
||||
if (data.studentSemesterNumber == 2 && data.semester2Id == 0)
|
||||
data.semester2Id = data.studentSemesterNumber
|
||||
|
||||
copyFromLoginStore()
|
||||
|
||||
if (data.profile != null && data.isApiLoginValid()) {
|
||||
onSuccess()
|
||||
}
|
||||
else {
|
||||
if (data.symbol.isNotNullNorEmpty() && data.apiToken[data.symbol].isNotNullNorEmpty() && data.apiPin[data.symbol].isNotNullNorEmpty()) {
|
||||
loginWithToken()
|
||||
}
|
||||
else {
|
||||
data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING))
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
private fun copyFromLoginStore() {
|
||||
data.loginStore.data.apply {
|
||||
// map form inputs to the symbol
|
||||
if (has("symbol")) {
|
||||
data.symbol = getString("symbol")
|
||||
remove("symbol")
|
||||
}
|
||||
if (has("deviceToken")) {
|
||||
data.apiToken = data.apiToken.toMutableMap().also {
|
||||
it[data.symbol] = getString("deviceToken")
|
||||
}
|
||||
remove("deviceToken")
|
||||
}
|
||||
if (has("devicePin")) {
|
||||
data.apiPin = data.apiPin.toMutableMap().also {
|
||||
it[data.symbol] = getString("devicePin")
|
||||
}
|
||||
remove("devicePin")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loginWithToken() {
|
||||
val szkolnyApi = SzkolnyApi(data.app)
|
||||
val hebe = VulcanHebe(data, null)
|
||||
|
||||
if (data.hebePublicKey == null || data.hebePrivateKey == null || data.hebePublicHash == null) {
|
||||
val (publicPem, privatePem, publicHash) = generateKeyPair()
|
||||
data.hebePublicKey = publicPem
|
||||
data.hebePrivateKey = privatePem
|
||||
data.hebePublicHash = publicHash
|
||||
}
|
||||
|
||||
szkolnyApi.runCatching({
|
||||
data.app.config.sync.tokenVulcanHebe = getFirebaseToken("vulcan")
|
||||
}, onError = {
|
||||
// screw errors
|
||||
})
|
||||
|
||||
hebe.apiPost(
|
||||
TAG,
|
||||
VULCAN_HEBE_ENDPOINT_REGISTER_NEW,
|
||||
payload = JsonObject(
|
||||
"OS" to "Android",
|
||||
"PIN" to data.apiPin[data.symbol],
|
||||
"Certificate" to data.hebePublicKey,
|
||||
"CertificateType" to "RSA_PEM",
|
||||
"DeviceModel" to VULCAN_API_DEVICE_NAME,
|
||||
"SecurityToken" to data.apiToken[data.symbol],
|
||||
"SelfIdentifier" to data.buildDeviceId(),
|
||||
"CertificateThumbprint" to data.hebePublicHash
|
||||
),
|
||||
baseUrl = true
|
||||
) { _: JsonObject, _ ->
|
||||
data.apiToken = data.apiToken.toMutableMap().also {
|
||||
it[data.symbol] = it[data.symbol]?.substring(0, 3)
|
||||
}
|
||||
data.loginStore.removeLoginData("apiPin")
|
||||
onSuccess()
|
||||
}
|
||||
}
|
||||
}
|
@ -182,6 +182,58 @@ object LoginInfo {
|
||||
ERROR_LOGIN_VULCAN_EXPIRED_TOKEN to R.string.login_error_expired_token
|
||||
)
|
||||
),
|
||||
Mode(
|
||||
loginMode = LOGIN_MODE_VULCAN_HEBE,
|
||||
name = R.string.login_mode_vulcan_api,
|
||||
icon = R.drawable.login_mode_vulcan_hebe,
|
||||
hintText = R.string.login_mode_vulcan_api_hint,
|
||||
guideText = R.string.login_mode_vulcan_api_guide,
|
||||
isTesting = true,
|
||||
credentials = listOf(
|
||||
FormField(
|
||||
keyName = "deviceToken",
|
||||
name = R.string.login_hint_token,
|
||||
icon = CommunityMaterial.Icon.cmd_code_braces,
|
||||
emptyText = R.string.login_error_no_token,
|
||||
invalidText = R.string.login_error_incorrect_token,
|
||||
errorCodes = mapOf(
|
||||
ERROR_LOGIN_VULCAN_INVALID_TOKEN to R.string.login_error_incorrect_token
|
||||
),
|
||||
isRequired = true,
|
||||
validationRegex = "[A-Z0-9]{5,12}",
|
||||
caseMode = FormField.CaseMode.UPPER_CASE
|
||||
),
|
||||
FormField(
|
||||
keyName = "symbol",
|
||||
name = R.string.login_hint_symbol,
|
||||
icon = CommunityMaterial.Icon2.cmd_school,
|
||||
emptyText = R.string.login_error_no_symbol,
|
||||
invalidText = R.string.login_error_incorrect_symbol,
|
||||
errorCodes = mapOf(
|
||||
ERROR_LOGIN_VULCAN_INVALID_SYMBOL to R.string.login_error_incorrect_symbol
|
||||
),
|
||||
isRequired = true,
|
||||
validationRegex = "[a-z0-9_-]+",
|
||||
caseMode = FormField.CaseMode.LOWER_CASE
|
||||
),
|
||||
FormField(
|
||||
keyName = "devicePin",
|
||||
name = R.string.login_hint_pin,
|
||||
icon = CommunityMaterial.Icon2.cmd_lock,
|
||||
emptyText = R.string.login_error_no_pin,
|
||||
invalidText = R.string.login_error_incorrect_pin,
|
||||
errorCodes = mapOf(
|
||||
ERROR_LOGIN_VULCAN_INVALID_PIN to R.string.login_error_incorrect_pin
|
||||
),
|
||||
isRequired = true,
|
||||
validationRegex = "[0-9]+",
|
||||
caseMode = FormField.CaseMode.LOWER_CASE
|
||||
)
|
||||
),
|
||||
errorCodes = mapOf(
|
||||
ERROR_LOGIN_VULCAN_EXPIRED_TOKEN to R.string.login_error_expired_token
|
||||
)
|
||||
),
|
||||
Mode(
|
||||
loginMode = LOGIN_MODE_VULCAN_WEB,
|
||||
name = R.string.login_mode_vulcan_web,
|
||||
|
BIN
app/src/main/res/drawable/login_mode_vulcan_hebe.png
Normal file
BIN
app/src/main/res/drawable/login_mode_vulcan_hebe.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.0 KiB |
Loading…
x
Reference in New Issue
Block a user