fix: concurrency

This commit is contained in:
2025-11-19 12:26:37 -07:00
parent e1396e2d24
commit 80e57eaa2b
19 changed files with 175 additions and 60 deletions

Binary file not shown.

View File

@@ -3,7 +3,6 @@ package middleware
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"io"
"log" "log"
"net/http" "net/http"
"net/url" "net/url"
@@ -44,7 +43,7 @@ func TokenIsNotExpired(access_token string) bool {
func GetUserProfile(context *gin.Context) { func GetUserProfile(context *gin.Context) {
session := sessions.Default(context) session := sessions.Default(context)
access_token := session.Get("access_token") /* access_token := session.Get("access_token")
user_profile_client := http.Client{} user_profile_client := http.Client{}
user_profile_url, err := url.Parse("https://" + os.Getenv("AUTH0_DOMAIN") + os.Getenv("AUTH0_USER_INFO_ENDPOINT")) user_profile_url, err := url.Parse("https://" + os.Getenv("AUTH0_DOMAIN") + os.Getenv("AUTH0_USER_INFO_ENDPOINT"))
@@ -71,10 +70,15 @@ func GetUserProfile(context *gin.Context) {
} }
defer user_profile_response.Body.Close() defer user_profile_response.Body.Close()
user_profile_bytes, _ := io.ReadAll(user_profile_response.Body) user_profile_bytes, _ := io.ReadAll(user_profile_response.Body) */
var user_profile dto.UserProfileResponse var user_profile dto.UserProfileResponse
json.Unmarshal(user_profile_bytes, &user_profile) profile_session := session.Get("profile").(map[string]interface{})
user_profile.Sub = profile_session["sub"].(string)
user_profile.Email = profile_session["email"].(string)
user_profile.Verified = profile_session["email_verified"].(bool)
user_profile.PictureUrl = profile_session["picture"].(string)
user_profile.Nickname = profile_session["nickname"].(string)
user_profile.Updated_at = profile_session["updated_at"].(string)
context.Set("user_profile", user_profile) context.Set("user_profile", user_profile)
context.Next() context.Next()
} }

View File

@@ -181,6 +181,8 @@ func GetCurrentAuthenticatedUser(pool *pgxpool.Pool) gin.HandlerFunc {
user_profile, _ := ctx.Get("user_profile") user_profile, _ := ctx.Get("user_profile")
log.Printf("%s", user_profile.(dto.UserProfileResponse).Sub)
sub_id := user_profile.(dto.UserProfileResponse).Sub sub_id := user_profile.(dto.UserProfileResponse).Sub
var user dto.UserResponse var user dto.UserResponse

View File

@@ -3,9 +3,9 @@ package dto
// User response for exposing to the front-end // User response for exposing to the front-end
// :3 // :3
type UserResponse struct { type UserResponse struct {
Id int `json: "id"` Id int
Name string `json: "name"` Name string
JobPosition string `json: "job_position"` JobPosition string
Active bool `json: "active"` Active bool
Admin bool `json: "admin"` Admin bool
} }

View File

@@ -64,6 +64,7 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mitchellh/mapstructure v1.5.0
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect

View File

@@ -135,6 +135,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=

View File

@@ -99,6 +99,7 @@ func main() {
router.GET("/item/history", corsmiddleware.CORSMiddleware, user_authenticated, middleware.GetUserProfile, user_in_db, user_active, user_is_admin, controllers.GetItemHistory(pool)) router.GET("/item/history", corsmiddleware.CORSMiddleware, user_authenticated, middleware.GetUserProfile, user_in_db, user_active, user_is_admin, controllers.GetItemHistory(pool))
router.POST("/position/create", corsmiddleware.CORSMiddleware, user_authenticated, middleware.GetUserProfile, user_in_db, user_active, user_is_admin, controllers.CreatePosition(pool)) router.POST("/position/create", corsmiddleware.CORSMiddleware, user_authenticated, middleware.GetUserProfile, user_in_db, user_active, user_is_admin, controllers.CreatePosition(pool))
router.POST("/item/create", corsmiddleware.CORSMiddleware, user_authenticated, middleware.GetUserProfile, user_in_db, user_active, user_is_admin, controllers.CreateItem(pool)) router.POST("/item/create", corsmiddleware.CORSMiddleware, user_authenticated, middleware.GetUserProfile, user_in_db, user_active, user_is_admin, controllers.CreateItem(pool))
router.OPTIONS("/item/create", corsmiddleware.CORSMiddleware)
router.POST("/order/create", corsmiddleware.CORSMiddleware, user_authenticated, middleware.GetUserProfile, user_in_db, user_active, controllers.CreateOrder(pool)) router.POST("/order/create", corsmiddleware.CORSMiddleware, user_authenticated, middleware.GetUserProfile, user_in_db, user_active, controllers.CreateOrder(pool))
router.OPTIONS("/order/create", corsmiddleware.CORSMiddleware) router.OPTIONS("/order/create", corsmiddleware.CORSMiddleware)

View File

@@ -54,7 +54,8 @@ FROM
ordr_user ordr_user
LEFT JOIN ordr_position LEFT JOIN ordr_position
ON job_position = ordr_position.id ON job_position = ordr_position.id
AND ordr_user.sub_id = $1; WHERE
ordr_user.sub_id = $1;
` `
const USER_SET_POSITION string = ` const USER_SET_POSITION string = `

View File

@@ -20,10 +20,7 @@ export const CreateItem = async (query: CreateItemQuery): Promise<ItemPriceRespo
item_price: query.item_price item_price: query.item_price
}) })
const res = await axios.post(process.env.API_URL + `/item/create?${queryParams.toString()}`) const res = await axios.post(process.env.NEXT_PUBLIC_API_URL + `/item/create?${queryParams.toString()}`, {}, {withCredentials: true})
if (res.data.Location) {
window.location.href = res.data.Location
}
return res.data return res.data
} }

View File

@@ -6,7 +6,7 @@ import {OrderTableQuery} from '../request/GetOrderTableRequest'
axios.interceptors.response.use(response => { axios.interceptors.response.use(response => {
return response; return response;
}, async error => { }, async error => {
if (error.response.status === 401) { if (error.response?.status === 401) {
const resp = await axios.get(process.env.NEXT_PUBLIC_API_URL + "/auth/login", {withCredentials: true}) const resp = await axios.get(process.env.NEXT_PUBLIC_API_URL + "/auth/login", {withCredentials: true})
window.location.href = resp.data.Location window.location.href = resp.data.Location
} }

View File

@@ -9,12 +9,10 @@ export const GetCurrentUser = async (): Promise<UserResponse | undefined> => {
console.log(process.env.NEXT_PUBLIC_API_URL + "/user/current") console.log(process.env.NEXT_PUBLIC_API_URL + "/user/current")
const res = await axios.get(process.env.NEXT_PUBLIC_API_URL + "/user/current", { const res = await axios.get(process.env.NEXT_PUBLIC_API_URL + "/user/current", {
maxRedirects: 0, maxRedirects: 0,
withCredentials: true,
validateStatus: (status) => { validateStatus: (status) => {
return status >= 200 && status < 400 return status >= 200 && status < 400
}}); }});
if(res.data.Location) {
window.location.href = res.data.Location
}
return res.data; return res.data;
}; };

View File

@@ -5,6 +5,8 @@ import { ItemHistoryTable } from "./ItemHistoryTable"
import useAsyncEffect from "use-async-effect" import useAsyncEffect from "use-async-effect"
import { useItemStore } from "../providers/ItemsProvider" import { useItemStore } from "../providers/ItemsProvider"
import { Mutex } from "async-mutex" import { Mutex } from "async-mutex"
import { useCurrentAuthenticatedUserStore } from "../providers"
import { useShallow } from "zustand/shallow"
type ItemTableListRowProps = { type ItemTableListRowProps = {
item: ItemPriceResponse item: ItemPriceResponse
@@ -56,15 +58,25 @@ export const ItemTableListRow = ({item}: ItemTableListRowProps) => {
const [shouldPushNewItemPrice, setShouldPushNewItemPrice] = useState<boolean>(false) const [shouldPushNewItemPrice, setShouldPushNewItemPrice] = useState<boolean>(false)
const authUserStore = useCurrentAuthenticatedUserStore(useShallow((state) => state))
useAsyncEffect(async () => { useAsyncEffect(async () => {
if(shouldPushNewItemPrice) if(shouldPushNewItemPrice && authUserStore.Admin)
{ {
const release = await itemTableListRowMutex.acquire() const release = await itemTableListRowMutex.acquire()
setShouldPushNewItemPrice(false) setShouldPushNewItemPrice(false)
await itemStore.setItemPrice(item.ItemId, newItemPrice) await itemStore.setItemPrice(item.ItemId, newItemPrice)
await release() await release()
} }
}, [shouldPushNewItemPrice]) }, [shouldPushNewItemPrice, authUserStore])
useAsyncEffect(async () => {
if (authUserStore.Id === -1) {
const release = await itemTableListRowMutex.acquire()
await authUserStore.sync()
await release()
}
}, [authUserStore])
return ( return (
<li> <li>
@@ -79,7 +91,7 @@ export const ItemTableListRow = ({item}: ItemTableListRowProps) => {
price: {Math.trunc(item.ItemPrice * 100) / 100} price: {Math.trunc(item.ItemPrice * 100) / 100}
</ItemFieldContainer> </ItemFieldContainer>
</ItemOverviewContainer> </ItemOverviewContainer>
{shouldShowDetails && ( {shouldShowDetails && authUserStore.Admin && (
<> <>
<ItemDetailsContainer> <ItemDetailsContainer>
<h1 className="text-xl">Set Item Price</h1> <h1 className="text-xl">Set Item Price</h1>

View File

@@ -1,11 +1,29 @@
'use client'
import Link from "next/link" import Link from "next/link"
import { useCurrentAuthenticatedUserStore } from "../providers"
import useAsyncEffect from "use-async-effect"
import { Mutex } from "async-mutex"
import { useShallow } from "zustand/shallow"
const navBarMutex = new Mutex()
export const NavBar = () => { export const NavBar = () => {
const authUserStore = useCurrentAuthenticatedUserStore(useShallow((state) => state))
useAsyncEffect(async () => {
if(authUserStore.Id === -1) {
const release = await navBarMutex.acquire()
await authUserStore.sync()
await release()
}
}, [authUserStore])
return ( return (
<nav> <nav>
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">
<Link className=" pl-7 pr-7 pt-3 pb-3 hover:bg-purple-950" href="/orders/0/0">Orders</Link> <Link className=" pl-7 pr-7 pt-3 pb-3 hover:bg-purple-950" href="/orders/0/0">Orders</Link>
<Link className=" pl-7 pr-7 pt-3 pb-3 hover:bg-purple-950" href="/users/0">Users</Link> {authUserStore.Admin && (<Link className=" pl-7 pr-7 pt-3 pb-3 hover:bg-purple-950" href="/users/0">Users</Link>)}
<Link className=" pl-7 pr-7 pt-3 pb-3 hover:bg-purple-950" href="/items">Items</Link> <Link className=" pl-7 pr-7 pt-3 pb-3 hover:bg-purple-950" href="/items">Items</Link>
</div> </div>
</nav> </nav>

View File

@@ -1,3 +1,4 @@
'for client'
import { useItemStore } from "../providers/ItemsProvider" import { useItemStore } from "../providers/ItemsProvider"
import { useState } from "react" import { useState } from "react"
import useAsyncEffect from "use-async-effect" import useAsyncEffect from "use-async-effect"

View File

@@ -2,9 +2,9 @@
import { useOrderStore } from "../providers/OrderProvider" import { useOrderStore } from "../providers/OrderProvider"
import { useShallow } from "zustand/shallow" import { useShallow } from "zustand/shallow"
import useAsyncEffect from "use-async-effect" import useAsyncEffect from "use-async-effect"
import { OrderTableRow } from "./OrderTableRow"
import { Mutex } from "async-mutex" import { Mutex } from "async-mutex"
import styled from "styled-components" import styled from "styled-components"
import { OrderTableRow } from "./OrderTableRow"
type OrderTableProps = { type OrderTableProps = {
page: number, page: number,

View File

@@ -1,3 +1,4 @@
'for client'
import { useState } from "react" import { useState } from "react"
import styled from "styled-components" import styled from "styled-components"
import { OrderItemTable } from "./OrderItemTable" import { OrderItemTable } from "./OrderItemTable"

View File

@@ -4,6 +4,8 @@ import { useUserStore } from "../providers/UsersProvider"
import useAsyncEffect from "use-async-effect" import useAsyncEffect from "use-async-effect"
import styled from "styled-components" import styled from "styled-components"
import { useRef, useState } from "react" import { useRef, useState } from "react"
import { useCurrentAuthenticatedUserStore } from "../providers"
import { Mutex } from "async-mutex"
type UserTableProps = { type UserTableProps = {
page: number page: number
@@ -35,6 +37,8 @@ const UserTableRow = styled.tr`
` `
const userTableMutex = new Mutex()
export const UserTable = ({page}: UserTableProps) => { export const UserTable = ({page}: UserTableProps) => {
const userStore = useUserStore(useShallow((state) => ({ const userStore = useUserStore(useShallow((state) => ({
@@ -43,47 +47,59 @@ export const UserTable = ({page}: UserTableProps) => {
console.log(page) console.log(page)
const [callLock, setCallLock] = useState<boolean>(false) const [callLock, setCallLock] = useState<boolean>(false)
const authUserStore = useCurrentAuthenticatedUserStore(useShallow((state) => state))
console.log(authUserStore)
const callLockRef = useRef(callLock) const callLockRef = useRef(callLock)
useAsyncEffect(async () => { useAsyncEffect(async () => {
if(!callLockRef.current) { if(!callLockRef.current && authUserStore.Admin) {
callLockRef.current = true callLockRef.current = true
setCallLock(true) setCallLock(true)
await userStore.sync(page) await userStore.sync(page)
callLockRef.current = false callLockRef.current = false
setCallLock(false) setCallLock(false)
} }
}, [page]) }, [page, authUserStore.Id, authUserStore.Name])
useAsyncEffect(async () => {
if(authUserStore.Id === -1) {
const release = await userTableMutex.acquire()
await authUserStore.sync()
console.log(authUserStore)
await release()
}
}, [authUserStore.Id])
console.log(userStore.tableUsers) console.log(userStore.tableUsers)
return ( return authUserStore.Admin && (
<UserTableStyle> <UserTableStyle>
<UserTableHead> <UserTableHead>
<UserTH>id</UserTH> <tr>
<UserTH>name</UserTH> <UserTH>id</UserTH>
<UserTH>position</UserTH> <UserTH>name</UserTH>
<UserTH>active</UserTH> <UserTH>position</UserTH>
<UserTH>admin</UserTH> <UserTH>active</UserTH>
</UserTableHead> <UserTH>admin</UserTH>
<UserTableBody> </tr>
{userStore.tableUsers.map((u) => ( </UserTableHead>
<UserTableRow key={u.Id}> <UserTableBody>
<UserTableItem>{u.Id}</UserTableItem> {userStore.tableUsers.map((u) => (
<UserTableItem>{u.Name}</UserTableItem> <UserTableRow key={u.Id}>
<UserTableItem>{u.JobPosition}</UserTableItem> <UserTableItem>{u.Id}</UserTableItem>
<UserTableItem><input type="checkbox" defaultValue={u.Active ? "yes" : "no"} onChange={async (e) => { <UserTableItem>{u.Name}</UserTableItem>
if(u.Active) <UserTableItem>{u.JobPosition}</UserTableItem>
await userStore.deactivateUser(u.Id) <UserTableItem><input type="checkbox" onChange={async (e) => {
else if(u.Active)
await userStore.activateUser(u.Id) await userStore.deactivateUser(u.Id)
}}/></UserTableItem> else
<UserTableItem><input type="checkbox" value={u.Admin ? "yes" : "no"} onChange={async (e) => { await userStore.activateUser(u.Id)
if(u.Admin) }} checked={u.Active}/></UserTableItem>
await userStore.demoteUser(u.Id) <UserTableItem><input type="checkbox" onChange={async (e) => {
else if(u.Admin)
await userStore.promoteUser(u.Id) await userStore.demoteUser(u.Id)
}}/></UserTableItem> else
</UserTableRow>))} await userStore.promoteUser(u.Id)
</UserTableBody> }} checked={u.Admin}/></UserTableItem>
</UserTableStyle> </UserTableRow>))}
</UserTableBody>
</UserTableStyle>
) )
} }

View File

@@ -1,10 +1,71 @@
'use client' 'use client'
import { useState } from "react"
import { ItemTableList } from "../components/ItemTableList" import { ItemTableList } from "../components/ItemTableList"
import useAsyncEffect from "use-async-effect"
import { Mutex } from "async-mutex"
import { useItemStore } from "../providers/ItemsProvider"
import { useCurrentAuthenticatedUserStore } from "../providers"
import { useShallow } from "zustand/shallow"
const itemPageApiMutex = new Mutex()
const Items = () => { const Items = () => {
const [itemName, setItemName] = useState("")
const [itemPrice, setItemPrice] = useState(0)
const [inSeason, setInSeason] = useState(false)
const [shouldSubmitDetails, setShouldSubmitDetails] = useState(false)
const itemStore = useItemStore((state) => state)
const authUserStore = useCurrentAuthenticatedUserStore(useShallow((state) => state))
useAsyncEffect(async () => {
if(shouldSubmitDetails && authUserStore.Admin)
{
setShouldSubmitDetails(false)
const release = await itemPageApiMutex.acquire()
await itemStore.createItem(itemName, inSeason, itemPrice)
await release()
}
}, [shouldSubmitDetails])
useAsyncEffect(async () => {
if(authUserStore.Id === -1) {
const release = await itemPageApiMutex.acquire()
await authUserStore.sync()
await release()
}
}, [authUserStore])
return ( return (
<ItemTableList /> <>
<ItemTableList />
{authUserStore.Admin &&
<>
<h1 className="text-xl font-bold">Create Item</h1>
<input placeholder="item name" onChange={(e) => {
setItemName(e.currentTarget.value)
}}/>
<input placeholder="item price" onChange={(e) => {
const int_value = parseInt(e.currentTarget.value)
if(!Number.isNaN(int_value))
setItemPrice(int_value)
}}/>
In Season
<input className="ml-3" type="checkbox" onChange={(e) => {
setInSeason(e.target.checked)
}} />
<br />
<button onClick={() => {
setShouldSubmitDetails(true)
}} className="border p-1 mt-3 hover:bg-white hover:text-black">Create Item</button>
</>
}
</>
) )
} }

View File

@@ -16,8 +16,8 @@ export const useCurrentAuthenticatedUserStore = create<UserResponse & UserAction
sync: async () => { sync: async () => {
const authUser = await GetCurrentUser() const authUser = await GetCurrentUser()
set((state) => ({ set((state) => ({
...authUser, ...state,
...state ...authUser
})) }))
return authUser return authUser
}, },