This commit is contained in:
@@ -5,9 +5,16 @@ import (
|
||||
)
|
||||
|
||||
type UserInfo struct {
|
||||
ID uint64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
RateLimit model.RateLimit `json:"rate_limit"`
|
||||
Permissions []model.Permission `json:"permissions"`
|
||||
ID uint64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
RateLimit model.RateLimit `json:"rate_limit"`
|
||||
RateLimitStatus UserRateLimitStatus `json:"rate_limit_status"`
|
||||
Permissions []model.Permission `json:"permissions"`
|
||||
}
|
||||
|
||||
type UserRateLimitStatus struct {
|
||||
RemainingBurst uint64 `json:"remaining_burst"`
|
||||
RemainingDaily uint64 `json:"remaining_daily"`
|
||||
RemainingMonthly uint64 `json:"remaining_monthly"`
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"git.itzana.me/StrafesNET/dev-service/pkg/authz"
|
||||
"git.itzana.me/StrafesNET/dev-service/pkg/datastore"
|
||||
"git.itzana.me/StrafesNET/dev-service/pkg/model"
|
||||
"git.itzana.me/StrafesNET/dev-service/pkg/ratelimit"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
)
|
||||
@@ -15,12 +16,14 @@ const (
|
||||
ErrMsgDatastore = "datastore is required"
|
||||
ErrMsgAuth = "auth service is required"
|
||||
ErrMsgUnauth = "Unauthorized"
|
||||
ErrMsgLimiter = "rate limiter is required"
|
||||
)
|
||||
|
||||
// Handler is a base handler that provides common functionality for all HTTP handlers.
|
||||
type Handler struct {
|
||||
Store datastore.Datastore
|
||||
Auth *authz.Service
|
||||
Store datastore.Datastore
|
||||
Auth *authz.Service
|
||||
Limiter *ratelimit.RateLimit
|
||||
}
|
||||
|
||||
// HandlerOption defines a functional option for configuring a Handler
|
||||
@@ -40,6 +43,13 @@ func WithAuthService(auth *authz.Service) HandlerOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithRatelimiter sets the rate limiter for the Handler
|
||||
func WithRatelimiter(limiter *ratelimit.RateLimit) HandlerOption {
|
||||
return func(h *Handler) {
|
||||
h.Limiter = limiter
|
||||
}
|
||||
}
|
||||
|
||||
// NewHandler creates a new Handler with the provided options.
|
||||
// It requires both a datastore and an authentication service to function properly.
|
||||
func NewHandler(options ...HandlerOption) (*Handler, error) {
|
||||
@@ -66,6 +76,9 @@ func (h *Handler) validateDependencies() error {
|
||||
if h.Auth == nil {
|
||||
return fmt.Errorf(ErrMsgAuth)
|
||||
}
|
||||
if h.Limiter == nil {
|
||||
return fmt.Errorf(ErrMsgLimiter)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package handlers
|
||||
import (
|
||||
"git.itzana.me/StrafesNET/dev-service/pkg/api/dto"
|
||||
"git.itzana.me/StrafesNET/dev-service/pkg/datastore"
|
||||
"git.itzana.me/StrafesNET/dev-service/pkg/ratelimit"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
)
|
||||
@@ -56,11 +57,28 @@ func (h *UserHandler) GetCurrentUser(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get rate limit
|
||||
limit, err := h.Limiter.GetRateLimitStatus(ctx, user.ID, ratelimit.RateLimitConfig{
|
||||
BurstLimit: user.RateLimit.BurstLimit,
|
||||
BurstDurationSeconds: user.RateLimit.BurstDuration,
|
||||
DailyLimit: user.RateLimit.DailyLimit,
|
||||
MonthlyLimit: user.RateLimit.MonthlyLimit,
|
||||
})
|
||||
if err != nil {
|
||||
h.RespondWithError(ctx, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.RespondWithData(ctx, &dto.UserInfo{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
AvatarURL: profile.AvatarURL,
|
||||
RateLimit: user.RateLimit,
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
AvatarURL: profile.AvatarURL,
|
||||
RateLimit: user.RateLimit,
|
||||
RateLimitStatus: dto.UserRateLimitStatus{
|
||||
RemainingBurst: limit.RemainingBurst,
|
||||
RemainingDaily: limit.RemainingDaily,
|
||||
RemainingMonthly: limit.RemainingMonthly,
|
||||
},
|
||||
Permissions: user.Permissions,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"git.itzana.me/StrafesNET/dev-service/pkg/api/middleware"
|
||||
"git.itzana.me/StrafesNET/dev-service/pkg/authz"
|
||||
"git.itzana.me/StrafesNET/dev-service/pkg/datastore"
|
||||
"git.itzana.me/StrafesNET/dev-service/pkg/ratelimit"
|
||||
"git.itzana.me/StrafesNET/dev-service/web"
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -25,6 +26,7 @@ type RouterConfig struct {
|
||||
context *cli.Context
|
||||
store datastore.Datastore
|
||||
auth *authz.Service
|
||||
limiter *ratelimit.RateLimit
|
||||
shutdownTimeout time.Duration
|
||||
}
|
||||
|
||||
@@ -56,6 +58,13 @@ func WithShutdownTimeout(timeout time.Duration) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithRateLimit sets the rate limiter for the router
|
||||
func WithRateLimit(limiter *ratelimit.RateLimit) Option {
|
||||
return func(cfg *RouterConfig) {
|
||||
cfg.limiter = limiter
|
||||
}
|
||||
}
|
||||
|
||||
// WithAuth sets the auth for the router
|
||||
func WithAuth(auth *authz.Service) Option {
|
||||
return func(cfg *RouterConfig) {
|
||||
@@ -72,6 +81,7 @@ func setupRoutes(cfg *RouterConfig) (*gin.Engine, error) {
|
||||
handlerOptions := []handlers.HandlerOption{
|
||||
handlers.WithDatastore(cfg.store),
|
||||
handlers.WithAuthService(cfg.auth),
|
||||
handlers.WithRatelimiter(cfg.limiter),
|
||||
}
|
||||
|
||||
userHandler, err := handlers.NewUserHandler(handlerOptions...)
|
||||
@@ -151,6 +161,10 @@ func NewRouter(options ...Option) error {
|
||||
return errors.New("auth service is required")
|
||||
}
|
||||
|
||||
if cfg.limiter == nil {
|
||||
return errors.New("rate limiter is required")
|
||||
}
|
||||
|
||||
routes, err := setupRoutes(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -66,6 +66,25 @@ var (
|
||||
Value: 8080,
|
||||
EnvVars: []string{"PORT"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "redis-addr",
|
||||
Usage: "Address of redis database",
|
||||
EnvVars: []string{"REDIS_ADDR"},
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "redis-pass",
|
||||
Usage: "Password of redis database",
|
||||
EnvVars: []string{"REDIS_PASS"},
|
||||
Required: false,
|
||||
Value: "",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "redis-db",
|
||||
Usage: "Number of database to connect to",
|
||||
EnvVars: []string{"REDIS_DB"},
|
||||
Required: true,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -18,27 +18,7 @@ func NewRpcCommand() *cli.Command {
|
||||
Name: "rpc",
|
||||
Usage: "Run dev rpc service",
|
||||
Action: rpcServer,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "redis-addr",
|
||||
Usage: "Address of redis database",
|
||||
EnvVars: []string{"REDIS_ADDR"},
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "redis-pass",
|
||||
Usage: "Password of redis database",
|
||||
EnvVars: []string{"REDIS_PASS"},
|
||||
Required: false,
|
||||
Value: "",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "redis-db",
|
||||
Usage: "Number of database to connect to",
|
||||
EnvVars: []string{"REDIS_DB"},
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@ import (
|
||||
"git.itzana.me/StrafesNET/dev-service/pkg/api"
|
||||
"git.itzana.me/StrafesNET/dev-service/pkg/authz"
|
||||
"git.itzana.me/StrafesNET/dev-service/pkg/datastore/gormstore"
|
||||
"git.itzana.me/StrafesNET/dev-service/pkg/ratelimit"
|
||||
"git.itzana.me/strafesnet/go-grpc/auth"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/urfave/cli/v2"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
@@ -39,6 +41,13 @@ func runWeb(ctx *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Redis setup
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: ctx.String("redis-addr"),
|
||||
Password: ctx.String("redis-pass"),
|
||||
DB: ctx.Int("redis-db"),
|
||||
})
|
||||
|
||||
// Auth service client
|
||||
conn, err := grpc.Dial(ctx.String("auth-rpc-host"), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
@@ -60,5 +69,6 @@ func runWeb(ctx *cli.Context) error {
|
||||
api.WithPort(ctx.Int("port")),
|
||||
api.WithDatastore(store),
|
||||
api.WithAuth(authClient),
|
||||
api.WithRateLimit(ratelimit.New(rdb)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -88,5 +88,39 @@ return {
|
||||
remainingDaily,
|
||||
remainingMonthly
|
||||
}
|
||||
`
|
||||
|
||||
// readOnlyRateLimitScript is a Lua script that checks rate limits without incrementing counters
|
||||
readOnlyRateLimitScript = `
|
||||
-- Keys: burstKey, dailyKey, monthlyKey
|
||||
-- Args: now, cutoff, burstLimit, dailyLimit, monthlyLimit
|
||||
local now = tonumber(ARGV[1])
|
||||
local cutoff = tonumber(ARGV[2])
|
||||
local burstLimit = tonumber(ARGV[3])
|
||||
local dailyLimit = tonumber(ARGV[4])
|
||||
local monthlyLimit = tonumber(ARGV[5])
|
||||
|
||||
-- Check burst limit (leaky bucket)
|
||||
local bursts = redis.call('ZRANGEBYSCORE', KEYS[1], cutoff, '+inf')
|
||||
local burstCount = #bursts
|
||||
local remainingBurst = burstLimit - burstCount
|
||||
|
||||
-- Check daily limit
|
||||
local dailyCount = redis.call('GET', KEYS[2])
|
||||
dailyCount = dailyCount and tonumber(dailyCount) or 0
|
||||
local remainingDaily = dailyLimit - dailyCount
|
||||
|
||||
-- Check monthly limit
|
||||
local monthlyCount = redis.call('GET', KEYS[3])
|
||||
monthlyCount = monthlyCount and tonumber(monthlyCount) or 0
|
||||
local remainingMonthly = monthlyLimit - monthlyCount
|
||||
|
||||
-- Check if all limits are satisfied
|
||||
local allowed = 1
|
||||
if remainingBurst <= 0 or remainingDaily <= 0 or remainingMonthly <= 0 then
|
||||
allowed = 0
|
||||
end
|
||||
|
||||
return {allowed, remainingBurst, remainingDaily, remainingMonthly}
|
||||
`
|
||||
)
|
||||
|
||||
@@ -118,6 +118,52 @@ func (rl *RateLimit) CheckRateLimits(ctx context.Context, user uint64, config Ra
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetRateLimitStatus retrieves the current usage status of daily and monthly rate limits
|
||||
// without consuming any of the limits.
|
||||
func (rl *RateLimit) GetRateLimitStatus(ctx context.Context, user uint64, config RateLimitConfig) (*RateLimitStatus, error) {
|
||||
now := time.Now().UnixNano()
|
||||
burstKey := formatKey(prefixLeakyBucket, user)
|
||||
dailyKey := formatKey(fmt.Sprintf("%s:%s", prefixQuota, periodDaily), user)
|
||||
monthlyKey := formatKey(fmt.Sprintf("%s:%s", prefixQuota, periodMonthly), user)
|
||||
|
||||
// Calculate parameters
|
||||
burstDuration := time.Duration(config.BurstDurationSeconds) * time.Second
|
||||
cutoff := now - burstDuration.Nanoseconds()
|
||||
|
||||
// Execute a read-only Lua script that doesn't modify any counters
|
||||
result, err := rl.redis.Eval(ctx, readOnlyRateLimitScript,
|
||||
[]string{burstKey, dailyKey, monthlyKey},
|
||||
now, // ARGV[1] - Current timestamp
|
||||
cutoff, // ARGV[2] - Cutoff timestamp
|
||||
config.BurstLimit, // ARGV[3] - Burst limit
|
||||
config.DailyLimit, // ARGV[4] - Daily limit
|
||||
config.MonthlyLimit, // ARGV[5] - Monthly limit
|
||||
).Result()
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute read-only rate limit script: %w", err)
|
||||
}
|
||||
|
||||
// Parse the response
|
||||
values, ok := result.([]interface{})
|
||||
if !ok || len(values) != 4 {
|
||||
return nil, fmt.Errorf("unexpected response format from read-only rate limit script")
|
||||
}
|
||||
|
||||
// Convert values to uint64
|
||||
allowed := values[0].(int64) == scriptAllowed
|
||||
remainingBurst, _ := values[1].(int64)
|
||||
remainingDaily, _ := values[2].(int64)
|
||||
remainingMonthly, _ := values[3].(int64)
|
||||
|
||||
return &RateLimitStatus{
|
||||
Allowed: allowed,
|
||||
RemainingBurst: uint64(remainingBurst),
|
||||
RemainingDaily: uint64(remainingDaily),
|
||||
RemainingMonthly: uint64(remainingMonthly),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// formatKey creates a consistent format for Redis keys
|
||||
func formatKey(prefix string, key uint64) string {
|
||||
return fmt.Sprintf("%s:%d", prefix, key)
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from '@mui/icons-material';
|
||||
import {UserInfo} from "../types";
|
||||
import {useConfig} from "../context/ConfigContext.tsx";
|
||||
import RateLimitDisplay from './RateLimitDisplay';
|
||||
|
||||
interface HeaderProps {
|
||||
user: UserInfo;
|
||||
@@ -59,6 +60,10 @@ const Header: React.FC<HeaderProps> = ({ user, onCreateAppClick }) => {
|
||||
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
||||
StrafesNET Developer Portal
|
||||
</Typography>
|
||||
|
||||
{/* Rate Limit Display */}
|
||||
<RateLimitDisplay rateLimit={user.rate_limit} rateLimitStatus={user.rate_limit_status} />
|
||||
|
||||
<Button
|
||||
color="inherit"
|
||||
startIcon={<AddIcon />}
|
||||
|
||||
167
web/src/components/RateLimitDisplay.tsx
Normal file
167
web/src/components/RateLimitDisplay.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Typography,
|
||||
Tooltip,
|
||||
Grid,
|
||||
Box,
|
||||
LinearProgress,
|
||||
Divider
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Speed as SpeedIcon,
|
||||
Today as TodayIcon,
|
||||
DateRange as DateRangeIcon,
|
||||
Apps as AppsIcon
|
||||
} from '@mui/icons-material';
|
||||
import {RateLimit, RateLimitStatus} from '../types';
|
||||
|
||||
interface RateLimitDisplayProps {
|
||||
rateLimit: RateLimit;
|
||||
rateLimitStatus: RateLimitStatus
|
||||
}
|
||||
|
||||
const RateLimitDisplay: React.FC<RateLimitDisplayProps> = ({ rateLimit, rateLimitStatus }) => {
|
||||
const dailyUsed = rateLimit.daily_limit - rateLimitStatus.remaining_daily;
|
||||
const monthlyUsed = rateLimit.monthly_limit - rateLimitStatus.remaining_monthly;
|
||||
|
||||
const dailyPercentage = (dailyUsed / rateLimit.daily_limit) * 100;
|
||||
const monthlyPercentage = (monthlyUsed / rateLimit.monthly_limit) * 100;
|
||||
|
||||
// Create tooltip content separately without a Paper component
|
||||
const tooltipContent = (
|
||||
<Box sx={{ p: 1.5, width: 300 }}>
|
||||
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
|
||||
API Rate Limits
|
||||
</Typography>
|
||||
<Divider sx={{ my: 1 }} />
|
||||
|
||||
<Grid container spacing={2} sx={{ mt: 1 }}>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 0.5 }}>
|
||||
<SpeedIcon fontSize="small" sx={{ mr: 1, color: 'primary.main' }} />
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
Burst Rate
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" sx={{ ml: 4 }}>
|
||||
{rateLimit.burst_limit} requests / {rateLimit.burst_duration} seconds
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 0.5 }}>
|
||||
<TodayIcon fontSize="small" sx={{ mr: 1, color: 'primary.main' }} />
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
Daily Usage
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ ml: 4 }}>
|
||||
<Typography variant="body2" sx={{ mb: 0.5 }}>
|
||||
{dailyUsed} / {rateLimit.daily_limit} requests
|
||||
</Typography>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={dailyPercentage}
|
||||
sx={{
|
||||
height: 5,
|
||||
borderRadius: 1,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
'& .MuiLinearProgress-bar': {
|
||||
backgroundColor: dailyPercentage > 75 ? 'error.main' : 'primary.main'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 0.5 }}>
|
||||
<DateRangeIcon fontSize="small" sx={{ mr: 1, color: 'primary.main' }} />
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
Monthly Usage
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ ml: 4 }}>
|
||||
<Typography variant="body2" sx={{ mb: 0.5 }}>
|
||||
{monthlyUsed} / {rateLimit.monthly_limit} requests
|
||||
</Typography>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={monthlyPercentage}
|
||||
sx={{
|
||||
height: 5,
|
||||
borderRadius: 1,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
'& .MuiLinearProgress-bar': {
|
||||
backgroundColor: monthlyPercentage > 75 ? 'error.main' : 'primary.main'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 0.5 }}>
|
||||
<AppsIcon fontSize="small" sx={{ mr: 1, color: 'primary.main' }} />
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
Max Applications
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" sx={{ ml: 4 }}>
|
||||
{rateLimit.max_applications}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={tooltipContent}
|
||||
arrow
|
||||
placement="bottom-end"
|
||||
componentsProps={{
|
||||
tooltip: {
|
||||
sx: {
|
||||
bgcolor: 'background.paper',
|
||||
color: 'text.primary',
|
||||
'& .MuiTooltip-arrow': {
|
||||
color: 'background.paper',
|
||||
},
|
||||
boxShadow: '0 2px 10px rgba(0, 0, 0, 0.2)',
|
||||
borderRadius: 1,
|
||||
p: 0, // Remove padding from tooltip to prevent double padding
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
px: 1.5,
|
||||
py: 0.75,
|
||||
mr: 2,
|
||||
borderRadius: 4,
|
||||
backgroundColor: 'rgba(149, 128, 255, 0.15)',
|
||||
border: '1px solid rgba(149, 128, 255, 0.3)',
|
||||
color: 'primary.main',
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(149, 128, 255, 0.25)',
|
||||
},
|
||||
transition: 'background-color 0.2s',
|
||||
height: 32,
|
||||
}}
|
||||
>
|
||||
<SpeedIcon fontSize="small" sx={{ mr: 1 }} />
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{dailyUsed}/{rateLimit.daily_limit} API calls today
|
||||
</Typography>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default RateLimitDisplay;
|
||||
@@ -9,6 +9,12 @@ export interface RateLimit {
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface RateLimitStatus {
|
||||
remaining_burst: number;
|
||||
remaining_daily: number;
|
||||
remaining_monthly: number;
|
||||
}
|
||||
|
||||
export interface Permission {
|
||||
id: number;
|
||||
service: string;
|
||||
@@ -48,6 +54,7 @@ export interface UserInfo {
|
||||
username: string;
|
||||
avatar_url: string;
|
||||
rate_limit: RateLimit;
|
||||
rate_limit_status: RateLimitStatus;
|
||||
permissions: Permission[];
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ export default defineConfig({
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
target: 'http://localhost:1234',
|
||||
changeOrigin: true
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user