diff --git a/.gitignore b/.gitignore index bdf87c8..b7bcbc9 100644 --- a/.gitignore +++ b/.gitignore @@ -19,9 +19,11 @@ bin/ ### IntelliJ IDEA ### .idea +.kotlin *.iws *.iml *.ipr +*.log out/ !**/src/main/**/out/ !**/src/test/**/out/ diff --git a/cloudapi-model/src/main/kotlin/cn/edu/buaa/scs/model/TicketResponse.kt b/cloudapi-model/src/main/kotlin/cn/edu/buaa/scs/model/TicketResponse.kt new file mode 100644 index 0000000..415abd8 --- /dev/null +++ b/cloudapi-model/src/main/kotlin/cn/edu/buaa/scs/model/TicketResponse.kt @@ -0,0 +1,13 @@ +package cn.edu.buaa.scs.model + +/** + * + * @param ticket 访问凭证 + * @param host 服务器主机地址 + */ +data class TicketResponse( + /* 访问凭证 */ + val ticket: kotlin.String? = null, + /* 服务器主机地址 */ + val host: kotlin.String? = null +) diff --git a/cloudapi-web/build.gradle.kts b/cloudapi-web/build.gradle.kts index 04a6fd3..cbf338b 100644 --- a/cloudapi-web/build.gradle.kts +++ b/cloudapi-web/build.gradle.kts @@ -43,6 +43,8 @@ dependencies { implementation("io.ktor:ktor-server-status-pages:$ktor_version") testImplementation("io.ktor:ktor-server-tests-jvm:$ktor_version") testImplementation("io.ktor:ktor-server-test-host-jvm:$ktor_version") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:1.7.3") + implementation("ch.qos.logback:logback-classic:1.4.11") // Redis implementation("io.lettuce:lettuce-core:6.1.5.RELEASE") diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/auth/Token.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/auth/Token.kt index bcb5e83..48b8f20 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/auth/Token.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/auth/Token.kt @@ -68,16 +68,16 @@ fun fetchToken(call: ApplicationCall) { else -> { // TODO 后续兼容JWT校验 - // 1. try to get token from cookies - possibleTokenKey.firstNotNullOfOrNull { call.request.cookies[it] } ?: - - // 2. try to get token from headers + // 1. try to get token from headers possibleTokenKey.firstNotNullOfOrNull { call.request.headers[it]?.let { auth -> if (auth.startsWith("Bearer")) auth.split(" ")[1] else auth } } ?: + // 2. try to get token from cookies + possibleTokenKey.firstNotNullOfOrNull { call.request.cookies[it] } ?: + // 3. try to get token from query parameters possibleTokenKey.firstNotNullOfOrNull { call.request.queryParameters[it] } @@ -110,7 +110,7 @@ fun fetchToken(call: ApplicationCall) { // redis uuid token authRedis.checkToken(token) ?: // error - throw throw AuthorizationException("incorrect token") + throw throw AuthorizationException("登录已过期,请重新登录") val user = User.id(userId) setUser(user) diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/ChatHistoryItem.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/ChatHistoryItem.kt new file mode 100644 index 0000000..fc94336 --- /dev/null +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/ChatHistoryItem.kt @@ -0,0 +1,30 @@ +/** +* cloudapi_v2 +* buaa scs cloud api v2 +* +* The version of the OpenAPI document: 2.0 +* Contact: loheagn@icloud.com +* +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ +package cn.edu.buaa.scs.controller.models + + +/** + * + * @param id + * @param chatId + * @param updateTime + * @param title + * @param top + */ +data class ChatHistoryItem( + val id: kotlin.Int? = null, + val chatId: kotlin.String? = null, + val updateTime: kotlin.Long? = null, + val title: kotlin.String? = null, + val top: kotlin.Boolean? = null +) + diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/CreateUserRequest.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/CreateUserRequest.kt index 04d6a3a..d5c4ae8 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/CreateUserRequest.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/CreateUserRequest.kt @@ -18,6 +18,7 @@ package cn.edu.buaa.scs.controller.models * @param departmentId 所在单位 * @param role 1 for student, 2 for teacher, 4 for admin * @param name 姓名 + * @param email 邮箱 */ data class CreateUserRequest( /* 学工号 */ @@ -27,6 +28,8 @@ data class CreateUserRequest( /* 1 for student, 2 for teacher, 4 for admin */ val role: kotlin.Int, /* 姓名 */ - val name: kotlin.String? = null + val name: kotlin.String? = null, + /* 邮箱 */ + val email: kotlin.String? = null ) diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/GetChatRecordsRequest.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/GetChatRecordsRequest.kt new file mode 100644 index 0000000..8ab9aa2 --- /dev/null +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/GetChatRecordsRequest.kt @@ -0,0 +1,30 @@ +/** +* cloudapi_v2 +* buaa scs cloud api v2 +* +* The version of the OpenAPI document: 2.0 +* Contact: loheagn@icloud.com +* +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ +package cn.edu.buaa.scs.controller.models + + +/** + * + * @param chatId + * @param appId + * @param offset + * @param pageSize + * @param loadCustomFeedbacks + */ +data class GetChatRecordsRequest( + val chatId: kotlin.String, + val appId: kotlin.String? = null, + val offset: kotlin.Int? = null, + val pageSize: kotlin.Int? = null, + val loadCustomFeedbacks: kotlin.Boolean? = null +) + diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/PatchUserRequest.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/PatchUserRequest.kt index fc55485..9081152 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/PatchUserRequest.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/PatchUserRequest.kt @@ -17,10 +17,12 @@ package cn.edu.buaa.scs.controller.models * @param name * @param email * @param nickname + * @param departmentId */ data class PatchUserRequest( val name: kotlin.String? = null, val email: kotlin.String? = null, - val nickname: kotlin.String? = null + val nickname: kotlin.String? = null, + val departmentId: kotlin.Int? = null ) diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/TicketResponse.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/TicketResponse.kt new file mode 100644 index 0000000..6052493 --- /dev/null +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/models/TicketResponse.kt @@ -0,0 +1,26 @@ +/** +* cloudapi_v2 +* buaa scs cloud api v2 +* +* The version of the OpenAPI document: 2.0 +* Contact: loheagn@icloud.com +* +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ +package cn.edu.buaa.scs.controller.models + + +/** + * + * @param ticket 访问凭证 + * @param host 服务器主机地址 + */ +data class TicketResponse( + /* 访问凭证 */ + val ticket: kotlin.String? = null, + /* 服务器主机地址 */ + val host: kotlin.String? = null +) + diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/plugins/Monitoring.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/plugins/Monitoring.kt index 242c4d4..bd61f73 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/plugins/Monitoring.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/plugins/Monitoring.kt @@ -41,7 +41,7 @@ fun Application.configureMonitoring() { pathDescription = "", headers = call.request.headers.toMap(), version = version, - realIp = call.request.headers["X-Custom-Remote-Addr"] ?: call.request.origin.host, + realIp = call.request.headers["X-Custom-Remote-Addr"] ?: call.request.origin.localHost, userAgent = userAgent, ) val logRecordResp = LogRecordResp( diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/plugins/Routing.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/plugins/Routing.kt index 2afe8c2..5cd8445 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/plugins/Routing.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/controller/plugins/Routing.kt @@ -11,6 +11,7 @@ fun Application.configureRouting() { route("/api/v2") { authRoute() userRoute() + chatRoute() courseRoute() experimentRoute() fileRoute() diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/kube/BusinessKubeClient.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/kube/BusinessKubeClient.kt index 4726cf9..9982db0 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/kube/BusinessKubeClient.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/kube/BusinessKubeClient.kt @@ -91,7 +91,7 @@ spec: apiVersion: cloudapi.scs.buaa.edu.cn/v1alpha1 kind: ResourcePool metadata: - name: personal-$userId + name: personal-${userId.lowercase()} labels: owner: $userId spec: diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/kube/crd/v1alpha1/VirtualMachine.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/kube/crd/v1alpha1/VirtualMachine.kt index 31600f9..295b6df 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/kube/crd/v1alpha1/VirtualMachine.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/kube/crd/v1alpha1/VirtualMachine.kt @@ -168,6 +168,8 @@ class VirtualMachineReconciler(val client: KubernetesClient) : Reconciler, IEntity { + var id: Int + var userId: String + var chatId: String + var updateTime: Long + var title: String + var top: Boolean + + companion object : Entity.Factory() + + override fun entityId(): IntOrString { + return IntOrString(this.id) + } +} + +object ChatHistories : Table("chat_history") { + val id = int("id").primaryKey().bindTo { it.id } + val userId = varchar("userId").bindTo { it.userId } + val chatId = varchar("chatId").bindTo { it.chatId } + val updateTime = long("updateTime").bindTo { it.updateTime } + val title = varchar("title").bindTo { it.title } + val top = boolean("top").bindTo { it.top } +} + +@Suppress("unused") +val Database.chatHistory + get() = this.sequenceOf(ChatHistories) diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/Admin.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/Admin.kt index 9025a37..45b2b89 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/Admin.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/Admin.kt @@ -16,8 +16,9 @@ fun Route.adminRoute() { val req = call.receive() val role = UserRole.fromLevel(req.role) val name = req.name + val email = req.email val departmentId = req.departmentId - call.respond(convertUserModel(call.admin.addUser(req.id, name, role, departmentId))) + call.respond(convertUserModel(call.admin.addUser(req.id, name, role, email, departmentId))) } delete { diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/Auth.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/Auth.kt index b3034c9..b2d5abe 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/Auth.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/Auth.kt @@ -39,7 +39,7 @@ fun Route.authRoute() { route("buaaSSOLogin") { post { - val ssoToken = call.request.queryParameters["ssoToken"] ?: throw BadRequestException("ssoToken is required") + val ssoToken = call.request.queryParameters["ssoToken"] ?: throw BadRequestException("请传入统一认证 token") call.respond( call.auth.buaaSSOLogin(ssoToken) ) @@ -55,8 +55,8 @@ fun Route.authRoute() { route("/tokenInfo") { post { val req = call.receiveParameters() - val token = req["token"] ?: throw BadRequestException("token is required") - val service = req["service"] ?: throw BadRequestException("service is required") + val token = req["token"] ?: throw BadRequestException("请传入统一认证 token") + val service = req["service"] ?: throw BadRequestException("请传入统一认证服务名") call.respond( call.auth.getTokenInfo(token, service) ) @@ -65,9 +65,9 @@ fun Route.authRoute() { route("/checkPermission") { get { - val entityType = call.parameters["entityType"] ?: throw BadRequestException("entityType is required") - val entityId = call.parameters["entityId"] ?: throw BadRequestException("entityId is required") - val action = call.parameters["action"] ?: throw BadRequestException("action is required") + val entityType = call.parameters["entityType"] ?: throw BadRequestException("请传入对象类型") + val entityId = call.parameters["entityId"] ?: throw BadRequestException("请传入对象Id") + val action = call.parameters["action"] ?: throw BadRequestException("请传入动作") call.respond(call.auth.checkPermission(entityType, entityId, action)) } } diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/Chat.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/Chat.kt new file mode 100644 index 0000000..d48406e --- /dev/null +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/Chat.kt @@ -0,0 +1,32 @@ +package cn.edu.buaa.scs.route + +import cn.edu.buaa.scs.controller.models.ChatHistoryItem +import cn.edu.buaa.scs.controller.models.GetChatRecordsRequest +import cn.edu.buaa.scs.service.chat +import cn.edu.buaa.scs.utils.userId +import io.ktor.server.application.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* + +fun Route.chatRoute() { + route("/chat") { + route("/history") { + get { + call.respond(call.chat.getChatHistoryByUserId(call.userId())) + } + + post { + val chatHistoryItem = call.receive() + call.chat.createChatHistory(call.userId(), chatHistoryItem) + call.respond("OK") + } + } + + post("/records") { + val requestBody = call.receive() + val records = call.chat.getChatRecords(requestBody) + call.respond(records) + } + } +} diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/Experiment.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/Experiment.kt index 95eb0e9..08483fa 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/Experiment.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/Experiment.kt @@ -55,6 +55,12 @@ fun Route.experimentRoute() { call.respond(call.convertExperimentResponse(experiment)) } + delete { + val experimentId = call.getExpIdFromPath() + call.experiment.deleteById(experimentId) + call.respond("OK") + } + route("/assignments") { get { call.respond( diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/Vm.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/Vm.kt index c9441e9..8bd4c9e 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/Vm.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/route/Vm.kt @@ -43,6 +43,11 @@ fun Route.vmRoute() { } } + post("/ticket") { + val vmId = call.getVmIdFromPath() + val ticketResponse = call.vm.getWebTicket(vmId) + call.respond(ticketResponse) + } } route("/template") { diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Admin.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Admin.kt index b25e924..a87d6ee 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Admin.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Admin.kt @@ -17,12 +17,12 @@ val ApplicationCall.admin: AdminService class AdminService(val call: ApplicationCall) : IService { companion object : IService.Caller() - fun addUser(id: String, name: String?, role: UserRole, departmentId: Int): User { + fun addUser(id: String, name: String?, role: UserRole, email: String?, departmentId: Int): User { if (!call.user().isAdmin()) { throw AuthorizationException("only admin can add user") } - return User.createNewUnActiveUser(id, name, role, departmentId) + return User.createNewUnActiveUser(id, name, role, email, departmentId) } fun deleteUsers(userIds: List) { diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Auth.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Auth.kt index 5656260..1b5685c 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Auth.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Auth.kt @@ -73,11 +73,11 @@ class AuthService(val call: ApplicationCall) : IService { ): LoginUserResponse { // check useId if (!mysql.users.exists { it.id.eq(userId) }) { - throw BadRequestException("") + throw BadRequestException("用户名或密码错误") } // check password val user = mysql.users.find { it.id.eq(userId) and it.password.eq(passwordHash) } - ?: throw BadRequestException("") + ?: throw BadRequestException("用户名或密码错误") // check active if (!user.isAccepted) { throw BadRequestException("账号未激活,或信息不完整,请重新激活账户") @@ -194,7 +194,7 @@ class AuthService(val call: ApplicationCall) : IService { fun sendActiveEmail(id: String, name: String, email: String) { val user = User.id(id) if (user.isAccepted) { - throw cn.edu.buaa.scs.error.BadRequestException("the user is already active") + throw cn.edu.buaa.scs.error.BadRequestException("用户已经激活") } val token = "${user.id}${user.password}${System.currentTimeMillis()}".md5() + UlidCreator.getUlid().toString() diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Chat.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Chat.kt new file mode 100644 index 0000000..8b62c60 --- /dev/null +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Chat.kt @@ -0,0 +1,78 @@ +package cn.edu.buaa.scs.service + +import cn.edu.buaa.scs.controller.models.ChatHistoryItem +import cn.edu.buaa.scs.controller.models.GetChatRecordsRequest +import cn.edu.buaa.scs.model.* +import cn.edu.buaa.scs.storage.mysql +import cn.edu.buaa.scs.utils.HttpClientWrapper +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.request.* +import io.ktor.http.* +import io.ktor.serialization.jackson.* +import io.ktor.server.application.* +import org.ktorm.dsl.* +import org.ktorm.entity.* + +val ApplicationCall.chat + get() = ChatService.getSvc(this) { ChatService(this) } + +class ChatService(val call: ApplicationCall) : IService { + companion object : IService.Caller() + + internal val client by lazy { + HttpClientWrapper( + HttpClient(CIO) { + install(ContentNegotiation) { // 确保 JSON 序列化功能可用 + jackson() + } + defaultRequest { + header(HttpHeaders.Authorization, "Bearer fastgpt-sbHq9MGmKbijSJZe3l43BcFWUxNlkcISo1SSBqAHKsqXv8bGQoSIGh28Vw4Dryeu") + } + }, + basePath = "http://10.251.254.178:3000/api" + ) + } + + // 获取指定用户的聊天记录 + fun getChatHistoryByUserId(userId: String): List { + return mysql.chatHistory.filter { it.userId eq userId } + .map { convertToChatHistoryItem(it) }.toList() + } + + // 创建新的聊天记录 + fun createChatHistory(userId: String, chatHistoryItem: ChatHistoryItem): Int { + return mysql.insertAndGenerateKey(ChatHistories) { + set(ChatHistories.userId, userId) + set(ChatHistories.chatId, chatHistoryItem.chatId) + set(ChatHistories.updateTime, chatHistoryItem.updateTime) + set(ChatHistories.title, chatHistoryItem.title) + set(ChatHistories.top, chatHistoryItem.top) + } as Int + } + + // 获取聊天记录 + suspend fun getChatRecords(request: GetChatRecordsRequest): String { + val body = GetChatRecordsRequest( + chatId = request.chatId, + appId = "679e3b3b5fbd929e37095abc", + offset = 0, + pageSize = 30, + loadCustomFeedbacks = false, + ) + val response = client.post("/core/chat/getPaginationRecords", body) + return response.getOrThrow() + } + + private fun convertToChatHistoryItem(chatHistory: ChatHistory): ChatHistoryItem { + return ChatHistoryItem( + id = chatHistory.id, + chatId = chatHistory.chatId, + updateTime = chatHistory.updateTime, + title = chatHistory.title, + top = chatHistory.top + ) + } +} diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Course.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Course.kt index bbe7d4d..39442fc 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Course.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Course.kt @@ -187,7 +187,7 @@ class CourseService(val call: ApplicationCall) : IService { // make sure students exist studentIdList.forEachAsync { studentId -> if (!mysql.users.exists { it.id.inList(studentId.lowerUpperNormal()) }) { - User.createNewUnActiveUser(studentId, null, UserRole.STUDENT, 0) + User.createNewUnActiveUser(studentId, null, UserRole.STUDENT, "", 0) } } diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Experiment.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Experiment.kt index 2b97ea6..172d079 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Experiment.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Experiment.kt @@ -69,6 +69,10 @@ class ExperimentService(val call: ApplicationCall) : IService, FileService.FileD return experiment } + fun deleteById(expId: Int) { + mysql.delete(Experiments) { it.id eq expId } + } + private fun patchExperimentPeerInfo( experiment: Experiment, peerDescription: String?, diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Project.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Project.kt index 4461565..79bf2e7 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Project.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Project.kt @@ -68,7 +68,7 @@ class ProjectService(val call: ApplicationCall) : IService, FileService.FileDeco } suspend fun createUser(userID: String) { - createUser(User.id(userID)) +// createUser(User.id(userID)) } suspend fun createUser(user: User) { diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/User.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/User.kt index 61c94f7..6463b65 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/User.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/User.kt @@ -24,11 +24,12 @@ fun User.Companion.getUerListByIdList(idList: List): List { else mysql.users.filter { it.id.inList(idList) }.toList() } -fun User.Companion.createNewUnActiveUser(id: String, name: String?, role: UserRole, departmentId: Int): User { +fun User.Companion.createNewUnActiveUser(id: String, name: String?, role: UserRole, email: String?, departmentId: Int): User { val user = User { this.id = id this.name = name ?: "未激活用户" this.role = role + this.email = email ?: "" this.departmentId = departmentId } mysql.users.add(user) @@ -102,6 +103,9 @@ class UserService(val call: ApplicationCall) : IService { if (req.nickname != null) { user.nickName = req.nickname } + if (req.departmentId != null) { + user.departmentId = req.departmentId + } user.flushChanges() } diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Vm.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Vm.kt index 3934853..dd3f4c0 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Vm.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/service/Vm.kt @@ -54,6 +54,10 @@ class VmService(val call: ApplicationCall) : IService { return sfClient.getHosts().getOrThrow() } + suspend fun getWebTicket(uuid: String): TicketResponse { + return vmClient.getWebTicket(uuid).getOrThrow() + } + fun getPersonalVms(): List { val vmApplyList = mysql.vmApplyList.filter { ((it.studentId eq call.userId()) or (it.teacherId eq call.userId())) and (it.experimentId eq 0) } diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/utils/ApplicationCall.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/utils/ApplicationCall.kt index 57403aa..bc15f38 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/utils/ApplicationCall.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/utils/ApplicationCall.kt @@ -3,10 +3,13 @@ package cn.edu.buaa.scs.utils import cn.edu.buaa.scs.model.User +import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.plugins.* import io.ktor.server.plugins.callid.* import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.utils.io.* fun ApplicationCall.headerString(key: String): String = this.request.header(key) ?: throw BadRequestException("missing header: $key") diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/vm/VMModule.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/vm/VMModule.kt index 3d8d94a..8069dbb 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/vm/VMModule.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/vm/VMModule.kt @@ -5,7 +5,7 @@ import cn.edu.buaa.scs.vm.sangfor.SangforClient import cn.edu.buaa.scs.vm.vcenter.VCenterClient import io.ktor.server.application.* -lateinit var vmClient: IVMClient +lateinit var vmClient: VCenterClient lateinit var sfClient: SangforClient @Suppress("unused") diff --git a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/vm/vcenter/VCenterClient.kt b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/vm/vcenter/VCenterClient.kt index 34acb48..dc2bffe 100644 --- a/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/vm/vcenter/VCenterClient.kt +++ b/cloudapi-web/src/main/kotlin/cn/edu/buaa/scs/vm/vcenter/VCenterClient.kt @@ -3,7 +3,10 @@ package cn.edu.buaa.scs.vm.vcenter import cn.edu.buaa.scs.config.globalConfig import cn.edu.buaa.scs.error.NotFoundException import cn.edu.buaa.scs.model.Host +import cn.edu.buaa.scs.model.TicketResponse import cn.edu.buaa.scs.model.VirtualMachine +import cn.edu.buaa.scs.model.virtualMachines +import cn.edu.buaa.scs.storage.mysql import cn.edu.buaa.scs.utils.HttpClientWrapper import cn.edu.buaa.scs.utils.schedule.waitForDone import cn.edu.buaa.scs.vm.* @@ -14,6 +17,9 @@ import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.request.* import io.ktor.http.* import io.ktor.serialization.jackson.* +import org.ktorm.dsl.and +import org.ktorm.dsl.eq +import org.ktorm.entity.find import org.ktorm.jackson.KtormModule object VCenterClient : IVMClient { @@ -51,10 +57,12 @@ object VCenterClient : IVMClient { client.get("/vm/$uuid").getOrThrow() } + suspend fun getWebTicket(uuid: String): Result = runCatching { + client.post("/vm/$uuid/ticket").getOrThrow() + } + override suspend fun getVMByName(name: String, applyId: String): Result = runCatching { - getAllVMs().getOrElse { listOf() }.find { vm -> - vm.name == name && vm.applyId == applyId - } ?: throw vmNotFound(name) + mysql.virtualMachines.find { (it.name eq name) and (it.applyId eq applyId) }?: throw vmNotFound(name) } override suspend fun powerOnSync(uuid: String): Result = runCatching { diff --git a/gradle.properties b/gradle.properties index 6f11cfc..c1bbeab 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ kotlin_version=1.7.10 -kotlin.code.style=official +kotlin.code.style=official \ No newline at end of file diff --git a/openapi/cloudapi_v2.yaml b/openapi/cloudapi_v2.yaml index d7f963a..6ca0edb 100644 --- a/openapi/cloudapi_v2.yaml +++ b/openapi/cloudapi_v2.yaml @@ -90,6 +90,22 @@ paths: $ref: '#/components/schemas/PutExperimentRequest' security: - Authorization: [] + delete: + summary: 删除一项实验(作业) + tags: + - 作业 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ExperimentResponse' + operationId: delete-experiment-experimentId + description: 删除一项实验(作业) + security: + - Authorization: [ ] + parameters: [ ] '/experiment/{experimentId}/assignment': parameters: - schema: @@ -1006,6 +1022,27 @@ paths: in: query name: sync description: 表示该请求是否同步返回,默认为false,即默认异步。client需要在后续查询具体的执行情况 + '/vm/{vmId}/ticket': + parameters: + - schema: + type: string + name: vmId + in: path + required: true + description: vm uuid + post: + summary: 创建 Web 访问凭证 + tags: + - 虚拟机 + description: 创建虚拟机的 Web 访问凭证,返回 ticket 和 host。 + operationId: post-vm-vmId-ticket + responses: + '200': + description: 成功创建访问凭证 + content: + application/json: + schema: + $ref: '#/components/schemas/TicketResponse' /vms: get: summary: get Virtual Machine list @@ -2709,6 +2746,41 @@ paths: description: 教师获取自己的全部助教 security: - Authorization: [] + /chat/history: + get: + summary: 按用户 id 获取历史聊天 + responses: + '200': + description: 聊天历史获取成功 + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ChatHistoryItem' + post: + summary: 新增一条聊天记录 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ChatHistoryItem' + responses: + '200': + description: 成功保存历史记录 + /chat/records: + post: + summary: 获取某个聊天的聊天记录 + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/GetChatRecordsRequest" + responses: + "200": + description: 成功获取到聊天记录 components: schemas: AssignmentResponse: @@ -4731,6 +4803,9 @@ components: type: integer format: int32 description: '所在单位' + email: + type: string + description: '邮箱' role: type: integer format: int32 @@ -4806,6 +4881,9 @@ components: type: string nickname: type: string + departmentId: + type: integer + format: int32 ChangePasswordRequest: title: ChangePasswordRequest x-stoplight: @@ -4891,6 +4969,44 @@ components: type: string required: - studentId + ChatHistoryItem: + type: object + properties: + id: + type: integer + chatId: + type: string + updateTime: + type: integer + format: int64 + title: + type: string + top: + type: boolean + GetChatRecordsRequest: + type: object + required: + - chatId + properties: + appId: + type: string + chatId: + type: string + offset: + type: integer + pageSize: + type: integer + loadCustomFeedbacks: + type: boolean + TicketResponse: + type: object + properties: + ticket: + type: string + description: 访问凭证 + host: + type: string + description: 服务器主机地址 securitySchemes: Authorization: type: apiKey diff --git a/vcenter/src/main/kotlin/cn/edu/buaa/scs/vcenter/Connection.kt b/vcenter/src/main/kotlin/cn/edu/buaa/scs/vcenter/Connection.kt index 02ac38f..fce0932 100644 --- a/vcenter/src/main/kotlin/cn/edu/buaa/scs/vcenter/Connection.kt +++ b/vcenter/src/main/kotlin/cn/edu/buaa/scs/vcenter/Connection.kt @@ -4,6 +4,7 @@ import com.vmware.photon.controller.model.adapters.vsphere.util.connection.Conne import com.vmware.photon.controller.model.adapters.vsphere.util.connection.GetMoRef import com.vmware.vim25.ManagedObjectReference import com.vmware.vim25.RetrieveOptions +import com.vmware.vim25.VirtualMachineTicket fun Connection.getMoRef(): GetMoRef { return GetMoRef(this) diff --git a/vcenter/src/main/kotlin/cn/edu/buaa/scs/vcenter/VCenterRoute.kt b/vcenter/src/main/kotlin/cn/edu/buaa/scs/vcenter/VCenterRoute.kt index fb6f4b2..f85f2db 100644 --- a/vcenter/src/main/kotlin/cn/edu/buaa/scs/vcenter/VCenterRoute.kt +++ b/vcenter/src/main/kotlin/cn/edu/buaa/scs/vcenter/VCenterRoute.kt @@ -59,6 +59,11 @@ fun Application.vcenterRouting() { VCenterWrapper.convertVMToTemplate(call.getVmUuid()).getOrThrow() call.respond("OK") } + + post("/ticket") { + val ticketResponse = VCenterWrapper.getWebTicket(call.getVmUuid()) + call.respond(ticketResponse) + } } route("/health") { diff --git a/vcenter/src/main/kotlin/cn/edu/buaa/scs/vcenter/VCenterWrapper.kt b/vcenter/src/main/kotlin/cn/edu/buaa/scs/vcenter/VCenterWrapper.kt index cdeef06..37cac5a 100644 --- a/vcenter/src/main/kotlin/cn/edu/buaa/scs/vcenter/VCenterWrapper.kt +++ b/vcenter/src/main/kotlin/cn/edu/buaa/scs/vcenter/VCenterWrapper.kt @@ -4,6 +4,7 @@ import cn.edu.buaa.scs.config.globalConfig import cn.edu.buaa.scs.model.VirtualMachine import cn.edu.buaa.scs.model.VirtualMachineExtraInfo import cn.edu.buaa.scs.model.applyExtraInfo +import cn.edu.buaa.scs.model.TicketResponse import cn.edu.buaa.scs.utils.jsonMapper import cn.edu.buaa.scs.utils.logger import cn.edu.buaa.scs.vm.ConfigVmOptions @@ -360,6 +361,17 @@ object VCenterWrapper { }.getOrThrow() } + suspend fun getWebTicket(uuid: String): TicketResponse { + return baseSyncTask { connection -> + val vmRef = connection.getVmRefByUuid(uuid) + val vmTicket = connection.vimPort.acquireTicket(vmRef, "webmks") + TicketResponse( + ticket = vmTicket.ticket, + host = vmTicket.host + ) + }.getOrThrow() + } + suspend fun convertVMToTemplate(uuid: String): Result { return baseSyncTask { connection -> val vmRef = connection.getVmRefByUuid(uuid)