Skip to content

Commit

Permalink
feat(auth): add auth options
Browse files Browse the repository at this point in the history
  • Loading branch information
kangfenmao committed Dec 5, 2023
1 parent 0f459f2 commit 1869b83
Show file tree
Hide file tree
Showing 30 changed files with 420 additions and 46 deletions.
22 changes: 15 additions & 7 deletions backend/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@ package api
import (
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"vortexnotes/backend/api/auth"
"vortexnotes/backend/api/configuration"
"vortexnotes/backend/api/middlewares"
"vortexnotes/backend/api/notes"
"vortexnotes/backend/api/website"
"vortexnotes/backend/config"
)

func Start() {
server := gin.Default()
server.Use(cors.Default())
corsConfig := cors.DefaultConfig()
corsConfig.AllowAllOrigins = true
corsConfig.AllowHeaders = []string{"Origin", "Content-Length", "Content-Type", "Authorization"}
server.Use(cors.New(corsConfig))

server.GET("/", website.ServeRoot)
server.GET("/assets/*filepath", website.ServeAssets)
Expand All @@ -20,12 +26,14 @@ func Start() {

api := server.Group("/api")
{
api.GET("/notes", notes.ListAllNotes)
api.GET("/notes/:id", notes.GetNote)
api.DELETE("/notes/:id", notes.DeleteNote)
api.PATCH("/notes/:id", notes.UpdateNote)
api.POST("/notes/new", notes.CreateNote)
api.GET("/search", notes.SearchNotes)
api.GET("/config", configuration.Config)
api.POST("/auth", auth.Auth)
api.GET("/search", middlewares.HasPermission("show"), notes.SearchNotes)
api.GET("/notes", middlewares.HasPermission("show"), notes.ListAllNotes)
api.GET("/notes/:id", middlewares.HasPermission("show"), notes.GetNote)
api.DELETE("/notes/:id", middlewares.HasPermission("delete"), notes.DeleteNote)
api.PATCH("/notes/:id", middlewares.HasPermission("edit"), notes.UpdateNote)
api.POST("/notes/new", middlewares.HasPermission("create"), notes.CreateNote)
}

server.SetTrustedProxies(nil)
Expand Down
36 changes: 36 additions & 0 deletions backend/api/auth/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package auth

import (
"github.com/gin-gonic/gin"
"net/http"
"os"
)

func Auth(c *gin.Context) {
passcode := os.Getenv("VORTEXNOTES_PASSCODE")

type RequestData struct {
Passcode string `json:"passcode"`
}

var requestData RequestData

if err := c.BindJSON(&requestData); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"message": err.Error()})
return
}

if requestData.Passcode != passcode {
c.JSON(http.StatusBadRequest, gin.H{"message": "ePasscode Invalid"})
return
}

authScopes := os.Getenv("VORTEXNOTES_AUTH_SCOPE")
if authScopes == "" {
authScopes = "show,create,edit,delete"
}

c.JSON(http.StatusOK, gin.H{
"auth_scope": authScopes,
})
}
26 changes: 26 additions & 0 deletions backend/api/configuration/configuration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package configuration

import (
"github.com/gin-gonic/gin"
"net/http"
"os"
)

func Config(c *gin.Context) {
needAuthScopes := os.Getenv("VORTEXNOTES_AUTH_SCOPE")
passcode := os.Getenv("VORTEXNOTES_PASSCODE")
auth := "none"

if needAuthScopes == "" {
needAuthScopes = "show,create,edit,delete"
}

if passcode != "" {
auth = "passcode"
}

c.JSON(http.StatusOK, gin.H{
"auth_scope": needAuthScopes,
"auth": auth,
})
}
35 changes: 35 additions & 0 deletions backend/api/middlewares/middlewares.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package middlewares

import (
"github.com/gin-gonic/gin"
"net/http"
"os"
"strings"
)

func HasPermission(scope string) gin.HandlerFunc {
return func(c *gin.Context) {
expectedPasscode := os.Getenv("VORTEXNOTES_PASSCODE")
authorizationHeader := c.GetHeader("Authorization")
passcode := strings.TrimPrefix(authorizationHeader, "Bearer ")

needAuthScopes := os.Getenv("VORTEXNOTES_AUTH_SCOPE")
if needAuthScopes == "" {
needAuthScopes = "show,create,edit,delete"
}

if expectedPasscode == "" {
c.Next()
return
}

if strings.Contains(needAuthScopes, scope) {
if passcode != expectedPasscode {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
return
}
}

c.Next()
}
}
4 changes: 2 additions & 2 deletions backend/api/notes/notes.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func CreateNote(c *gin.Context) {
var requestData RequestData

if err := c.BindJSON(&requestData); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"message": err.Error()})
return
}

Expand Down Expand Up @@ -156,7 +156,7 @@ func UpdateNote(c *gin.Context) {
err := indexer.DeleteNote(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"error": err,
"message": err,
})
return
}
Expand Down
9 changes: 9 additions & 0 deletions backend/utils/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,12 @@ func FileExists(filePath string) bool {
_, err := os.Stat(filePath)
return !os.IsNotExist(err)
}

func ArrayContainsString(arr []string, target string) bool {
for _, str := range arr {
if str == target {
return true
}
}
return false
}
3 changes: 3 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ services:
vortexnotes:
container_name: vortexnotes
image: vortexnotes:latest
environment:
VORTEXNOTES_PASSCODE: 123456
VORTEXNOTES_AUTH_SCOPE: create,edit,delete
ports:
- "0.0.0.0:7701:7701"
volumes:
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import '@uiw/react-markdown-preview/markdown.css'
import NotesScreen from '@/screens/NotesScreen'
import TopViewContainer from '@/components/TopView.tsx'
import Root from '@/components/Root.tsx'
import AuthScreen from '@/screens/AuthScreen'
import InitScreen from '@/screens/InitScreen'

const router = createBrowserRouter([
{
Expand Down Expand Up @@ -41,6 +43,14 @@ const router = createBrowserRouter([
element: <NewNoteScreen />
}
]
},
{
path: '/init',
element: <InitScreen />
},
{
path: '/auth',
element: <AuthScreen />
}
])

Expand Down
62 changes: 55 additions & 7 deletions frontend/src/components/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Link, useLocation, useNavigate, useSearchParams } from 'react-router-dom'
import React, { useEffect, useState } from 'react'
import useTheme from '@/hooks/useTheme.ts'
import { onSearch as search } from '@/utils'
import { hasPasscode, hasPermission, onSearch as search } from '@/utils'

interface Props {}

Expand All @@ -20,6 +20,10 @@ const Navbar: React.FC<Props> = () => {
setInput(keywords)
}, [keywords])

const onLogout = () => {
localStorage.removeItem('vortexnotes_passcode')
}

const navbarBg = isHome ? '' : 'bg-white dark:bg-black dark:bg-transparent-20'
const navbarBorder = isHome
? 'border-b border-transparent'
Expand Down Expand Up @@ -92,12 +96,56 @@ const Navbar: React.FC<Props> = () => {
<path d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z" />
</svg>
</label>
<Link to="/new">
<div className="flex flex-row items-center ml-5 opacity-60 hover:opacity-80 transition-opacity">
<i className="iconfont icon-add-circle text-black dark:text-white text-2xl"></i>
<button className="text-black dark:text-white ml-1">New Note</button>
</div>
</Link>
{hasPermission('create') && (
<Link to="/new">
<button className="flex flex-row items-center ml-5 opacity-60 hover:opacity-80 transition-opacity">
<i className="iconfont icon-add-circle text-black dark:text-white text-2xl"></i>
<span className="text-black dark:text-white ml-1" style={{ marginTop: '-2px' }}>
New Note
</span>
</button>
</Link>
)}
<div className="dropdown dropdown-end">
<button className="flex flex-row items-center ml-5 opacity-60 hover:opacity-80 transition-opacity">
<i className="iconfont icon-menu text-black dark:text-white text-2xl"></i>
<span className="text-black dark:text-white ml-1" style={{ marginTop: '-2px' }}>
Menu
</span>
</button>
<ul
tabIndex={0}
className="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-btn w-36 mt-2">
<li>
<Link to="/notes">
<div className="flex flex-row items-center opacity-60">
<i className="iconfont icon-Notes text-black dark:text-white text-2xl"></i>
<span className="text-black dark:text-white ml-2">All Notes</span>
</div>
</Link>
</li>
{!hasPasscode() && (
<li>
<Link to="/auth">
<div className="flex flex-row items-center opacity-60">
<i className="iconfont icon-user text-black dark:text-white text-2xl"></i>
<span className="text-black dark:text-white ml-2">Login</span>
</div>
</Link>
</li>
)}
{hasPasscode() && (
<li>
<Link to="" onClick={onLogout}>
<div className="flex flex-row items-center opacity-60">
<i className="iconfont icon-logout text-black dark:text-white text-2xl"></i>
<span className="text-black dark:text-white ml-2">Logout</span>
</div>
</Link>
</li>
)}
</ul>
</div>
</div>
</div>
</div>
Expand Down
21 changes: 19 additions & 2 deletions frontend/src/components/Root.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,25 @@
import { Outlet } from 'react-router-dom'
import { Outlet, useNavigate } from 'react-router-dom'
import Navbar from '@/components/Navbar.tsx'
import React from 'react'
import React, { useEffect } from 'react'
import { fetchConfig } from '@/utils/api.ts'

const Root: React.FC = () => {
const navigate = useNavigate()

useEffect(() => {
const onLoad = async () => {
if (!localStorage.vortexnotes_auth_scope) {
navigate('/init', { replace: true })
} else {
await fetchConfig()
}
}

window.addEventListener('load', onLoad)

return () => window.removeEventListener('load', onLoad)
}, [navigate])

return (
<div className="flex flex-col flex-1 w-full h-full">
<Navbar />
Expand Down
31 changes: 27 additions & 4 deletions frontend/src/config/http.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,30 @@
import axios from 'axios'

const http = axios.create({
baseURL: location.origin.replace('7702', '7701') + '/api/'
})
export const getAxiosInstance = () => {
const instance = axios.create({
baseURL: location.origin.replace('7702', '7701') + '/api/',
headers: {
Authorization: localStorage.vortexnotes_passcode
? 'Bearer ' + localStorage.vortexnotes_passcode
: undefined
}
})

window.$http = http
instance.interceptors.response.use(
response => response,
error => {
if (error.response) {
if (error.response.status === 401) {
localStorage.removeItem('vortexnotes_passcode')
localStorage.removeItem('vortexnotes_auth_scope')
location.href = '/auth'
}
}
return Promise.reject(error)
}
)

return instance
}

window.$http = getAxiosInstance()
13 changes: 13 additions & 0 deletions frontend/src/hooks/usePermissionCheckEffect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useEffect } from 'react'
import { hasPermission } from '@/utils'
import { useNavigate } from 'react-router-dom'

export default function usePermissionCheckEffect(scope: string) {
const navigate = useNavigate()

useEffect(() => {
if (!hasPermission(scope)) {
navigate('/auth')
}
}, [navigate, scope])
}
2 changes: 1 addition & 1 deletion frontend/src/index.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@import url('https://fonts.googleapis.com/css2?family=Major+Mono+Display&family=Readex+Pro:wght@200;300;400;500;600;700&display=swap');
@import url('https://at.alicdn.com/t/c/font_4344275_jt0ar688xla.css');
@import url('https://at.alicdn.com/t/c/font_4344275_ibfdwgo51bi.css');

@tailwind base;
@tailwind components;
Expand Down
Loading

0 comments on commit 1869b83

Please sign in to comment.