Realms utilities

This commit is contained in:
LittleSheep 2024-02-09 15:19:43 +08:00
parent 012ee55c3a
commit 57dc2771c2
6 changed files with 304 additions and 4 deletions

View File

@ -10,6 +10,7 @@ func RunMigration(source *gorm.DB) error {
&models.Account{}, &models.Account{},
&models.AccountMembership{}, &models.AccountMembership{},
&models.Realm{}, &models.Realm{},
&models.RealmMember{},
&models.Category{}, &models.Category{},
&models.Tag{}, &models.Tag{},
&models.Post{}, &models.Post{},

View File

@ -69,7 +69,7 @@ func inviteRealm(c *fiber.Ctx) error {
realmId, _ := c.ParamsInt("realmId", 0) realmId, _ := c.ParamsInt("realmId", 0)
var data struct { var data struct {
AccountID uint `json:"account_id" validate:"required"` AccountName string `json:"account_name" validate:"required"`
} }
if err := BindAndValidate(c, &data); err != nil { if err := BindAndValidate(c, &data); err != nil {
@ -86,7 +86,7 @@ func inviteRealm(c *fiber.Ctx) error {
var account models.Account var account models.Account
if err := database.C.Where(&models.Account{ if err := database.C.Where(&models.Account{
BaseModel: models.BaseModel{ID: uint(realmId)}, Name: data.AccountName,
}).First(&account).Error; err != nil { }).First(&account).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error()) return fiber.NewError(fiber.StatusNotFound, err.Error())
} }
@ -98,6 +98,40 @@ func inviteRealm(c *fiber.Ctx) error {
} }
} }
func kickRealm(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
realmId, _ := c.ParamsInt("realmId", 0)
var data struct {
AccountName string `json:"account_name" validate:"required"`
}
if err := BindAndValidate(c, &data); err != nil {
return err
}
var realm models.Realm
if err := database.C.Where(&models.Realm{
BaseModel: models.BaseModel{ID: uint(realmId)},
AccountID: user.ID,
}).First(&realm).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
var account models.Account
if err := database.C.Where(&models.Account{
Name: data.AccountName,
}).First(&account).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
if err := services.KickRealmMember(account, realm); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else {
return c.SendStatus(fiber.StatusOK)
}
}
func editRealm(c *fiber.Ctx) error { func editRealm(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account) user := c.Locals("principal").(models.Account)
id, _ := c.ParamsInt("realmId", 0) id, _ := c.ParamsInt("realmId", 0)

View File

@ -81,6 +81,7 @@ func NewServer() {
api.Get("/realms/:realmId", getRealm) api.Get("/realms/:realmId", getRealm)
api.Post("/realms", auth, createRealm) api.Post("/realms", auth, createRealm)
api.Post("/realms/:realmId/invite", auth, inviteRealm) api.Post("/realms/:realmId/invite", auth, inviteRealm)
api.Post("/realms/:realmId/kick", auth, kickRealm)
api.Put("/realms/:realmId", auth, editRealm) api.Put("/realms/:realmId", auth, editRealm)
api.Delete("/realms/:realmId", auth, deleteRealm) api.Delete("/realms/:realmId", auth, deleteRealm)
} }

View File

@ -50,6 +50,19 @@ func InviteRealmMember(user models.Account, target models.Realm) error {
return err return err
} }
func KickRealmMember(user models.Account, target models.Realm) error {
var member models.RealmMember
if err := database.C.Where(&models.RealmMember{
RealmID: target.ID,
AccountID: user.ID,
}).First(&member).Error; err != nil {
return err
}
return database.C.Delete(&member).Error
}
func EditRealm(realm models.Realm, name, description string, isPublic bool) (models.Realm, error) { func EditRealm(realm models.Realm, name, description string, isPublic bool) (models.Realm, error) {
realm.Name = name realm.Name = name
realm.Description = description realm.Description = description

View File

@ -1,7 +1,10 @@
import { createSignal, For, Show } from "solid-js"; import { createSignal, For, Show } from "solid-js";
import { closeModel, openModel } from "../../scripts/modals.ts";
import { getAtk } from "../../stores/userinfo.tsx";
export default function RealmDirectoryPage() { export default function RealmDirectoryPage() {
const [error, setError] = createSignal<string | null>(null); const [error, setError] = createSignal<string | null>(null);
const [submitting, setSubmitting] = createSignal(false);
const [realms, setRealms] = createSignal<any>(null); const [realms, setRealms] = createSignal<any>(null);
@ -16,6 +19,32 @@ export default function RealmDirectoryPage() {
readRealms(); readRealms();
async function createRealm(evt: SubmitEvent) {
evt.preventDefault();
const form = evt.target as HTMLFormElement;
const data = Object.fromEntries(new FormData(form));
setSubmitting(true);
const res = await fetch("/api/realms", {
method: "POST",
headers: { "Authorization": `Bearer ${getAtk()}`, "Content-Type": "application/json" },
body: JSON.stringify({
name: data.name,
description: data.description,
is_public: data.is_public != null
})
});
if (res.status !== 200) {
setError(await res.text());
} else {
await readRealms();
closeModel("#create-realm");
form.reset();
}
setSubmitting(false);
}
return ( return (
<> <>
<div id="alerts"> <div id="alerts">
@ -31,6 +60,13 @@ export default function RealmDirectoryPage() {
</Show> </Show>
</div> </div>
<div class="mt-1 px-7 flex items-center justify-between">
<h3 class="py-3 font-bold">Realms directory</h3>
<button type="button" class="btn btn-primary" onClick={() => openModel("#create-realm")}>
<i class="fa-solid fa-plus"></i>
</button>
</div>
<For each={realms()}> <For each={realms()}>
{item => <div class="px-7 pt-7 pb-5 border-t border-base-200"> {item => <div class="px-7 pt-7 pb-5 border-t border-base-200">
<h2 class="text-xl font-bold">{item.name}</h2> <h2 class="text-xl font-bold">{item.name}</h2>
@ -41,6 +77,38 @@ export default function RealmDirectoryPage() {
</div> </div>
</div>} </div>}
</For> </For>
<dialog id="create-realm" class="modal">
<div class="modal-box">
<h2 class="card-title px-1">Create a realm</h2>
<form class="mt-2" onSubmit={createRealm}>
<label class="form-control w-full">
<div class="label">
<span class="label-text">Realm name</span>
</div>
<input name="name" type="text" placeholder="Type here" class="input input-bordered w-full" />
</label>
<label class="form-control w-full">
<div class="label">
<span class="label-text">Realm description</span>
</div>
<textarea name="description" placeholder="Type here" class="textarea textarea-bordered w-full" />
</label>
<div class="form-control mt-2">
<label class="label cursor-pointer">
<span class="label-text">Make it public</span>
<input type="checkbox" name="is_public" class="checkbox checkbox-primary" />
</label>
</div>
<button type="submit" class="btn btn-primary mt-2" disabled={submitting()}>
<Show when={submitting()} fallback={"Submit"}>
<span class="loading"></span>
</Show>
</button>
</form>
</div>
</dialog>
</> </>
); );
} }

View File

@ -1,20 +1,26 @@
import { createSignal, Show } from "solid-js"; import { createSignal, Show } from "solid-js";
import { createStore } from "solid-js/store"; import { createStore } from "solid-js/store";
import { useParams } from "@solidjs/router"; import { useNavigate, useParams } from "@solidjs/router";
import PostList from "../../components/PostList.tsx"; import PostList from "../../components/PostList.tsx";
import PostPublish from "../../components/PostPublish.tsx"; import PostPublish from "../../components/PostPublish.tsx";
import styles from "./realm.module.css"; import styles from "./realm.module.css";
import { getAtk, useUserinfo } from "../../stores/userinfo.tsx";
import { closeModel, openModel } from "../../scripts/modals.ts";
export default function RealmPage() { export default function RealmPage() {
const userinfo = useUserinfo();
const [error, setError] = createSignal<string | null>(null); const [error, setError] = createSignal<string | null>(null);
const [submitting, setSubmitting] = createSignal(false);
const [realm, setRealm] = createSignal<any>(null); const [realm, setRealm] = createSignal<any>(null);
const [page, setPage] = createSignal(0); const [page, setPage] = createSignal(0);
const [info, setInfo] = createSignal<any>(null); const [info, setInfo] = createSignal<any>(null);
const params = useParams(); const params = useParams();
const navigate = useNavigate();
async function readRealm() { async function readRealm() {
const res = await fetch(`/api/realms/${params["realmId"]}`); const res = await fetch(`/api/realms/${params["realmId"]}`);
@ -32,7 +38,7 @@ export default function RealmPage() {
const res = await fetch(`/api/posts?` + new URLSearchParams({ const res = await fetch(`/api/posts?` + new URLSearchParams({
take: (10).toString(), take: (10).toString(),
offset: ((page() - 1) * 10).toString(), offset: ((page() - 1) * 10).toString(),
realmId: params["realmId"], realmId: params["realmId"]
})); }));
if (res.status !== 200) { if (res.status !== 200) {
setError(await res.text()); setError(await res.text());
@ -42,6 +48,90 @@ export default function RealmPage() {
} }
} }
async function editRealm(evt: SubmitEvent) {
evt.preventDefault();
const form = evt.target as HTMLFormElement;
const data = Object.fromEntries(new FormData(form));
setSubmitting(true);
const res = await fetch(`/api/realms/${params["realmId"]}`, {
method: "PUT",
headers: { "Authorization": `Bearer ${getAtk()}`, "Content-Type": "application/json" },
body: JSON.stringify({
name: data.name,
description: data.description,
is_public: data.is_public != null
})
});
if (res.status !== 200) {
setError(await res.text());
} else {
await readRealm();
closeModel("#edit-realm");
form.reset();
}
setSubmitting(false);
}
async function inviteMember(evt: SubmitEvent) {
evt.preventDefault();
const form = evt.target as HTMLFormElement;
const data = Object.fromEntries(new FormData(form));
setSubmitting(true);
const res = await fetch(`/api/realms/${params["realmId"]}/invite`, {
method: "POST",
headers: { "Authorization": `Bearer ${getAtk()}`, "Content-Type": "application/json" },
body: JSON.stringify(data)
});
if (res.status !== 200) {
setError(await res.text());
} else {
await readRealm();
closeModel("#invite-member");
form.reset();
}
setSubmitting(false);
}
async function kickMember(evt: SubmitEvent) {
evt.preventDefault();
const form = evt.target as HTMLFormElement;
const data = Object.fromEntries(new FormData(form));
setSubmitting(true);
const res = await fetch(`/api/realms/${params["realmId"]}/kick`, {
method: "POST",
headers: { "Authorization": `Bearer ${getAtk()}`, "Content-Type": "application/json" },
body: JSON.stringify(data)
});
if (res.status !== 200) {
setError(await res.text());
} else {
await readRealm();
closeModel("#kick-member");
form.reset();
}
setSubmitting(false);
}
async function breakRealm() {
if (!confirm("Are you sure about that? All posts in this realm will disappear forever.")) return;
const res = await fetch(`/api/realms/${params["realmId"]}`, {
method: "DELETE",
headers: { "Authorization": `Bearer ${getAtk()}` }
});
if (res.status !== 200) {
setError(await res.text());
} else {
navigate("/realms");
}
}
function setMeta(data: any, field: string, scroll = true) { function setMeta(data: any, field: string, scroll = true) {
const meta: { [id: string]: any } = { const meta: { [id: string]: any } = {
reposting: null, reposting: null,
@ -81,6 +171,14 @@ export default function RealmPage() {
<div class={`${styles.description} text-sm mt-3`}> <div class={`${styles.description} text-sm mt-3`}>
<p>Realm #{realm()?.id}</p> <p>Realm #{realm()?.id}</p>
<Show when={realm()?.account_id === userinfo?.profiles?.id}>
<div class="flex gap-2">
<button class="link" onClick={() => openModel("#edit-realm")}>Edit</button>
<button class="link" onClick={() => openModel("#invite-member")}>Invite</button>
<button class="link" onClick={() => openModel("#kick-member")}>Kick</button>
<button class="link" onClick={() => breakRealm()}>Break-up</button>
</div>
</Show>
</div> </div>
</div> </div>
@ -102,6 +200,91 @@ export default function RealmPage() {
onReply={(item) => setMeta(item, "replying")} onReply={(item) => setMeta(item, "replying")}
onEdit={(item) => setMeta(item, "editing")} onEdit={(item) => setMeta(item, "editing")}
/> />
<dialog id="edit-realm" class="modal">
<div class="modal-box">
<h2 class="card-title px-1">Create a realm</h2>
<form class="mt-2" onSubmit={editRealm}>
<label class="form-control w-full">
<div class="label">
<span class="label-text">Realm name</span>
</div>
<input value={realm()?.name} name="name" type="text" placeholder="Type here"
class="input input-bordered w-full" />
</label>
<label class="form-control w-full">
<div class="label">
<span class="label-text">Realm description</span>
</div>
<textarea value={realm()?.description} name="description" placeholder="Type here"
class="textarea textarea-bordered w-full" />
</label>
<div class="form-control mt-2">
<label class="label cursor-pointer">
<span class="label-text">Make it public</span>
<input checked={realm()?.is_public} type="checkbox" name="is_public"
class="checkbox checkbox-primary" />
</label>
</div>
<button type="submit" class="btn btn-primary mt-2" disabled={submitting()}>
<Show when={submitting()} fallback={"Submit"}>
<span class="loading"></span>
</Show>
</button>
</form>
</div>
</dialog>
<dialog id="invite-member" class="modal">
<div class="modal-box">
<h2 class="card-title px-1">Invite someone as a member</h2>
<form class="mt-2" onSubmit={inviteMember}>
<label class="form-control w-full">
<div class="label">
<span class="label-text">Username</span>
</div>
<input name="account_name" type="text" placeholder="Type here" class="input input-bordered w-full" />
<div class="label">
<span class="label-text-alt">
Invite someone via their username so that they can publish content in non-public realm.
</span>
</div>
</label>
<button type="submit" class="btn btn-primary mt-2" disabled={submitting()}>
<Show when={submitting()} fallback={"Submit"}>
<span class="loading"></span>
</Show>
</button>
</form>
</div>
</dialog>
<dialog id="kick-member" class="modal">
<div class="modal-box">
<h2 class="card-title px-1">Kick someone out of your realm</h2>
<form class="mt-2" onSubmit={kickMember}>
<label class="form-control w-full">
<div class="label">
<span class="label-text">Username</span>
</div>
<input name="account_name" type="text" placeholder="Type here" class="input input-bordered w-full" />
<div class="label">
<span class="label-text-alt">
Remove someone out of your realm.
</span>
</div>
</label>
<button type="submit" class="btn btn-primary mt-2" disabled={submitting()}>
<Show when={submitting()} fallback={"Submit"}>
<span class="loading"></span>
</Show>
</button>
</form>
</div>
</dialog>
</> </>
); );
} }