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 {ProfileComponent} from "./profile/profile.component";
|
||||||
import {ApiComponent} from "./api/api.component";
|
import {ApiComponent} from "./api/api.component";
|
||||||
import {ApiPythonComponent} from "./api-python/api-python.component";
|
import {ApiPythonComponent} from "./api-python/api-python.component";
|
||||||
|
import {MetabaseComponent} from "./metabase/metabase.component";
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{path: 'sus/:f', component: ViewSuspiciousScoresComponent, title: '/sus/'},
|
{path: 'sus/:f', component: ViewSuspiciousScoresComponent, title: '/sus/'},
|
||||||
@ -36,6 +37,8 @@ const routes: Routes = [
|
|||||||
{path: 'docs', component: ApiComponent, title: '/api/ explanation'},
|
{path: 'docs', component: ApiComponent, title: '/api/ explanation'},
|
||||||
{path: 'docs/py', component: ApiPythonComponent, title: 'python lib'},
|
{path: 'docs/py', component: ApiPythonComponent, title: 'python lib'},
|
||||||
|
|
||||||
|
{path: 'neko', component: MetabaseComponent, title: 'metabase integration'},
|
||||||
|
|
||||||
{path: '**', component: HomeComponent, title: '/nise.moe/'},
|
{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