v0.0.3: Dark Mode and User List
This commit is contained in:
parent
cfadca8c06
commit
b6f872343d
BIN
client/Logo.png
Normal file
BIN
client/Logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 354 KiB |
|
@ -2,9 +2,16 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Cosmos</title>
|
<title>Cosmos</title>
|
||||||
|
<link rel="icon" type="image/x-icon" href="/Logo.png">
|
||||||
|
<style>
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
html {
|
||||||
|
background-color: #141414;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import * as auth from './authentication.jsx';
|
import * as auth from './authentication.jsx';
|
||||||
|
import * as users from './users.jsx';
|
||||||
export {
|
export {
|
||||||
auth
|
auth,
|
||||||
|
users
|
||||||
};
|
};
|
14
client/src/api/users.jsx
Normal file
14
client/src/api/users.jsx
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
|
||||||
|
function list() {
|
||||||
|
return fetch('/cosmos/api/users', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
list,
|
||||||
|
};
|
|
@ -9,7 +9,7 @@ import logo from '../../../../../Logo.png';
|
||||||
const AuthBackground = () => {
|
const AuthBackground = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
return (
|
return (
|
||||||
<Box sx={{ position: 'fixed', float: 'left', height: 'calc(100vh - 50px)', overflow: 'hidden', filter: 'blur(25px)', zIndex: -1, top: 100, left: -500 }}>
|
<Box sx={{ position: 'fixed', float: 'left', height: 'calc(100vh - 50px)', overflow: 'hidden', filter: 'blur(25px)', zIndex: 0, top: 100, left: -500 }}>
|
||||||
<img src={logo} style={{ display:'inline'}} alt="Cosmos" width="1100" />
|
<img src={logo} style={{ display:'inline'}} alt="Cosmos" width="1100" />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
1
client/src/assets/images/icons/discord_white.svg
Normal file
1
client/src/assets/images/icons/discord_white.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"><defs><style>.cls-1{fill:#fff;}</style></defs><g id="图层_2" data-name="图层 2"><g id="Discord_Logos" data-name="Discord Logos"><g id="Discord_Logo_-_Large_-_White" data-name="Discord Logo - Large - White"><path class="cls-1" d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"/></g></g></g></svg>
|
After Width: | Height: | Size: 985 B |
|
@ -28,7 +28,7 @@ const MainCard = forwardRef(
|
||||||
divider = true,
|
divider = true,
|
||||||
elevation,
|
elevation,
|
||||||
secondary,
|
secondary,
|
||||||
shadow,
|
// shadow,
|
||||||
sx = {},
|
sx = {},
|
||||||
title,
|
title,
|
||||||
codeHighlight,
|
codeHighlight,
|
||||||
|
@ -37,7 +37,7 @@ const MainCard = forwardRef(
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
boxShadow = theme.palette.mode === 'dark' ? boxShadow || true : boxShadow;
|
boxShadow = false; // theme.palette.mode === 'dark' ? boxShadow || true : boxShadow;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
|
|
|
@ -6,7 +6,7 @@ const config = {
|
||||||
i18n: 'en',
|
i18n: 'en',
|
||||||
miniDrawer: false,
|
miniDrawer: false,
|
||||||
container: true,
|
container: true,
|
||||||
mode: 'light',
|
mode: 'dark',
|
||||||
presetColor: 'default',
|
presetColor: 'default',
|
||||||
themeDirection: 'ltr'
|
themeDirection: 'ltr'
|
||||||
};
|
};
|
||||||
|
|
|
@ -62,8 +62,8 @@ const Notification = () => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const iconBackColorOpen = 'grey.300';
|
const iconBackColor = theme.palette.mode === 'dark' ? 'grey.700' : 'grey.100';
|
||||||
const iconBackColor = 'grey.100';
|
const iconBackColorOpen = theme.palette.mode === 'dark' ? 'grey.800' : 'grey.200';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ flexShrink: 0, ml: 0.75 }}>
|
<Box sx={{ flexShrink: 0, ml: 0.75 }}>
|
||||||
|
|
|
@ -17,8 +17,8 @@ const Header = ({ open, handleDrawerToggle }) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const matchDownMD = useMediaQuery(theme.breakpoints.down('lg'));
|
const matchDownMD = useMediaQuery(theme.breakpoints.down('lg'));
|
||||||
|
|
||||||
const iconBackColor = 'grey.100';
|
const iconBackColor = theme.palette.mode === 'dark' ? 'grey.700' : 'grey.100';
|
||||||
const iconBackColorOpen = 'grey.200';
|
const iconBackColorOpen = theme.palette.mode === 'dark' ? 'grey.800' : 'grey.200';
|
||||||
|
|
||||||
// common header
|
// common header
|
||||||
const mainHeader = (
|
const mainHeader = (
|
||||||
|
@ -28,7 +28,7 @@ const Header = ({ open, handleDrawerToggle }) => {
|
||||||
aria-label="open drawer"
|
aria-label="open drawer"
|
||||||
onClick={handleDrawerToggle}
|
onClick={handleDrawerToggle}
|
||||||
edge="start"
|
edge="start"
|
||||||
color="secondary"
|
color="red"
|
||||||
sx={{ color: 'text.primary', bgcolor: open ? iconBackColorOpen : iconBackColor, ml: { xs: 0, lg: -2 } }}
|
sx={{ color: 'text.primary', bgcolor: open ? iconBackColorOpen : iconBackColor, ml: { xs: 0, lg: -2 } }}
|
||||||
>
|
>
|
||||||
{!open ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
{!open ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||||
|
|
|
@ -50,7 +50,7 @@ const MainLayout = () => {
|
||||||
<Drawer open={open} handleDrawerToggle={handleDrawerToggle} />
|
<Drawer open={open} handleDrawerToggle={handleDrawerToggle} />
|
||||||
<Box component="main" sx={{ width: '100%', flexGrow: 1, p: { xs: 2, sm: 3 } }}>
|
<Box component="main" sx={{ width: '100%', flexGrow: 1, p: { xs: 2, sm: 3 } }}>
|
||||||
<Toolbar />
|
<Toolbar />
|
||||||
<Breadcrumbs navigation={navigation} title titleBottom card={false} divider={false} />
|
<Breadcrumbs navigation={navigation} title divider={false} />
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
// assets
|
// assets
|
||||||
import { GithubOutlined, QuestionOutlined } from '@ant-design/icons';
|
import { GithubOutlined, QuestionOutlined } from '@ant-design/icons';
|
||||||
import DiscordOutlined from '../assets/images/icons/discord.svg'
|
import DiscordOutlined from '../assets/images/icons/discord.svg'
|
||||||
|
import DiscordOutlinedWhite from '../assets/images/icons/discord_white.svg'
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
|
||||||
// ==============================|| MENU ITEMS - SAMPLE PAGE & DOCUMENTATION ||============================== //
|
// ==============================|| MENU ITEMS - SAMPLE PAGE & DOCUMENTATION ||============================== //
|
||||||
|
|
||||||
const DiscordOutlinedIcon = (props) => {
|
const DiscordOutlinedIcon = (props) => {
|
||||||
|
const theme = useTheme();
|
||||||
return (
|
return (
|
||||||
<img src={DiscordOutlined} width="16px" alt="Discord" {...props} />
|
<img src={
|
||||||
|
theme.palette.mode === 'dark' ? DiscordOutlinedWhite : DiscordOutlined} width="16px" alt="Discord" {...props} />
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -8,16 +8,16 @@ import MainCard from '../../components/MainCard';
|
||||||
|
|
||||||
// ==============================|| AUTHENTICATION - CARD WRAPPER ||============================== //
|
// ==============================|| AUTHENTICATION - CARD WRAPPER ||============================== //
|
||||||
|
|
||||||
const AuthCard = ({ children, ...other }) => (
|
const AuthCard = ({ children, ...other }) => (<MainCard
|
||||||
<MainCard
|
|
||||||
sx={{
|
sx={{
|
||||||
maxWidth: { xs: 400, lg: 475 },
|
maxWidth: { xs: 400, lg: 475 },
|
||||||
margin: { xs: 2.5, md: 3 },
|
margin: { xs: 2.5, md: 3 },
|
||||||
'& > *': {
|
'& > *': {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
flexBasis: '50%'
|
flexBasis: '50%'
|
||||||
}
|
},
|
||||||
}}
|
}}
|
||||||
|
style={{ zIndex: 1, position: 'relative' }}
|
||||||
content={false}
|
content={false}
|
||||||
{...other}
|
{...other}
|
||||||
border={false}
|
border={false}
|
||||||
|
|
69
client/src/pages/config/users/usermanagement.jsx
Normal file
69
client/src/pages/config/users/usermanagement.jsx
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
// material-ui
|
||||||
|
import { Button, Typography } from '@mui/material';
|
||||||
|
|
||||||
|
import Table from '@mui/material/Table';
|
||||||
|
import TableBody from '@mui/material/TableBody';
|
||||||
|
import TableCell from '@mui/material/TableCell';
|
||||||
|
import TableContainer from '@mui/material/TableContainer';
|
||||||
|
import TableHead from '@mui/material/TableHead';
|
||||||
|
import TableRow from '@mui/material/TableRow';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
|
||||||
|
import * as API from '../../../api';
|
||||||
|
|
||||||
|
// project import
|
||||||
|
import MainCard from '../../../components/MainCard';
|
||||||
|
import isLoggedIn from '../../../isLoggedIn';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
// ==============================|| SAMPLE PAGE ||============================== //
|
||||||
|
|
||||||
|
const UserManagement = () => {
|
||||||
|
|
||||||
|
const [rows, setRows] = useState([
|
||||||
|
{ id: 0, nickname: '12313', reset:'123132' },
|
||||||
|
{ id: 1, nickname: '354345', reset:'345345' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
isLoggedIn();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
API.users.list()
|
||||||
|
.then(data => {
|
||||||
|
console.log(data);
|
||||||
|
setRows(data.data);
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return <MainCard title="Users">
|
||||||
|
<TableContainer component={Paper}>
|
||||||
|
<Table aria-label="simple table">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Nickname</TableCell>
|
||||||
|
<TableCell>Role</TableCell>
|
||||||
|
<TableCell>Password</TableCell>
|
||||||
|
<TableCell>Actions</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{rows.map((row) => (
|
||||||
|
<TableRow
|
||||||
|
key={row.nickname}
|
||||||
|
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
|
||||||
|
>
|
||||||
|
<TableCell component="th" scope="row">
|
||||||
|
{row.nickname}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>User</TableCell>
|
||||||
|
<TableCell><Button variant="contained" color="primary">Send Password Link</Button></TableCell>
|
||||||
|
<TableCell><Button variant="contained" color="error">Delete</Button></TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</MainCard>
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserManagement;
|
|
@ -3,6 +3,7 @@ import { lazy } from 'react';
|
||||||
// project import
|
// project import
|
||||||
import Loadable from '../components/Loadable';
|
import Loadable from '../components/Loadable';
|
||||||
import MainLayout from '../layout/MainLayout';
|
import MainLayout from '../layout/MainLayout';
|
||||||
|
import UserManagement from '../pages/config/users/usermanagement';
|
||||||
|
|
||||||
// render - dashboard
|
// render - dashboard
|
||||||
const DashboardDefault = Loadable(lazy(() => import('../pages/dashboard')));
|
const DashboardDefault = Loadable(lazy(() => import('../pages/dashboard')));
|
||||||
|
@ -40,8 +41,8 @@ const MainRoutes = {
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'sample-page',
|
path: 'config/users',
|
||||||
element: <SamplePage />
|
element: <UserManagement />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'shadow',
|
path: 'shadow',
|
||||||
|
|
|
@ -14,7 +14,9 @@ import componentsOverride from './overrides';
|
||||||
// ==============================|| DEFAULT THEME - MAIN ||============================== //
|
// ==============================|| DEFAULT THEME - MAIN ||============================== //
|
||||||
|
|
||||||
export default function ThemeCustomization({ children }) {
|
export default function ThemeCustomization({ children }) {
|
||||||
const theme = Palette('light', 'default');
|
const theme = Palette(
|
||||||
|
window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ?
|
||||||
|
'dark' : 'light');
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
const themeTypography = Typography(`'Public Sans', sans-serif`);
|
const themeTypography = Typography(`'Public Sans', sans-serif`);
|
||||||
|
|
|
@ -32,28 +32,48 @@ const Palette = (mode) => {
|
||||||
|
|
||||||
const paletteColor = ThemeOption(colors);
|
const paletteColor = ThemeOption(colors);
|
||||||
|
|
||||||
return createTheme({
|
return createTheme(mode === 'dark' ? {
|
||||||
palette: {
|
palette: {
|
||||||
mode,
|
mode,
|
||||||
common: {
|
common: {
|
||||||
black: '#000',
|
black: '#fff',
|
||||||
white: '#fff'
|
white: '#000'
|
||||||
},
|
},
|
||||||
...paletteColor,
|
...paletteColor,
|
||||||
text: {
|
text: {
|
||||||
primary: paletteColor.grey[700],
|
primary: paletteColor.grey[0],
|
||||||
secondary: paletteColor.grey[500],
|
secondary: paletteColor.grey[200],
|
||||||
disabled: paletteColor.grey[400]
|
disabled: paletteColor.grey[300]
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
disabled: paletteColor.grey[300]
|
disabled: paletteColor.grey[300]
|
||||||
},
|
},
|
||||||
divider: paletteColor.grey[200],
|
divider: paletteColor.grey[600],
|
||||||
background: {
|
background: {
|
||||||
paper: paletteColor.grey[0],
|
paper: paletteColor.grey[700],
|
||||||
default: paletteColor.grey.A50
|
default: paletteColor.grey[800]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} : {
|
||||||
|
mode,
|
||||||
|
common: {
|
||||||
|
black: '#000',
|
||||||
|
white: '#fff'
|
||||||
|
},
|
||||||
|
...paletteColor,
|
||||||
|
text: {
|
||||||
|
primary: paletteColor.grey[700],
|
||||||
|
secondary: paletteColor.grey[500],
|
||||||
|
disabled: paletteColor.grey[400]
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
disabled: paletteColor.grey[300]
|
||||||
|
},
|
||||||
|
divider: paletteColor.grey[200],
|
||||||
|
background: {
|
||||||
|
paper: paletteColor.grey[0],
|
||||||
|
default: paletteColor.grey.A50
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
"npm://@esbuild/linux-x64": "0.16.17",
|
"npm://@esbuild/linux-x64": "0.16.17",
|
||||||
"npm://@mui/lab": "^5.0.0-alpha.100",
|
"npm://@mui/lab": "^5.0.0-alpha.100",
|
||||||
"npm://@mui/material": "^5.10.6",
|
"npm://@mui/material": "^5.10.6",
|
||||||
|
"npm://@mui/x-data-grid": "6.0.1",
|
||||||
"npm://@reduxjs/toolkit": "^1.8.5",
|
"npm://@reduxjs/toolkit": "^1.8.5",
|
||||||
"npm://@testing-library/jest-dom": "^5.16.5",
|
"npm://@testing-library/jest-dom": "^5.16.5",
|
||||||
"npm://@testing-library/react": "^13.4.0",
|
"npm://@testing-library/react": "^13.4.0",
|
||||||
|
@ -85,6 +86,6 @@
|
||||||
},
|
},
|
||||||
"description": "Cosmos Server",
|
"description": "Cosmos Server",
|
||||||
"name": "cosmos-server",
|
"name": "cosmos-server",
|
||||||
"version": "0.0.2",
|
"version": "0.0.3",
|
||||||
"wrapInstallFolder": "src"
|
"wrapInstallFolder": "src"
|
||||||
}
|
}
|
|
@ -5,14 +5,16 @@
|
||||||
# Cosmos Server
|
# Cosmos Server
|
||||||
|
|
||||||
```
|
```
|
||||||
Disclaimer: Cosmos is still in early Alpha stage, please be careful when you use it. It is not (yet, at least ;p) a replacement for proper control and mindfulness of your own security.
|
**Disclaimer**: Cosmos is still in early Alpha stage, please be careful when you use it. It is not (yet, at least ;p) a replacement for proper control and mindfulness of your own security.
|
||||||
```
|
```
|
||||||
|
|
||||||
Looking for a **secure** and **robust** way to run your **self-hosted applications**? With **Cosmos**, you can take control of your data and privacy without sacrificing security and stability.
|
Looking for the right way to run your **self-hosted applications**? With **Cosmos**, you can take control of your data and privacy without sacrificing security and stability. **Safe** and **secure** platform by design, and most importantly, **easy to setup** without ambiguity. It is a combination of a **reverse proxy**, an **authentication provider** and an **application manager**.
|
||||||
|
|
||||||
|
![screenshot1](./screenshot1.png)
|
||||||
|
|
||||||
Whether you have a **server**, a **NAS**, or a **Raspberry Pi** with applications such as **Plex**, **HomeAssistant** or even a blog, Cosmos is the perfect solution to secure it all. Simply install Cosmos on your server and connect to your applications through it to enjoy built-in security and robustness for all your services, right out of the box.
|
Whether you have a **server**, a **NAS**, or a **Raspberry Pi** with applications such as **Plex**, **HomeAssistant** or even a blog, Cosmos is the perfect solution to secure it all. Simply install Cosmos on your server and connect to your applications through it to enjoy built-in security and robustness for all your services, right out of the box.
|
||||||
|
|
||||||
* **Authentication** Connect to all your application with the same account, including strong security and multi-factor authentication
|
* **Authentication** Connect to all your application with the same account, including strong security and **multi-factor authentication**
|
||||||
* **Automatic HTTPS** certificates provision
|
* **Automatic HTTPS** certificates provision
|
||||||
* **Anti-bot** protections such as Captcha and IP rate limiting
|
* **Anti-bot** protections such as Captcha and IP rate limiting
|
||||||
* **Anti-DDOS** protections such as variable timeouts/throttling, IP rate limiting and IP blacklisting
|
* **Anti-DDOS** protections such as variable timeouts/throttling, IP rate limiting and IP blacklisting
|
||||||
|
|
BIN
screenshot1.png
Normal file
BIN
screenshot1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 424 KiB |
|
@ -172,8 +172,8 @@ func StartServer() {
|
||||||
|
|
||||||
// srapi.Use(utils.AcceptHeader("*/*"))
|
// srapi.Use(utils.AcceptHeader("*/*"))
|
||||||
srapi.Use(utils.CORSHeader(utils.GetMainConfig().HTTPConfig.Hostname))
|
srapi.Use(utils.CORSHeader(utils.GetMainConfig().HTTPConfig.Hostname))
|
||||||
srapi.Use(utils.MiddlewareTimeout(5 * time.Second))
|
srapi.Use(utils.MiddlewareTimeout(20 * time.Second))
|
||||||
srapi.Use(httprate.Limit(20, 1*time.Minute,
|
srapi.Use(httprate.Limit(60, 1*time.Minute,
|
||||||
httprate.WithKeyFuncs(httprate.KeyByIP),
|
httprate.WithKeyFuncs(httprate.KeyByIP),
|
||||||
httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
|
httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
|
||||||
utils.Error("Too many requests. Throttling", nil)
|
utils.Error("Too many requests. Throttling", nil)
|
||||||
|
|
Loading…
Reference in a new issue