Merge pull request #8 from Nekiiinkognito/improvement/ABACImplementation

Improvement/abac implementation
This commit is contained in:
Maxim 2025-01-29 13:30:18 +03:00 committed by GitHub
commit c816a4612c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 499 additions and 512 deletions

4
.gitignore vendored
View File

@ -13,6 +13,10 @@ dist-ssr
*.local *.local
secret.env secret.env
privkey.pem
fullchain.pem
gin.log
# Editor directories and files # Editor directories and files
.vscode/* .vscode/*
!.vscode/extensions.json !.vscode/extensions.json

25
compose.yml Normal file
View File

@ -0,0 +1,25 @@
services:
nginx:
build: ./enshi
ports:
- 127.0.0.1:80:80
- 127.0.0.1:443:443
networks:
- app-network
restart: unless-stopped
enshi_back:
build: ./enshi_back
ports:
- 127.0.0.1:9876:9876
networks:
- app-network
environment:
- ENV=docker
- DOMAIN=localhost
restart: unless-stopped
networks:
app-network:
driver: bridge

1
enshi/.dockerignore Normal file
View File

@ -0,0 +1 @@
node_modules

22
enshi/Dockerfile Normal file
View File

@ -0,0 +1,22 @@
FROM node:18-alpine as builder
WORKDIR /app
ENV VITE_ENV=docker
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY ./nginx/nginx.conf /etc/nginx/conf.d/default.conf
# Certificates
COPY ./nginx/fullchain.pem /etc/nginx/ssl/
COPY ./nginx/privkey.pem /etc/nginx/ssl/
EXPOSE 80
EXPOSE 443

View File

@ -1,50 +1 @@
# React + TypeScript + Vite # README
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
```js
export default tseslint.config({
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
- Optionally add `...tseslint.configs.stylisticTypeChecked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
```js
// eslint.config.js
import react from 'eslint-plugin-react'
export default tseslint.config({
// Set the react version
settings: { react: { version: '18.3' } },
plugins: {
// Add the react plugin
react,
},
rules: {
// other rules...
// Enable its recommended rules
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
},
})
```

View File

@ -1,28 +0,0 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)

37
enshi/nginx/nginx.conf Normal file
View File

@ -0,0 +1,37 @@
server {
listen 80;
server_name localhost;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
# server_name nekiiinkognito.ru www.nekiiinkognito.ru;
server_name localhost;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers EECDH+AESGCM:EDH+AESGCM;
ssl_ecdh_curve secp384r1;
ssl_session_timeout 10m;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
location /api/v1/ {
proxy_pass http://enshi_back:9876/;
}
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}

View File

@ -24,6 +24,7 @@
"i18next": "^23.14.0", "i18next": "^23.14.0",
"i18next-browser-languagedetector": "^8.0.0", "i18next-browser-languagedetector": "^8.0.0",
"immer": "^10.1.1", "immer": "^10.1.1",
"interweave": "^13.1.0",
"jotai": "^2.9.3", "jotai": "^2.9.3",
"jotai-immer": "^0.4.1", "jotai-immer": "^0.4.1",
"primereact": "^10.8.2", "primereact": "^10.8.2",
@ -4425,6 +4426,12 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/escape-string-regexp": { "node_modules/escape-string-regexp": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
@ -5321,6 +5328,22 @@
"integrity": "sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g==", "integrity": "sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/interweave": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/interweave/-/interweave-13.1.0.tgz",
"integrity": "sha512-JIDq0+2NYg0cgL7AB26fBcV0yZdiJvPDBp+aF6k8gq6Cr1kH5Gd2/Xqn7j8z+TGb8jCWZn739jzalCz+nPYwcA==",
"license": "MIT",
"dependencies": {
"escape-html": "^1.0.3"
},
"funding": {
"type": "ko-fi",
"url": "https://ko-fi.com/milesjohnson"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/invariant": { "node_modules/invariant": {
"version": "2.2.4", "version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",

View File

@ -1,11 +1,12 @@
{ {
"name": "enshi", "name": "enshi",
"private": true, "private": true,
"version": "0.0.0", "version": "0.1.6",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc -b && vite build", "lint-build": "tsc -b && vite build",
"build": "vite build",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview" "preview": "vite preview"
}, },
@ -26,6 +27,7 @@
"i18next": "^23.14.0", "i18next": "^23.14.0",
"i18next-browser-languagedetector": "^8.0.0", "i18next-browser-languagedetector": "^8.0.0",
"immer": "^10.1.1", "immer": "^10.1.1",
"interweave": "^13.1.0",
"jotai": "^2.9.3", "jotai": "^2.9.3",
"jotai-immer": "^0.4.1", "jotai-immer": "^0.4.1",
"primereact": "^10.8.2", "primereact": "^10.8.2",

View File

@ -1,8 +1,12 @@
import axios from "axios"; import axios from "axios";
const environment = import.meta.env.VITE_ENV || 'development';
// const environment = "docker"
const baseURL = environment === "docker" ? "https://localhost/api/v1/" : "http://127.0.0.1:9876/";
export const axiosLocalhost = axios.create( export const axiosLocalhost = axios.create(
{ {
baseURL: `http://127.0.0.1:9876/`, baseURL: baseURL,
withCredentials: true, withCredentials: true,
headers: { headers: {

View File

View File

@ -1,5 +1,5 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({

View File

@ -1,8 +1,32 @@
package global package global
const PathForCookies = "/" import (
const DomainForCookies = "127.0.0.1" "fmt"
"os"
)
var PathForCookies = "/"
var DomainForCookies = "127.0.0.1"
const SecureForCookies = false const SecureForCookies = false
const HttpOnlyForCookies = false const HttpOnlyForCookies = false
const GinWorkPath = "localhost:9876" // Change to 0.0.0.0 when docker this
const GinWorkPath = "127.0.0.1:9876"
func GetGinWorkPath() string {
if os.Getenv("DOMAIN") != "" {
DomainForCookies = os.Getenv("DOMAIN")
PathForCookies = "/api/v1/"
fmt.Println("DomainForCookies is", DomainForCookies)
}
if os.Getenv("ENV") == "docker" {
fmt.Println("GinWorkPath is docker 0.0.0.0:9876")
return "0.0.0.0:9876"
}
fmt.Println("GinWorkPath is local 127.0.0.1:9876")
return GinWorkPath
}

View File

@ -8,6 +8,9 @@ import (
"enshi/global" "enshi/global"
"enshi/routes" "enshi/routes"
"fmt" "fmt"
"io"
"log"
"os"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -27,12 +30,20 @@ func main() {
defer db_connection.Dbx_connection.Close(context.Background()) defer db_connection.Dbx_connection.Close(context.Background())
router := gin.Default() router := gin.Default()
f, err := os.OpenFile("gin.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
log.Fatal(err)
}
defer f.Close()
gin.DefaultWriter = io.MultiWriter(f)
if err := routes.SetupRotes(router); err != nil { if err := routes.SetupRotes(router); err != nil {
fmt.Println(err.Error()) fmt.Println(err.Error())
return return
} }
// Transaction // Test Transaction
tx, _ := db_connection.Dbx.Begin(context.Background()) tx, _ := db_connection.Dbx.Begin(context.Background())
defer tx.Rollback(context.Background()) defer tx.Rollback(context.Background())
@ -48,7 +59,7 @@ func main() {
return return
} }
router.Run(global.GinWorkPath) router.Run(global.GetGinWorkPath())
fmt.Printf("Hey!, %v", "you") fmt.Printf("Hey!, %v", "you")
} }

View File

@ -0,0 +1,118 @@
package middleware
import (
"enshi/ABAC/rules"
"fmt"
"github.com/gin-gonic/gin"
)
type WorkRule struct {
Rules []rules.RuleFunction
MustBeCompleted int
}
type Policy func(c *gin.Context) (bool, []error)
type RuleSets map[string]rules.RuleFunction
type RulesToCheck map[string]WorkRule
type MiddlewareProvider struct {
Policies map[string]Policy
}
func CreateRuleFunction(rulesToCheck []rules.RuleFunction, mustBeCompleted int) rules.RuleFunction {
return func(c *gin.Context) (bool, []error) {
isAllowed, errors := rules.CheckRules(
c,
rulesToCheck,
mustBeCompleted,
)
return isAllowed, errors
}
}
func CreatePolicy(ruleSets RuleSets) Policy {
return func(c *gin.Context) (bool, []error) {
targetAction, exists := c.Get("target")
if !exists {
return false, nil
}
for action, rule := range ruleSets {
if action == targetAction {
return rules.CheckRule(c, rule)
}
}
return false, nil
}
}
// Accepts
//
// ruleSetName -> `string` name of the policy(like old one "postPolicy" etc.)
//
// rulesToCheck -> map where keys like ["GET", "POST", etc.] and values are struct of type {rules: [list of rules to check], mustBeCompleted: how many rules must be completed from the list before}
func (m *MiddlewareProvider) RegisterPolicy(
ruleSetName string,
rulesToCheck RulesToCheck,
) error {
for k := range m.Policies {
if k == ruleSetName {
return fmt.Errorf("name: " + ruleSetName + " already exists")
}
}
newRuleSets := make(RuleSets)
for setName, workRule := range rulesToCheck {
newRuleFunction := CreateRuleFunction(workRule.Rules, workRule.MustBeCompleted)
newRuleSets[setName] = newRuleFunction
}
newPolicy := CreatePolicy(newRuleSets)
m.Policies[ruleSetName] = newPolicy
return nil
}
func (m *MiddlewareProvider) GetMiddleware(
policyName string,
) gin.HandlerFunc {
return func(c *gin.Context) {
validName := false
for key := range m.Policies {
if key == policyName {
validName = true
}
}
if !validName {
c.Abort()
fmt.Println("invalid policy name: " + policyName)
return
}
isAllowed, errors := m.Policies[policyName](c)
if rules.ShouldAbortRequest(c, isAllowed, errors) {
c.Abort()
return
}
c.Next()
}
}
func (m *MiddlewareProvider) InitMiddlewareProvider(policies map[string]RulesToCheck) {
for middlewareName, rulesToCheck := range policies {
m.RegisterPolicy(middlewareName, rulesToCheck)
}
}

View File

@ -0,0 +1,30 @@
package middleware
import (
"github.com/gin-gonic/gin"
)
const (
GET = "GET"
PUT = "PUT"
POST = "POST"
DELETE = "DELETE"
)
func TargetMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
switch c.Request.Method {
case "DELETE":
c.Set("target", DELETE)
case "PUT":
c.Set("target", PUT)
case "POST":
c.Set("target", POST)
case "GET":
c.Set("target", DELETE)
}
c.Next()
}
}

View File

@ -0,0 +1,159 @@
package routes
import (
globalrules "enshi/ABAC/GlobalRules"
"enshi/ABAC/rules"
"enshi/middleware"
)
const (
POST_MIDDLEWARE = "POST_MIDDLEWARE"
BLOG_MIDDLEWARE = "BLOG_MIDDLEWARE"
PROFILE_MIDDLEWARE = "PROFILE_MIDDLEWARE"
BOOKMARK_MIDDLEWARE = "BOOKMARK_MIDDLEWARE"
POST_BLOG_MIDDLEWARE = "POST_BLOG_MIDDLEWARE"
POST_VOTE_MIDDLEWARE = "POST_VOTE_MIDDLEWARE"
POST_VOTES_MIDDLEWARE = "POST_VOTES_MIDDLEWARE"
)
var MiddlewareProvider = middleware.MiddlewareProvider{
Policies: make(map[string]middleware.Policy),
}
var policiesToRegister = map[string]middleware.RulesToCheck{
POST_MIDDLEWARE: {
middleware.GET: {
Rules: make([]rules.RuleFunction, 0),
MustBeCompleted: rules.ALL_RULES_MUST_BE_COMPLETED,
},
middleware.POST: {
Rules: []rules.RuleFunction{
globalrules.AuthorizedRule,
},
MustBeCompleted: rules.ALL_RULES_MUST_BE_COMPLETED,
},
middleware.PUT: {
Rules: []rules.RuleFunction{
globalrules.AuthorizedRule,
globalrules.IsOwnerOfThePostRule,
},
MustBeCompleted: rules.ALL_RULES_MUST_BE_COMPLETED,
},
middleware.DELETE: {
Rules: []rules.RuleFunction{
globalrules.AuthorizedRule,
globalrules.IsOwnerOfThePostRule,
globalrules.IsAdminRule,
},
MustBeCompleted: 2,
},
},
BOOKMARK_MIDDLEWARE: {
middleware.GET: {
Rules: []rules.RuleFunction{
globalrules.AuthorizedRule,
},
MustBeCompleted: rules.ALL_RULES_MUST_BE_COMPLETED,
},
middleware.DELETE: {
Rules: []rules.RuleFunction{
globalrules.AuthorizedRule,
},
MustBeCompleted: rules.ALL_RULES_MUST_BE_COMPLETED,
},
middleware.POST: {
Rules: []rules.RuleFunction{
globalrules.AuthorizedRule,
},
MustBeCompleted: rules.ALL_RULES_MUST_BE_COMPLETED,
},
},
BLOG_MIDDLEWARE: {
middleware.GET: {
Rules: make([]rules.RuleFunction, 0),
MustBeCompleted: rules.ALL_RULES_MUST_BE_COMPLETED,
},
middleware.POST: {
Rules: []rules.RuleFunction{
globalrules.AuthorizedRule,
},
MustBeCompleted: rules.ALL_RULES_MUST_BE_COMPLETED,
},
middleware.PUT: {
Rules: []rules.RuleFunction{
globalrules.AuthorizedRule,
globalrules.IsOwnerOfTheBlogRule,
},
MustBeCompleted: rules.ALL_RULES_MUST_BE_COMPLETED,
},
middleware.DELETE: {
Rules: []rules.RuleFunction{
globalrules.AuthorizedRule,
globalrules.IsOwnerOfTheBlogRule,
globalrules.IsAdminRule,
},
MustBeCompleted: 2,
},
},
POST_VOTE_MIDDLEWARE: {
middleware.GET: {
Rules: []rules.RuleFunction{
globalrules.AuthorizedRule,
},
MustBeCompleted: rules.ALL_RULES_MUST_BE_COMPLETED,
},
middleware.POST: {
Rules: []rules.RuleFunction{
globalrules.AuthorizedRule,
},
MustBeCompleted: rules.ALL_RULES_MUST_BE_COMPLETED,
},
middleware.DELETE: {
Rules: make([]rules.RuleFunction, 0),
MustBeCompleted: rules.ALL_RULES_MUST_BE_COMPLETED,
},
},
POST_VOTES_MIDDLEWARE: {
middleware.GET: {
Rules: make([]rules.RuleFunction, 0),
MustBeCompleted: rules.ALL_RULES_MUST_BE_COMPLETED,
},
},
PROFILE_MIDDLEWARE: {
middleware.PUT: {
Rules: []rules.RuleFunction{
globalrules.AuthorizedRule,
},
MustBeCompleted: rules.ALL_RULES_MUST_BE_COMPLETED,
},
},
POST_BLOG_MIDDLEWARE: {
middleware.PUT: {
Rules: []rules.RuleFunction{
globalrules.AuthorizedRule,
globalrules.IsOwnerOfThePostRule,
globalrules.IsOwnerOfTheBlogRule,
},
MustBeCompleted: rules.ALL_RULES_MUST_BE_COMPLETED,
},
middleware.DELETE: {
Rules: []rules.RuleFunction{
globalrules.AuthorizedRule,
globalrules.IsOwnerOfThePostRule,
globalrules.IsOwnerOfTheBlogRule,
},
MustBeCompleted: rules.ALL_RULES_MUST_BE_COMPLETED,
},
},
}
func InitMiddlewareProvider() {
MiddlewareProvider.InitMiddlewareProvider(policiesToRegister)
}

View File

@ -43,7 +43,36 @@ func testAuth(c *gin.Context) {
} }
func SetupRotes(g *gin.Engine) error { func SetupRotes(g *gin.Engine) error {
InitMiddlewareProvider()
g.Use(middleware.CORSMiddleware()) g.Use(middleware.CORSMiddleware())
g.Use(middleware.TargetMiddleware())
testGroup := g.Group("/test/")
testGroup.Use(MiddlewareProvider.GetMiddleware(POST_MIDDLEWARE))
testGroup.GET(
"posts/:post-id",
postsRoutes.GetPost,
)
testGroup.GET(
"posts/random",
postsRoutes.GetRandomPost,
)
testGroup.PUT(
"posts/:post-id",
postsRoutes.UpdatePost,
)
testGroup.POST(
"posts",
postsRoutes.CreatePost,
)
testGroup.DELETE(
"posts/:post-id",
postsRoutes.DeletePost,
)
// Free group routes // Free group routes
freeGroup := g.Group("/") freeGroup := g.Group("/")

418
package-lock.json generated
View File

@ -1,418 +0,0 @@
{
"name": "Enshi",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-toast": "^1.2.2",
"interweave": "^13.1.0"
}
},
"node_modules/@radix-ui/primitive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
"integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==",
"license": "MIT"
},
"node_modules/@radix-ui/react-collection": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz",
"integrity": "sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-context": "1.1.0",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-slot": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-context": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz",
"integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz",
"integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-context": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
"integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.1.tgz",
"integrity": "sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.0",
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-escape-keydown": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-icons": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz",
"integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==",
"license": "MIT",
"peerDependencies": {
"react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/@radix-ui/react-portal": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz",
"integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-presence": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz",
"integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-primitive": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz",
"integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toast": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.2.tgz",
"integrity": "sha512-Z6pqSzmAP/bFJoqMAston4eSNa+ud44NSZTiZUmUen+IOZ5nBY8kzuU5WDBVyFXPtcW6yUalOHsxM/BP6Sv8ww==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.0",
"@radix-ui/react-collection": "1.1.0",
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.1",
"@radix-ui/react-portal": "1.1.2",
"@radix-ui/react-presence": "1.1.1",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-controllable-state": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0",
"@radix-ui/react-visually-hidden": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
"integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz",
"integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-callback-ref": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-escape-keydown": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
"integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-callback-ref": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz",
"integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-visually-hidden": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.0.tgz",
"integrity": "sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/interweave": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/interweave/-/interweave-13.1.0.tgz",
"integrity": "sha512-JIDq0+2NYg0cgL7AB26fBcV0yZdiJvPDBp+aF6k8gq6Cr1kH5Gd2/Xqn7j8z+TGb8jCWZn739jzalCz+nPYwcA==",
"license": "MIT",
"dependencies": {
"escape-html": "^1.0.3"
},
"funding": {
"type": "ko-fi",
"url": "https://ko-fi.com/milesjohnson"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"license": "MIT",
"peer": true
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
},
"peerDependencies": {
"react": "^18.3.1"
}
},
"node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
}
}
}
}

View File

@ -1,7 +0,0 @@
{
"dependencies": {
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-toast": "^1.2.2",
"interweave": "^13.1.0"
}
}