metabase integration
This commit is contained in:
parent
8ac197515b
commit
ee8c8423e7
@ -0,0 +1,69 @@
|
||||
package com.nisemoe.nise.controller
|
||||
|
||||
import com.nisemoe.nise.integrations.MetabaseService
|
||||
import com.nisemoe.nise.service.AuthService
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@RestController
|
||||
@RequestMapping("metabase")
|
||||
class MetabaseController(
|
||||
private val authService: AuthService,
|
||||
private val metabaseService: MetabaseService
|
||||
) {
|
||||
|
||||
data class GetMetabaseUserResponse(
|
||||
val email: String
|
||||
)
|
||||
|
||||
@GetMapping("status")
|
||||
fun getMetabaseStatus(): ResponseEntity<GetMetabaseUserResponse> {
|
||||
if(!this.authService.isLoggedIn() || !this.authService.isAdmin())
|
||||
return ResponseEntity.badRequest().build()
|
||||
|
||||
val currentUser = this.authService.getCurrentUser()
|
||||
|
||||
val metabaseUser = this.metabaseService.getUser(currentUser.username)
|
||||
?: return ResponseEntity.notFound().build()
|
||||
|
||||
return ResponseEntity.ok(GetMetabaseUserResponse(metabaseUser))
|
||||
}
|
||||
|
||||
data class CreateMetabaseUserResponse(
|
||||
val email: String,
|
||||
val password: String
|
||||
)
|
||||
|
||||
@GetMapping("create")
|
||||
fun createMetabaseUser(): ResponseEntity<CreateMetabaseUserResponse> {
|
||||
if(!this.authService.isLoggedIn() || !this.authService.isAdmin())
|
||||
return ResponseEntity.badRequest().build()
|
||||
|
||||
val currentUser = this.authService.getCurrentUser()
|
||||
|
||||
try {
|
||||
val newUser = this.metabaseService.createNewUser(currentUser.username)
|
||||
return ResponseEntity.ok(CreateMetabaseUserResponse(newUser.email, newUser.password))
|
||||
} catch (e: Exception) {
|
||||
return ResponseEntity.badRequest().build()
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("reset")
|
||||
fun resetMetabaseUser(): ResponseEntity<CreateMetabaseUserResponse> {
|
||||
if(!this.authService.isLoggedIn() || !this.authService.isAdmin())
|
||||
return ResponseEntity.badRequest().build()
|
||||
|
||||
val currentUser = this.authService.getCurrentUser()
|
||||
|
||||
try {
|
||||
val newUser = this.metabaseService.resetPassword(currentUser.username)
|
||||
return ResponseEntity.ok(CreateMetabaseUserResponse(newUser.email, newUser.password))
|
||||
} catch (e: Exception) {
|
||||
return ResponseEntity.badRequest().build()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,164 @@
|
||||
package com.nisemoe.nise.integrations
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import org.springframework.beans.factory.InitializingBean
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.stereotype.Service
|
||||
import java.net.URI
|
||||
import java.net.http.HttpClient
|
||||
import java.net.http.HttpRequest
|
||||
import java.net.http.HttpResponse
|
||||
import java.security.SecureRandom
|
||||
import java.util.*
|
||||
|
||||
@Service
|
||||
class MetabaseService : InitializingBean {
|
||||
|
||||
@Value("\${METABASE_API_KEY}")
|
||||
private lateinit var metabaseApiKey: String
|
||||
|
||||
@Value("\${METABASE_URL:https://neko.nise.moe}")
|
||||
private lateinit var metabaseUrl: String
|
||||
|
||||
override fun afterPropertiesSet() {
|
||||
if (this.metabaseApiKey.isBlank())
|
||||
throw IllegalArgumentException("METABASE_API_KEY is not set")
|
||||
}
|
||||
|
||||
private val httpClient: HttpClient = HttpClient.newBuilder()
|
||||
.version(HttpClient.Version.HTTP_1_1)
|
||||
.build()
|
||||
|
||||
data class CreateNewUserResponse(
|
||||
val email: String,
|
||||
val password: String
|
||||
)
|
||||
|
||||
private fun getUserDataFromUsername(name: String): UserData? {
|
||||
val email = usernameToEmail(name)
|
||||
|
||||
val request = HttpRequest.newBuilder()
|
||||
.uri(URI.create("$metabaseUrl/api/user/?query=$email"))
|
||||
.GET()
|
||||
.header("Content-type", "application/json")
|
||||
.header("x-api-key", this.metabaseApiKey)
|
||||
.build()
|
||||
|
||||
val response = HttpClient.newBuilder().build().send(request, HttpResponse.BodyHandlers.ofString())
|
||||
|
||||
return if (response.statusCode() == 200) {
|
||||
val json = Json { ignoreUnknownKeys = true }
|
||||
val responseBody = json.decodeFromString<GetUserResponse>(response.body())
|
||||
responseBody.data.firstOrNull { it.email == email }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateRandomPassword(length: Int = 16): String {
|
||||
val charPool = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
val secureRandom = SecureRandom()
|
||||
return (1..length)
|
||||
.map { secureRandom.nextInt(charPool.length) }
|
||||
.map(charPool::get)
|
||||
.joinToString("")
|
||||
}
|
||||
|
||||
fun createNewUser(name: String): CreateNewUserResponse {
|
||||
val email = usernameToEmail(name)
|
||||
val password = generateRandomPassword()
|
||||
|
||||
val requestBody = buildJsonObject {
|
||||
put("email", email)
|
||||
put("password", password)
|
||||
}
|
||||
|
||||
val request = HttpRequest.newBuilder()
|
||||
.uri(URI.create("$metabaseUrl/api/user/"))
|
||||
.POST(HttpRequest.BodyPublishers.ofString(Json.encodeToString(requestBody)))
|
||||
.header("Content-type", "application/json")
|
||||
.header("x-api-key", this.metabaseApiKey)
|
||||
.build()
|
||||
|
||||
val response = httpClient.send(request, HttpResponse.BodyHandlers.ofString())
|
||||
|
||||
if (response.statusCode() == 200) {
|
||||
val responseBody = Json.decodeFromString<Map<String, Any>>(response.body())
|
||||
if (responseBody["email"] == email &&
|
||||
responseBody["is_active"] == true &&
|
||||
responseBody["is_superuser"] == false
|
||||
) {
|
||||
return CreateNewUserResponse(
|
||||
email = email,
|
||||
password = password
|
||||
)
|
||||
} else {
|
||||
throw RuntimeException("Failed to validate user creation response: ${response.body()}")
|
||||
}
|
||||
} else if (response.statusCode() == 400) {
|
||||
throw IllegalArgumentException("User already exists.")
|
||||
} else {
|
||||
throw RuntimeException("Failed to create user: ${response.body()}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun usernameToEmail(name: String): String {
|
||||
val cleanedName = name.replace(" ", "_").lowercase(Locale.getDefault())
|
||||
val email = "$cleanedName@nise.moe"
|
||||
return email
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class GetUserResponse(
|
||||
val data: List<UserData>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UserData(
|
||||
val id: Int,
|
||||
val email: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Fetches a user from Metabase by their username.
|
||||
* @param name The username to fetch.
|
||||
* @return The user's email if found, null otherwise.
|
||||
*/
|
||||
fun getUser(name: String): String? {
|
||||
val user = getUserDataFromUsername(name)
|
||||
return user?.email
|
||||
}
|
||||
|
||||
fun resetPassword(name: String): CreateNewUserResponse {
|
||||
val user = getUserDataFromUsername(name) ?: throw IllegalArgumentException("User not found.")
|
||||
val password = generateRandomPassword()
|
||||
|
||||
val requestBody = buildJsonObject {
|
||||
put("password", password)
|
||||
}
|
||||
|
||||
val request = HttpRequest.newBuilder()
|
||||
.uri(URI.create("$metabaseUrl/api/user/${user.id}/password"))
|
||||
.PUT(HttpRequest.BodyPublishers.ofString(Json.encodeToString(requestBody)))
|
||||
.header("Content-type", "application/json")
|
||||
.header("x-api-key", this.metabaseApiKey)
|
||||
.build()
|
||||
|
||||
val response = httpClient.send(request, HttpResponse.BodyHandlers.ofString())
|
||||
|
||||
if (response.statusCode() == 204) {
|
||||
return CreateNewUserResponse(
|
||||
email = user.email,
|
||||
password = password
|
||||
)
|
||||
} else {
|
||||
throw RuntimeException("Failed to reset password: ${response.body()}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -12,6 +12,7 @@ import {BanlistComponent} from "./banlist/banlist.component";
|
||||
import {ProfileComponent} from "./profile/profile.component";
|
||||
import {ApiComponent} from "./api/api.component";
|
||||
import {ApiPythonComponent} from "./api-python/api-python.component";
|
||||
import {MetabaseComponent} from "./metabase/metabase.component";
|
||||
|
||||
const routes: Routes = [
|
||||
{path: 'sus/:f', component: ViewSuspiciousScoresComponent, title: '/sus/'},
|
||||
@ -36,6 +37,8 @@ const routes: Routes = [
|
||||
{path: 'docs', component: ApiComponent, title: '/api/ explanation'},
|
||||
{path: 'docs/py', component: ApiPythonComponent, title: 'python lib'},
|
||||
|
||||
{path: 'neko', component: MetabaseComponent, title: 'metabase integration'},
|
||||
|
||||
{path: '**', component: HomeComponent, title: '/nise.moe/'},
|
||||
];
|
||||
|
||||
|
||||
23
nise-frontend/src/app/metabase/metabase.component.html
Normal file
23
nise-frontend/src/app/metabase/metabase.component.html
Normal file
@ -0,0 +1,23 @@
|
||||
<div class="main term mb-2">
|
||||
<h1 class="mb-4"># <span class="board">/neko/</span> integration</h1>
|
||||
|
||||
<ng-container *ngIf="!this.isLoading" class="fade-stuff">
|
||||
<ng-container *ngIf="this.user; else createUserTemplate">
|
||||
<p>it looks look you already have a <a href="https://neko.nise.moe" target="_blank">neko.nise.moe</a> user. feel
|
||||
free to use it any time.</p>
|
||||
<button (click)="this.resetPassword()">reset pw</button>
|
||||
</ng-container>
|
||||
<ng-template #createUserTemplate>
|
||||
<p>it looks like you don't have a <a href="https://neko.nise.moe" target="_blank">neko.nise.moe</a> user. you can
|
||||
create one here.</p>
|
||||
<button (click)="this.createNewUser()">create user</button>
|
||||
</ng-template>
|
||||
<ng-container *ngIf="this.createResponse">
|
||||
<p>email: <code>{{ this.createResponse.email }}</code></p>
|
||||
<p>password: <code>{{ this.createResponse.password }}</code> <span
|
||||
style="margin-left: 4px; background-color: indianred; color: black; padding: 3px"> save, wont be shown again.</span>
|
||||
</p>
|
||||
<p>^ use the above credentials to login @ <a href="https://neko.nise.moe" target="_blank">neko.nise.moe</a> (feel free to change the password afterwards)</p>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
76
nise-frontend/src/app/metabase/metabase.component.ts
Normal file
76
nise-frontend/src/app/metabase/metabase.component.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {environment} from "../../environments/environment";
|
||||
import {NgIf} from "@angular/common";
|
||||
import {CuteLoadingComponent} from "../../corelib/components/cute-loading/cute-loading.component";
|
||||
|
||||
interface CreateMetabaseUserResponse {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
interface GetMetabaseUserResponse {
|
||||
email: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-metabase',
|
||||
standalone: true,
|
||||
imports: [
|
||||
NgIf,
|
||||
CuteLoadingComponent
|
||||
],
|
||||
templateUrl: './metabase.component.html',
|
||||
styleUrl: './metabase.component.css'
|
||||
})
|
||||
export class MetabaseComponent {
|
||||
|
||||
isLoading: boolean = true;
|
||||
user: GetMetabaseUserResponse | null = null;
|
||||
createResponse: CreateMetabaseUserResponse | null = null;
|
||||
|
||||
constructor(private httpClient: HttpClient) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.getUser();
|
||||
}
|
||||
|
||||
resetPassword() {
|
||||
this.httpClient.get<CreateMetabaseUserResponse>(`${environment.apiUrl}/metabase/reset`, { withCredentials: true })
|
||||
.subscribe({
|
||||
next: (data) => {
|
||||
this.createResponse = data;
|
||||
},
|
||||
error: () => {
|
||||
this.createResponse = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getUser() {
|
||||
this.httpClient.get<GetMetabaseUserResponse>(`${environment.apiUrl}/metabase/status`, { withCredentials: true })
|
||||
.subscribe({
|
||||
next: (data) => {
|
||||
this.user = data;
|
||||
this.isLoading = false;
|
||||
},
|
||||
error: () => {
|
||||
this.user = null;
|
||||
this.isLoading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createNewUser() {
|
||||
this.httpClient.get<CreateMetabaseUserResponse>(`${environment.apiUrl}/metabase/create`, { withCredentials: true })
|
||||
.subscribe({
|
||||
next: (data) => {
|
||||
this.createResponse = data;
|
||||
},
|
||||
error: () => {
|
||||
this.createResponse = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user