diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 877de8eaf8..2a73652149 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,11 +29,14 @@ jobs: QUESTION_SERVICE_URL: ${{ vars.QUESTION_SERVICE_URL }} USER_SERVICE_URL: ${{ vars.USER_SERVICE_URL }} MATCHING_SERVICE_URL: ${{ vars.MATCHING_SERVICE_URL }} + HISTORY_SERVICE_URL: ${{ vars.HISTORY_SERVICE_URL }} JWT_SECRET: ${{ secrets.JWT_SECRET }} - FIREBASE_CREDENTIAL_PATH: ${{ vars.QUESTION_SERVICE_FIREBASE_CREDENTIAL_PATH }} + QUESTION_FIREBASE_CREDENTIAL_PATH: ${{ vars.QUESTION_SERVICE_FIREBASE_CREDENTIAL_PATH }} + HISTORY_FIREBASE_CREDENTIAL_PATH: ${{ vars.HISTORY_SERVICE_FIREBASE_CREDENTIAL_PATH }} DB_CLOUD_URI: ${{ secrets.USER_SERVICE_DB_CLOUD_URI }} USER_SERVICE_PORT: ${{ vars.USER_SERVICE_PORT }} MATCHING_SERVICE_PORT: ${{ vars.MATCHING_SERVICE_PORT }} + HISTORY_SERVICE_PORT: ${{ vars.HISTORY_SERVICE_PORT }} MATCHING_SERVICE_TIMEOUT: ${{ vars.MATCHING_SERVICE_TIMEOUT }} REDIS_URL: ${{ vars.REDIS_URL }} QUESTION_SERVICE_GRPC_URL: ${{ vars.QUESTION_SERVICE_GPRC_URL }} @@ -42,9 +45,10 @@ jobs: echo "NEXT_PUBLIC_QUESTION_SERVICE_URL=$QUESTION_SERVICE_URL" >> .env echo "NEXT_PUBLIC_USER_SERVICE_URL=$USER_SERVICE_URL" >> .env echo "NEXT_PUBLIC_MATCHING_SERVICE_URL=$MATCHING_SERVICE_URL" >> .env + echo "NEXT_PUBLIC_HISTORY_SERVICE_URL=$HISTORY_SERVICE_URL" >> .env cd ../question-service - echo "FIREBASE_CREDENTIAL_PATH=$FIREBASE_CREDENTIAL_PATH" >> .env + echo "FIREBASE_CREDENTIAL_PATH=$QUESTION_FIREBASE_CREDENTIAL_PATH" >> .env echo "JWT_SECRET=$JWT_SECRET" >> .env cd ../user-service @@ -59,13 +63,22 @@ jobs: echo "REDIS_URL=$REDIS_URL" >> .env echo "QUESTION_SERVICE_GRPC_URL=$QUESTION_SERVICE_GRPC_URL" >> .env + cd ../history-service + echo "FIREBASE_CREDENTIAL_PATH=$HISTORY_FIREBASE_CREDENTIAL_PATH" >> .env + echo "PORT=$HISTORY_SERVICE_PORT" >> .env + - name: Create Database Credential Files env: - FIREBASE_JSON: ${{ secrets.QUESTION_SERVICE_FIREBASE_CREDENTIAL }} - FIREBASE_CREDENTIAL_PATH: ${{ vars.QUESTION_SERVICE_FIREBASE_CREDENTIAL_PATH }} + QUESTION_FIREBASE_JSON: ${{ secrets.QUESTION_SERVICE_FIREBASE_CREDENTIAL }} + QUESTION_FIREBASE_CREDENTIAL_PATH: ${{ vars.QUESTION_SERVICE_FIREBASE_CREDENTIAL_PATH }} + HISTORY_FIREBASE_JSON: ${{ secrets.HISTORY_SERVICE_FIREBASE_CREDENTIAL }} + HISTORY_FIREBASE_CREDENTIAL_PATH: ${{ vars.HISTORY_SERVICE_FIREBASE_CREDENTIAL_PATH }} run: | cd ./apps/question-service - echo "$FIREBASE_JSON" > "./$FIREBASE_CREDENTIAL_PATH" + echo "$QUESTION_FIREBASE_JSON" > "./$QUESTION_FIREBASE_CREDENTIAL_PATH" + + cd ../history-service + echo "$HISTORY_FIREBASE_JSON" > "./$HISTORY_FIREBASE_CREDENTIAL_PATH" - name: Build and Run Services run: | @@ -87,6 +100,7 @@ jobs: USER_SERVICE_URL: ${{ vars.USER_SERVICE_URL }} QUESTION_SERVICE_URL: ${{ vars.QUESTION_SERVICE_URL }} MATCHING_SERVICE_URL: ${{ vars.MATCHING_SERVICE_URL }} + HISTORY_SERVICE_URL: ${{ vars.HISTORY_SERVICE_URL }} run: | echo "Testing Question Service..." curl -sSL -o /dev/null $QUESTION_SERVICE_URL && echo "Question Service is up" @@ -94,6 +108,8 @@ jobs: curl -fsSL -o /dev/null $USER_SERVICE_URL && echo "User Service is up" echo "Testing Frontend..." curl -fsSL -o /dev/null $FRONTEND_URL && echo "Frontend is up" + echo "Testing History Service..." + curl -fsSL -o /dev/null $HISTORY_SERVICE_URL && echo "History Service is up" echo "Testing Matching Service..." if ! (echo "Hello" | websocat $MATCHING_SERVICE_URL); then echo "WebSocket for Matching Service is not live" diff --git a/apps/docker-compose.yml b/apps/docker-compose.yml index a8ee1986f9..0a0ef34fdf 100644 --- a/apps/docker-compose.yml +++ b/apps/docker-compose.yml @@ -54,6 +54,19 @@ services: depends_on: - redis + history-service: + build: + context: ./history-service + dockerfile: Dockerfile + ports: + - 8082:8082 + env_file: + - ./history-service/.env + networks: + - apps_network + volumes: + - ./history-service:/history-service + redis: image: redis:latest networks: diff --git a/apps/frontend/.env.example b/apps/frontend/.env.example index 5b9fb29501..6482575b4e 100644 --- a/apps/frontend/.env.example +++ b/apps/frontend/.env.example @@ -2,4 +2,5 @@ NEXT_PUBLIC_QUESTION_SERVICE_URL="http://localhost:8080/" NEXT_PUBLIC_USER_SERVICE_URL="http://localhost:3001/" NEXT_PUBLIC_MATCHING_SERVICE_URL="ws://localhost:8081/match" -NEXT_PUBLIC_SIGNALLING_SERVICE_URL="ws://localhost:4444/" \ No newline at end of file +NEXT_PUBLIC_SIGNALLING_SERVICE_URL="ws://localhost:4444/" +NEXT_PUBLIC_HISTORY_SERVICE_URL="http://localhost:8082/" \ No newline at end of file diff --git a/apps/frontend/src/app/collaboration/[id]/page.tsx b/apps/frontend/src/app/collaboration/[id]/page.tsx index fbca214fb4..1e9c5548c8 100644 --- a/apps/frontend/src/app/collaboration/[id]/page.tsx +++ b/apps/frontend/src/app/collaboration/[id]/page.tsx @@ -5,6 +5,7 @@ import { Col, Input, Layout, + message, Row, Select, Tabs, @@ -15,7 +16,7 @@ import { import { Content } from "antd/es/layout/layout"; import "./styles.scss"; import { useRouter, useSearchParams } from "next/navigation"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { GetSingleQuestion, Question } from "@/app/services/question"; import { ClockCircleOutlined, @@ -27,22 +28,31 @@ import { } from "@ant-design/icons"; import { ProgrammingLanguageOptions } from "@/utils/SelectOptions"; import CollaborativeEditor from "@/components/CollaborativeEditor/CollaborativeEditor"; +import { CreateOrUpdateHistory } from "@/app/services/history"; +import { Language } from "@codemirror/language"; +import { WebrtcProvider } from "y-webrtc"; interface CollaborationProps {} export default function CollaborationPage(props: CollaborationProps) { const router = useRouter(); + const providerRef = useRef(null); const [isLoading, setIsLoading] = useState(false); // Code Editor States + const [historyDocRefId, setHistoryDocRefId] = useState( + undefined + ); + const [code, setCode] = useState(""); const [questionTitle, setQuestionTitle] = useState( undefined ); + const [questionDocRefId, setQuestionDocRefId] = useState(undefined); const [complexity, setComplexity] = useState(undefined); const [categories, setCategories] = useState([]); // Store the selected filter categories const [description, setDescription] = useState(undefined); - const [selectedLanguage, setSelectedLanguage] = useState("javascript"); // State to hold the selected language item + const [selectedLanguage, setSelectedLanguage] = useState("Javascript"); // State to hold the selected language item // Session states const [collaborationId, setCollaborationId] = useState( @@ -50,6 +60,7 @@ export default function CollaborationPage(props: CollaborationProps) { ); const [currentUser, setCurrentUser] = useState(undefined); const [matchedUser, setMatchedUser] = useState(undefined); + const [matchedTopics, setMatchedTopics] = useState(undefined); // Chat states const [messageToSend, setMessageToSend] = useState( @@ -61,6 +72,47 @@ export default function CollaborationPage(props: CollaborationProps) { undefined ); + // Message + const [messageApi, contextHolder] = message.useMessage(); + + const successMessage = (message: string) => { + messageApi.open({ + type: "success", + content: message, + }); + }; + + const sendCodeSavedStatusToMatchedUser = () => { + if (!providerRef.current) { + throw new Error("Provider not initialized"); + } + providerRef.current.awareness.setLocalStateField("codeSavedStatus", true); + } + + const handleSubmitCode = async () => { + if (!collaborationId) { + throw new Error("Collaboration ID not found"); + } + const data = await CreateOrUpdateHistory({ + title: questionTitle ?? "", + code: code, + language: selectedLanguage, + user: currentUser ?? "", + matchedUser: matchedUser ?? "", + matchId: collaborationId ?? "", + matchedTopics: matchedTopics ?? [], + questionDocRefId: questionDocRefId ?? "", + questionDifficulty: complexity ?? "", + questionTopics: categories, + }, collaborationId); + successMessage("Code saved successfully!"); + sendCodeSavedStatusToMatchedUser(); + } + + const handleCodeChange = (code: string) => { + setCode(code); + } + // Retrieve the docRefId from query params during page navigation // const searchParams = useSearchParams(); @@ -71,17 +123,20 @@ export default function CollaborationPage(props: CollaborationProps) { } // Retrieve details from localstorage - const docRefId: string = localStorage.getItem("docRefId") ?? ""; + const questionDocRefId: string = localStorage.getItem("questionDocRefId") ?? ""; const collabId: string = localStorage.getItem("collabId") ?? ""; const matchedUser: string = localStorage.getItem("matchedUser") ?? ""; const currentUser: string = localStorage.getItem("user") ?? ""; + const matchedTopics: string[] = localStorage.getItem("matchedTopics")?.split(",") ?? []; // Set states from localstorage setCollaborationId(collabId); setMatchedUser(matchedUser); setCurrentUser(currentUser); + setMatchedTopics(matchedTopics); + setQuestionDocRefId(questionDocRefId); - GetSingleQuestion(docRefId).then((data: Question) => { + GetSingleQuestion(questionDocRefId).then((data: Question) => { setQuestionTitle(`${data.id}. ${data.title}`); setComplexity(data.complexity); setCategories(data.categories); @@ -121,8 +176,9 @@ export default function CollaborationPage(props: CollaborationProps) { // Remove localstorage variables for collaboration localStorage.removeItem("user"); localStorage.removeItem("matchedUser"); - localStorage.removeItem("collaId"); - localStorage.removeItem("docRefId"); + localStorage.removeItem("collabId"); + localStorage.removeItem("questionDocRefId"); + localStorage.removeItem("matchedTopics"); // Redirect back to matching page router.push("/matching"); @@ -130,6 +186,7 @@ export default function CollaborationPage(props: CollaborationProps) { return ( + {contextHolder}
@@ -189,7 +246,11 @@ export default function CollaborationPage(props: CollaborationProps) { Code {/* TODO: Link to execution service for code submission */} - @@ -207,6 +268,9 @@ export default function CollaborationPage(props: CollaborationProps) { user={currentUser} collaborationId={collaborationId} language={selectedLanguage} + providerRef={providerRef} + matchedUser={matchedUser} + onCodeChange={handleCodeChange} /> )} diff --git a/apps/frontend/src/app/matching/MatchingModal.tsx b/apps/frontend/src/app/matching/MatchingModal.tsx index 9fa9334b5f..cbb4e67a75 100644 --- a/apps/frontend/src/app/matching/MatchingModal.tsx +++ b/apps/frontend/src/app/matching/MatchingModal.tsx @@ -59,8 +59,8 @@ const MatchingModal: React.FC = ({ cancel={() => { setClosedType("cancelled"); }} - name1={matchingState.info?.myName || ""} - name2={matchingState.info?.partnerName || ""} + name1={matchingState.info?.user || ""} + name2={matchingState.info?.matchedUser || ""} /> ); } @@ -97,19 +97,20 @@ const MatchingModal: React.FC = ({ join={() => { matchingState.ok(); setClosedType("joined"); - localStorage.setItem("user", matchingState.info.myName); + localStorage.setItem("user", matchingState.info.user); localStorage.setItem( "matchedUser", - matchingState.info.partnerName + matchingState.info.matchedUser ); localStorage.setItem("collabId", matchingState.info.matchId); - localStorage.setItem("docRefId", matchingState.info.docRefId); + localStorage.setItem("questionDocRefId", matchingState.info.questionDocRefId); + localStorage.setItem("matchedTopics", matchingState.info.matchedTopics.join(",")); // Redirect to collaboration page router.push(`/collaboration/${matchingState.info.matchId}`); }} - name1={matchingState.info.myName} - name2={matchingState.info.partnerName} + name1={matchingState.info.user} + name2={matchingState.info.matchedUser} /> ); case "timeout": diff --git a/apps/frontend/src/app/services/history.ts b/apps/frontend/src/app/services/history.ts new file mode 100644 index 0000000000..fc01f913ec --- /dev/null +++ b/apps/frontend/src/app/services/history.ts @@ -0,0 +1,84 @@ +const HISTORY_SERVICE_URL = process.env.NEXT_PUBLIC_HISTORY_SERVICE_URL; + +export interface History { + title: string; + code: string; + language: string; + user: string; + matchedUser: string; + matchId: string; + matchedTopics: string[]; + questionDocRefId: string; + questionDifficulty: string; + questionTopics: string[]; + createdAt?: string; + updatedAt?: string; +} + +export const CreateOrUpdateHistory = async ( + history: History, + matchId: string, +): Promise => { + const response = await fetch( + `${HISTORY_SERVICE_URL}histories/${matchId}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(history), + } + ); + + if (response.status === 200) { + return response.json(); + } else { + throw new Error( + `Error saving history: ${response.status} ${response.statusText}` + ); + } +} + +export const GetHistory = async ( + matchId: string, +): Promise => { + const response = await fetch( + `${HISTORY_SERVICE_URL}histories/${matchId}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + } + ); + + if (response.status === 200) { + return response.json(); + } else { + throw new Error( + `Error reading history: ${response.status} ${response.statusText}` + ); + } +} + +export const GetUserHistories = async ( + username: string, +): Promise => { + const response = await fetch( + `${HISTORY_SERVICE_URL}histories/${username}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + } + ); + + if (response.status === 200) { + return response.json(); + } else { + throw new Error( + `Error reading user histories: ${response.status} ${response.statusText}` + ); + } +} diff --git a/apps/frontend/src/app/services/use-matching.ts b/apps/frontend/src/app/services/use-matching.ts index 21fac18be6..18baad27c3 100644 --- a/apps/frontend/src/app/services/use-matching.ts +++ b/apps/frontend/src/app/services/use-matching.ts @@ -136,8 +136,9 @@ export default function useMatching(): MatchState { function parseInfoFromResponse(responseJson: MatchFoundResponse): MatchInfo { return { matchId: responseJson.match_id?.toString() ?? "unknown", - partnerName: responseJson.matched_user ?? "unknown", - myName: responseJson.user ?? "unknown", - docRefId: responseJson.question_doc_ref_id ?? "unknown", + matchedUser: responseJson.matched_user ?? "unknown", + user: responseJson.user ?? "unknown", + questionDocRefId: responseJson.question_doc_ref_id ?? "unknown", + matchedTopics: responseJson.matched_topics ?? [], }; } diff --git a/apps/frontend/src/components/CollaborativeEditor/CollaborativeEditor.tsx b/apps/frontend/src/components/CollaborativeEditor/CollaborativeEditor.tsx index 17b60a401b..5b5c01c06f 100644 --- a/apps/frontend/src/components/CollaborativeEditor/CollaborativeEditor.tsx +++ b/apps/frontend/src/components/CollaborativeEditor/CollaborativeEditor.tsx @@ -1,5 +1,5 @@ // Referenced from example in https://www.npmjs.com/package/y-codemirror.next -import React, { useEffect, useRef, useState } from "react"; +import React, { MutableRefObject, useEffect, useRef, useState } from "react"; import * as Y from "yjs"; import { yCollab } from "y-codemirror.next"; import { WebrtcProvider } from "y-webrtc"; @@ -19,6 +19,24 @@ interface CollaborativeEditorProps { user: string; collaborationId: string; language: string; + providerRef: MutableRefObject; + matchedUser: string | undefined; + onCodeChange: (code: string) => void; +} + +interface AwarenessUpdate { + added: number[]; + updated: number[]; + removed: number[]; +} + +interface Awareness { + user: { + name: string; + color: string; + colorLight: string; + }; + codeSavedStatus: boolean; } export const usercolors = [ @@ -49,6 +67,10 @@ const CollaborativeEditor = (props: CollaborativeEditorProps) => { if (!tr.docChanged) return null; const snippet = tr.newDoc.sliceString(0, 100); + + // Handle code change + props.onCodeChange(tr.newDoc.toString()); + // Test for various language const docIsPython = /^\s*(def|class)\s/.test(snippet); const docIsJava = /^\s*(class|public\s+static\s+void\s+main)\s/.test( @@ -149,6 +171,7 @@ const CollaborativeEditor = (props: CollaborativeEditorProps) => { const provider = new WebrtcProvider(props.collaborationId, ydoc, { signaling: [process.env.NEXT_PUBLIC_SIGNALLING_SERVICE_URL], }); + props.providerRef.current = provider; const ytext = ydoc.getText("codemirror"); const undoManager = new Y.UndoManager(ytext); @@ -158,6 +181,20 @@ const CollaborativeEditor = (props: CollaborativeEditorProps) => { colorLight: userColor.light, }); + // Listener for awareness updates to receive status changes from peers + provider.awareness.on("update", ({ added, updated } : AwarenessUpdate) => { + added.concat(updated).filter(clientId => clientId !== provider.awareness.clientID).forEach((clientID) => { + const state = provider.awareness.getStates().get(clientID) as Awareness; + if (state && state.codeSavedStatus) { + // Display the received status message + messageApi.open({ + type: "success", + content: `${props.matchedUser ?? "Peer"} saved code successfully!`, + }); + } + }); + }); + const state = EditorState.create({ doc: ytext.toString(), extensions: [ diff --git a/apps/frontend/src/contexts/websocketcontext.tsx b/apps/frontend/src/contexts/websocketcontext.tsx index b4946ec225..aae5d720ce 100644 --- a/apps/frontend/src/contexts/websocketcontext.tsx +++ b/apps/frontend/src/contexts/websocketcontext.tsx @@ -13,12 +13,15 @@ export type SocketState = { cancel(): void; timeout(): void; }; + export type MatchInfo = { matchId: string; - myName: string; - partnerName: string; - docRefId: string; + user: string; + matchedUser: string; + questionDocRefId: string; + matchedTopics: string[]; } + export type MatchState = SocketState | { state: "found"; info: MatchInfo; diff --git a/apps/history-service/.dockerignore b/apps/history-service/.dockerignore new file mode 100644 index 0000000000..c6228e8d6c --- /dev/null +++ b/apps/history-service/.dockerignore @@ -0,0 +1,9 @@ +.env.example + +.git +.gitignore + +.dockerignore +Dockerfile + +README.md diff --git a/apps/history-service/.env.example b/apps/history-service/.env.example new file mode 100644 index 0000000000..b3bd4db2b4 --- /dev/null +++ b/apps/history-service/.env.example @@ -0,0 +1,2 @@ +FIREBASE_CREDENTIAL_PATH=cs3219-staging-codehisto-bb61c-firebase-adminsdk-egopb-95cfaf9b87.json +PORT=8082 \ No newline at end of file diff --git a/apps/history-service/.gitignore b/apps/history-service/.gitignore new file mode 100644 index 0000000000..4881ecbb8f --- /dev/null +++ b/apps/history-service/.gitignore @@ -0,0 +1,2 @@ +.env +cs3219-staging-codehisto-bb61c-firebase-adminsdk-egopb-95cfaf9b87.json \ No newline at end of file diff --git a/apps/history-service/Dockerfile b/apps/history-service/Dockerfile new file mode 100644 index 0000000000..325d4e3751 --- /dev/null +++ b/apps/history-service/Dockerfile @@ -0,0 +1,16 @@ +FROM golang:1.23 + +WORKDIR /usr/src/app + +# pre-copy/cache go.mod for pre-downloading dependencies and only redownloading them in subsequent builds if they change +COPY go.mod go.sum ./ + +RUN go mod tidy && go mod download && go mod verify + +COPY . . + +RUN go build -v -o /usr/local/bin/app ./main.go + +EXPOSE 8082 8082 + +CMD ["app"] diff --git a/apps/history-service/README.md b/apps/history-service/README.md new file mode 100644 index 0000000000..681c629bcd --- /dev/null +++ b/apps/history-service/README.md @@ -0,0 +1,75 @@ +# Question Service + +## Overview + +The History Service is built with Go, utilizing Firestore as the database and Chi as the HTTP router. It allows for basic operations such as creating, reading, updating, and deleting question records. + +## Features + +- Create new collaboration history +- Read collaboration history by collaboration history ID +- Update collaboration history code +- Delete collaboration history + +## Technologies Used + +- Go (Golang) +- Firestore (Google Cloud Firestore) +- Chi (HTTP router) + +## Getting Started + +### Prerequisites + +- Go 1.16 or later +- Google Cloud SDK +- Firestore database setup in your Google Cloud project + +### Installation + +1. Clone the repository + +2. Set up your Firestore client + +3. Install dependencies: + +```bash +go mod tidy +``` + +4. Create the `.env` file from copying the `.env.example`, copying the firebase json key file into history-service directory, and fill in the `FIREBASE_CREDENTIAL_PATH` with the path of the firebase credential JSON file. + +### Running the Application + +To start the server, run the following command: + +```bash +go run main.go +``` + +The server will be available at http://localhost:8082. + +## Running the Application via Docker + +To run the application via Docker, run the following command: + +```bash +docker build -t history-service . +``` + +```bash +docker run -p 8082:8082 -d history-service +``` + +The server will be available at http://localhost:8082. + +## API Endpoints + +- `POST /histories` +- `GET /histories/{docRefId}` +- `PUT /histories/{docRefId}` +- `DELETE /histories/{docRefId}` + +```bash +go run main.go +``` diff --git a/apps/history-service/go.mod b/apps/history-service/go.mod new file mode 100644 index 0000000000..37d6005bf1 --- /dev/null +++ b/apps/history-service/go.mod @@ -0,0 +1,52 @@ +module history-service + +go 1.23.1 + +require ( + cloud.google.com/go/firestore v1.17.0 + firebase.google.com/go/v4 v4.15.0 + github.com/go-chi/chi/v5 v5.1.0 + github.com/go-chi/cors v1.2.1 + github.com/joho/godotenv v1.5.1 + google.golang.org/api v0.203.0 + google.golang.org/grpc v1.67.1 +) + +require ( + cloud.google.com/go v0.116.0 // indirect + cloud.google.com/go/auth v0.9.9 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect + cloud.google.com/go/compute/metadata v0.5.2 // indirect + cloud.google.com/go/iam v1.2.1 // indirect + cloud.google.com/go/longrunning v0.6.1 // indirect + cloud.google.com/go/storage v1.43.0 // indirect + github.com/MicahParks/keyfunc v1.9.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/s2a-go v0.1.8 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect + github.com/googleapis/gax-go/v2 v2.13.0 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect + go.opentelemetry.io/otel v1.29.0 // indirect + go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/otel/trace v1.29.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/oauth2 v0.23.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect + golang.org/x/time v0.7.0 // indirect + google.golang.org/appengine/v2 v2.0.2 // indirect + google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect + google.golang.org/protobuf v1.35.1 // indirect +) diff --git a/apps/history-service/go.sum b/apps/history-service/go.sum new file mode 100644 index 0000000000..ce7234e8f6 --- /dev/null +++ b/apps/history-service/go.sum @@ -0,0 +1,195 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= +cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= +cloud.google.com/go/auth v0.9.9 h1:BmtbpNQozo8ZwW2t7QJjnrQtdganSdmqeIBxHxNkEZQ= +cloud.google.com/go/auth v0.9.9/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI= +cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY= +cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc= +cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= +cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= +cloud.google.com/go/firestore v1.17.0 h1:iEd1LBbkDZTFsLw3sTH50eyg4qe8eoG6CjocmEXO9aQ= +cloud.google.com/go/firestore v1.17.0/go.mod h1:69uPx1papBsY8ZETooc71fOhoKkD70Q1DwMrtKuOT/Y= +cloud.google.com/go/iam v1.2.1 h1:QFct02HRb7H12J/3utj0qf5tobFh9V4vR6h9eX5EBRU= +cloud.google.com/go/iam v1.2.1/go.mod h1:3VUIJDPpwT6p/amXRC5GY8fCCh70lxPygguVtI0Z4/g= +cloud.google.com/go/longrunning v0.6.1 h1:lOLTFxYpr8hcRtcwWir5ITh1PAKUD/sG2lKrTSYjyMc= +cloud.google.com/go/longrunning v0.6.1/go.mod h1:nHISoOZpBcmlwbJmiVk5oDRz0qG/ZxPynEGs1iZ79s0= +cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyXFs= +cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0= +firebase.google.com/go/v4 v4.15.0 h1:k27M+cHbyN1YpBI2Cf4NSjeHnnYRB9ldXwpqA5KikN0= +firebase.google.com/go/v4 v4.15.0/go.mod h1:S/4MJqVZn1robtXkHhpRUbwOC4gdYtgsiMMJQ4x+xmQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o= +github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= +github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= +github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= +github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= +github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= +github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= +github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s= +github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= +go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.203.0 h1:SrEeuwU3S11Wlscsn+LA1kb/Y5xT8uggJSkIhD08NAU= +google.golang.org/api v0.203.0/go.mod h1:BuOVyCSYEPwJb3npWvDnNmFI92f3GeRnHNkETneT3SI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine/v2 v2.0.2 h1:MSqyWy2shDLwG7chbwBJ5uMyw6SNqJzhJHNDwYB0Akk= +google.golang.org/appengine/v2 v2.0.2/go.mod h1:PkgRUWz4o1XOvbqtWTkBtCitEJ5Tp4HoVEdMMYQR/8E= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53 h1:Df6WuGvthPzc+JiQ/G+m+sNX24kc0aTBqoDN/0yyykE= +google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53/go.mod h1:fheguH3Am2dGp1LfXkrvwqC/KlFq8F0nLq3LryOMrrE= +google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 h1:T6rh4haD3GVYsgEfWExoCZA2o2FmbNyKpTuAxbEFPTg= +google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:wp2WsuBYj6j8wUdo3ToZsdxxixbvQNAHqVJrTgi5E5M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/apps/history-service/handlers/create.go b/apps/history-service/handlers/create.go new file mode 100644 index 0000000000..d981fb9190 --- /dev/null +++ b/apps/history-service/handlers/create.go @@ -0,0 +1,82 @@ +package handlers + +import ( + "cloud.google.com/go/firestore" + "encoding/json" + "google.golang.org/api/iterator" + "history-service/models" + "history-service/utils" + "net/http" +) + +// Create a new code snippet +func (s *Service) CreateHistory(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + // Parse request + var collaborationHistory models.CollaborationHistory + if err := utils.DecodeJSONBody(w, r, &collaborationHistory); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // Document reference ID in firestore mapped to the match ID in model + docRef := s.Client.Collection("collaboration-history").Doc(collaborationHistory.MatchID) + + _, err := docRef.Set(ctx, map[string]interface{}{ + "title": collaborationHistory.Title, + "code": collaborationHistory.Code, + "language": collaborationHistory.Language, + "user": collaborationHistory.User, + "matchedUser": collaborationHistory.MatchedUser, + "matchedTopics": collaborationHistory.MatchedTopics, + "questionDocRefId": collaborationHistory.QuestionDocRefID, + "questionDifficulty": collaborationHistory.QuestionDifficulty, + "questionTopics": collaborationHistory.QuestionTopics, + "createdAt": firestore.ServerTimestamp, + "updatedAt": firestore.ServerTimestamp, + }) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Get data + doc, err := docRef.Get(ctx) + if err != nil { + if err != iterator.Done { + http.Error(w, "History not found", http.StatusNotFound) + return + } + http.Error(w, "Failed to get history", http.StatusInternalServerError) + return + } + + // Map data + if err := doc.DataTo(&collaborationHistory); err != nil { + http.Error(w, "Failed to map history data", http.StatusInternalServerError) + return + } + collaborationHistory.MatchID = doc.Ref.ID + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(collaborationHistory) +} + +//curl -X POST http://localhost:8082/histories \ +//-H "Content-Type: application/json" \ +//-d "{ +//\"title\": \"Hello World in Python\", +//\"code\": \"print('Hello, World!')\", +//\"language\": \"Python\", +//\"user\": \"user789\", +//\"matchedUser\": \"user123\", +//\"matchId\": \"match123\", +//\"matchedTopics\": [\"Python\", \"Programming\"], +//\"questionDocRefId\": \"question123\", +//\"questionDifficulty\": \"Easy\", +//\"questionTopics\": [\"Python\", \"Programming\"], +//\"createdAt\": \"2024-10-27T10:00:00Z\", +//\"updatedAt\": \"2024-10-27T10:00:00Z\" +//}" diff --git a/apps/history-service/handlers/createOrUpdate.go b/apps/history-service/handlers/createOrUpdate.go new file mode 100644 index 0000000000..f9df4bcc33 --- /dev/null +++ b/apps/history-service/handlers/createOrUpdate.go @@ -0,0 +1,125 @@ +package handlers + +import ( + "cloud.google.com/go/firestore" + "encoding/json" + "github.com/go-chi/chi/v5" + "google.golang.org/api/iterator" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "history-service/models" + "history-service/utils" + "net/http" +) + +func (s *Service) CreateOrUpdateHistory(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + // Parse request + matchId := chi.URLParam(r, "matchId") + var collaborationHistory models.CollaborationHistory + if err := utils.DecodeJSONBody(w, r, &collaborationHistory); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // Reference document + docRef := s.Client.Collection("collaboration-history").Doc(matchId) + + // Check if exists + _, err := docRef.Get(ctx) + if err != nil { + if status.Code(err) == codes.NotFound { + // Create collaboration history + _, err := docRef.Set(ctx, map[string]interface{}{ + "title": collaborationHistory.Title, + "code": collaborationHistory.Code, + "language": collaborationHistory.Language, + "user": collaborationHistory.User, + "matchedUser": collaborationHistory.MatchedUser, + "matchedTopics": collaborationHistory.MatchedTopics, + "questionDocRefId": collaborationHistory.QuestionDocRefID, + "questionDifficulty": collaborationHistory.QuestionDifficulty, + "questionTopics": collaborationHistory.QuestionTopics, + "createdAt": firestore.ServerTimestamp, + "updatedAt": firestore.ServerTimestamp, + }) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Get data + doc, err := docRef.Get(ctx) + if err != nil { + if err != iterator.Done { + http.Error(w, "History not found", http.StatusNotFound) + return + } + http.Error(w, "Failed to get history", http.StatusInternalServerError) + return + } + + // Map data + if err := doc.DataTo(&collaborationHistory); err != nil { + http.Error(w, "Failed to map history data", http.StatusInternalServerError) + return + } + collaborationHistory.MatchID = doc.Ref.ID + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(collaborationHistory) + return + } + } + + // Update collaboration history + + // Validation + // Check if exists + _, err = docRef.Get(ctx) + if err != nil { + if status.Code(err) == codes.NotFound { + http.Error(w, "History not found", http.StatusNotFound) + return + } + http.Error(w, "Error fetching history", http.StatusInternalServerError) + return + } + + // Prepare the update data. + updates := []firestore.Update{ + {Path: "code", Value: collaborationHistory.Code}, + {Path: "updatedAt", Value: firestore.ServerTimestamp}, + } + + // Update database + _, err = docRef.Update(ctx, updates) + if err != nil { + http.Error(w, "Error updating history", http.StatusInternalServerError) + return + } + + // Get data + doc, err := docRef.Get(ctx) + if err != nil { + if err != iterator.Done { + http.Error(w, "History not found", http.StatusNotFound) + return + } + http.Error(w, "Failed to get history", http.StatusInternalServerError) + return + } + + // Map data + if err := doc.DataTo(&collaborationHistory); err != nil { + http.Error(w, "Failed to map history data", http.StatusInternalServerError) + return + } + collaborationHistory.MatchID = doc.Ref.ID + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(collaborationHistory) +} diff --git a/apps/history-service/handlers/delete.go b/apps/history-service/handlers/delete.go new file mode 100644 index 0000000000..a450b58ea4 --- /dev/null +++ b/apps/history-service/handlers/delete.go @@ -0,0 +1,44 @@ +package handlers + +import ( + "github.com/go-chi/chi/v5" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "net/http" +) + +// Delete a code snippet by ID +func (s *Service) DeleteHistory(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + // Parse request + matchId := chi.URLParam(r, "matchId") + + // Reference document + docRef := s.Client.Collection("collaboration-history").Doc(matchId) + + // Validation + // Check if exists + doc, err := docRef.Get(ctx) + if err != nil { + if status.Code(err) == codes.NotFound { + http.Error(w, "History not found", http.StatusNotFound) + return + } + http.Error(w, "Error fetching history", http.StatusInternalServerError) + return + } + + // Update database + _, err = doc.Ref.Delete(ctx) + if err != nil { + http.Error(w, "Error deleting history", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) +} + +//curl -X DELETE http://localhost:8082/histories/largSKbROswF5pveMkEL \ +//-H "Content-Type: application/json" diff --git a/apps/history-service/handlers/list.go b/apps/history-service/handlers/list.go new file mode 100644 index 0000000000..554dc159ca --- /dev/null +++ b/apps/history-service/handlers/list.go @@ -0,0 +1,69 @@ +package handlers + +import ( + "encoding/json" + "github.com/go-chi/chi/v5" + "google.golang.org/api/iterator" + "history-service/models" + "net/http" +) + +func (s *Service) ListUserHistories(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + // Parse request + username := chi.URLParam(r, "username") + + // Reference collection + collRef := s.Client.Collection("collaboration-history") + + // Query data + iterUser := collRef.Where("user", "==", username).Documents(ctx) + iterMatchedUser := collRef.Where("matchedUser", "==", username).Documents(ctx) + + // Map data + var histories []models.CollaborationHistory + for { + doc, err := iterUser.Next() + if err == iterator.Done { + break + } + if err != nil { + http.Error(w, "Failed to get histories for user", http.StatusInternalServerError) + return + } + + var history models.CollaborationHistory + if err := doc.DataTo(&history); err != nil { + http.Error(w, "Failed to map history data for user", http.StatusInternalServerError) + return + } + history.MatchID = doc.Ref.ID + + histories = append(histories, history) + } + + for { + doc, err := iterMatchedUser.Next() + if err == iterator.Done { + break + } + if err != nil { + http.Error(w, "Failed to get histories for matched user", http.StatusInternalServerError) + return + } + + var history models.CollaborationHistory + if err := doc.DataTo(&history); err != nil { + http.Error(w, "Failed to map history data for matched user", http.StatusInternalServerError) + return + } + history.MatchID = doc.Ref.ID + + histories = append(histories, history) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(histories) +} diff --git a/apps/history-service/handlers/read.go b/apps/history-service/handlers/read.go new file mode 100644 index 0000000000..fd97c5d031 --- /dev/null +++ b/apps/history-service/handlers/read.go @@ -0,0 +1,45 @@ +package handlers + +import ( + "encoding/json" + "github.com/go-chi/chi/v5" + "google.golang.org/api/iterator" + "history-service/models" + "net/http" +) + +// Read a code snippet by ID +func (s *Service) ReadHistory(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + matchId := chi.URLParam(r, "matchId") + + // Reference document + docRef := s.Client.Collection("collaboration-history").Doc(matchId) + + // Get data + doc, err := docRef.Get(ctx) + if err != nil { + if err != iterator.Done { + http.Error(w, "History not found", http.StatusNotFound) + return + } + http.Error(w, "Failed to get history", http.StatusInternalServerError) + return + } + + // Map data + var collaborationHistory models.CollaborationHistory + if err := doc.DataTo(&collaborationHistory); err != nil { + http.Error(w, "Failed to map history data", http.StatusInternalServerError) + return + } + collaborationHistory.MatchID = doc.Ref.ID + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(collaborationHistory) +} + +//curl -X GET http://localhost:8082/histories/largSKbROswF5pveMkEL \ +//-H "Content-Type: application/json" diff --git a/apps/history-service/handlers/server.go b/apps/history-service/handlers/server.go new file mode 100644 index 0000000000..5829c0ad39 --- /dev/null +++ b/apps/history-service/handlers/server.go @@ -0,0 +1,7 @@ +package handlers + +import "cloud.google.com/go/firestore" + +type Service struct { + Client *firestore.Client +} diff --git a/apps/history-service/handlers/update.go b/apps/history-service/handlers/update.go new file mode 100644 index 0000000000..b6cb953709 --- /dev/null +++ b/apps/history-service/handlers/update.go @@ -0,0 +1,93 @@ +package handlers + +import ( + "cloud.google.com/go/firestore" + "encoding/json" + "github.com/go-chi/chi/v5" + "google.golang.org/api/iterator" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "history-service/models" + "history-service/utils" + "net/http" +) + +// Update an existing code snippet +func (s *Service) UpdateHistory(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + // Parse request + matchId := chi.URLParam(r, "matchId") + var updatedHistory models.CollaborationHistory + if err := utils.DecodeJSONBody(w, r, &updatedHistory); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // Reference document + docRef := s.Client.Collection("collaboration-history").Doc(matchId) + + // Validation + // Check if exists + _, err := docRef.Get(ctx) + if err != nil { + if status.Code(err) == codes.NotFound { + http.Error(w, "History not found", http.StatusNotFound) + return + } + http.Error(w, "Error fetching history", http.StatusInternalServerError) + return + } + + // Prepare the update data. + updates := []firestore.Update{ + {Path: "code", Value: updatedHistory.Code}, + {Path: "updatedAt", Value: firestore.ServerTimestamp}, + } + + // Update database + _, err = docRef.Update(ctx, updates) + if err != nil { + http.Error(w, "Error updating history", http.StatusInternalServerError) + return + } + + // Get data + doc, err := docRef.Get(ctx) + if err != nil { + if err != iterator.Done { + http.Error(w, "History not found", http.StatusNotFound) + return + } + http.Error(w, "Failed to get history", http.StatusInternalServerError) + return + } + + // Map data + if err := doc.DataTo(&updatedHistory); err != nil { + http.Error(w, "Failed to map history data", http.StatusInternalServerError) + return + } + updatedHistory.MatchID = doc.Ref.ID + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(updatedHistory) +} + +//curl -X PUT http://localhost:8082/histories/largSKbROswF5pveMkEL \ +//-H "Content-Type: application/json" \ +//-d "{ +//\"title\": \"Hello World in Python\", +//\"code\": \"print('Hello, Universe!')\", +//\"language\": \"Python\", +//\"user\": \"user789\", +//\"matchedUser\": \"user123\", +//\"matchId\": \"match123\", +//\"matchedTopics\": [\"Python\", \"Programming\"], +//\"questionDocRefId\": \"question123\", +//\"questionDifficulty\": \"Easy\", +//\"questionTopics\": [\"Python\", \"Programming\"], +//\"createdAt\": \"2024-10-27T10:00:00Z\", +//\"updatedAt\": \"2024-10-27T10:00:00Z\" +//}" diff --git a/apps/history-service/main.go b/apps/history-service/main.go new file mode 100644 index 0000000000..cf69c934d2 --- /dev/null +++ b/apps/history-service/main.go @@ -0,0 +1,103 @@ +package main + +import ( + "cloud.google.com/go/firestore" + "context" + firebase "firebase.google.com/go/v4" + "fmt" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/cors" + "github.com/joho/godotenv" + "google.golang.org/api/option" + "history-service/handlers" + "log" + "net/http" + "os" + "time" +) + +func main() { + err := godotenv.Load() + if err != nil { + log.Fatal("Error loading .env file") + } + + // Initialize Firestore client + ctx := context.Background() + client, err := initFirestore(ctx) + if err != nil { + log.Fatalf("Failed to initialize Firestore client: %v", err) + } + defer client.Close() + + service := &handlers.Service{Client: client} + + r := initChiRouter(service) + initRestServer(r) +} + +func initFirestore(ctx context.Context) (*firestore.Client, error) { + credentialsPath := os.Getenv("FIREBASE_CREDENTIAL_PATH") + opt := option.WithCredentialsFile(credentialsPath) + app, err := firebase.NewApp(ctx, nil, opt) + if err != nil { + return nil, fmt.Errorf("failed to initialize Firebase App: %v", err) + } + + client, err := app.Firestore(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get Firestore client: %v", err) + } + return client, nil +} + +func initChiRouter(service *handlers.Service) *chi.Mux { + r := chi.NewRouter() + r.Use(middleware.Logger) + r.Use(middleware.Timeout(60 * time.Second)) + + r.Use(cors.Handler(cors.Options{ + // AllowedOrigins: []string{"http://localhost:3000"}, // Use this to allow specific origin hosts + AllowedOrigins: []string{"https://*", "http://*"}, + // AllowOriginFunc: func(r *http.Request, origin string) bool { return true }, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, + ExposedHeaders: []string{"Link"}, + AllowCredentials: false, + MaxAge: 300, // Maximum value not ignored by any of major browsers + })) + + registerRoutes(r, service) + + return r +} + +func registerRoutes(r *chi.Mux, service *handlers.Service) { + r.Route("/histories", func(r chi.Router) { + r.Get("/{username}", service.ListUserHistories) + //r.Post("/", service.CreateHistory) + + r.Route("/{matchId}", func(r chi.Router) { + r.Put("/", service.CreateOrUpdateHistory) + r.Get("/", service.ReadHistory) + //r.Put("/", service.UpdateHistory) + //r.Delete("/", service.DeleteHistory) + }) + }) +} + +func initRestServer(r *chi.Mux) { + // Serve on port 8082 if no port found + port := os.Getenv("PORT") + if port == "" { + port = "8082" + } + + // Start the server + log.Printf("Starting REST server on http://localhost:%s", port) + err := http.ListenAndServe(fmt.Sprintf(":%s", port), r) + if err != nil { + log.Fatalf("Failed to start server: %v", err) + } +} diff --git a/apps/history-service/models/models.go b/apps/history-service/models/models.go new file mode 100644 index 0000000000..9928b8809b --- /dev/null +++ b/apps/history-service/models/models.go @@ -0,0 +1,20 @@ +package models + +import "time" + +type CollaborationHistory struct { + Title string `json:"title" firestore:"title"` + Code string `json:"code" firestore:"code"` + Language string `json:"language" firestore:"language"` + User string `json:"user" firestore:"user"` + MatchedUser string `json:"matchedUser" firestore:"matchedUser"` + MatchID string `json:"matchId" firestore:"matchId"` + MatchedTopics []string `json:"matchedTopics" firestore:"matchedTopics"` + QuestionDocRefID string `json:"questionDocRefId" firestore:"questionDocRefId"` + QuestionDifficulty string `json:"questionDifficulty" firestore:"questionDifficulty"` + QuestionTopics []string `json:"questionTopics" firestore:"questionTopics"` + + // Special DB fields + CreatedAt time.Time `json:"createdAt" firestore:"createdAt"` + UpdatedAt time.Time `json:"updatedAt" firestore:"updatedAt"` +} diff --git a/apps/history-service/utils/decode.go b/apps/history-service/utils/decode.go new file mode 100644 index 0000000000..981df9a617 --- /dev/null +++ b/apps/history-service/utils/decode.go @@ -0,0 +1,20 @@ +package utils + +import ( + "encoding/json" + "fmt" + "net/http" +) + +// DecodeJSONBody decodes the request body into the given destination +func DecodeJSONBody(w http.ResponseWriter, r *http.Request, dst interface{}) error { + decoder := json.NewDecoder(r.Body) + decoder.DisallowUnknownFields() + + err := decoder.Decode(&dst) + if err != nil { + return fmt.Errorf("Invalid request payload: " + err.Error()) + } + + return nil +}