Compare commits
No commits in common. "main" and "BKCH" have entirely different histories.
43
.gitignore
vendored
|
@ -1,41 +1,2 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
justfile
|
||||
static/*
|
||||
|
|
40
README.md
|
@ -1,40 +0,0 @@
|
|||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
|
||||
|
||||
# Structure
|
||||
Store on local machine and use a middleware to process (compress) the file and facilitate storage.
|
100
admin/create/index.html
Normal file
|
@ -0,0 +1,100 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>NIMING NewUser</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||
</head>
|
||||
|
||||
<body class="m-0 grid place-center w-dvw h-dvh bg-zinc-800 text-zinc-900">
|
||||
<script>
|
||||
let key = ""
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
let username = document.getElementById("username")
|
||||
let password = document.getElementById("password")
|
||||
let gen_img = document.getElementById("gen")
|
||||
let img = document.getElementById("totp_img")
|
||||
let create = document.getElementById("create")
|
||||
const get_img = () => {
|
||||
console.log(username.value)
|
||||
if (username.value.length == 0) {
|
||||
alert("缺少名稱")
|
||||
}
|
||||
gen_img.disabled = true
|
||||
fetch(`/api/admin/new_totp?name=${encodeURIComponent(username.value)}`).then(async resp => {
|
||||
const resp_obj = await resp.json()
|
||||
key = resp_obj["key"]
|
||||
img.src = "data:image/png;base64, " + resp_obj["img"]
|
||||
}).finally(() => {
|
||||
gen_img.disabled = false
|
||||
})
|
||||
}
|
||||
gen_img.onclick = get_img;
|
||||
const create_func = () => {
|
||||
if (username.value.length == 0) {
|
||||
alert("username required")
|
||||
return
|
||||
}
|
||||
if (password.value.length < 8) {
|
||||
alert("password must contain over 8 characters")
|
||||
return
|
||||
}
|
||||
if (key.length == 0) {
|
||||
alert("Didn't set a totp code")
|
||||
return
|
||||
}
|
||||
|
||||
fetch("/api/admin/create", {
|
||||
body: JSON.stringify(
|
||||
{
|
||||
"username": username.value
|
||||
, "password": password.value
|
||||
, "totp_secret": key
|
||||
}),
|
||||
headers: {"Content-Type": "application/json"},
|
||||
method: "POST"
|
||||
}).then(resp => {
|
||||
if (resp.status == 201) {
|
||||
alert("created")
|
||||
username.value = ""
|
||||
password.value = ""
|
||||
key = ""
|
||||
img.src = ""
|
||||
} else {
|
||||
alert(`Not succeeded, status: ${resp.statusText}`)
|
||||
|
||||
}
|
||||
}).catch(err => {
|
||||
alert(err)
|
||||
|
||||
})
|
||||
}
|
||||
create.onclick = create_func
|
||||
})
|
||||
|
||||
</script>
|
||||
<div
|
||||
class="drop-shadow-lg flex flex-col box-border py-5 px-3 rounded-xl bg-zinc-400 m-auto h-fit w-10/12 md:w-5/12 lg:w-1/4 gap-5">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label>使用者名稱</label>
|
||||
<input type="text" name="username"
|
||||
class="focus:border-zinc-600 outline-zinc-500 pl-2 border-2 rounded-lg border-zinc-800 caret-zinc-800 outline-none"
|
||||
id="username" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label>密碼</label>
|
||||
<input type="text" name="password"
|
||||
class="focus:border-zinc-600 outline-zinc-500 pl-2 border-2 rounded-lg border-zinc-800 caret-zinc-800 outline-none"
|
||||
id="password" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<button id="gen">產生TOTP QrCODE</button>
|
||||
<img src="" id="totp_img" class="max-w-30 max-h-30 mx-auto" />
|
||||
</div>
|
||||
<button class="border-2 border-slate-700 w-50 rounded-xl mx-auto" id="create">確認</button>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
127
admin/login/index.html
Normal file
|
@ -0,0 +1,127 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>NIMING Login</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||
<script src="https://unpkg.com/htmx.org@2.0.4"
|
||||
integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
</head>
|
||||
|
||||
|
||||
<body class="grid place-center w-dvw h-dvh bg-zinc-900">
|
||||
<script>
|
||||
const totp_length = 6;
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
let totp_cell = document.getElementById("totp_cell");
|
||||
let totp_container = document.getElementById("totp");
|
||||
|
||||
for (let i = 0; i < totp_length; i++) {
|
||||
totp_cell.content.firstElementChild.dataset.indexNumber = i
|
||||
totp_cell.content.firstElementChild.name = `totp-${i}`
|
||||
let node = document.importNode(totp_cell.content, true)
|
||||
totp_container.appendChild(node);
|
||||
}
|
||||
|
||||
const keyup_ev = (ev) => {
|
||||
if (ev.keyCode <= 0x39 && ev.keyCode >= 0x30) {
|
||||
ev.target.value = ev.key
|
||||
if (ev.target.nextElementSibling) {
|
||||
ev.target.nextElementSibling.focus()
|
||||
}
|
||||
}
|
||||
if (ev.key = "a" && ev.ctrlKey) {
|
||||
console.log("Select all")
|
||||
for (let i = 1; i <= totp_length; i++) {
|
||||
totp_container.children[i].select()
|
||||
}
|
||||
}
|
||||
if (ev.key == "Backspace" && ev.target.dataset.indexNumber > 0) {
|
||||
const previous = totp_container.querySelector(`[data-index-number='${ev.target.dataset.indexNumber - 1}']`)
|
||||
previous.focus()
|
||||
}
|
||||
|
||||
}
|
||||
const paste_input_ev = (ev) => {
|
||||
if (ev.inputType == "insertFromPaste") {
|
||||
let data = ev.data
|
||||
console.log(data)
|
||||
for (let i = 1; i <= totp_length; i++) {
|
||||
totp_container.children[i].value = data.slice(0, 1)
|
||||
data = data.slice(1, data.length)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const cell of totp_container.children) {
|
||||
if (cell instanceof HTMLInputElement) {
|
||||
cell.addEventListener("keyup", keyup_ev)
|
||||
cell.addEventListener("input", paste_input_ev)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const form_onsubmit = (ev) => {
|
||||
let form = new FormData(ev.target)
|
||||
let miss = form.entries().find(p => p[1] == "")
|
||||
if (miss) {
|
||||
alert(`Missing ${miss[0]}`)
|
||||
ev.preventDefault()
|
||||
}
|
||||
console.log(form)
|
||||
|
||||
}
|
||||
const form_formdata = (ev) => {
|
||||
const data = ev.formData
|
||||
let totp = new Array(totp_length)
|
||||
for (const [k, v] of data.entries()) {
|
||||
if (k.match(/totp-\d/g)) {
|
||||
totp[parseInt(k.split("-")[1])] = v
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < totp_length; i++) {
|
||||
data.delete(`totp-${i}`)
|
||||
}
|
||||
data.set("totp_code", totp.join(""))
|
||||
}
|
||||
const form = document.getElementById("login_form")
|
||||
form.addEventListener("submit", form_onsubmit)
|
||||
form.addEventListener("formdata", form_formdata)
|
||||
});
|
||||
</script>
|
||||
|
||||
<form id="login_form"
|
||||
class="m-auto h-fit w-10/12 md:w-5/12 lg:w-1/4 bg-zinc-400 drop-shadow-lg flex flex-col px-10 box-border py-5 gap-10 rounded-xl"
|
||||
action="/admin/login" method="post">
|
||||
<h1 class="text-3xl text-center">中工匿名管理</h1>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="username">名稱</label>
|
||||
<input name="name" id="username" type="text"
|
||||
class="block rounded outline-none ring-3 border-box px-3" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="password">密碼</label>
|
||||
<input name="password" id="password" type="password"
|
||||
class="block rounded outline-none ring-3 border-box px-3" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="totp">TOTP</label>
|
||||
<div class="flex flex-row gap-3 w-full px-auto justify-center box-border" id="totp">
|
||||
<template id="totp_cell">
|
||||
<input type="text" inputmode="numeric" pattern="[0-9]{1}"
|
||||
class="block rounded outline-none ring-2 border-box w-7 h-7 text-center totp"
|
||||
data-index-number="0" maxlength="1" minlength="1" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="w-fit px-5 py-1 mx-auto bg-sky-500 rounded-lg drop-shadow-cyan-500/50 drop-shadow-lg">確認</button>
|
||||
</form>
|
||||
</body>
|
||||
|
||||
</html>
|
163
admin/panel/api.js
Normal file
|
@ -0,0 +1,163 @@
|
|||
class Post {
|
||||
content;
|
||||
signing;
|
||||
post_at;
|
||||
media;
|
||||
id;
|
||||
constructor(id, content, signing, post_at, media) {
|
||||
this.id = id;
|
||||
this.content = content;
|
||||
this.signing = signing;
|
||||
this.post_at = post_at;
|
||||
this.media = media;
|
||||
}
|
||||
verify(check) {
|
||||
this.check = check;
|
||||
}
|
||||
get verify_blk() {
|
||||
return {
|
||||
post: this.id,
|
||||
check: this.check,
|
||||
};
|
||||
}
|
||||
}
|
||||
const Fetch = async () => {
|
||||
let result = await fetch("/api/admin/fetch_post", {
|
||||
credentials: "include",
|
||||
});
|
||||
return (await result.json()).map((e) => {
|
||||
return new Post(e.id, e.content, e.signing, e.post_at, e.enclosure);
|
||||
});
|
||||
};
|
||||
let sending = [];
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
let verix_container = document.getElementById("verix_container");
|
||||
|
||||
const verify = (id, obj, ck) => {
|
||||
obj.verify(ck);
|
||||
sending.push(obj);
|
||||
let child = document.getElementById(`post-${id}`);
|
||||
verix_container.removeChild(child);
|
||||
};
|
||||
|
||||
let hv = document.getElementById("hv");
|
||||
const disable_display = () => {
|
||||
hv.classList.add("hidden");
|
||||
};
|
||||
hv.onclick = disable_display;
|
||||
|
||||
const hover_display_img = (src) => {
|
||||
hv.classList.remove("hidden");
|
||||
console.log(hv.children);
|
||||
hv.children[0].src = src;
|
||||
};
|
||||
|
||||
Fetch()
|
||||
.then((e) => {
|
||||
if (e.length == 0) {
|
||||
let nothing = document.createElement("div");
|
||||
nothing.appendChild(document.createTextNode("Nothing!!!"));
|
||||
nothing.className = "text-slate-300 text-3xl mx-auto";
|
||||
verix_container.appendChild(nothing);
|
||||
} else
|
||||
e.forEach((x) => {
|
||||
let post = document.createElement("div");
|
||||
post.className =
|
||||
"rounded bg-zinc-800 w-full h-fit text-slate-200 px-2 relative break-all";
|
||||
let post_content = document.createElement("p");
|
||||
post_content.className = "h-fit text-md min-h-20 mr-[30px]";
|
||||
post_content.appendChild(document.createTextNode(x.content));
|
||||
post.appendChild(post_content);
|
||||
|
||||
let media = document.createElement("div");
|
||||
media.className = "w-full flex flex-row gap-2 h-fit overflow-x-auto";
|
||||
post.appendChild(media);
|
||||
|
||||
if (x.media) {
|
||||
x.media.forEach((src) => {
|
||||
let media_cell = document.createElement("img");
|
||||
media_cell.className = "h-30 w-auto my-auto";
|
||||
media_cell.src = `/static/${src}`;
|
||||
media_cell.onclick = () => {
|
||||
hover_display_img(media_cell.src);
|
||||
};
|
||||
media.appendChild(media_cell);
|
||||
});
|
||||
}
|
||||
|
||||
let btm_bar = document.createElement("div");
|
||||
let msg = document.createElement("p");
|
||||
msg.className = "text-sm border-t-3";
|
||||
msg.appendChild(
|
||||
document.createTextNode(
|
||||
`id: ${x.id}, ${x.signing ? "sign: " + x.signing + ", " : ""}post_at: ${new Date(x.post_at).toDateString()}`,
|
||||
),
|
||||
);
|
||||
btm_bar.appendChild(msg);
|
||||
post.appendChild(btm_bar);
|
||||
|
||||
let right_bar = document.createElement("div");
|
||||
right_bar.classList =
|
||||
"h-10 w-fit absolute right-0 top-0 flex flex-col gap-2 text-lg";
|
||||
post.appendChild(right_bar);
|
||||
let btn_cls = "rounded-2xl w-[30px] h-[30px] bg-zinc-400";
|
||||
let revoke = document.createElement("button");
|
||||
revoke.className = btn_cls;
|
||||
revoke.onclick = () => {
|
||||
verify(x.id, x, false);
|
||||
};
|
||||
revoke.appendChild(document.createTextNode("X"));
|
||||
let issue = document.createElement("button");
|
||||
issue.className = btn_cls;
|
||||
issue.onclick = () => {
|
||||
verify(x.id, x, true);
|
||||
};
|
||||
issue.appendChild(document.createTextNode("Y"));
|
||||
right_bar.appendChild(revoke);
|
||||
right_bar.appendChild(issue);
|
||||
post.id = `post-${x.id}`;
|
||||
|
||||
verix_container.appendChild(post);
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
alert(err);
|
||||
});
|
||||
|
||||
const sendbtn = document.getElementById("send");
|
||||
|
||||
const send = () => {
|
||||
sendbtn.disabled = true;
|
||||
sendbtn.classList.remove("text-zinc-200");
|
||||
sendbtn.classList.add("text-zinc-400");
|
||||
if (sending.length > 0) {
|
||||
fetch("/api/admin/verify_post", {
|
||||
body: JSON.stringify({ choice: sending.map((e) => e.verify_blk) }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
method: "PUT",
|
||||
})
|
||||
.then(async (resp) => {
|
||||
if (resp.status == 200) {
|
||||
sending = [];
|
||||
alert("成功");
|
||||
} else {
|
||||
alert(`Err: ${resp.statusText}, ${await resp.text()}`);
|
||||
}
|
||||
})
|
||||
.catch((err) => alert(err))
|
||||
.finally(() => {
|
||||
sendbtn.disabled = false;
|
||||
sendbtn.classList.add("text-zinc-200");
|
||||
sendbtn.classList.remove("text-zinc-400");
|
||||
});
|
||||
} else {
|
||||
alert("Nothing");
|
||||
sendbtn.classList.add("text-zinc-200");
|
||||
sendbtn.classList.remove("text-zinc-400");
|
||||
sendbtn.disabled = false;
|
||||
}
|
||||
};
|
||||
sendbtn.onclick = send;
|
||||
});
|
29
admin/panel/index.html
Normal file
|
@ -0,0 +1,29 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Panel</title>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||
<script src="https://unpkg.com/htmx.org@2.0.4"
|
||||
integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="/admin/panel/api.js"></script>
|
||||
</head>
|
||||
|
||||
<body class="m-0 w-dvw h-dvh bg-zinc-800 grid place-center">
|
||||
<div class="mx-auto h-dvh w-[90dvw] lg:w-[70dvw]">
|
||||
<button class="h-1/20 w-full text-zinc-200 border-2 rounded my-[25px]" id="send">Send</button>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 bg-zinc-700 px-2 overflow-auto h-[calc(95dvh-50px)] box-border"
|
||||
id="verix_container">
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden fixed top-0 left-0 w-dvw h-dvh z-10 backdrop-brightness-30" id="hv">
|
||||
<img src="" class="h-70dvh lg:w-[40dvw] fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-20"
|
||||
alt="img" />
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
63
adminadd.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"golang.org/x/crypto/scrypt"
|
||||
"nim.jasinco.work/app/nimdb"
|
||||
)
|
||||
|
||||
func main() {
|
||||
pgurl := os.Getenv("POSTGRES_URL")
|
||||
conn, err := pgx.Connect(context.Background(), pgurl)
|
||||
if err != nil {
|
||||
log.Fatalln(err.Error())
|
||||
}
|
||||
|
||||
salt := os.Getenv("SALT")
|
||||
|
||||
db := nimdb.New(conn)
|
||||
tx, err := conn.Begin(context.Background())
|
||||
if err != nil {
|
||||
log.Fatalln(err.Error())
|
||||
}
|
||||
qtx := db.WithTx(tx)
|
||||
defer tx.Rollback(context.Background())
|
||||
|
||||
fmt.Print("UserName and password (split by space): ")
|
||||
var name, password string
|
||||
_, err = fmt.Scanf("%s %s", &name, &password)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
key, err := totp.Generate(totp.GenerateOpts{Issuer: "TCIVS_NIMING", AccountName: name})
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
secret := key.Secret()
|
||||
log.Println(secret, key.Issuer(), key.AccountName())
|
||||
fmt.Print("Verify TOTP Code: ")
|
||||
var code string
|
||||
_, err = fmt.Scanf("%s", &code)
|
||||
if !totp.Validate(code, secret) {
|
||||
gen, err := totp.GenerateCode(secret, time.Now())
|
||||
if err != nil {
|
||||
log.Fatalln("Validation not succed, can't gen code, err:", err.Error())
|
||||
}
|
||||
log.Fatalln("Velidation not succed, CODE should be: ", gen)
|
||||
}
|
||||
hashed, err := scrypt.Key([]byte(password), []byte(salt), 32768, 8, 1, 32)
|
||||
if err != nil {
|
||||
log.Fatalln(err.Error())
|
||||
}
|
||||
qtx.AdminCreateAccount(context.Background(), nimdb.AdminCreateAccountParams{Username: name, Password: base64.StdEncoding.EncodeToString(hashed), Totp: secret})
|
||||
tx.Commit(context.Background())
|
||||
}
|
BIN
bun.lockb
39
go.mod
Normal file
|
@ -0,0 +1,39 @@
|
|||
module nim.jasinco.work/app
|
||||
|
||||
go 1.24.3
|
||||
|
||||
require (
|
||||
github.com/gofiber/contrib/jwt v1.1.2
|
||||
github.com/gofiber/fiber/v2 v2.52.8
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/jackc/pgx/v5 v5.7.5
|
||||
github.com/pquerna/otp v1.5.0
|
||||
golang.org/x/crypto v0.38.0
|
||||
google.golang.org/grpc v1.72.2
|
||||
google.golang.org/protobuf v1.36.6
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/MicahParks/keyfunc/v2 v2.1.0 // indirect
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
github.com/tinylib/msgp v1.2.5 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.62.0 // indirect
|
||||
golang.org/x/net v0.40.0 // indirect
|
||||
golang.org/x/sync v0.14.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
|
||||
)
|
96
go.sum
Normal file
|
@ -0,0 +1,96 @@
|
|||
github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k=
|
||||
github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/gofiber/contrib/jwt v1.1.2 h1:GmWnOqT4A15EkA8IPXwSpvNUXZR4u5SMj+geBmyLAjs=
|
||||
github.com/gofiber/contrib/jwt v1.1.2/go.mod h1:CpIwrkUQ3Q6IP8y9n3f0wP9bOnSKx39EDp2fBVgMFVk=
|
||||
github.com/gofiber/fiber/v2 v2.52.8 h1:xl4jJQ0BV5EJTA2aWiKw/VddRpHrKeZLF0QPUxqn0x4=
|
||||
github.com/gofiber/fiber/v2 v2.52.8/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
|
||||
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
|
||||
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=
|
||||
github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.62.0 h1:8dKRBX/y2rCzyc6903Zu1+3qN0H/d2MsxPPmVNamiH0=
|
||||
github.com/valyala/fasthttp v1.62.0/go.mod h1:FCINgr4GKdKqV8Q0xv8b+UxPV+H/O5nNFo3D+r54Htg=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
|
||||
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
|
||||
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
|
||||
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
|
||||
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
|
||||
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
|
||||
google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8=
|
||||
google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
211
igapi/interface.pb.go
Normal file
|
@ -0,0 +1,211 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.6
|
||||
// protoc v6.31.0
|
||||
// source: igapi/interface.proto
|
||||
|
||||
package igapi
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type Request struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
Igid string `protobuf:"bytes,2,opt,name=igid,proto3" json:"igid,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Request) Reset() {
|
||||
*x = Request{}
|
||||
mi := &file_igapi_interface_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Request) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Request) ProtoMessage() {}
|
||||
|
||||
func (x *Request) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_igapi_interface_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Request.ProtoReflect.Descriptor instead.
|
||||
func (*Request) Descriptor() ([]byte, []int) {
|
||||
return file_igapi_interface_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *Request) GetId() int64 {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Request) GetIgid() string {
|
||||
if x != nil {
|
||||
return x.Igid
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type Reply struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Err int64 `protobuf:"varint,1,opt,name=err,proto3" json:"err,omitempty"`
|
||||
Result map[string]string `protobuf:"bytes,2,rep,name=result,proto3" json:"result,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Reply) Reset() {
|
||||
*x = Reply{}
|
||||
mi := &file_igapi_interface_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Reply) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Reply) ProtoMessage() {}
|
||||
|
||||
func (x *Reply) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_igapi_interface_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Reply.ProtoReflect.Descriptor instead.
|
||||
func (*Reply) Descriptor() ([]byte, []int) {
|
||||
return file_igapi_interface_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *Reply) GetErr() int64 {
|
||||
if x != nil {
|
||||
return x.Err
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Reply) GetResult() map[string]string {
|
||||
if x != nil {
|
||||
return x.Result
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_igapi_interface_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_igapi_interface_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x15igapi/interface.proto\"-\n" +
|
||||
"\aRequest\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" +
|
||||
"\x04igid\x18\x02 \x01(\tR\x04igid\"\x80\x01\n" +
|
||||
"\x05Reply\x12\x10\n" +
|
||||
"\x03err\x18\x01 \x01(\x03R\x03err\x12*\n" +
|
||||
"\x06result\x18\x02 \x03(\v2\x12.Reply.ResultEntryR\x06result\x1a9\n" +
|
||||
"\vResultEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x012\xbf\x01\n" +
|
||||
"\x05IGAPI\x12\x1b\n" +
|
||||
"\x05login\x12\b.Request\x1a\x06.Reply\"\x00\x12\"\n" +
|
||||
"\faccount_info\x12\b.Request\x1a\x06.Reply\"\x00\x12\x1c\n" +
|
||||
"\x06upload\x12\b.Request\x1a\x06.Reply\"\x00\x12\x1c\n" +
|
||||
"\x06delete\x12\b.Request\x1a\x06.Reply\"\x00\x12\x1b\n" +
|
||||
"\x05queue\x12\b.Request\x1a\x06.Reply\"\x00\x12\x1c\n" +
|
||||
"\x06search\x12\b.Request\x1a\x06.Reply\"\x00B\x1cZ\x1anim.jasinco.work/app/igapib\x06proto3"
|
||||
|
||||
var (
|
||||
file_igapi_interface_proto_rawDescOnce sync.Once
|
||||
file_igapi_interface_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_igapi_interface_proto_rawDescGZIP() []byte {
|
||||
file_igapi_interface_proto_rawDescOnce.Do(func() {
|
||||
file_igapi_interface_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_igapi_interface_proto_rawDesc), len(file_igapi_interface_proto_rawDesc)))
|
||||
})
|
||||
return file_igapi_interface_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_igapi_interface_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
|
||||
var file_igapi_interface_proto_goTypes = []any{
|
||||
(*Request)(nil), // 0: Request
|
||||
(*Reply)(nil), // 1: Reply
|
||||
nil, // 2: Reply.ResultEntry
|
||||
}
|
||||
var file_igapi_interface_proto_depIdxs = []int32{
|
||||
2, // 0: Reply.result:type_name -> Reply.ResultEntry
|
||||
0, // 1: IGAPI.login:input_type -> Request
|
||||
0, // 2: IGAPI.account_info:input_type -> Request
|
||||
0, // 3: IGAPI.upload:input_type -> Request
|
||||
0, // 4: IGAPI.delete:input_type -> Request
|
||||
0, // 5: IGAPI.queue:input_type -> Request
|
||||
0, // 6: IGAPI.search:input_type -> Request
|
||||
1, // 7: IGAPI.login:output_type -> Reply
|
||||
1, // 8: IGAPI.account_info:output_type -> Reply
|
||||
1, // 9: IGAPI.upload:output_type -> Reply
|
||||
1, // 10: IGAPI.delete:output_type -> Reply
|
||||
1, // 11: IGAPI.queue:output_type -> Reply
|
||||
1, // 12: IGAPI.search:output_type -> Reply
|
||||
7, // [7:13] is the sub-list for method output_type
|
||||
1, // [1:7] is the sub-list for method input_type
|
||||
1, // [1:1] is the sub-list for extension type_name
|
||||
1, // [1:1] is the sub-list for extension extendee
|
||||
0, // [0:1] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_igapi_interface_proto_init() }
|
||||
func file_igapi_interface_proto_init() {
|
||||
if File_igapi_interface_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_igapi_interface_proto_rawDesc), len(file_igapi_interface_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 3,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
GoTypes: file_igapi_interface_proto_goTypes,
|
||||
DependencyIndexes: file_igapi_interface_proto_depIdxs,
|
||||
MessageInfos: file_igapi_interface_proto_msgTypes,
|
||||
}.Build()
|
||||
File_igapi_interface_proto = out.File
|
||||
file_igapi_interface_proto_goTypes = nil
|
||||
file_igapi_interface_proto_depIdxs = nil
|
||||
}
|
26
igapi/interface.proto
Normal file
|
@ -0,0 +1,26 @@
|
|||
syntax = "proto3";
|
||||
option go_package = "nim.jasinco.work/app/igapi";
|
||||
|
||||
service IGAPI {
|
||||
rpc login(Request) returns (Reply) {}
|
||||
|
||||
rpc account_info(Request) returns (Reply) {}
|
||||
|
||||
rpc upload(Request) returns (Reply) {}
|
||||
|
||||
rpc delete (Request) returns (Reply) {}
|
||||
|
||||
rpc queue(Request) returns (Reply) {}
|
||||
|
||||
rpc search(Request) returns (Reply) {}
|
||||
}
|
||||
|
||||
message Request {
|
||||
int64 id = 1;
|
||||
string igid = 2;
|
||||
}
|
||||
|
||||
message Reply {
|
||||
int64 err = 1;
|
||||
map<string, string> result = 2;
|
||||
}
|
311
igapi/interface_grpc.pb.go
Normal file
|
@ -0,0 +1,311 @@
|
|||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v6.31.0
|
||||
// source: igapi/interface.proto
|
||||
|
||||
package igapi
|
||||
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.64.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
IGAPI_Login_FullMethodName = "/IGAPI/login"
|
||||
IGAPI_AccountInfo_FullMethodName = "/IGAPI/account_info"
|
||||
IGAPI_Upload_FullMethodName = "/IGAPI/upload"
|
||||
IGAPI_Delete_FullMethodName = "/IGAPI/delete"
|
||||
IGAPI_Queue_FullMethodName = "/IGAPI/queue"
|
||||
IGAPI_Search_FullMethodName = "/IGAPI/search"
|
||||
)
|
||||
|
||||
// IGAPIClient is the client API for IGAPI service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type IGAPIClient interface {
|
||||
Login(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Reply, error)
|
||||
AccountInfo(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Reply, error)
|
||||
Upload(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Reply, error)
|
||||
Delete(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Reply, error)
|
||||
Queue(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Reply, error)
|
||||
Search(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Reply, error)
|
||||
}
|
||||
|
||||
type iGAPIClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewIGAPIClient(cc grpc.ClientConnInterface) IGAPIClient {
|
||||
return &iGAPIClient{cc}
|
||||
}
|
||||
|
||||
func (c *iGAPIClient) Login(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Reply, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(Reply)
|
||||
err := c.cc.Invoke(ctx, IGAPI_Login_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *iGAPIClient) AccountInfo(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Reply, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(Reply)
|
||||
err := c.cc.Invoke(ctx, IGAPI_AccountInfo_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *iGAPIClient) Upload(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Reply, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(Reply)
|
||||
err := c.cc.Invoke(ctx, IGAPI_Upload_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *iGAPIClient) Delete(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Reply, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(Reply)
|
||||
err := c.cc.Invoke(ctx, IGAPI_Delete_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *iGAPIClient) Queue(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Reply, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(Reply)
|
||||
err := c.cc.Invoke(ctx, IGAPI_Queue_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *iGAPIClient) Search(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Reply, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(Reply)
|
||||
err := c.cc.Invoke(ctx, IGAPI_Search_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// IGAPIServer is the server API for IGAPI service.
|
||||
// All implementations must embed UnimplementedIGAPIServer
|
||||
// for forward compatibility.
|
||||
type IGAPIServer interface {
|
||||
Login(context.Context, *Request) (*Reply, error)
|
||||
AccountInfo(context.Context, *Request) (*Reply, error)
|
||||
Upload(context.Context, *Request) (*Reply, error)
|
||||
Delete(context.Context, *Request) (*Reply, error)
|
||||
Queue(context.Context, *Request) (*Reply, error)
|
||||
Search(context.Context, *Request) (*Reply, error)
|
||||
mustEmbedUnimplementedIGAPIServer()
|
||||
}
|
||||
|
||||
// UnimplementedIGAPIServer must be embedded to have
|
||||
// forward compatible implementations.
|
||||
//
|
||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedIGAPIServer struct{}
|
||||
|
||||
func (UnimplementedIGAPIServer) Login(context.Context, *Request) (*Reply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Login not implemented")
|
||||
}
|
||||
func (UnimplementedIGAPIServer) AccountInfo(context.Context, *Request) (*Reply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AccountInfo not implemented")
|
||||
}
|
||||
func (UnimplementedIGAPIServer) Upload(context.Context, *Request) (*Reply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Upload not implemented")
|
||||
}
|
||||
func (UnimplementedIGAPIServer) Delete(context.Context, *Request) (*Reply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Delete not implemented")
|
||||
}
|
||||
func (UnimplementedIGAPIServer) Queue(context.Context, *Request) (*Reply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Queue not implemented")
|
||||
}
|
||||
func (UnimplementedIGAPIServer) Search(context.Context, *Request) (*Reply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Search not implemented")
|
||||
}
|
||||
func (UnimplementedIGAPIServer) mustEmbedUnimplementedIGAPIServer() {}
|
||||
func (UnimplementedIGAPIServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeIGAPIServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to IGAPIServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeIGAPIServer interface {
|
||||
mustEmbedUnimplementedIGAPIServer()
|
||||
}
|
||||
|
||||
func RegisterIGAPIServer(s grpc.ServiceRegistrar, srv IGAPIServer) {
|
||||
// If the following call pancis, it indicates UnimplementedIGAPIServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||
t.testEmbeddedByValue()
|
||||
}
|
||||
s.RegisterService(&IGAPI_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _IGAPI_Login_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(Request)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(IGAPIServer).Login(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: IGAPI_Login_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(IGAPIServer).Login(ctx, req.(*Request))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _IGAPI_AccountInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(Request)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(IGAPIServer).AccountInfo(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: IGAPI_AccountInfo_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(IGAPIServer).AccountInfo(ctx, req.(*Request))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _IGAPI_Upload_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(Request)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(IGAPIServer).Upload(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: IGAPI_Upload_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(IGAPIServer).Upload(ctx, req.(*Request))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _IGAPI_Delete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(Request)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(IGAPIServer).Delete(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: IGAPI_Delete_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(IGAPIServer).Delete(ctx, req.(*Request))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _IGAPI_Queue_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(Request)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(IGAPIServer).Queue(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: IGAPI_Queue_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(IGAPIServer).Queue(ctx, req.(*Request))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _IGAPI_Search_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(Request)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(IGAPIServer).Search(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: IGAPI_Search_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(IGAPIServer).Search(ctx, req.(*Request))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// IGAPI_ServiceDesc is the grpc.ServiceDesc for IGAPI service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var IGAPI_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "IGAPI",
|
||||
HandlerType: (*IGAPIServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "login",
|
||||
Handler: _IGAPI_Login_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "account_info",
|
||||
Handler: _IGAPI_AccountInfo_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "upload",
|
||||
Handler: _IGAPI_Upload_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "delete",
|
||||
Handler: _IGAPI_Delete_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "queue",
|
||||
Handler: _IGAPI_Queue_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "search",
|
||||
Handler: _IGAPI_Search_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "igapi/interface.proto",
|
||||
}
|
270
internal/handlers/admin.go
Normal file
|
@ -0,0 +1,270 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"image/png"
|
||||
"log"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"golang.org/x/crypto/scrypt"
|
||||
"nim.jasinco.work/app/internal"
|
||||
"nim.jasinco.work/app/nimdb"
|
||||
)
|
||||
|
||||
type NewAdmin struct {
|
||||
Name string `json:"name"`
|
||||
Password string `json:"password"`
|
||||
TOTP_Secret string `json:"totp_secret"`
|
||||
}
|
||||
type AdminAuth struct {
|
||||
Name string `json:"name"`
|
||||
Password string `json:"password"`
|
||||
TOTP_code string `json:"totp_code"`
|
||||
}
|
||||
|
||||
func Admin_create(c *fiber.Ctx) error {
|
||||
cfg := new(NewAdmin)
|
||||
if err := c.BodyParser(cfg); err != nil {
|
||||
return fiber.ErrBadRequest
|
||||
}
|
||||
|
||||
hashed, err := scrypt.Key([]byte(cfg.Password), []byte(internal.SALT), 32768, 8, 1, 32)
|
||||
if err != nil {
|
||||
return c.SendStatus(fiber.StatusInternalServerError)
|
||||
}
|
||||
|
||||
err = internal.NIMDB.AdminCreateAccount(c.Context(), nimdb.AdminCreateAccountParams{Username: cfg.Name, Password: base64.StdEncoding.EncodeToString(hashed), Totp: cfg.TOTP_Secret})
|
||||
if err != nil {
|
||||
log.Println(base64.StdEncoding.EncodeToString(hashed), err)
|
||||
return c.SendStatus(fiber.StatusBadRequest)
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusCreated)
|
||||
}
|
||||
|
||||
func Admin_new_totp_code(c *fiber.Ctx) error {
|
||||
name := c.Query("name")
|
||||
if len(name) == 0 {
|
||||
return fiber.ErrBadRequest
|
||||
}
|
||||
key, err := totp.Generate(totp.GenerateOpts{Issuer: "TCIVS_NIMING", AccountName: name})
|
||||
if err != nil {
|
||||
return fiber.ErrInternalServerError
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
img, err := key.Image(200, 200)
|
||||
png.Encode(&buf, img)
|
||||
return c.JSON(fiber.Map{"key": key.Secret(), "img": base64.StdEncoding.EncodeToString(buf.Bytes())})
|
||||
}
|
||||
|
||||
const debug = false
|
||||
|
||||
func Admin_Login_JWT(c *fiber.Ctx) (*string, error) {
|
||||
cred := new(AdminAuth)
|
||||
|
||||
if err := c.BodyParser(cred); err != nil {
|
||||
return nil, fiber.ErrBadRequest
|
||||
}
|
||||
hashed, err := scrypt.Key([]byte(cred.Password), []byte(internal.SALT), 32768, 8, 1, 32)
|
||||
if err != nil {
|
||||
return nil, fiber.ErrInternalServerError
|
||||
}
|
||||
|
||||
var claims jwt.MapClaims
|
||||
|
||||
if !debug {
|
||||
|
||||
rec, err := internal.NIMDB.AdminLoginGetTOTP(c.Context(), nimdb.AdminLoginGetTOTPParams{Username: cred.Name, Password: base64.StdEncoding.EncodeToString(hashed)})
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
c.SendString("Failed to login with wrong account or password")
|
||||
return nil, fiber.ErrUnauthorized
|
||||
}
|
||||
if !totp.Validate(cred.TOTP_code, rec.Totp) {
|
||||
log.Println(totp.GenerateCode(rec.Totp, time.Now()))
|
||||
c.SendString("Failed to login with wrong totp")
|
||||
return nil, fiber.ErrUnauthorized
|
||||
}
|
||||
claims = jwt.MapClaims{"name": cred.Name, "admin": rec.Super}
|
||||
|
||||
} else {
|
||||
claims = jwt.MapClaims{"name": "test", "admin": true}
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
|
||||
t, err := token.SignedString([]byte(internal.JWT_SECRET))
|
||||
if err != nil {
|
||||
return nil, fiber.ErrInternalServerError
|
||||
}
|
||||
cookie := new(fiber.Cookie)
|
||||
cookie.Name = "token"
|
||||
cookie.Value = t
|
||||
cookie.Expires = time.Now().Add(5 * time.Hour)
|
||||
c.Cookie(cookie)
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func Admin_Fetch_Post(c *fiber.Ctx) error {
|
||||
user := c.Locals("user").(*jwt.Token)
|
||||
claims := user.Claims.(jwt.MapClaims)
|
||||
super := claims["admin"].(bool)
|
||||
ctx := c.Context()
|
||||
if !super {
|
||||
rec, err := internal.NIMDB.AdminGetPost(ctx)
|
||||
if err != nil {
|
||||
|
||||
return c.SendStatus(fiber.StatusInternalServerError)
|
||||
}
|
||||
type recwithimg struct {
|
||||
Id int32 `json:"id"`
|
||||
Content string `json:"content"`
|
||||
Signing pgtype.Text `json:"signing"`
|
||||
Post_at time.Time `json:"post_at"`
|
||||
Enclosue []string `json:"enclosure"`
|
||||
}
|
||||
rec_with_img := make([]recwithimg, len(rec))
|
||||
for id, k := range rec {
|
||||
rec_with_img[id] = recwithimg{Id: k.ID, Content: k.Content, Signing: k.Signing, Post_at: k.PostAt.Time}
|
||||
imgrec, err := internal.NIMDB.AdminGetMedia(ctx, pgtype.Int4{Int32: k.ID, Valid: true})
|
||||
if err != nil {
|
||||
return c.SendStatus(fiber.StatusInternalServerError)
|
||||
}
|
||||
rec_with_img[id].Enclosue = imgrec
|
||||
}
|
||||
return c.JSON(rec_with_img)
|
||||
} else {
|
||||
|
||||
rec, err := internal.NIMDB.SuperAdminGetPost(ctx)
|
||||
if err != nil {
|
||||
|
||||
return c.SendStatus(fiber.StatusInternalServerError)
|
||||
}
|
||||
type recwithimg struct {
|
||||
Id int32 `json:"id"`
|
||||
Content string `json:"content"`
|
||||
Signing pgtype.Text `json:"signing"`
|
||||
Post_at time.Time `json:"post_at"`
|
||||
Enclosue []string `json:"enclosure"`
|
||||
Phase nimdb.PostPhase `json:"phase"`
|
||||
}
|
||||
rec_with_img := make([]recwithimg, len(rec))
|
||||
for id, k := range rec {
|
||||
rec_with_img[id] = recwithimg{Id: k.ID, Content: k.Content, Signing: k.Signing, Post_at: k.PostAt.Time, Phase: k.Phase}
|
||||
imgrec, err := internal.NIMDB.AdminGetMedia(ctx, pgtype.Int4{Int32: k.ID, Valid: true})
|
||||
if err != nil {
|
||||
return c.SendStatus(fiber.StatusInternalServerError)
|
||||
}
|
||||
rec_with_img[id].Enclosue = imgrec
|
||||
}
|
||||
return c.JSON(rec_with_img)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type Verify struct {
|
||||
Id int32 `json:"post"`
|
||||
Check bool `json:"check"`
|
||||
}
|
||||
type VerifyList struct {
|
||||
Choice []Verify `json:"choice"`
|
||||
}
|
||||
|
||||
func AdminVerify(c *fiber.Ctx) error {
|
||||
user := c.Locals("user").(*jwt.Token)
|
||||
claims := user.Claims.(jwt.MapClaims)
|
||||
super := claims["admin"].(bool)
|
||||
ctx := c.Context()
|
||||
tx, err := internal.POOL.Begin(ctx)
|
||||
defer tx.Rollback(ctx)
|
||||
if err != nil {
|
||||
return c.SendStatus(fiber.StatusInternalServerError)
|
||||
}
|
||||
qtx := internal.NIMDB.WithTx(tx)
|
||||
post_verify_list := new(VerifyList)
|
||||
if err = c.BodyParser(post_verify_list); err != nil {
|
||||
log.Printf("Can't parse verify body: %s", err.Error())
|
||||
return c.SendStatus(fiber.StatusBadRequest)
|
||||
}
|
||||
|
||||
if !super {
|
||||
for _, post_verify := range post_verify_list.Choice {
|
||||
if post_verify.Check {
|
||||
_, err = qtx.AdminVerify(ctx, nimdb.AdminVerifyParams{ID: post_verify.Id, Phase: nimdb.PostPhaseOk})
|
||||
if err != nil {
|
||||
log.Println(err.Error(), post_verify.Id)
|
||||
return c.SendStatus(fiber.StatusBadRequest)
|
||||
}
|
||||
|
||||
err = qtx.AdminUpdateImage(ctx, pgtype.Int4{Int32: post_verify.Id, Valid: true})
|
||||
if err != nil {
|
||||
return c.SendStatus(fiber.StatusInternalServerError)
|
||||
|
||||
}
|
||||
if internal.IGAPI_ACTIVATE {
|
||||
internal.IGAPI_CHAN <- internal.PR{ACT_TYPE: 0, ID: post_verify.Id}
|
||||
}
|
||||
|
||||
} else {
|
||||
_, err = qtx.AdminVerify(ctx, nimdb.AdminVerifyParams{ID: post_verify.Id, Phase: nimdb.PostPhaseRejected})
|
||||
if err != nil {
|
||||
log.Println(err.Error())
|
||||
return c.SendStatus(fiber.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
for _, post_verify := range post_verify_list.Choice {
|
||||
if post_verify.Check {
|
||||
_, err = qtx.SuperAdminVerify(ctx, nimdb.SuperAdminVerifyParams{ID: post_verify.Id, Phase: nimdb.PostPhaseOk})
|
||||
if err != nil {
|
||||
log.Println(err.Error(), post_verify.Id)
|
||||
return c.SendStatus(fiber.StatusBadRequest)
|
||||
}
|
||||
|
||||
err = qtx.AdminUpdateImage(ctx, pgtype.Int4{Int32: post_verify.Id, Valid: true})
|
||||
if err != nil {
|
||||
return c.SendStatus(fiber.StatusInternalServerError)
|
||||
|
||||
}
|
||||
if internal.IGAPI_ACTIVATE {
|
||||
internal.IGAPI_CHAN <- internal.PR{ACT_TYPE: 0, ID: post_verify.Id}
|
||||
}
|
||||
|
||||
} else {
|
||||
_, err = qtx.SuperAdminVerify(ctx, nimdb.SuperAdminVerifyParams{ID: post_verify.Id, Phase: nimdb.PostPhaseAdminRejected})
|
||||
if err != nil {
|
||||
log.Println(err.Error())
|
||||
return c.SendStatus(fiber.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
const adminpage_basepath = "./admin_panel/dist"
|
||||
|
||||
func AdminSendPage(c *fiber.Ctx) error {
|
||||
pagepath := c.Params("*")
|
||||
if pagepath == "login" || pagepath == "panel" || pagepath == "new_account" {
|
||||
err := c.SendFile(path.Join(adminpage_basepath, "index.html"))
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return c.SendStatus(fiber.StatusNotFound)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
err := c.SendFile(path.Join(adminpage_basepath, pagepath))
|
||||
if err != nil {
|
||||
c.SendStatus(404)
|
||||
}
|
||||
return nil
|
||||
}
|
17
internal/handlers/heart.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"nim.jasinco.work/app/internal"
|
||||
)
|
||||
|
||||
func Add_heart(c *fiber.Ctx) error {
|
||||
hearts, err := strconv.Atoi(c.Query("post"))
|
||||
if err != nil {
|
||||
return fiber.ErrBadRequest
|
||||
}
|
||||
dbhearts, err := internal.NIMDB.AddPostHeart(c.Context(), int32(hearts))
|
||||
return c.JSON(fiber.Map{"hearts": dbhearts})
|
||||
}
|
182
internal/handlers/post.go
Normal file
|
@ -0,0 +1,182 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"log"
|
||||
"mime"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"nim.jasinco.work/app/internal"
|
||||
"nim.jasinco.work/app/nimdb"
|
||||
)
|
||||
|
||||
var supported_filetype = []string{"image/png", "image/jpeg", "image/webp", "image/gif", "image/heif", "video/H264", "video/H265", "video/mp4"}
|
||||
|
||||
func find_supported(mimetype string) bool {
|
||||
for _, supported := range supported_filetype {
|
||||
if strings.Compare(supported, mimetype) == 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func Fetch_post(c *fiber.Ctx) error {
|
||||
cursor := c.Query("cursor")
|
||||
|
||||
ctx := c.Context()
|
||||
|
||||
var rec []nimdb.GetPostRow
|
||||
var err error
|
||||
if len(cursor) == 0 {
|
||||
rec, err = internal.NIMDB.GetPost(ctx, internal.FETCH_LENGTH)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return fiber.ErrInternalServerError
|
||||
}
|
||||
} else if cursor_num, err := strconv.Atoi(cursor); err == nil {
|
||||
rec_cur, err := internal.NIMDB.GetPostWithCursor(ctx, nimdb.GetPostWithCursorParams{ID: int32(cursor_num), Limit: internal.FETCH_LENGTH})
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return c.Status(500).SendString("Failed to Query DB")
|
||||
}
|
||||
rec = make([]nimdb.GetPostRow, len(rec_cur))
|
||||
for id, val := range rec_cur {
|
||||
rec[id] = nimdb.GetPostRow(val)
|
||||
}
|
||||
} else {
|
||||
return c.Status(fiber.StatusBadRequest).SendString("Can't parse cursor")
|
||||
}
|
||||
if len(rec) == 0 {
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
||||
|
||||
return c.JSON(rec)
|
||||
}
|
||||
|
||||
func Delete_post(c *fiber.Ctx) error {
|
||||
post := c.Query("post")
|
||||
hash := c.Query("hash")
|
||||
ctx := c.Context()
|
||||
post_id, err := strconv.Atoi(post)
|
||||
post_id_i32 := int32(post_id)
|
||||
if err != nil {
|
||||
return c.Status(500).SendString("Failed to Parse Int")
|
||||
}
|
||||
|
||||
tx, err := internal.POOL.Begin(ctx)
|
||||
if err != nil {
|
||||
return c.Status(500).SendString("Failed to Start Transaction")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
qtx := internal.NIMDB.WithTx(tx)
|
||||
if err = qtx.DeletePost(ctx, nimdb.DeletePostParams{ID: post_id_i32, Hash: hash}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = qtx.InvalidateMedia(ctx, pgtype.Int4{Int32: post_id_i32}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if tx.Commit(ctx) != nil {
|
||||
return c.Status(500).SendString("Failed to Commit")
|
||||
}
|
||||
|
||||
if internal.IGAPI_ACTIVATE {
|
||||
internal.IGAPI_CHAN <- internal.PR{ACT_TYPE: 1, ID: post_id_i32}
|
||||
}
|
||||
|
||||
return c.SendStatus(200)
|
||||
}
|
||||
|
||||
func Insert_Post(c *fiber.Ctx) error {
|
||||
ctx := c.Context()
|
||||
|
||||
tx, err := internal.POOL.Begin(ctx)
|
||||
if err != nil {
|
||||
return c.Status(500).SendString("Failed to Start Transaction")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
qtx := internal.NIMDB.WithTx(tx)
|
||||
|
||||
if form, err := c.MultipartForm(); err == nil {
|
||||
|
||||
content := form.Value["content"]
|
||||
signing := form.Value["signing"]
|
||||
files := form.File["enclosure"]
|
||||
if len(content) == 0 || len(content[0]) == 0 {
|
||||
return fiber.ErrBadRequest
|
||||
}
|
||||
post_hash := sha256.New()
|
||||
post_hash.Write([]byte(content[0]))
|
||||
signing_pgt := new(pgtype.Text)
|
||||
signing_pgt.Valid = false
|
||||
if len(signing) > 0 && len(signing[0]) > 0 {
|
||||
post_hash.Write([]byte(signing[0]))
|
||||
signing_pgt.Valid = true
|
||||
signing_pgt.String = signing[0]
|
||||
}
|
||||
now, err := time.Now().MarshalBinary()
|
||||
if err != nil {
|
||||
c.SendString("Failed to get time")
|
||||
return c.SendStatus(500)
|
||||
}
|
||||
|
||||
credential, err := qtx.InsertPost(ctx,
|
||||
nimdb.InsertPostParams{
|
||||
Content: content[0],
|
||||
Signing: *signing_pgt,
|
||||
Hash: base64.StdEncoding.EncodeToString(post_hash.Sum(now)),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
log.Println(err.Error())
|
||||
return c.Status(500).SendString("Failed to insert post")
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
filemime, _, err := mime.ParseMediaType(file.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return c.Status(fiber.ErrBadRequest.Code).SendString("Failed to parse content type")
|
||||
}
|
||||
if !find_supported(filemime) {
|
||||
return c.Status(fiber.ErrBadRequest.Code).SendString("Unsupported Type")
|
||||
}
|
||||
|
||||
filetype := strings.Split(filemime, "/")[0]
|
||||
mid, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
c.SendString("Faild to generate UUIDv4")
|
||||
return c.SendStatus(500)
|
||||
}
|
||||
|
||||
fxnamepath := filetype + "/" + mid.String()
|
||||
if err = c.SaveFile(file, path.Join("./static", fxnamepath)); err != nil {
|
||||
log.Println("Can't save to ./static", err.Error())
|
||||
return fiber.ErrInternalServerError
|
||||
}
|
||||
|
||||
if err := qtx.InsertPostImage(ctx, nimdb.InsertPostImageParams{
|
||||
Url: fxnamepath,
|
||||
PostID: pgtype.Int4{Int32: credential.ID, Valid: true},
|
||||
}); err != nil {
|
||||
log.Println(err.Error())
|
||||
return c.Status(500).SendString("Failed to insert image")
|
||||
}
|
||||
}
|
||||
if err = tx.Commit(ctx); err != nil {
|
||||
log.Println(err.Error())
|
||||
c.SendString("Failed to send Commit")
|
||||
return c.SendStatus(500)
|
||||
}
|
||||
return c.JSON(credential)
|
||||
}
|
||||
|
||||
return fiber.ErrBadRequest
|
||||
}
|
9
internal/pool.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"nim.jasinco.work/app/nimdb"
|
||||
)
|
||||
|
||||
var POOL *pgxpool.Pool = nil
|
||||
var NIMDB *nimdb.Queries = nil
|
97
internal/settings.go
Normal file
|
@ -0,0 +1,97 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"nim.jasinco.work/app/igapi"
|
||||
)
|
||||
|
||||
type PR struct {
|
||||
ACT_TYPE int //0 for post, 1 for revoke
|
||||
ID int32
|
||||
}
|
||||
|
||||
var (
|
||||
SALT string
|
||||
POSTGRES_URL string
|
||||
JWT_SECRET string
|
||||
FETCH_LENGTH int32
|
||||
PREFORK bool
|
||||
IGAPI_HOST string
|
||||
CORS_ALLOW string
|
||||
IGAPI_ACTIVATE bool
|
||||
IGAPI_CHAN = make(chan PR, 20)
|
||||
err error
|
||||
conv int64
|
||||
)
|
||||
|
||||
func ReadFromENV() error {
|
||||
SALT = os.Getenv("SALT")
|
||||
if len(SALT) < 8 {
|
||||
return errors.New("Invalid Salt")
|
||||
}
|
||||
POSTGRES_URL = os.Getenv("POSTGRES_URL")
|
||||
if len(POSTGRES_URL) < 1 {
|
||||
return errors.New("POSTGRES_URL NOT FOUND")
|
||||
}
|
||||
JWT_SECRET = os.Getenv("JWT_SECRET")
|
||||
if len(JWT_SECRET) < 10 {
|
||||
return errors.New("INVALID JWT SECRET")
|
||||
}
|
||||
fetch_len_str := os.Getenv("FETCH_LENGTH")
|
||||
if len(fetch_len_str) == 0 {
|
||||
FETCH_LENGTH = 10
|
||||
} else {
|
||||
conv, err = strconv.ParseInt(fetch_len_str, 10, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if conv < 1 {
|
||||
return errors.New("FETCH_LENGTH should be a positive number as well over 0")
|
||||
}
|
||||
FETCH_LENGTH = int32(conv)
|
||||
|
||||
}
|
||||
prefork_str := os.Getenv("PREFORK")
|
||||
if len(prefork_str) == 0 {
|
||||
PREFORK = false
|
||||
} else {
|
||||
PREFORK, err = strconv.ParseBool(prefork_str)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
IGAPI_HOST = os.Getenv("IGAPI_HOST")
|
||||
IGAPI_ACTIVATE = true
|
||||
if len(IGAPI_HOST) == 0 {
|
||||
log.Println("Didn't get IGAPI_HOST, it will work without it")
|
||||
IGAPI_ACTIVATE = false
|
||||
}
|
||||
CORS_ALLOW = os.Getenv("CORS_ALLOW")
|
||||
return nil
|
||||
}
|
||||
|
||||
func IGAPI_establish() (*grpc.ClientConn, igapi.IGAPIClient) {
|
||||
conn, err := grpc.NewClient(IGAPI_HOST, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
log.Panicf("Can't connect to igapi: %s", err.Error())
|
||||
}
|
||||
c := igapi.NewIGAPIClient(conn)
|
||||
return conn, c
|
||||
}
|
||||
func IGAPI_chan_exec(c igapi.IGAPIClient) {
|
||||
for id := range IGAPI_CHAN {
|
||||
if id.ACT_TYPE == 0 {
|
||||
c.Upload(context.Background(), &igapi.Request{Id: int64(id.ID)})
|
||||
}
|
||||
if id.ACT_TYPE == 1 {
|
||||
c.Delete(context.Background(), &igapi.Request{Id: int64(id.ID)})
|
||||
}
|
||||
}
|
||||
}
|
6
justfile
|
@ -1,6 +0,0 @@
|
|||
fake:
|
||||
POSTGRES_URL="postgres://test:test@192.168.50.14:5432/posts" bun run ./tools/gen_fake.ts
|
||||
fetch_post:
|
||||
POSTGRES_URL="postgres://test:test@192.168.50.14:5432/posts" bun run ./tools/post_db.ts
|
||||
create_schema:
|
||||
POSTGRES_URL="postgres://test:test@192.168.50.14:5432/posts" bun run ./tools/create_schema.ts
|
|
@ -1,7 +0,0 @@
|
|||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
};
|
||||
|
||||
export default nextConfig;
|
32
nimdb/db.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
|
||||
package nimdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
)
|
||||
|
||||
type DBTX interface {
|
||||
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
|
||||
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
|
||||
QueryRow(context.Context, string, ...interface{}) pgx.Row
|
||||
}
|
||||
|
||||
func New(db DBTX) *Queries {
|
||||
return &Queries{db: db}
|
||||
}
|
||||
|
||||
type Queries struct {
|
||||
db DBTX
|
||||
}
|
||||
|
||||
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
|
||||
return &Queries{
|
||||
db: tx,
|
||||
}
|
||||
}
|
92
nimdb/models.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
|
||||
package nimdb
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
type PostPhase string
|
||||
|
||||
const (
|
||||
PostPhasePending PostPhase = "pending"
|
||||
PostPhaseRejected PostPhase = "rejected"
|
||||
PostPhaseAdminRejected PostPhase = "admin_rejected"
|
||||
PostPhaseOk PostPhase = "ok"
|
||||
PostPhaseDeleted PostPhase = "deleted"
|
||||
)
|
||||
|
||||
func (e *PostPhase) Scan(src interface{}) error {
|
||||
switch s := src.(type) {
|
||||
case []byte:
|
||||
*e = PostPhase(s)
|
||||
case string:
|
||||
*e = PostPhase(s)
|
||||
default:
|
||||
return fmt.Errorf("unsupported scan type for PostPhase: %T", src)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type NullPostPhase struct {
|
||||
PostPhase PostPhase `json:"post_phase"`
|
||||
Valid bool `json:"valid"` // Valid is true if PostPhase is not NULL
|
||||
}
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (ns *NullPostPhase) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
ns.PostPhase, ns.Valid = "", false
|
||||
return nil
|
||||
}
|
||||
ns.Valid = true
|
||||
return ns.PostPhase.Scan(value)
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface.
|
||||
func (ns NullPostPhase) Value() (driver.Value, error) {
|
||||
if !ns.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
return string(ns.PostPhase), nil
|
||||
}
|
||||
|
||||
type Admin struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Totp string `json:"totp"`
|
||||
Super bool `json:"super"`
|
||||
}
|
||||
|
||||
type Comment struct {
|
||||
ID int32 `json:"id"`
|
||||
PostID int32 `json:"post_id"`
|
||||
Content string `json:"content"`
|
||||
Signing pgtype.Text `json:"signing"`
|
||||
Hash string `json:"hash"`
|
||||
PostAt pgtype.Timestamptz `json:"post_at"`
|
||||
Heart int32 `json:"heart"`
|
||||
}
|
||||
|
||||
type Medium struct {
|
||||
Url string `json:"url"`
|
||||
PostID pgtype.Int4 `json:"post_id"`
|
||||
CommentID pgtype.Int4 `json:"comment_id"`
|
||||
Visible pgtype.Bool `json:"visible"`
|
||||
}
|
||||
|
||||
type Post struct {
|
||||
ID int32 `json:"id"`
|
||||
Content string `json:"content"`
|
||||
Signing pgtype.Text `json:"signing"`
|
||||
Hash string `json:"hash"`
|
||||
PostAt pgtype.Timestamptz `json:"post_at"`
|
||||
Heart int32 `json:"heart"`
|
||||
Phase PostPhase `json:"phase"`
|
||||
Igid pgtype.Text `json:"igid"`
|
||||
}
|
490
nimdb/query.sql.go
Normal file
|
@ -0,0 +1,490 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
// source: query.sql
|
||||
|
||||
package nimdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const addCommentHeart = `-- name: AddCommentHeart :one
|
||||
UPDATE comment SET heart = heart + 1 WHERE id=$1 RETURNING heart
|
||||
`
|
||||
|
||||
func (q *Queries) AddCommentHeart(ctx context.Context, id int32) (int32, error) {
|
||||
row := q.db.QueryRow(ctx, addCommentHeart, id)
|
||||
var heart int32
|
||||
err := row.Scan(&heart)
|
||||
return heart, err
|
||||
}
|
||||
|
||||
const addPostHeart = `-- name: AddPostHeart :one
|
||||
UPDATE posts SET heart = heart + 1 WHERE id=$1 RETURNING heart
|
||||
`
|
||||
|
||||
func (q *Queries) AddPostHeart(ctx context.Context, id int32) (int32, error) {
|
||||
row := q.db.QueryRow(ctx, addPostHeart, id)
|
||||
var heart int32
|
||||
err := row.Scan(&heart)
|
||||
return heart, err
|
||||
}
|
||||
|
||||
const adminCreateAccount = `-- name: AdminCreateAccount :exec
|
||||
INSERT INTO admin (username, password, totp) VALUES ($1, $2,$3)
|
||||
`
|
||||
|
||||
type AdminCreateAccountParams struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Totp string `json:"totp"`
|
||||
}
|
||||
|
||||
func (q *Queries) AdminCreateAccount(ctx context.Context, arg AdminCreateAccountParams) error {
|
||||
_, err := q.db.Exec(ctx, adminCreateAccount, arg.Username, arg.Password, arg.Totp)
|
||||
return err
|
||||
}
|
||||
|
||||
const adminGetMedia = `-- name: AdminGetMedia :many
|
||||
SELECT url from media WHERE post_id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) AdminGetMedia(ctx context.Context, postID pgtype.Int4) ([]string, error) {
|
||||
rows, err := q.db.Query(ctx, adminGetMedia, postID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []string
|
||||
for rows.Next() {
|
||||
var url string
|
||||
if err := rows.Scan(&url); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, url)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const adminGetPost = `-- name: AdminGetPost :many
|
||||
SELECT id, content, signing, post_at FROM posts WHERE phase = 'pending' ORDER BY id DESC LIMIT 100
|
||||
`
|
||||
|
||||
type AdminGetPostRow struct {
|
||||
ID int32 `json:"id"`
|
||||
Content string `json:"content"`
|
||||
Signing pgtype.Text `json:"signing"`
|
||||
PostAt pgtype.Timestamptz `json:"post_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) AdminGetPost(ctx context.Context) ([]AdminGetPostRow, error) {
|
||||
rows, err := q.db.Query(ctx, adminGetPost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []AdminGetPostRow
|
||||
for rows.Next() {
|
||||
var i AdminGetPostRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Content,
|
||||
&i.Signing,
|
||||
&i.PostAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const adminLoginGetTOTP = `-- name: AdminLoginGetTOTP :one
|
||||
SELECT totp, super FROM admin WHERE username = $1 AND password = $2
|
||||
`
|
||||
|
||||
type AdminLoginGetTOTPParams struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type AdminLoginGetTOTPRow struct {
|
||||
Totp string `json:"totp"`
|
||||
Super bool `json:"super"`
|
||||
}
|
||||
|
||||
func (q *Queries) AdminLoginGetTOTP(ctx context.Context, arg AdminLoginGetTOTPParams) (AdminLoginGetTOTPRow, error) {
|
||||
row := q.db.QueryRow(ctx, adminLoginGetTOTP, arg.Username, arg.Password)
|
||||
var i AdminLoginGetTOTPRow
|
||||
err := row.Scan(&i.Totp, &i.Super)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const adminUpdateImage = `-- name: AdminUpdateImage :exec
|
||||
UPDATE media SET visible = true WHERE post_id=$1
|
||||
`
|
||||
|
||||
func (q *Queries) AdminUpdateImage(ctx context.Context, postID pgtype.Int4) error {
|
||||
_, err := q.db.Exec(ctx, adminUpdateImage, postID)
|
||||
return err
|
||||
}
|
||||
|
||||
const adminVerify = `-- name: AdminVerify :one
|
||||
UPDATE posts SET phase = $1 WHERE id=$2 AND phase = 'pending' RETURNING id
|
||||
`
|
||||
|
||||
type AdminVerifyParams struct {
|
||||
Phase PostPhase `json:"phase"`
|
||||
ID int32 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) AdminVerify(ctx context.Context, arg AdminVerifyParams) (int32, error) {
|
||||
row := q.db.QueryRow(ctx, adminVerify, arg.Phase, arg.ID)
|
||||
var id int32
|
||||
err := row.Scan(&id)
|
||||
return id, err
|
||||
}
|
||||
|
||||
const deletePost = `-- name: DeletePost :exec
|
||||
UPDATE posts SET phase = 'deleted' WHERE id = $1 AND hash=$2
|
||||
`
|
||||
|
||||
type DeletePostParams struct {
|
||||
ID int32 `json:"id"`
|
||||
Hash string `json:"hash"`
|
||||
}
|
||||
|
||||
func (q *Queries) DeletePost(ctx context.Context, arg DeletePostParams) error {
|
||||
_, err := q.db.Exec(ctx, deletePost, arg.ID, arg.Hash)
|
||||
return err
|
||||
}
|
||||
|
||||
const getComment = `-- name: GetComment :many
|
||||
SELECT id, content, signing, post_at
|
||||
FROM comment
|
||||
WHERE phase = 'ok' AND post_id = $1
|
||||
ORDER BY id DESC
|
||||
LIMIT $2
|
||||
`
|
||||
|
||||
type GetCommentParams struct {
|
||||
PostID int32 `json:"post_id"`
|
||||
Limit int32 `json:"limit"`
|
||||
}
|
||||
|
||||
type GetCommentRow struct {
|
||||
ID int32 `json:"id"`
|
||||
Content string `json:"content"`
|
||||
Signing pgtype.Text `json:"signing"`
|
||||
PostAt pgtype.Timestamptz `json:"post_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetComment(ctx context.Context, arg GetCommentParams) ([]GetCommentRow, error) {
|
||||
rows, err := q.db.Query(ctx, getComment, arg.PostID, arg.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetCommentRow
|
||||
for rows.Next() {
|
||||
var i GetCommentRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Content,
|
||||
&i.Signing,
|
||||
&i.PostAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getCommentWithCursor = `-- name: GetCommentWithCursor :many
|
||||
SELECT id, content, signing, post_at
|
||||
FROM comment
|
||||
WHERE id <= $1 AND phase = 'ok' AND post_id = $2
|
||||
ORDER BY id DESC
|
||||
LIMIT $3
|
||||
`
|
||||
|
||||
type GetCommentWithCursorParams struct {
|
||||
ID int32 `json:"id"`
|
||||
PostID int32 `json:"post_id"`
|
||||
Limit int32 `json:"limit"`
|
||||
}
|
||||
|
||||
type GetCommentWithCursorRow struct {
|
||||
ID int32 `json:"id"`
|
||||
Content string `json:"content"`
|
||||
Signing pgtype.Text `json:"signing"`
|
||||
PostAt pgtype.Timestamptz `json:"post_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetCommentWithCursor(ctx context.Context, arg GetCommentWithCursorParams) ([]GetCommentWithCursorRow, error) {
|
||||
rows, err := q.db.Query(ctx, getCommentWithCursor, arg.ID, arg.PostID, arg.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetCommentWithCursorRow
|
||||
for rows.Next() {
|
||||
var i GetCommentWithCursorRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Content,
|
||||
&i.Signing,
|
||||
&i.PostAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getPost = `-- name: GetPost :many
|
||||
SELECT id, content, signing, post_at, heart, igid, array_agg(url) as enclosure
|
||||
FROM posts
|
||||
LEFT JOIN media ON post_id is not null AND post_id = id AND visible
|
||||
WHERE phase = 'ok'
|
||||
GROUP BY id
|
||||
ORDER BY id DESC
|
||||
LIMIT $1
|
||||
`
|
||||
|
||||
type GetPostRow struct {
|
||||
ID int32 `json:"id"`
|
||||
Content string `json:"content"`
|
||||
Signing pgtype.Text `json:"signing"`
|
||||
PostAt pgtype.Timestamptz `json:"post_at"`
|
||||
Heart int32 `json:"heart"`
|
||||
Igid pgtype.Text `json:"igid"`
|
||||
Enclosure interface{} `json:"enclosure"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetPost(ctx context.Context, limit int32) ([]GetPostRow, error) {
|
||||
rows, err := q.db.Query(ctx, getPost, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetPostRow
|
||||
for rows.Next() {
|
||||
var i GetPostRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Content,
|
||||
&i.Signing,
|
||||
&i.PostAt,
|
||||
&i.Heart,
|
||||
&i.Igid,
|
||||
&i.Enclosure,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getPostWithCursor = `-- name: GetPostWithCursor :many
|
||||
SELECT id, content, signing, post_at, heart, igid, array_agg(url) as enclosure
|
||||
FROM posts
|
||||
LEFT JOIN media ON post_id is not null AND post_id = id AND visible
|
||||
WHERE id <= $1 AND phase = 'ok'
|
||||
GROUP BY id
|
||||
ORDER BY id DESC
|
||||
LIMIT $2
|
||||
`
|
||||
|
||||
type GetPostWithCursorParams struct {
|
||||
ID int32 `json:"id"`
|
||||
Limit int32 `json:"limit"`
|
||||
}
|
||||
|
||||
type GetPostWithCursorRow struct {
|
||||
ID int32 `json:"id"`
|
||||
Content string `json:"content"`
|
||||
Signing pgtype.Text `json:"signing"`
|
||||
PostAt pgtype.Timestamptz `json:"post_at"`
|
||||
Heart int32 `json:"heart"`
|
||||
Igid pgtype.Text `json:"igid"`
|
||||
Enclosure interface{} `json:"enclosure"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetPostWithCursor(ctx context.Context, arg GetPostWithCursorParams) ([]GetPostWithCursorRow, error) {
|
||||
rows, err := q.db.Query(ctx, getPostWithCursor, arg.ID, arg.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetPostWithCursorRow
|
||||
for rows.Next() {
|
||||
var i GetPostWithCursorRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Content,
|
||||
&i.Signing,
|
||||
&i.PostAt,
|
||||
&i.Heart,
|
||||
&i.Igid,
|
||||
&i.Enclosure,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const insertCommentImage = `-- name: InsertCommentImage :exec
|
||||
INSERT INTO media (url, comment_id) VALUES ($1, $2)
|
||||
`
|
||||
|
||||
type InsertCommentImageParams struct {
|
||||
Url string `json:"url"`
|
||||
CommentID pgtype.Int4 `json:"comment_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) InsertCommentImage(ctx context.Context, arg InsertCommentImageParams) error {
|
||||
_, err := q.db.Exec(ctx, insertCommentImage, arg.Url, arg.CommentID)
|
||||
return err
|
||||
}
|
||||
|
||||
const insertPost = `-- name: InsertPost :one
|
||||
INSERT INTO posts (content,signing,hash)
|
||||
VALUES ($1, $2 ,$3)
|
||||
RETURNING id,hash
|
||||
`
|
||||
|
||||
type InsertPostParams struct {
|
||||
Content string `json:"content"`
|
||||
Signing pgtype.Text `json:"signing"`
|
||||
Hash string `json:"hash"`
|
||||
}
|
||||
|
||||
type InsertPostRow struct {
|
||||
ID int32 `json:"id"`
|
||||
Hash string `json:"hash"`
|
||||
}
|
||||
|
||||
func (q *Queries) InsertPost(ctx context.Context, arg InsertPostParams) (InsertPostRow, error) {
|
||||
row := q.db.QueryRow(ctx, insertPost, arg.Content, arg.Signing, arg.Hash)
|
||||
var i InsertPostRow
|
||||
err := row.Scan(&i.ID, &i.Hash)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const insertPostImage = `-- name: InsertPostImage :exec
|
||||
INSERT INTO media (url, post_id) VALUES ($1, $2)
|
||||
`
|
||||
|
||||
type InsertPostImageParams struct {
|
||||
Url string `json:"url"`
|
||||
PostID pgtype.Int4 `json:"post_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) InsertPostImage(ctx context.Context, arg InsertPostImageParams) error {
|
||||
_, err := q.db.Exec(ctx, insertPostImage, arg.Url, arg.PostID)
|
||||
return err
|
||||
}
|
||||
|
||||
const invalidateMedia = `-- name: InvalidateMedia :exec
|
||||
UPDATE media SET visible = false WHERE post_id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) InvalidateMedia(ctx context.Context, postID pgtype.Int4) error {
|
||||
_, err := q.db.Exec(ctx, invalidateMedia, postID)
|
||||
return err
|
||||
}
|
||||
|
||||
const superAdminGetPost = `-- name: SuperAdminGetPost :many
|
||||
SELECT id, content, signing, post_at, phase FROM posts WHERE phase = 'pending' OR phase = 'rejected' ORDER BY id DESC LIMIT 100
|
||||
`
|
||||
|
||||
type SuperAdminGetPostRow struct {
|
||||
ID int32 `json:"id"`
|
||||
Content string `json:"content"`
|
||||
Signing pgtype.Text `json:"signing"`
|
||||
PostAt pgtype.Timestamptz `json:"post_at"`
|
||||
Phase PostPhase `json:"phase"`
|
||||
}
|
||||
|
||||
func (q *Queries) SuperAdminGetPost(ctx context.Context) ([]SuperAdminGetPostRow, error) {
|
||||
rows, err := q.db.Query(ctx, superAdminGetPost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []SuperAdminGetPostRow
|
||||
for rows.Next() {
|
||||
var i SuperAdminGetPostRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Content,
|
||||
&i.Signing,
|
||||
&i.PostAt,
|
||||
&i.Phase,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const superAdminVerify = `-- name: SuperAdminVerify :one
|
||||
UPDATE posts SET phase = $1 WHERE id=$2 AND (phase = 'pending' OR phase = 'rejected') RETURNING id
|
||||
`
|
||||
|
||||
type SuperAdminVerifyParams struct {
|
||||
Phase PostPhase `json:"phase"`
|
||||
ID int32 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) SuperAdminVerify(ctx context.Context, arg SuperAdminVerifyParams) (int32, error) {
|
||||
row := q.db.QueryRow(ctx, superAdminVerify, arg.Phase, arg.ID)
|
||||
var id int32
|
||||
err := row.Scan(&id)
|
||||
return id, err
|
||||
}
|
||||
|
||||
const updatePostIGID = `-- name: UpdatePostIGID :exec
|
||||
UPDATE posts set igid = $1 WHERE id = $2
|
||||
`
|
||||
|
||||
type UpdatePostIGIDParams struct {
|
||||
Igid pgtype.Text `json:"igid"`
|
||||
ID int32 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdatePostIGID(ctx context.Context, arg UpdatePostIGIDParams) error {
|
||||
_, err := q.db.Exec(ctx, updatePostIGID, arg.Igid, arg.ID)
|
||||
return err
|
||||
}
|
26
package.json
|
@ -1,26 +0,0 @@
|
|||
{
|
||||
"name": "nim",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@faker-js/faker": "^9.7.0",
|
||||
"bun-types": "^1.2.9",
|
||||
"next": "15.3.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"tailwindcss": "^4"
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
const config = {
|
||||
plugins: ["@tailwindcss/postcss"],
|
||||
};
|
||||
|
||||
export default config;
|
|
@ -1 +0,0 @@
|
|||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
Before Width: | Height: | Size: 391 B |
|
@ -1 +0,0 @@
|
|||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
Before Width: | Height: | Size: 1 KiB |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
Before Width: | Height: | Size: 1.3 KiB |
|
@ -1 +0,0 @@
|
|||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
Before Width: | Height: | Size: 128 B |
|
@ -1 +0,0 @@
|
|||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
Before Width: | Height: | Size: 385 B |
129
server.go
Normal file
|
@ -0,0 +1,129 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
jwtware "github.com/gofiber/contrib/jwt"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
"github.com/gofiber/fiber/v2/middleware/limiter"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"nim.jasinco.work/app/internal"
|
||||
"nim.jasinco.work/app/internal/handlers"
|
||||
"nim.jasinco.work/app/nimdb"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
err := internal.ReadFromENV()
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
dbpool, err := pgxpool.New(context.Background(), internal.POSTGRES_URL)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to open connection to db")
|
||||
}
|
||||
defer dbpool.Close()
|
||||
internal.POOL = dbpool
|
||||
internal.NIMDB = nimdb.New(dbpool)
|
||||
|
||||
if internal.IGAPI_ACTIVATE {
|
||||
conn, client := internal.IGAPI_establish()
|
||||
defer conn.Close()
|
||||
go internal.IGAPI_chan_exec(client)
|
||||
}
|
||||
|
||||
app := fiber.New(fiber.Config{Prefork: internal.PREFORK})
|
||||
app.Static("/", "./static/webpage/")
|
||||
|
||||
app.Use(limiter.New(limiter.Config{
|
||||
Next: func(c *fiber.Ctx) bool {
|
||||
return c.IP() == "127.0.0.1"
|
||||
},
|
||||
Max: 50,
|
||||
Expiration: 30 * time.Second,
|
||||
KeyGenerator: func(c *fiber.Ctx) string {
|
||||
return c.Get("x-forwarded-for")
|
||||
},
|
||||
LimitReached: func(c *fiber.Ctx) error {
|
||||
return c.SendFile("./toofast.html")
|
||||
},
|
||||
}))
|
||||
if len(internal.CORS_ALLOW) > 0 {
|
||||
app.Use(cors.New(cors.Config{
|
||||
AllowOrigins: internal.CORS_ALLOW,
|
||||
AllowCredentials: true,
|
||||
}))
|
||||
}
|
||||
app.Get("/api/post", handlers.Fetch_post)
|
||||
app.Post("/api/post", handlers.Insert_Post)
|
||||
app.Delete("/api/post", handlers.Delete_post)
|
||||
app.Get("/api/heart", handlers.Add_heart)
|
||||
app.Static("/static", "./static/")
|
||||
|
||||
app.Get("/admin", func(c *fiber.Ctx) error {
|
||||
return c.Redirect("/admin/login")
|
||||
})
|
||||
app.Post("/api/admin/login", func(c *fiber.Ctx) error {
|
||||
token, err := handlers.Admin_Login_JWT(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.JSON(fiber.Map{"token": *token})
|
||||
})
|
||||
app.Post("/admin/login", func(c *fiber.Ctx) error {
|
||||
_, err := handlers.Admin_Login_JWT(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Redirect("/admin/panel")
|
||||
})
|
||||
app.Static("/admin/login", "./admin/login", fiber.Static{Index: "index.html"})
|
||||
app.Use(jwtware.New(jwtware.Config{
|
||||
SigningKey: jwtware.SigningKey{Key: []byte(internal.JWT_SECRET)},
|
||||
TokenLookup: "cookie:token",
|
||||
ErrorHandler: func(ctx *fiber.Ctx, err error) error {
|
||||
webpanel, regxerr := regexp.Match("^/admin/.*", []byte(ctx.Path()))
|
||||
if regxerr == nil && webpanel {
|
||||
return ctx.Redirect("/admin/login")
|
||||
} else if regxerr != nil {
|
||||
log.Printf("RegErr %s\n", regxerr.Error())
|
||||
}
|
||||
if !strings.ContainsAny(ctx.Path(), "admin") {
|
||||
return ctx.SendStatus(404)
|
||||
}
|
||||
if err == jwtware.ErrJWTMissingOrMalformed {
|
||||
return ctx.Status(400).SendString(err.Error())
|
||||
}
|
||||
if err == jwtware.ErrJWTAlg {
|
||||
return ctx.Status(401).SendString(err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}))
|
||||
app.Static("/admin/panel", "./admin/panel", fiber.Static{Index: "index.html"})
|
||||
app.Use("/admin/create", func(c *fiber.Ctx) error {
|
||||
user := c.Locals("user").(*jwt.Token)
|
||||
claims := user.Claims.(jwt.MapClaims)
|
||||
super := claims["admin"].(bool)
|
||||
if !super {
|
||||
return c.SendStatus(fiber.StatusNotFound)
|
||||
}
|
||||
return c.Next()
|
||||
})
|
||||
app.Static("/admin/create", "./admin/create", fiber.Static{Index: "index.html"})
|
||||
|
||||
app.Get("/api/admin/new_totp", handlers.Admin_new_totp_code)
|
||||
app.Get("/api/admin/fetch_post", handlers.Admin_Fetch_Post)
|
||||
app.Put("/api/admin/verify_post", handlers.AdminVerify)
|
||||
app.Post("/api/admin/create", handlers.Admin_create)
|
||||
|
||||
log.Panic(app.Listen(":3000"))
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
services:
|
||||
postgres:
|
||||
image: postgres:17.4-alpine3.21
|
||||
restart: always
|
||||
shm_size: 128mb
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=test
|
||||
- POSTGRES_USER=test
|
||||
ports:
|
||||
- 5432:5432
|
||||
adminer:
|
||||
image: adminer
|
||||
restart: always
|
||||
ports:
|
||||
- 8080:8080
|
82
sql/query.sql
Normal file
|
@ -0,0 +1,82 @@
|
|||
-- name: GetPost :many
|
||||
SELECT id, content, signing, post_at, heart, igid, array_agg(url) as enclosure
|
||||
FROM posts
|
||||
LEFT JOIN media ON post_id is not null AND post_id = id AND visible
|
||||
WHERE phase = 'ok'
|
||||
GROUP BY id
|
||||
ORDER BY id DESC
|
||||
LIMIT $1;
|
||||
|
||||
-- name: GetPostWithCursor :many
|
||||
SELECT id, content, signing, post_at, heart, igid, array_agg(url) as enclosure
|
||||
FROM posts
|
||||
LEFT JOIN media ON post_id is not null AND post_id = id AND visible
|
||||
WHERE id <= $1 AND phase = 'ok'
|
||||
GROUP BY id
|
||||
ORDER BY id DESC
|
||||
LIMIT $2;
|
||||
|
||||
-- name: InsertPost :one
|
||||
INSERT INTO posts (content,signing,hash)
|
||||
VALUES ($1, $2 ,$3)
|
||||
RETURNING id,hash;
|
||||
|
||||
-- name: UpdatePostIGID :exec
|
||||
UPDATE posts set igid = $1 WHERE id = $2;
|
||||
|
||||
-- name: DeletePost :exec
|
||||
UPDATE posts SET phase = 'deleted' WHERE id = $1 AND hash=$2;
|
||||
|
||||
-- name: InvalidateMedia :exec
|
||||
UPDATE media SET visible = false WHERE post_id = $1;
|
||||
|
||||
-- name: GetComment :many
|
||||
SELECT id, content, signing, post_at
|
||||
FROM comment
|
||||
WHERE phase = 'ok' AND post_id = $1
|
||||
ORDER BY id DESC
|
||||
LIMIT $2;
|
||||
|
||||
-- name: GetCommentWithCursor :many
|
||||
SELECT id, content, signing, post_at
|
||||
FROM comment
|
||||
WHERE id <= $1 AND phase = 'ok' AND post_id = $2
|
||||
ORDER BY id DESC
|
||||
LIMIT $3;
|
||||
|
||||
|
||||
-- name: InsertPostImage :exec
|
||||
INSERT INTO media (url, post_id) VALUES ($1, $2);
|
||||
|
||||
-- name: InsertCommentImage :exec
|
||||
INSERT INTO media (url, comment_id) VALUES ($1, $2);
|
||||
|
||||
-- name: AddPostHeart :one
|
||||
UPDATE posts SET heart = heart + 1 WHERE id=$1 RETURNING heart;
|
||||
|
||||
-- name: AddCommentHeart :one
|
||||
UPDATE comment SET heart = heart + 1 WHERE id=$1 RETURNING heart;
|
||||
|
||||
-- name: AdminGetPost :many
|
||||
SELECT id, content, signing, post_at FROM posts WHERE phase = 'pending' ORDER BY id DESC LIMIT 100;
|
||||
|
||||
-- name: SuperAdminGetPost :many
|
||||
SELECT id, content, signing, post_at, phase FROM posts WHERE phase = 'pending' OR phase = 'rejected' ORDER BY id DESC LIMIT 100;
|
||||
|
||||
-- name: AdminGetMedia :many
|
||||
SELECT url from media WHERE post_id = $1;
|
||||
|
||||
-- name: AdminVerify :one
|
||||
UPDATE posts SET phase = $1 WHERE id=$2 AND phase = 'pending' RETURNING id;
|
||||
|
||||
-- name: SuperAdminVerify :one
|
||||
UPDATE posts SET phase = $1 WHERE id=$2 AND (phase = 'pending' OR phase = 'rejected') RETURNING id;
|
||||
|
||||
-- name: AdminUpdateImage :exec
|
||||
UPDATE media SET visible = true WHERE post_id=$1;
|
||||
|
||||
-- name: AdminLoginGetTOTP :one
|
||||
SELECT totp, super FROM admin WHERE username = $1 AND password = $2;
|
||||
|
||||
-- name: AdminCreateAccount :exec
|
||||
INSERT INTO admin (username, password, totp) VALUES ($1, $2,$3);
|
37
sql/schema.sql
Normal file
|
@ -0,0 +1,37 @@
|
|||
CREATE TYPE post_phase AS ENUM('pending', 'rejected', 'admin_rejected','ok', 'deleted');
|
||||
|
||||
CREATE TABLE posts (
|
||||
id serial PRIMARY KEY,
|
||||
content varchar(500) not null,
|
||||
signing varchar(20),
|
||||
hash char(64) UNIQUE NOT NULL,
|
||||
post_at timestamp with time zone DEFAULT now(),
|
||||
heart integer NOT NULL DEFAULT 0,
|
||||
phase post_phase NOT NULL DEFAULT 'pending',
|
||||
igid varchar(22)
|
||||
);
|
||||
|
||||
CREATE TABLE comment (
|
||||
id serial PRIMARY KEY,
|
||||
post_id integer not null REFERENCES posts (id),
|
||||
content varchar(200) not null,
|
||||
signing varchar(20),
|
||||
hash char(64) UNIQUE NOT NULL,
|
||||
post_at timestamp with time zone DEFAULT now(),
|
||||
heart integer NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE media (
|
||||
url varchar(59) NOT NULL UNIQUE,
|
||||
post_id integer REFERENCES posts (id),
|
||||
comment_id integer REFERENCES comment (id),
|
||||
visible boolean DEFAULT false,
|
||||
CONSTRAINT uni_id CHECK ((post_id IS NULL) <> (comment_id IS NULL))
|
||||
);
|
||||
|
||||
CREATE TABLE admin (
|
||||
username varchar(20) NOT NULL UNIQUE PRIMARY KEY,
|
||||
password char(45) NOT NULL,
|
||||
totp char(33) NOT NULL,
|
||||
super bool NOT NULL DEFAULT false
|
||||
);
|
11
sqlc.yml
Normal file
|
@ -0,0 +1,11 @@
|
|||
version: "2"
|
||||
sql:
|
||||
- engine: "postgresql"
|
||||
queries: "./sql/query.sql"
|
||||
schema: "./sql/schema.sql"
|
||||
gen:
|
||||
go:
|
||||
package: "nimdb"
|
||||
out: "nimdb"
|
||||
sql_package: "pgx/v5"
|
||||
emit_json_tags: true
|
Before Width: | Height: | Size: 25 KiB |
|
@ -1,26 +0,0 @@
|
|||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
import { insert_post, NewPost } from "@/db";
|
||||
import { Blob } from "buffer";
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
let form = await req.formData();
|
||||
let text = form.get("content")?.toString()
|
||||
if (!text) {
|
||||
return Response.error()
|
||||
}
|
||||
let request: NewPost = { content: text, image: [] };
|
||||
let [id, hash] = await insert_post(request);
|
||||
|
||||
return Response.json({ id: id, hash: hash })
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
import { NextRequest } from "next/server";
|
||||
|
||||
export function Post(req: NextRequest) {
|
||||
|
||||
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
|
||||
export default function Home() {
|
||||
|
||||
return (
|
||||
<div className="relative min-h-screen px-8 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
|
||||
<div className="w-full h-5dvh fixed top-0 left-0">
|
||||
<h1 className="text-4xl mt-4 ml-3">匿名中工</h1>
|
||||
</div>
|
||||
<div className="h-[90dvh] w-[85dvw] absolute left-1/2 top-1/2 mt-5 -translate-1/2">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
|
||||
import { Suspense } from "react";
|
||||
import Image from "next/image";
|
||||
import { Attachment, MultiMediaType } from "@/db";
|
||||
|
||||
export default function Post(post_content: string, attachments: Attachment[]) {
|
||||
let images = [];
|
||||
let videos = [];
|
||||
attachments.forEach(attachment => {
|
||||
if (attachment.type == MultiMediaType.video) {
|
||||
attachment.urls.forEach(url => {
|
||||
videos.push(
|
||||
<Suspense fallback={<p>加載中</p>}>
|
||||
<video controls preload="none" aria-label="Video player">
|
||||
<source src={url} type={attachment.type.toString()} />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</Suspense>
|
||||
)
|
||||
})
|
||||
} else if (attachment.type == MultiMediaType.image) {
|
||||
attachment.urls.forEach(url => {
|
||||
images.push(<Image src={url} alt="Uploaded" width={300} height={200}></Image>)
|
||||
})
|
||||
}
|
||||
})
|
||||
return (<div className="w-full h-fit">
|
||||
<div>
|
||||
{post_content}
|
||||
</div>
|
||||
<div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>)
|
||||
}
|
93
src/db.ts
|
@ -1,93 +0,0 @@
|
|||
import { env, ReservedSQL, CryptoHasher, sql } from 'bun'
|
||||
import { MIMEType } from 'util';
|
||||
|
||||
export enum MultiMediaType {
|
||||
video, image
|
||||
}
|
||||
|
||||
export interface Attachment {
|
||||
urls: string[],
|
||||
type: MultiMediaType
|
||||
};
|
||||
|
||||
export interface Post {
|
||||
post: string,
|
||||
post_time: string,
|
||||
hash: string,
|
||||
attachments: Attachment[],
|
||||
}
|
||||
interface SQLPostCast {
|
||||
hash: string, post: string, post_time: string, images: number[]
|
||||
};
|
||||
const SQLPostCast2Post = (obj: SQLPostCast) => {
|
||||
let x: Post =
|
||||
{
|
||||
post: obj.post,
|
||||
hash: obj.hash,
|
||||
post_time: obj.post_time,
|
||||
attachments: []
|
||||
};
|
||||
if (obj.images && obj.images.length > 0) {
|
||||
x.attachments.push(
|
||||
{
|
||||
type: MultiMediaType.image, urls: obj.images.map(img => {
|
||||
return `/img/${obj.hash}_${img}`
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
|
||||
export interface NewPost {
|
||||
image: Blob[]
|
||||
content: string,
|
||||
}
|
||||
|
||||
export async function insert_post(post: NewPost): Promise<[number, string]> {
|
||||
let post_hash = new CryptoHasher("sha256");
|
||||
post_hash.update(post.content);
|
||||
post_hash.update(Date.now().toString())
|
||||
let populated = post_hash.digest("base64");
|
||||
let [{ id, hash }] = await sql`INSERT INTO niming.posts (post, hash) VALUES (${post.content}, ${populated}) RETURNING id, hash`
|
||||
return [id, hash]
|
||||
|
||||
}
|
||||
|
||||
export async function init_db() {
|
||||
await sql.begin(async sql => {
|
||||
await sql`CREATE SCHEMA niming`
|
||||
await sql`CREATE TABLE niming.posts (id SERIAL PRIMARY KEY, hash char(44) UNIQUE, post VARCHAR(500), post_time TIMESTAMPTZ DEFAULT now())`
|
||||
await sql`CREATE TABLE niming.images (id INTEGER PRIMARY KEY REFERENCES niming.posts (id), fileid integer[] )`
|
||||
})
|
||||
}
|
||||
|
||||
export class PostFetcher {
|
||||
conn: Promise<ReservedSQL>;
|
||||
|
||||
constructor() {
|
||||
this.conn = sql.reserve();
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.conn.then(async e => {
|
||||
await e`DECLARE post_ptr CURSOR WITH HOLD FOR SELECT niming.posts.*, niming.images.fileid
|
||||
AS images FROM niming.posts
|
||||
LEFT JOIN niming.images ON niming.images.hash=niming.posts.hash ORDER BY niming.posts.post_time DESC, niming.posts.hash ASC`
|
||||
}).catch(err => {
|
||||
console.log(err);
|
||||
})
|
||||
}
|
||||
|
||||
async postgres_fetch() {
|
||||
let x: SQLPostCast[] = []
|
||||
if (this.conn) {
|
||||
x = await (await this.conn)`FETCH 10 IN post_ptr`;
|
||||
}
|
||||
return x.map(e => {
|
||||
return SQLPostCast2Post(e)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
import { init_db } from "@/db";
|
||||
|
||||
init_db()
|
|
@ -1,13 +0,0 @@
|
|||
import { insert_post, NewPost } from '@/db';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
|
||||
|
||||
for (let i = 0; i < 100; i++) {
|
||||
let text = faker.string.alpha(20);
|
||||
let x: NewPost = { image: [], content: text };
|
||||
let [p_id, p_hash] = await insert_post(x)
|
||||
console.log(p_id, p_hash)
|
||||
|
||||
}
|
||||
|
4
tools/insert_post.hurl
Normal file
|
@ -0,0 +1,4 @@
|
|||
POST http://127.0.0.1:3000/api/post
|
||||
[Multipart]
|
||||
content: Test
|
||||
signing: Test
|
|
@ -1,13 +0,0 @@
|
|||
import { PostFetcher, Post } from "@/db"
|
||||
import { sql } from "bun";
|
||||
|
||||
let x: Post[] = [];
|
||||
const fetcher = new PostFetcher();
|
||||
await fetcher.init()
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
x = await fetcher.postgres_fetch()
|
||||
|
||||
console.log(x)
|
||||
|
||||
}
|
BIN
tools/test.png
Normal file
After Width: | Height: | Size: 85 KiB |
|
@ -1,28 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "Bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"types":["bun-types"]
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|