mirror of
https://github.com/wgh136/nysoure.git
synced 2025-09-27 04:17:23 +00:00
Add server configuration management.
This commit is contained in:
@@ -7,3 +7,12 @@ export function ErrorAlert({message, className}: {message: string, className?: s
|
||||
<span>{message}</span>
|
||||
</div>;
|
||||
}
|
||||
|
||||
export function InfoAlert({ message, className }: { message: string, className?: string }) {
|
||||
return <div role="alert" className={`alert alert-info ${className}`}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="h-6 w-6 shrink-0 stroke-current">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span>{message}</span>
|
||||
</div>;
|
||||
}
|
22
frontend/src/components/input.tsx
Normal file
22
frontend/src/components/input.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
interface InputProps {
|
||||
type?: string;
|
||||
placeholder?: string;
|
||||
value: string;
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
label: string;
|
||||
inlineLabel?: boolean;
|
||||
}
|
||||
|
||||
export default function Input(props: InputProps) {
|
||||
if (props.inlineLabel) {
|
||||
return <label className="input w-full">
|
||||
{props.label}
|
||||
<input type={props.type} className="grow" placeholder={props.placeholder} value={props.value} onChange={props.onChange} />
|
||||
</label>
|
||||
} else {
|
||||
return <fieldset className="fieldset w-full">
|
||||
<legend className="fieldset-legend">{props.label}</legend>
|
||||
<input type={props.type} className="input w-full" placeholder={props.placeholder} value={props.value} onChange={props.onChange} />
|
||||
</fieldset>
|
||||
}
|
||||
}
|
@@ -108,3 +108,12 @@ export interface CommentWithResource {
|
||||
user: User;
|
||||
resource: Resource;
|
||||
}
|
||||
|
||||
export interface ServerConfig {
|
||||
max_uploading_size_in_mb: number;
|
||||
max_file_size_in_mb: number;
|
||||
max_downloads_per_day_for_single_ip: number;
|
||||
allow_register: boolean;
|
||||
cloudflare_turnstile_site_key: string;
|
||||
cloudflare_turnstile_secret_key: string;
|
||||
}
|
||||
|
@@ -13,7 +13,8 @@ import {
|
||||
User,
|
||||
UserWithToken,
|
||||
Comment,
|
||||
CommentWithResource
|
||||
CommentWithResource,
|
||||
ServerConfig
|
||||
} from "./models.ts";
|
||||
|
||||
class Network {
|
||||
@@ -635,6 +636,32 @@ class Network {
|
||||
return { success: false, message: e.toString() };
|
||||
}
|
||||
}
|
||||
|
||||
async getServerConfig(): Promise<Response<ServerConfig>> {
|
||||
try {
|
||||
const response = await axios.get(`${this.apiBaseUrl}/config`);
|
||||
return response.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
return {
|
||||
success: false,
|
||||
message: e.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async setServerConfig(config: ServerConfig): Promise<Response<void>> {
|
||||
try {
|
||||
const response = await axios.post(`${this.apiBaseUrl}/config`, config);
|
||||
return response.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
return {
|
||||
success: false,
|
||||
message: e.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const network = new Network();
|
||||
|
@@ -4,6 +4,7 @@ import StorageView from "./manage_storage_page.tsx";
|
||||
import UserView from "./manage_user_page.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ManageMePage } from "./manage_me_page.tsx";
|
||||
import ManageServerConfigPage from "./manage_server_config_page.tsx";
|
||||
|
||||
export default function ManagePage() {
|
||||
const { t } = useTranslation();
|
||||
@@ -43,13 +44,15 @@ export default function ManagePage() {
|
||||
const pageNames = [
|
||||
t("My Info"),
|
||||
t("Storage"),
|
||||
t("Users")
|
||||
t("Users"),
|
||||
t("Server"),
|
||||
]
|
||||
|
||||
const pageComponents = [
|
||||
<ManageMePage/>,
|
||||
<StorageView/>,
|
||||
<UserView/>
|
||||
<UserView/>,
|
||||
<ManageServerConfigPage/>,
|
||||
]
|
||||
|
||||
return <div className="drawer lg:drawer-open">
|
||||
@@ -80,6 +83,7 @@ export default function ManagePage() {
|
||||
{buildItem(t("My Info"), <MdOutlineBadge className={"text-xl"}/>, 0)}
|
||||
{buildItem(t("Storage"), <MdOutlineStorage className={"text-xl"}/>, 1)}
|
||||
{buildItem(t("Users"), <MdOutlinePerson className={"text-xl"}/>, 2)}
|
||||
{buildItem(t("Server"), <MdOutlineStorage className={"text-xl"}/>, 3)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
92
frontend/src/pages/manage_server_config_page.tsx
Normal file
92
frontend/src/pages/manage_server_config_page.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { app } from "../app"
|
||||
import { ErrorAlert, InfoAlert } from "../components/alert"
|
||||
import { useEffect, useState } from "react";
|
||||
import { ServerConfig } from "../network/models";
|
||||
import Loading from "../components/loading";
|
||||
import Input from "../components/input";
|
||||
import { network } from "../network/network";
|
||||
import showToast from "../components/toast";
|
||||
import Button from "../components/button";
|
||||
|
||||
export default function ManageServerConfigPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [config, setConfig] = useState<ServerConfig | null>(null);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
network.getServerConfig().then((res) => {
|
||||
if (res.success) {
|
||||
setConfig(res.data!);
|
||||
} else {
|
||||
showToast({
|
||||
message: res.message,
|
||||
type: "error",
|
||||
})
|
||||
}
|
||||
})
|
||||
}, []);
|
||||
|
||||
if (!app.user) {
|
||||
return <ErrorAlert className={"m-4"} message={t("You are not logged in. Please log in to access this page.")} />
|
||||
}
|
||||
|
||||
if (!app.user?.is_admin) {
|
||||
return <ErrorAlert className={"m-4"} message={t("You are not authorized to access this page.")} />
|
||||
}
|
||||
|
||||
if (config == null) {
|
||||
return <Loading />
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
const res = await network.setServerConfig(config);
|
||||
if (res.success) {
|
||||
showToast({
|
||||
message: t("Update server config successfully"),
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
showToast({
|
||||
message: res.message,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
return <form className="px-4" onSubmit={handleSubmit}>
|
||||
<Input type="number" value={config.max_uploading_size_in_mb.toString()} label="Max uploading size (MB)" onChange={(e) => {
|
||||
setConfig({...config, max_uploading_size_in_mb: parseInt(e.target.value) })
|
||||
}}></Input>
|
||||
<Input type="number" value={config.max_file_size_in_mb.toString()} label="Max file size (MB)" onChange={(e) => {
|
||||
setConfig({...config, max_file_size_in_mb: parseInt(e.target.value) })
|
||||
}}></Input>
|
||||
<Input type="number" value={config.max_downloads_per_day_for_single_ip.toString()} label="Max downloads per day for single IP" onChange={(e) => {
|
||||
setConfig({...config, max_downloads_per_day_for_single_ip: parseInt(e.target.value) })
|
||||
}}></Input>
|
||||
<fieldset className="fieldset w-full">
|
||||
<legend className="fieldset-legend">Allow register</legend>
|
||||
<input type="checkbox" checked={config.allow_register} className="toggle-primary toggle" onChange={(e) => {
|
||||
setConfig({ ...config, allow_register: e.target.checked })
|
||||
}} />
|
||||
</fieldset>
|
||||
<Input type="text" value={config.cloudflare_turnstile_site_key} label="Cloudflare Turnstile Site Key" onChange={(e) => {
|
||||
setConfig({...config, cloudflare_turnstile_site_key: e.target.value })
|
||||
}}></Input>
|
||||
<Input type="text" value={config.cloudflare_turnstile_secret_key} label="Cloudflare Turnstile Secret Key" onChange={(e) => {
|
||||
setConfig({...config, cloudflare_turnstile_secret_key: e.target.value })
|
||||
}}></Input>
|
||||
<InfoAlert className="my-2" message="If the cloudflare turnstile keys are not empty, the turnstile will be used for register and download." />
|
||||
<div className="flex justify-end">
|
||||
<Button className="btn-accent shadow" isLoading={isLoading}>{t("Submit")}</Button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
@@ -96,7 +96,7 @@ export default function ResourcePage() {
|
||||
}
|
||||
</p>
|
||||
<div className="tabs tabs-box my-4 mx-2 p-4">
|
||||
<label className="tab">
|
||||
<label className="tab transition-all">
|
||||
<input type="radio" name="my_tabs" checked={page === 0} onChange={() => {
|
||||
setPage(0)
|
||||
}}/>
|
||||
@@ -109,7 +109,7 @@ export default function ResourcePage() {
|
||||
<Article article={resource.article}/>
|
||||
</div>
|
||||
|
||||
<label className="tab">
|
||||
<label className="tab transition-all">
|
||||
<input type="radio" name="my_tabs" checked={page === 1} onChange={() => {
|
||||
setPage(1)
|
||||
}}/>
|
||||
@@ -122,7 +122,7 @@ export default function ResourcePage() {
|
||||
<Files files={resource.files} resourceID={resource.id}/>
|
||||
</div>
|
||||
|
||||
<label className="tab">
|
||||
<label className="tab transition-all">
|
||||
<input type="radio" name="my_tabs" checked={page === 2} onChange={() => {
|
||||
setPage(2)
|
||||
}}/>
|
||||
|
8
main.go
8
main.go
@@ -1,12 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/gofiber/fiber/v3/middleware/cors"
|
||||
"github.com/gofiber/fiber/v3/middleware/logger"
|
||||
"log"
|
||||
"nysoure/server/api"
|
||||
"nysoure/server/middleware"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/gofiber/fiber/v3/middleware/cors"
|
||||
"github.com/gofiber/fiber/v3/middleware/logger"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -33,6 +34,7 @@ func main() {
|
||||
api.AddStorageRoutes(apiG)
|
||||
api.AddFileRoutes(apiG)
|
||||
api.AddCommentRoutes(apiG)
|
||||
api.AddConfigRoutes(apiG)
|
||||
}
|
||||
|
||||
log.Fatal(app.Listen(":3000"))
|
||||
|
64
server/api/config.go
Normal file
64
server/api/config.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"nysoure/server/config"
|
||||
"nysoure/server/model"
|
||||
"nysoure/server/service"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/gofiber/fiber/v3/log"
|
||||
)
|
||||
|
||||
func getServerConfig(c fiber.Ctx) error {
|
||||
uid, ok := c.Locals("uid").(uint)
|
||||
if !ok {
|
||||
return model.NewRequestError("You are not logged in")
|
||||
}
|
||||
isAdmin, err := service.CheckUserIsAdmin(uid)
|
||||
if err != nil {
|
||||
log.Error("Error checking user admin status: ", err)
|
||||
return model.NewInternalServerError("Error checking user admin status")
|
||||
}
|
||||
if !isAdmin {
|
||||
return model.NewUnAuthorizedError("You do not have permission to access this resource")
|
||||
}
|
||||
sc := config.GetConfig()
|
||||
return c.JSON(model.Response[config.ServerConfig]{
|
||||
Success: true,
|
||||
Data: sc,
|
||||
})
|
||||
}
|
||||
|
||||
func setServerConfig(c fiber.Ctx) error {
|
||||
uid, ok := c.Locals("uid").(uint)
|
||||
if !ok {
|
||||
return model.NewRequestError("You are not logged in")
|
||||
}
|
||||
isAdmin, err := service.CheckUserIsAdmin(uid)
|
||||
if err != nil {
|
||||
log.Error("Error checking user admin status: ", err)
|
||||
return model.NewInternalServerError("Error checking user admin status")
|
||||
}
|
||||
if !isAdmin {
|
||||
return model.NewUnAuthorizedError("You do not have permission to access this resource")
|
||||
}
|
||||
|
||||
var sc config.ServerConfig
|
||||
if err := c.Bind().Body(&sc); err != nil {
|
||||
return model.NewRequestError("Invalid request parameters")
|
||||
}
|
||||
|
||||
config.SetConfig(sc)
|
||||
|
||||
return c.JSON(model.Response[any]{
|
||||
Success: true,
|
||||
})
|
||||
}
|
||||
|
||||
func AddConfigRoutes(r fiber.Router) {
|
||||
configGroup := r.Group("/config")
|
||||
{
|
||||
configGroup.Get("/", getServerConfig)
|
||||
configGroup.Post("/", setServerConfig)
|
||||
}
|
||||
}
|
@@ -2,11 +2,12 @@ package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"nysoure/server/model"
|
||||
"nysoure/server/service"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func AddFileRoutes(router fiber.Router) {
|
||||
|
76
server/config/config.go
Normal file
76
server/config/config.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"nysoure/server/utils"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var config *ServerConfig
|
||||
|
||||
type ServerConfig struct {
|
||||
// MaxUploadingSizeInMB is the maximum size of files that are being uploaded at the same time.
|
||||
MaxUploadingSizeInMB int `json:"max_uploading_size_in_mb"`
|
||||
// MaxFileSizeInMB is the maximum size of a single file that can be uploaded.
|
||||
MaxFileSizeInMB int `json:"max_file_size_in_mb"`
|
||||
// MaxDownloadsForSingleIP is the maximum number of downloads allowed from a single IP address.
|
||||
MaxDownloadsPerDayForSingleIP int `json:"max_downloads_per_day_for_single_ip"`
|
||||
// AllowRegister indicates whether user registration is allowed.
|
||||
AllowRegister bool `json:"allow_register"`
|
||||
// CloudflareTurnstileSiteKey is the site key for Cloudflare Turnstile.
|
||||
CloudflareTurnstileSiteKey string `json:"cloudflare_turnstile_site_key"`
|
||||
// CloudflareTurnstileSecretKey is the secret key for Cloudflare Turnstile.
|
||||
CloudflareTurnstileSecretKey string `json:"cloudflare_turnstile_secret_key"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
filepath := filepath.Join(utils.GetStoragePath(), "config.json")
|
||||
if _, err := os.Stat(filepath); os.IsNotExist(err) {
|
||||
config = &ServerConfig{
|
||||
MaxUploadingSizeInMB: 20 * 1024, // 20GB
|
||||
MaxFileSizeInMB: 8 * 1024, // 8GB
|
||||
MaxDownloadsPerDayForSingleIP: 20,
|
||||
AllowRegister: true,
|
||||
CloudflareTurnstileSiteKey: "",
|
||||
CloudflareTurnstileSecretKey: "",
|
||||
}
|
||||
} else {
|
||||
data, err := os.ReadFile(filepath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
config = &ServerConfig{}
|
||||
if err := json.Unmarshal(data, config); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GetConfig() ServerConfig {
|
||||
return *config
|
||||
}
|
||||
|
||||
func SetConfig(newConfig ServerConfig) {
|
||||
config = &newConfig
|
||||
data, err := json.MarshalIndent(config, "", " ")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
filepath := filepath.Join(utils.GetStoragePath(), "config.json")
|
||||
if err := os.WriteFile(filepath, data, 0644); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func MaxUploadingSize() int64 {
|
||||
return int64(config.MaxUploadingSizeInMB) * 1024 * 1024
|
||||
}
|
||||
|
||||
func MaxFileSize() int64 {
|
||||
return int64(config.MaxFileSizeInMB) * 1024 * 1024
|
||||
}
|
||||
|
||||
func AllowRegister() bool {
|
||||
return config.AllowRegister
|
||||
}
|
@@ -1,8 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v3/log"
|
||||
"github.com/google/uuid"
|
||||
"nysoure/server/config"
|
||||
"nysoure/server/dao"
|
||||
"nysoure/server/model"
|
||||
"nysoure/server/storage"
|
||||
@@ -11,17 +10,15 @@ import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v3/log"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
blockSize = 4 * 1024 * 1024 // 4MB
|
||||
)
|
||||
|
||||
var (
|
||||
maxUploadingSize = int64(1024 * 1024 * 1024 * 20) // TODO: make this configurable
|
||||
maxFileSize = int64(1024 * 1024 * 1024 * 8) // TODO: make this configurable
|
||||
)
|
||||
|
||||
func getUploadingSize(uid uint) int64 {
|
||||
return dao.GetStatistic("uploading_size")
|
||||
}
|
||||
@@ -83,12 +80,12 @@ func CreateUploadingFile(uid uint, filename string, description string, fileSize
|
||||
return nil, model.NewUnAuthorizedError("user cannot upload file")
|
||||
}
|
||||
|
||||
if fileSize > maxFileSize {
|
||||
if fileSize > config.MaxFileSize() {
|
||||
return nil, model.NewRequestError("file size exceeds the limit")
|
||||
}
|
||||
|
||||
currentUploadingSize := getUploadingSize(uid)
|
||||
if currentUploadingSize+fileSize > maxUploadingSize {
|
||||
if currentUploadingSize+fileSize > config.MaxUploadingSize() {
|
||||
log.Info("A new uploading file is rejected due to max uploading size limit")
|
||||
return nil, model.NewRequestError("server is busy, please try again later")
|
||||
}
|
||||
@@ -305,7 +302,7 @@ func DeleteFile(uid uint, fid string) error {
|
||||
return model.NewNotFoundError("file not found")
|
||||
}
|
||||
|
||||
isAdmin, err := checkUserIsAdmin(uid)
|
||||
isAdmin, err := CheckUserIsAdmin(uid)
|
||||
if err != nil {
|
||||
log.Error("failed to check user permission: ", err)
|
||||
return model.NewInternalServerError("failed to check user permission")
|
||||
@@ -342,7 +339,7 @@ func UpdateFile(uid uint, fid string, filename string, description string) (*mod
|
||||
return nil, model.NewNotFoundError("file not found")
|
||||
}
|
||||
|
||||
isAdmin, err := checkUserIsAdmin(uid)
|
||||
isAdmin, err := CheckUserIsAdmin(uid)
|
||||
if err != nil {
|
||||
log.Error("failed to check user permission: ", err)
|
||||
return nil, model.NewInternalServerError("failed to check user permission")
|
||||
|
@@ -89,7 +89,7 @@ func SearchResource(keyword string, page int) ([]model.ResourceView, int, error)
|
||||
}
|
||||
|
||||
func DeleteResource(uid, id uint) error {
|
||||
isAdmin, err := checkUserIsAdmin(uid)
|
||||
isAdmin, err := CheckUserIsAdmin(uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@@ -1,11 +1,12 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v3/log"
|
||||
"nysoure/server/dao"
|
||||
"nysoure/server/model"
|
||||
"nysoure/server/storage"
|
||||
"os"
|
||||
|
||||
"github.com/gofiber/fiber/v3/log"
|
||||
)
|
||||
|
||||
type CreateS3StorageParams struct {
|
||||
@@ -18,7 +19,7 @@ type CreateS3StorageParams struct {
|
||||
}
|
||||
|
||||
func CreateS3Storage(uid uint, params CreateS3StorageParams) error {
|
||||
isAdmin, err := checkUserIsAdmin(uid)
|
||||
isAdmin, err := CheckUserIsAdmin(uid)
|
||||
if err != nil {
|
||||
log.Errorf("check user is admin failed: %s", err)
|
||||
return model.NewInternalServerError("check user is admin failed")
|
||||
@@ -49,7 +50,7 @@ type CreateLocalStorageParams struct {
|
||||
}
|
||||
|
||||
func CreateLocalStorage(uid uint, params CreateLocalStorageParams) error {
|
||||
isAdmin, err := checkUserIsAdmin(uid)
|
||||
isAdmin, err := CheckUserIsAdmin(uid)
|
||||
if err != nil {
|
||||
log.Errorf("check user is admin failed: %s", err)
|
||||
return model.NewInternalServerError("check user is admin failed")
|
||||
@@ -88,7 +89,7 @@ func ListStorages() ([]model.StorageView, error) {
|
||||
}
|
||||
|
||||
func DeleteStorage(uid, id uint) error {
|
||||
isAdmin, err := checkUserIsAdmin(uid)
|
||||
isAdmin, err := CheckUserIsAdmin(uid)
|
||||
if err != nil {
|
||||
log.Errorf("check user is admin failed: %s", err)
|
||||
return model.NewInternalServerError("check user is admin failed")
|
||||
|
@@ -3,6 +3,7 @@ package service
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"nysoure/server/config"
|
||||
"nysoure/server/dao"
|
||||
"nysoure/server/model"
|
||||
"nysoure/server/static"
|
||||
@@ -18,6 +19,9 @@ const (
|
||||
)
|
||||
|
||||
func CreateUser(username, password string) (model.UserViewWithToken, error) {
|
||||
if !config.AllowRegister() {
|
||||
return model.UserViewWithToken{}, model.NewRequestError("User registration is not allowed")
|
||||
}
|
||||
if len(username) < 3 || len(username) > 20 {
|
||||
return model.UserViewWithToken{}, model.NewRequestError("Username must be between 3 and 20 characters")
|
||||
}
|
||||
|
@@ -10,7 +10,7 @@ func checkUserCanUpload(uid uint) (bool, error) {
|
||||
return user.IsAdmin || user.CanUpload, nil
|
||||
}
|
||||
|
||||
func checkUserIsAdmin(uid uint) (bool, error) {
|
||||
func CheckUserIsAdmin(uid uint) (bool, error) {
|
||||
user, err := dao.GetUserByID(uid)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
Reference in New Issue
Block a user