metabase integration

This commit is contained in:
nise.moe 2024-06-15 01:49:36 +02:00
parent 8ac197515b
commit ee8c8423e7
6 changed files with 335 additions and 0 deletions

View File

@ -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()
}
}
}

View File

@ -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()}")
}
}
}

View File

@ -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/'},
];

View 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 &#64; <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>

View 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;
}
});
}
}