mirror of
https://github.com/JuLi0n21/pwa-player.git
synced 2026-04-19 15:30:05 +00:00
auth finished
This commit is contained in:
85
backend/HttpClient.cs
Normal file
85
backend/HttpClient.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using OsuParsers.Enums.Replays;
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace shitweb
|
||||
{
|
||||
public class ApiClient
|
||||
{
|
||||
private const string CookiesFilePath = "cookies.json";
|
||||
private HttpClient _client;
|
||||
private HttpClientHandler _handler;
|
||||
|
||||
public ApiClient()
|
||||
{
|
||||
_handler = new HttpClientHandler();
|
||||
_client = new HttpClient(_handler);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
LoadCookies();
|
||||
|
||||
}
|
||||
|
||||
public void SaveCookies(String cookie)
|
||||
{
|
||||
var cookies = _handler.CookieContainer.GetCookies(new Uri("https://proxy.illegalesachen.download"));
|
||||
var Cookie = new CookieInfo();
|
||||
|
||||
Cookie.Name = "session_cookie";
|
||||
Cookie.Value = cookie;
|
||||
var json = JsonSerializer.Serialize(Cookie, new JsonSerializerOptions { WriteIndented = true });
|
||||
File.WriteAllText(CookiesFilePath, json);
|
||||
LoadCookies();
|
||||
}
|
||||
|
||||
public Boolean LoadCookies()
|
||||
{
|
||||
if (File.Exists(CookiesFilePath))
|
||||
{
|
||||
var json = File.ReadAllText(CookiesFilePath);
|
||||
var cookieInfo = JsonSerializer.Deserialize<CookieInfo>(json);
|
||||
|
||||
var cookie = new Cookie(cookieInfo.Name, cookieInfo.Value);
|
||||
_handler.CookieContainer.Add(new Uri("https://proxy.illegalesachen.download"), cookie);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public async Task UpdateSettingsAsync(string endpoint)
|
||||
{
|
||||
var requestContent = new JsonContent(new { endpoint = endpoint });
|
||||
var response = await _client.PostAsync("https://proxy.illegalesachen.download/settings", requestContent);
|
||||
try
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
System.Console.WriteLine(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class CookieInfo
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
|
||||
public class JsonContent : StringContent
|
||||
{
|
||||
public JsonContent(object obj)
|
||||
: base(JsonSerializer.Serialize(obj), System.Text.Encoding.UTF8, "application/json")
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,17 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using shitweb;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Drawing.Text;
|
||||
using System.Net.Http;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -25,18 +31,24 @@ builder.Services.AddCors(options =>
|
||||
var app = builder.Build();
|
||||
app.UseCors("AllowAll");
|
||||
|
||||
var apiClient = new ApiClient();
|
||||
|
||||
|
||||
app.MapGet("/ping", () => "pong");
|
||||
|
||||
|
||||
// Define the API routes
|
||||
app.MapGet("/api/v1/songs/{hash}", (string hash) =>
|
||||
{
|
||||
app.MapGet("/login", () => {
|
||||
return Results.Redirect("https://proxy.illegalesachen.download/login");
|
||||
});
|
||||
|
||||
return Results.Ok(new { hash });
|
||||
});
|
||||
app.MapGet("/api/v1/songs/{hash}", (string hash) =>
|
||||
{
|
||||
|
||||
return Results.Ok(new { hash });
|
||||
});
|
||||
|
||||
app.MapGet("/api/v1/songs/recent", (int? limit, int? offset) =>
|
||||
{
|
||||
app.MapGet("/api/v1/songs/recent", (HttpContext httpContext, int? limit, int? offset) =>
|
||||
{
|
||||
var limitValue = limit ?? 100; // default to 10 if not provided
|
||||
var offsetValue = offset ?? 0; // default to 0 if not provided
|
||||
|
||||
@@ -225,4 +237,63 @@ static ImageFormat GetImageFormat(string extension)
|
||||
}
|
||||
|
||||
Osudb.Instance.ToString();
|
||||
app.Run();
|
||||
startCloudflared();
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
Thread.Sleep(500);
|
||||
if (!apiClient.LoadCookies())
|
||||
{
|
||||
Console.WriteLine("Please visit this link and paste the Value back into here: ");
|
||||
|
||||
var cookie = Console.ReadLine();
|
||||
|
||||
apiClient.SaveCookies(cookie);
|
||||
}
|
||||
|
||||
Console.WriteLine("Ur Osu songs should now be available, please delete the cookies.json if it doesnt show up and try again.");
|
||||
});
|
||||
|
||||
await apiClient.InitializeAsync();
|
||||
app.Run();
|
||||
|
||||
async Task startCloudflared() {
|
||||
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "cloudflared",
|
||||
Arguments = "tunnel --url http://localhost:5153",
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
process.ErrorDataReceived += (sender, e) => {
|
||||
if (!string.IsNullOrEmpty(e.Data))
|
||||
{
|
||||
ParseForUrls(e.Data);
|
||||
}
|
||||
};
|
||||
|
||||
process.Start();
|
||||
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
await Task.Run(() => process.WaitForExit());
|
||||
}
|
||||
|
||||
void ParseForUrls(string data)
|
||||
{
|
||||
var urlRegex = new Regex(@"https?://[^\s]*\.trycloudflare\.com");
|
||||
var matches = urlRegex.Matches(data);
|
||||
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
Console.WriteLine($"Login here if not done already: {match.Value}/login");
|
||||
apiClient.UpdateSettingsAsync(match.Value);
|
||||
}
|
||||
}
|
||||
|
||||
4
backend/cookies.json
Normal file
4
backend/cookies.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"Name": "session_cookie",
|
||||
"Value": "session_cookie_c_PhxGlrrYcWY1h9cQ6mu_LxsDIVu0u_Nv0qAK1WmvcZiZXQ_FcwXZZXDk5Tm0qrqZTHHs800Pv5bTMGIrfa8Q=="
|
||||
}
|
||||
@@ -6,12 +6,19 @@ export type Song = {
|
||||
url: string;
|
||||
previewimage: string;
|
||||
mapper: string;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
export type CollectionPreview = {
|
||||
index: number;
|
||||
name: string;
|
||||
length: number;
|
||||
previewimage: string;
|
||||
};
|
||||
|
||||
|
||||
export type Me = {
|
||||
id: number;
|
||||
name: string;
|
||||
avatar_url: string;
|
||||
endpoint: string;
|
||||
share: boolean;
|
||||
};
|
||||
|
||||
@@ -1,10 +1,27 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import type { Song, CollectionPreview } from '@/script/types';
|
||||
import type { Song, CollectionPreview, Me } from '@/script/types';
|
||||
|
||||
export const useUserStore = defineStore('userStore', () => {
|
||||
const userId = ref(null)
|
||||
const baseUrl = ref('https://service.illegalesachen.download/')
|
||||
const proxyUrl = ref('https://proxy.illegalesachen.download/')
|
||||
|
||||
const User = ref<Me>(null)
|
||||
|
||||
function saveUser(user: Me) {
|
||||
localStorage.setItem('activeUser', JSON.stringify(user));
|
||||
}
|
||||
|
||||
function loadUser(): Me | null {
|
||||
const user = localStorage.getItem('activeUser');
|
||||
return user ? JSON.parse(user) : null;
|
||||
}
|
||||
|
||||
function setUser(user: Me) {
|
||||
User.value = user;
|
||||
saveUser(user)
|
||||
}
|
||||
|
||||
async function fetchSong(hash: string): Promise<Song> {
|
||||
try {
|
||||
@@ -30,7 +47,7 @@ export const useUserStore = defineStore('userStore', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchWithCache<T>(cacheKey: string, url: string, cacheDuration: number = 24 * 60 * 60 * 1000): Promise<T> {
|
||||
async function fetchWithCache<T>(cacheKey: string, url: string, cacheDuration: number = 24 * 60 * 60 * 1): Promise<T> {
|
||||
const cacheTimestampKey = `${cacheKey}_timestamp`;
|
||||
|
||||
const cachedData = localStorage.getItem(cacheKey);
|
||||
@@ -96,5 +113,35 @@ export const useUserStore = defineStore('userStore', () => {
|
||||
return fetchWithCache<Song[]>(cacheKey, url);
|
||||
}
|
||||
|
||||
return { fetchSong, fetchActiveSearch, fetchSearchArtist, fetchCollections, fetchCollection, fetchRecent, fetchFavorites, userId, baseUrl }
|
||||
async function fetchMe(): Promise<Me | {}> {
|
||||
const url = `${proxyUrl.value}me`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
credentials: 'include'
|
||||
});
|
||||
console.log(response);
|
||||
|
||||
if (response.redirected) {
|
||||
window.open(response.url, '_blank');
|
||||
return { "redirected": true };
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`Fetch failed with status: ${response.status} ${response.statusText}`);
|
||||
return { id: -1 } as Me;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Fetch error:', error);
|
||||
return {} as Me;
|
||||
}
|
||||
}
|
||||
|
||||
setUser(loadUser());
|
||||
|
||||
return { fetchSong, fetchActiveSearch, fetchSearchArtist, fetchCollections, fetchCollection, fetchRecent, fetchFavorites, fetchMe, userId, baseUrl, proxyUrl, User, setUser }
|
||||
})
|
||||
|
||||
@@ -12,6 +12,8 @@ const actionColor = ref('');
|
||||
const infoColor = ref('');
|
||||
const borderColor = ref('');
|
||||
|
||||
const loginStatus = ref('Login');
|
||||
|
||||
function update() {
|
||||
var input = document.getElementById("url-input") as HTMLAudioElement;
|
||||
console.log(input.value)
|
||||
@@ -34,6 +36,24 @@ function save(bg: string | null, main: string | null, info: string | null, borde
|
||||
console.log("bg", bgColor.value, "action:", actionColor.value, "info", infoColor.value, "border", borderColor.value)
|
||||
}
|
||||
|
||||
async function getMe() {
|
||||
|
||||
const data = await userStore.fetchMe() as Me;
|
||||
if (data.redirected == true) {
|
||||
loginStatus.value = "waiting for login, click to refresh!"
|
||||
console.log("redirect detected");
|
||||
}
|
||||
|
||||
console.log(data)
|
||||
if (data.id === null || data.id === undefined || Object.keys(data).length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log("active user: ", data.name)
|
||||
userStore.setUser(data);
|
||||
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
reset();
|
||||
})
|
||||
@@ -69,12 +89,17 @@ function reset() {
|
||||
<h1> Meeeeee </h1>
|
||||
<input @change="update" type="text" id="url-input" :value="userStore.baseUrl" />
|
||||
<br>
|
||||
<div class="flex p-5 justify-between">
|
||||
<img :src="'https://a.ppy.sh/14100399'" class="w-1/3">
|
||||
<button v-if="!userStore.User" @click="getMe" class="border bordercolor rounded-lg p-0.5">{{ loginStatus }}</button>
|
||||
<div v-if="userStore.User" class="flex p-5 justify-between">
|
||||
<img :src="userStore.User.avatar_url" class="w-1/3">
|
||||
<div>
|
||||
<p>User: {{ 'JuLi0n_' }}</p>
|
||||
<p>Api: Not Connected</p>
|
||||
<p>Sharing: <button class="border bordercolor rounded-lg p-0.5"> Off </button></p>
|
||||
<p>{{ userStore.User.name }}</p>
|
||||
<p>{{ userStore.User.endpoint == "" ? 'Not Connected' : 'Connected' }}</p>
|
||||
<p>Sharing: <button @click="share" class="border bordercolor rounded-lg p-0.5">{{ userStore.User.share
|
||||
}}</button></p>
|
||||
<button @click="getMe" class="border bordercolor rounded-lg p-0.5"> Refresh
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -86,8 +86,7 @@ func (c *OsuApiClient) sendRequest(req *http.Request, v interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func LoginRedirect(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func LoginMiddlePage(w http.ResponseWriter, r *http.Request) {
|
||||
cookie, ok := r.Context().Value("cookie").(string)
|
||||
|
||||
if !ok || cookie == "" {
|
||||
@@ -98,13 +97,32 @@ func LoginRedirect(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var clientid = os.Getenv("CLIENT_ID")
|
||||
var redirect_uri = os.Getenv("REDIRECT_URI") + "/oauth/code"
|
||||
http.Redirect(w, r,
|
||||
fmt.Sprintf("https://osu.ppy.sh/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s",
|
||||
clientid,
|
||||
redirect_uri,
|
||||
strings.Join(scopes, " "),
|
||||
cookie),
|
||||
http.StatusTemporaryRedirect)
|
||||
|
||||
html := `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login Required</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Redirecting...</p>
|
||||
<a href="%s">Click here if ur not being Redirected!</a>
|
||||
<script>
|
||||
window.location.href = "%s";
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
loginURL := fmt.Sprintf("https://osu.ppy.sh/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s",
|
||||
clientid,
|
||||
redirect_uri,
|
||||
strings.Join(scopes, " "),
|
||||
cookie)
|
||||
fmt.Fprintf(w, html, loginURL, loginURL)
|
||||
return
|
||||
}
|
||||
|
||||
func Oauth(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -204,13 +222,41 @@ func Oauth(w http.ResponseWriter, r *http.Request) {
|
||||
user.UserID = apiuser.ID
|
||||
user.Name = apiuser.Username
|
||||
user.AvatarUrl = apiuser.AvatarURL
|
||||
user.Share = false
|
||||
|
||||
SaveCookie(user.UserID, cookie)
|
||||
if err = SaveUser(user); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
JSONResponse(w, http.StatusCreated, user)
|
||||
var html = fmt.Sprintf(`
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Login Success</title>
|
||||
<body>
|
||||
|
||||
<input type="password" value="%s" id="myInput" disabled>
|
||||
<button onclick="copyToClipboard()">Copy Text</button>
|
||||
|
||||
<script>
|
||||
function copyToClipboard() {
|
||||
var copyText = document.getElementById("myInput");
|
||||
copyText.select();
|
||||
copyText.setSelectionRange(0, 99999); // For mobile devices
|
||||
navigator.clipboard.writeText(copyText.value);
|
||||
}
|
||||
|
||||
window.close(); // Close the window after copy
|
||||
</script>
|
||||
</head>
|
||||
</html>
|
||||
`, cookie)
|
||||
|
||||
fmt.Fprint(w, html)
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
type AuthToken struct {
|
||||
|
||||
Binary file not shown.
@@ -14,6 +14,7 @@ type User struct {
|
||||
Name string `json:"name"`
|
||||
AvatarUrl string `json:"avatar_url"`
|
||||
EndPoint string `json:"endpoint"`
|
||||
Share bool `json:"share"`
|
||||
Token `json:"-"`
|
||||
}
|
||||
|
||||
@@ -127,3 +128,9 @@ func UpdateUserEndPoint(userID int, endPoint string) error {
|
||||
_, err := db.Exec(query, endPoint, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
func UpdateUserShare(userID int, sharing bool) error {
|
||||
query := "UPDATE users SET sharing = ? WHERE id = ?"
|
||||
_, err := db.Exec(query, sharing, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ func AuthMiddleware(next http.Handler) http.Handler {
|
||||
|
||||
user, err := GetUserByCookie(cookie.Value)
|
||||
if err != nil || cookie.Value == "" {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -43,10 +43,13 @@ func CookieMiddleware(next http.Handler) http.Handler {
|
||||
HttpOnly: true,
|
||||
Secure: true,
|
||||
Path: "/",
|
||||
Domain: ".illegalesachen.download",
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
}
|
||||
|
||||
http.SetCookie(w, newCookie)
|
||||
cookie = newCookie
|
||||
r.AddCookie(cookie)
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), "cookie", cookie.Value)
|
||||
@@ -54,6 +57,30 @@ func CookieMiddleware(next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
func CORS(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.Header().Set("Access-Control-Allow-Origin", "https://music.illegalesachen.download")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func Logger(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
fmt.Println(r.URL)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func generateRandomString(length int) string {
|
||||
bytes := make([]byte, length)
|
||||
_, err := rand.Read(bytes)
|
||||
|
||||
@@ -12,17 +12,29 @@ func run() error {
|
||||
port := os.Getenv("PORT")
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.Handle("/me", AuthMiddleware(http.HandlerFunc(MeHandler)))
|
||||
mux.Handle("/login", http.HandlerFunc(LoginRedirect))
|
||||
mux.Handle("GET /me", AuthMiddleware(http.HandlerFunc(MeHandler)))
|
||||
mux.Handle("GET /login", http.HandlerFunc(LoginMiddlePage))
|
||||
mux.Handle("GET /oauth/code", http.HandlerFunc(Oauth))
|
||||
mux.Handle("POST /settings", AuthMiddleware(http.HandlerFunc(Settings)))
|
||||
|
||||
mux.Handle("/oauth/code", http.HandlerFunc(Oauth))
|
||||
// mux.Handle("POST /setting", );
|
||||
|
||||
fmt.Println("Starting Server on", port)
|
||||
|
||||
//global middleware
|
||||
handler := CookieMiddleware(mux)
|
||||
handler := CORS(CookieMiddleware(Logger(mux)))
|
||||
|
||||
return http.ListenAndServe(port, handler)
|
||||
finalMux := http.NewServeMux()
|
||||
|
||||
finalMux.Handle("/", handler)
|
||||
|
||||
finalMux.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("pong"))
|
||||
})
|
||||
|
||||
return http.ListenAndServe(port, finalMux)
|
||||
}
|
||||
|
||||
func MeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -37,6 +49,37 @@ func MeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
JSONResponse(w, http.StatusOK, user)
|
||||
}
|
||||
|
||||
func Settings(w http.ResponseWriter, r *http.Request) {
|
||||
type settings struct {
|
||||
Sharing *bool `json:"sharing"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
}
|
||||
|
||||
user, ok := r.Context().Value("user").(*User)
|
||||
if !ok || user == nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
var s settings
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&s); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if s.Endpoint != "" {
|
||||
UpdateUserEndPoint(user.UserID, s.Endpoint)
|
||||
}
|
||||
|
||||
if s.Sharing != nil {
|
||||
UpdateUserShare(user.UserID, *s.Sharing)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
func JSONResponse(w http.ResponseWriter, statusCode int, data interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user