Compare commits

...

15 Commits

Author SHA1 Message Date
718dce60ef feat: API Dockerfile
All checks were successful
ordr deploy / build (push) Successful in -47s
2025-11-22 19:48:33 +00:00
311f55e68e feat: github workflow
Some checks failed
ordr deploy / build (push) Failing after -1m18s
2025-11-22 12:45:55 -07:00
72f073fad6 feat: github workflow
Some checks failed
ordr deploy / build (push) Failing after -1m18s
2025-11-22 12:45:25 -07:00
c1bbbd7d8c feat: github workflow
Some checks failed
ordr deploy / build (push) Failing after -1m21s
2025-11-22 12:44:32 -07:00
1bf12c7fb3 feat: github workflow
Some checks failed
ordr deploy / build (push) Failing after -1m21s
2025-11-22 12:43:39 -07:00
7db1f2dcf2 feat: github workflow
Some checks failed
ordr deploy / build (push) Failing after -24s
2025-11-22 12:40:55 -07:00
b408570fdf feat: github workflow 2025-11-22 12:39:43 -07:00
1b695e44f2 feat: github workflow 2025-11-22 12:36:00 -07:00
9088b1f479 fix: dockerfile ui 2025-11-21 19:00:15 +00:00
f15b8683a6 feat: dockerfile for api 2025-11-21 11:57:29 -07:00
2f818c34b0 fix: gitignore 2025-11-21 17:39:10 +00:00
6cdce103a5 feat: dockerfile 2025-11-21 10:31:30 -07:00
3382c0ef4e fix: item history page now live updates 2025-11-19 21:08:18 -07:00
17f8f18574 fix: CORS stuff 2025-11-19 18:52:59 -07:00
0100b8b6c5 fix: CORS Origin 2025-11-19 18:38:17 -07:00
17 changed files with 938 additions and 1061 deletions

View File

@@ -0,0 +1,24 @@
name: ordr deploy
run-name: ${{ gitea.actor }} is deploying ordr
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Create SSH key
run: |
mkdir -p ~/.ssh/
echo "$SSH_PRIVATE_KEY" > ~/private.key
sudo chmod 600 ~/private.key
echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
shell: bash
env:
SSH_PRIVATE_KEY: ${{secrets.SSH_PRIVATE_KEY}}
SSH_KNOWN_HOSTS: ${{secrets.SSH_KNOWN_HOSTS}}
- name: Up the services
run: |
ssh -i ~/private.key ada@conway.engineer "cd /home/shared/ordr/ordr && git pull && cd .. && echo ${{secrets.SSH_PASSWORD}} | sudo -S docker compose down && echo ${{secrets.SSH_PASSWORD}} | sudo -S docker compose up --build --force-recreate --detach"

7
.gitignore vendored
View File

@@ -18,4 +18,9 @@
*.tmp *.tmp
# dotenv # dotenv
.env .env
.env.local
ordr-api
./nohup.out

34
api/Dockerfile Normal file
View File

@@ -0,0 +1,34 @@
# Stage 1: Build the application
FROM golang:1.25.3-alpine AS base
FROM base AS builder
COPY . /app
WORKDIR /app
# Copy go.mod and go.sum first to leverage Docker's caching
ENV GOPROXY=direct
RUN apk add git
RUN go mod download
# Build the Go application
# CGO_ENABLED=0 disables Cgo for a fully static binary
# -a links all packages statically
# -installsuffix cgo_non_shared avoids issues with shared libraries
RUN CGO_ENABLED=0 GOOS=linux go build
RUN ls -al
# Stage 2: Create the final lean image
FROM base
WORKDIR /root/
# Copy the built binary from the builder stage
COPY --from=builder /app/ordr-api .
COPY --from=builder /app/.env .
# Expose the port your Gin application listens on (e.g., 8080)
EXPOSE 8080
# Command to run the application
ENV GIN_MODE=release
CMD ["./ordr-api"]

BIN
api/__debug_bin2336935653 Executable file

Binary file not shown.

View File

@@ -2,13 +2,14 @@ package corsmiddleware
import ( import (
"log" "log"
"os"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
func CORSMiddleware(c *gin.Context) { func CORSMiddleware(c *gin.Context) {
log.Printf("%s", c.Request.Method) log.Printf("%s", c.Request.Method)
c.Writer.Header().Set("Access-Control-Allow-Origin", "http://localhost:3000") c.Writer.Header().Set("Access-Control-Allow-Origin", os.Getenv("ALLOWED_ORIGIN"))
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE") c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")

View File

@@ -39,7 +39,7 @@ func main() {
// Configure CORS middleware // Configure CORS middleware
r.NoRoute(func(c *gin.Context) { r.NoRoute(func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "http://localhost:3000") c.Writer.Header().Set("Access-Control-Allow-Origin", os.Getenv("ALLOWED_ORIGIN"))
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT") c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT")
@@ -59,7 +59,8 @@ func main() {
authenticator, auth_err := auth.New() authenticator, auth_err := auth.New()
if auth_err != nil { if auth_err != nil {
log.Fatal("ERROR: Failed to initialize Authenticator") log.Printf("ERROR: Failed to initialize Authenticator %s", auth_err.Error())
log.Fatal("ERROR: Failed to initialize authenticator...")
return return
} }

Binary file not shown.

7
ordr-ui/.dockerignore Normal file
View File

@@ -0,0 +1,7 @@
.git
.next
node_modules
npm-debug.log
Dockerfile
docker-compose.yml
README.md

65
ordr-ui/Dockerfile Normal file
View File

@@ -0,0 +1,65 @@
FROM node:20-alpine AS base
# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
WORKDIR /app
RUN apk add --no-cache libc6-compat
# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
else npm install --force; \
fi
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED=1
RUN \
if [ -f yarn.lock ]; then yarn run build; \
elif [ -f package-lock.json ]; then npm run build; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
else echo "Lockfile not found." && exit 1; \
fi
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/config/next-config-js/output
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]

View File

@@ -25,16 +25,15 @@ export const CreateItem = async (query: CreateItemQuery): Promise<ItemPriceRespo
return res.data return res.data
} }
export const SetItemPrice = async (query: SetItemPriceQuery) => { export const SetItemPrice = async (query: SetItemPriceQuery): Promise<ItemPriceResponse> => {
const queryParams = new URLSearchParams({ const queryParams = new URLSearchParams({
item_id: query.item_id, item_id: query.item_id,
item_price: query.item_price item_price: query.item_price
}) })
const res = await axios.put(process.env.NEXT_PUBLIC_API_URL + `/item/price?${queryParams.toString()}`, {}, {withCredentials: true}) const res = await axios.put(process.env.NEXT_PUBLIC_API_URL + `/item/price?${queryParams.toString()}`, {}, {withCredentials: true})
if (res.data.Location) {
window.location.href = res.data.Location return res.data as ItemPriceResponse
}
} }

View File

@@ -1,9 +1,7 @@
import { Mutex } from "async-mutex" import { Mutex } from "async-mutex"
import { useState } from "react"
import styled from "styled-components" import styled from "styled-components"
import useAsyncEffect from "use-async-effect" import useAsyncEffect from "use-async-effect"
import { GetItemHistory } from "../client/controllers" import { useItemStore } from "../providers/ItemsProvider"
import { ItemHistoryResponse } from "../client/response"
type ItemHistoryTableProps = { type ItemHistoryTableProps = {
itemId: number itemId: number
@@ -39,17 +37,15 @@ const ItemHistoryTableRow = styled.tr`
const itemHistoryMutex = new Mutex() const itemHistoryMutex = new Mutex()
export const ItemHistoryTable = ({itemId}: ItemHistoryTableProps) => { export const ItemHistoryTable = ({itemId}: ItemHistoryTableProps) => {
const [itemPriceHistory, setItemPriceHistory] = useState<ItemHistoryResponse[]>([]) const itemStore = useItemStore((state) => state)
useAsyncEffect(async () => { useAsyncEffect(async () => {
if(itemPriceHistory.length === 0) { if(itemStore.itemHistories.filter((ih) => parseInt(ih.ItemId) === itemId).length === 0) {
const release = await itemHistoryMutex.acquire() const release = await itemHistoryMutex.acquire()
setItemPriceHistory(await GetItemHistory(itemId)) await itemStore.getItemHistory(itemId)
await release() await release()
} }
}, []) }, [itemStore.itemHistories])
console.log(itemPriceHistory)
return ( return (
<ItemHistoryTableStyle> <ItemHistoryTableStyle>
@@ -70,7 +66,7 @@ export const ItemHistoryTable = ({itemId}: ItemHistoryTableProps) => {
</tr> </tr>
</ItemHistoryTableHead> </ItemHistoryTableHead>
<ItemHistoryTableBody> <ItemHistoryTableBody>
{itemPriceHistory.map((iph) => ( {itemStore.itemHistories.filter((ih) => parseInt(ih.ItemId) === itemId).map((iph) => (
<ItemHistoryTableRow key = {iph.ItemId + new Date(iph.ValidFrom).getMilliseconds()}> <ItemHistoryTableRow key = {iph.ItemId + new Date(iph.ValidFrom).getMilliseconds()}>
<ItemHistoryTableItem> <ItemHistoryTableItem>
{iph.ItemName} {iph.ItemName}

View File

@@ -1,11 +1,10 @@
'use client' 'use client'
import Image from "next/image"; import { useRouter } from "next/navigation"
import { useEffect } from "react"
import { useAsyncEffect } from 'use-async-effect' import { useAsyncEffect } from 'use-async-effect'
import { useShallow } from 'zustand/react/shallow' import { useShallow } from 'zustand/react/shallow'
import { UserResponse } from './client/response' import { UserResponse } from './client/response'
import { useCurrentAuthenticatedUserStore, UserActions } from './providers' import { useCurrentAuthenticatedUserStore, UserActions } from './providers'
import { useRouter } from "next/navigation";
import { useEffect } from "react";
export default function Home() { export default function Home() {
const authenticatedUserStore: UserResponse & UserActions = useCurrentAuthenticatedUserStore(useShallow((state) => ({ const authenticatedUserStore: UserResponse & UserActions = useCurrentAuthenticatedUserStore(useShallow((state) => ({

View File

@@ -2,11 +2,12 @@ import { create } from 'zustand'
import * as ItemController from '../client/controllers/ItemController' import * as ItemController from '../client/controllers/ItemController'
import { CreateItemQuery } from '../client/queries/CreateItemQuery' import { CreateItemQuery } from '../client/queries/CreateItemQuery'
import { SetItemPriceQuery } from '../client/queries/SetItemPriceQuery' import { SetItemPriceQuery } from '../client/queries/SetItemPriceQuery'
import { ItemPriceResponse, OrderFilledResponse, OrderItemPriceResponse } from '../client/response' import { ItemHistoryResponse, ItemPriceResponse, OrderFilledResponse, OrderItemPriceResponse } from '../client/response'
export type ItemData = { export type ItemData = {
items: ItemPriceResponse[], items: ItemPriceResponse[],
orderItems: OrderItemPriceResponse[] orderItems: OrderItemPriceResponse[]
itemHistories: ItemHistoryResponse[]
} }
export type ItemActions = { export type ItemActions = {
@@ -20,11 +21,13 @@ export type ItemActions = {
setItemQuantity: (orderId: number, itemId: number, quantity: number) => Promise<OrderFilledResponse> setItemQuantity: (orderId: number, itemId: number, quantity: number) => Promise<OrderFilledResponse>
deleteOrderItem: (orderId: number, itemId: number) => Promise<void> deleteOrderItem: (orderId: number, itemId: number) => Promise<void>
deleteItem: (itemId: number) => Promise<void> deleteItem: (itemId: number) => Promise<void>
getItemHistory: (itemId: number) => Promise<ItemHistoryResponse[]>
} }
export const useItemStore = create<ItemData & ItemActions>((set, get) => ({ export const useItemStore = create<ItemData & ItemActions>((set, get) => ({
items: [], items: [],
orderItems: [], orderItems: [],
itemHistories: [],
sync: async (): Promise<void> => { sync: async (): Promise<void> => {
const itemPrices = await ItemController.GetItems() const itemPrices = await ItemController.GetItems()
set((state) => ({ set((state) => ({
@@ -52,7 +55,7 @@ export const useItemStore = create<ItemData & ItemActions>((set, get) => ({
item_price: price.toString() item_price: price.toString()
} }
await ItemController.SetItemPrice(itemPriceQuery) const itemPrice = await ItemController.SetItemPrice(itemPriceQuery)
set((state) => { set((state) => {
const item = state.items.filter((i) => i.ItemId === itemId)[0] const item = state.items.filter((i) => i.ItemId === itemId)[0]
@@ -68,7 +71,14 @@ export const useItemStore = create<ItemData & ItemActions>((set, get) => ({
if(!a.InSeason && b.InSeason) if(!a.InSeason && b.InSeason)
return -1 return -1
return a.ItemId - b.ItemId return a.ItemId - b.ItemId
}) }),
itemHistories: [...state.itemHistories, {
ItemId: itemPrice.ItemId.toString(),
ItemName: itemPrice.ItemName,
ItemPrice: (Math.trunc(itemPrice.ItemPrice * 100) / 100).toString(),
ValidFrom: new Date().toLocaleDateString(),
ValidTo: new Date().toLocaleDateString()
} as ItemHistoryResponse]
} }
}) })
}, },
@@ -151,7 +161,7 @@ export const useItemStore = create<ItemData & ItemActions>((set, get) => ({
const orderItem = state.orderItems.filter((oi) => oi.ItemId === itemId && oi.OrderId === orderId)[0] const orderItem = state.orderItems.filter((oi) => oi.ItemId === itemId && oi.OrderId === orderId)[0]
const orderItemsWithoutOrderItem = state.orderItems.filter((oi) => oi.ItemId !== itemId || oi.OrderId !== orderId) const orderItemsWithoutOrderItem = state.orderItems.filter((oi) => oi.ItemId !== itemId || oi.OrderId !== orderId)
orderItem.Quantity = quantity orderItem.Quantity= quantity
return { return {
...state, ...state,
@@ -183,5 +193,18 @@ export const useItemStore = create<ItemData & ItemActions>((set, get) => ({
orderItems: orderItemsWithoutItem orderItems: orderItemsWithoutItem
} }
}) })
},
getItemHistory: async (itemId: number): Promise<ItemHistoryResponse[]> => {
const currHist = get().itemHistories.filter((ih) => parseInt(ih.ItemId) === itemId)
if(currHist.length > 0)
return currHist
const itemHistory = await ItemController.GetItemHistory(itemId)
set((state) => ({
...state,
itemHistories: [...state.itemHistories, ...itemHistory]
}))
return itemHistory
} }
})) }))

View File

@@ -1,7 +1,8 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
/* config options here */ output: 'standalone',
}; // other configurations
};
export default nextConfig; export default nextConfig;

86
ordr-ui/nohup.out Normal file
View File

@@ -0,0 +1,86 @@
> ordr-ui@0.1.0 start
> next start
▲ Next.js 16.0.3
- Local: http://localhost:3000
- Network: http://49.12.213.49:3000
✓ Starting...
✓ Ready in 439ms
> ordr-ui@0.1.0 start
> next start
▲ Next.js 16.0.3
- Local: http://localhost:3000
- Network: http://49.12.213.49:3000
✓ Starting...
✓ Ready in 416ms
0
0
0
0
0
{
Id: -1,
Name: '',
JobPosition: '',
Active: false,
Admin: false,
sync: [AsyncFunction: sync],
updateName: [AsyncFunction: updateName]
}
[]
0
{
Id: -1,
Name: '',
JobPosition: '',
Active: false,
Admin: false,
sync: [AsyncFunction: sync],
updateName: [AsyncFunction: updateName]
}
[]
0
0
0
{
Id: -1,
Name: '',
JobPosition: '',
Active: false,
Admin: false,
sync: [AsyncFunction: sync],
updateName: [AsyncFunction: updateName]
}
[]
0
0
{
Id: -1,
Name: '',
JobPosition: '',
Active: false,
Admin: false,
sync: [AsyncFunction: sync],
updateName: [AsyncFunction: updateName]
}
[]
0
0
0
0
0
{
Id: -1,
Name: '',
JobPosition: '',
Active: false,
Admin: false,
sync: [AsyncFunction: sync],
updateName: [AsyncFunction: updateName]
}
[]

View File

@@ -3892,6 +3892,7 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@rtsao/scc": "^1.1.0", "@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9", "array-includes": "^3.1.9",

File diff suppressed because it is too large Load Diff