18 | );
19 | };
20 |
21 | export default StatBlock;
22 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/question.yml:
--------------------------------------------------------------------------------
1 | name: Question
2 | description: Ask a question or open discussion on a topic
3 | labels: ["question"]
4 | body:
5 | - type: markdown
6 | attributes:
7 | value: If you have any questions or want to create an open discussion about a feature, please enter it here!
8 | - type: textarea
9 | id: question
10 | attributes:
11 | label: What is your question? Describe in detail.
12 | description: A clear and concise description of what the question is.
13 | placeholder: ex. Should this screen be changed to...?
14 | - type: textarea
15 | id: context
16 | attributes:
17 | label: Additional context
18 | description: Put additional context for your question here. This can be screenshots, code, or anything else!
19 |
--------------------------------------------------------------------------------
/server/router/state.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "server/judging"
5 | "server/logging"
6 | "server/models"
7 |
8 | "github.com/gin-gonic/gin"
9 | "go.mongodb.org/mongo-driver/mongo"
10 | )
11 |
12 | type State struct {
13 | Db *mongo.Database
14 | Clock *models.SafeClock
15 | Comps *judging.Comparisons
16 | Logger *logging.Logger
17 | Limiter *Limiter
18 | }
19 |
20 | func NewState(db *mongo.Database, clock *models.SafeClock, comps *judging.Comparisons, logger *logging.Logger, limiter *Limiter) *State {
21 | return &State{
22 | Db: db,
23 | Clock: clock,
24 | Comps: comps,
25 | Logger: logger,
26 | Limiter: limiter,
27 | }
28 | }
29 |
30 | func GetState(ctx *gin.Context) *State {
31 | state := ctx.MustGet("state").(*State)
32 | return state
33 | }
34 |
--------------------------------------------------------------------------------
/client/src/components/judge/dnd/DragHamburger.tsx:
--------------------------------------------------------------------------------
1 | const DragHamburger = () => {
2 | return (
3 |
4 |
16 |
17 | );
18 | };
19 |
20 | export default DragHamburger;
21 |
--------------------------------------------------------------------------------
/tests/src/util.go:
--------------------------------------------------------------------------------
1 | package src
2 |
3 | import (
4 | "log"
5 | "os"
6 | "time"
7 | )
8 |
9 | // GetEnv returns the value of the environmental variable or panics if it does not exist
10 | func GetEnv(key string) string {
11 | val, ok := os.LookupEnv(key)
12 | if !ok {
13 | log.Fatalf("ERROR: %s environmental variable not defined\n", key)
14 | return ""
15 | }
16 | return val
17 | }
18 |
19 | // GetOptEnv returns the value of the environmental variable or the default value if it does not exist
20 | func GetOptEnv(key string, defaultVal string) string {
21 | val, ok := os.LookupEnv(key)
22 | if !ok {
23 | return defaultVal
24 | }
25 | return val
26 | }
27 |
28 | // GetDateTime returns a formatted datetime string
29 | func GetDateTime() string {
30 | now := time.Now()
31 | return now.Format("January 2, 2006 @ 03:04 PM MST")
32 | }
33 |
--------------------------------------------------------------------------------
/docs/docs/usage/expo.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 6
3 | title: Project Expo
4 | description: Details about the Project Expo page
5 | ---
6 |
7 | # Project Expo
8 |
9 | The project expo page is the public project list, showing all projects with their table numbers and [groups](/docs/usage/admin/groups) (if enabled):
10 |
11 | 
12 |
13 | Clicking on the headers will sort by table number or name (alphabetically). You can click on the names of each project to go to the linked `url` of the project.
14 |
15 | The dropdown at the top lets you select which track to view. Note that ALL tracks will be listed here, not only the ones that are going to be judged by Jury.
16 |
17 | Click on **Print this page** to get a printable view of the page. Note that the selected track will be displayed on the print page as well.
18 |
--------------------------------------------------------------------------------
/client/src/components/admin/add-judges/AddJudgeStatsPanel.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import StatBlock from '../../StatBlock';
3 | import { useAdminStore } from '../../../store';
4 |
5 | const AddJudgeStatsPanel = () => {
6 | const stats = useAdminStore((state) => state.judgeStats);
7 | const fetchStats = useAdminStore((state) => state.fetchJudgeStats);
8 |
9 | useEffect(() => {
10 | fetchStats();
11 | }, []);
12 |
13 | return (
14 |
29 | );
30 | };
31 |
32 | export default ChallengeBlock;
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Michael Zhao
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
1 | name: Feature Request
2 | description: Suggest an idea or a feature
3 | labels: ["new feature"]
4 | body:
5 | - type: markdown
6 | attributes:
7 | value: Thanks for helping out and suggesting a new feature!!
8 | - type: textarea
9 | id: description
10 | attributes:
11 | label: Describe the feature you'd like
12 | description: A clear and concise description of what you want to happen or the feature you want.
13 | placeholder: ex. I would like to add a button...
14 | validations:
15 | required: true
16 | - type: textarea
17 | id: justification
18 | attributes:
19 | label: Pre-existing Issue or Justification
20 | description: Is your feature request related to a problem? Please describe the current issue you are facing or reason this feature should be added.
21 | placeholder: ex. I'm always frustrated when [...]
22 | - type: textarea
23 | id: context
24 | attributes:
25 | label: Additional context
26 | description: Put additional context for your feature request here. This can be screenshots, code, or anything else!
27 |
--------------------------------------------------------------------------------
/docs/src/css/custom.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Any CSS included here will be global. The classic template
3 | * bundles Infima by default. Infima is a CSS framework designed to
4 | * work well for content-centric websites.
5 | */
6 |
7 | /* You can override the default Infima variables here. */
8 | :root {
9 | --ifm-color-primary: #0092c3;
10 | --ifm-color-primary-dark: #007197;
11 | --ifm-color-primary-darker: #005a79;
12 | --ifm-color-primary-darkest: #00465e;
13 | --ifm-color-primary-light: #18bcf2;
14 | --ifm-color-primary-lighter: #39cbfc;
15 | --ifm-color-primary-lightest: #5ed7ff;
16 | --ifm-code-font-size: 95%;
17 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
18 | }
19 |
20 | /* For readability concerns, you should choose a lighter palette in dark mode. */
21 | [data-theme='dark'] {
22 | --ifm-color-primary: #00ACE6;
23 | --ifm-color-primary-dark: #0092c3;
24 | --ifm-color-primary-darker: #007197;
25 | --ifm-color-primary-darkest: #005a79;
26 | --ifm-color-primary-light: #18bcf2;
27 | --ifm-color-primary-lighter: #39cbfc;
28 | --ifm-color-primary-lightest: #5ed7ff;
29 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
30 | }
31 |
--------------------------------------------------------------------------------
/client/src/components/judge/dnd/SortableItem.tsx:
--------------------------------------------------------------------------------
1 | import { useSortable } from '@dnd-kit/sortable';
2 | import { CSS } from '@dnd-kit/utilities';
3 | import RankItem from './RankItem';
4 |
5 | interface SortableItemProps {
6 | item: SortableJudgedProject;
7 | ranking: number;
8 | children?: React.ReactNode;
9 | disabled?: boolean;
10 | }
11 |
12 | const SortableItem = (props: SortableItemProps) => {
13 | if (!props.item) {
14 | return null;
15 | }
16 | const { attributes, isDragging, listeners, setNodeRef, transform, transition } = useSortable({
17 | id: props.item.id,
18 | disabled: props.disabled,
19 | });
20 |
21 | const style = {
22 | transform: CSS.Transform.toString(transform),
23 | transition,
24 | touchAction: 'initial',
25 | };
26 |
27 | return (
28 |
36 | {props.children}
37 |
38 | );
39 | };
40 |
41 | export default SortableItem;
42 |
--------------------------------------------------------------------------------
/client/src/pages/Home.tsx:
--------------------------------------------------------------------------------
1 | import Button from '../components/Button';
2 | import Container from '../components/Container';
3 |
4 | const App = () => {
5 | return (
6 |
7 |
26 | The dev server should be run with Docker. If you are running manually, run the frontend
27 | node app and backend rust app separately. Note that on the development instance, this
28 | will be the page that the button takes you to due to the nature of the app. Simply go to
29 | the judge login page and enter the code.
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/docs/static/img/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/client/src/components/InfoPopup.tsx:
--------------------------------------------------------------------------------
1 | import Button from './Button';
2 | import Popup from './Popup';
3 |
4 | interface InfoPopupProps {
5 | /* State variable for open/closed */
6 | enabled: boolean;
7 |
8 | /* Function to modify the popup state variable */
9 | setEnabled: React.Dispatch>;
10 |
11 | /* Title Text */
12 | title: string;
13 |
14 | /* Submit Text */
15 | submitText: string;
16 |
17 | /* React children, corresponds to the body content */
18 | children?: React.ReactNode;
19 |
20 | /* If true, button is red */
21 | red?: boolean;
22 | }
23 |
24 | const InfoPopup = (props: InfoPopupProps) => {
25 | if (!props.enabled) {
26 | return null;
27 | }
28 |
29 | return (
30 |
31 |
{props.title}
32 |
{props.children}
33 |
34 |
41 |
42 |
43 | );
44 | };
45 |
46 | export default InfoPopup;
47 |
--------------------------------------------------------------------------------
/docs/docs/details/frontend/routing.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 1
3 | title: Routing
4 | description: How routing works on the frontend application.
5 | ---
6 |
7 | # Routing (Specifially React Routing)
8 |
9 | Jury uses [React Router](https://reactrouter.com/en/main) to do routing. All routes are defined in the `client/src/index.tsx` page. Every component used on this page for the main pages should be placed in the `client/src/pages` directory. While the placement of the page components don't influence their actual location, it's good practice to place the pages in subdirectories corresponding to their path.
10 |
11 | ## Page Format
12 |
13 | Generally, a page should look like the following. The `Container` component wraps the entire page to limit its width, used for all of the judging pages. The `Helmet` component sets the title, and the `JuryHeader` component displays the title of the app and some other things, based on props set (such as a logout button and a back button).
14 |
15 | ```jsx
16 | import Container from '../components/Container';
17 |
18 | const PageName = () => {
19 | return (
20 | <>
21 |
22 | Title of Page | Jury
23 |
24 |
25 |
26 | {/* Page content */}
27 |
28 | >
29 | );
30 | };
31 |
32 | export default PageName;
33 | ```
34 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # STEP 0: Statically build node client
2 | FROM node:lts-hydrogen AS client-builder
3 | WORKDIR /client
4 | COPY client ./
5 | COPY ["client/package.json", "client/tailwind.config.js", "client/tsconfig.json", "./"]
6 |
7 | ARG VITE_JURY_NAME
8 | ARG VITE_HUB
9 | ARG VITE_JURY_URL
10 |
11 | RUN yarn install
12 |
13 | ARG NODE_ENV=production
14 | RUN yarn build
15 |
16 | # STEP 1: Compile backend
17 | FROM golang:1.23 AS builder
18 | WORKDIR /usr/src/jury
19 |
20 | # Copy over the app
21 | COPY server ./
22 | RUN rm -rf public
23 | COPY --from=client-builder /client/build public
24 |
25 | # Install dependencies
26 | RUN go mod download
27 |
28 | ARG MONGODB_URI=$MONGODB_URI
29 | ARG JURY_ADMIN_PASSWORD=$JURY_ADMIN_PASSWORD
30 | ARG EMAIL_HOST=$EMAIL_HOST
31 | ARG EMAIL_PORT=$EMAIL_PORT
32 | ARG EMAIL_FROM=$EMAIL_FROM
33 | ARG EMAIL_FROM_NAME=$EMAIL_FROM_NAME
34 | ARG EMAIL_USERNAME=$EMAIL_USERNAME
35 | ARG EMAIL_PASSWORD=$EMAIL_PASSWORD
36 | ARG SENDGRID_API_KEY=$SENDGRID_API_KEY
37 |
38 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /go/bin/jury
39 |
40 | # STEP 2: Main running container
41 | FROM scratch
42 |
43 | COPY --from=builder /go/bin/jury .
44 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
45 | COPY --from=client-builder /client/build /public
46 | COPY ./server/email.html /email.html
47 |
48 | ENV GIN_MODE=release
49 | EXPOSE $PORT
50 |
51 | ENTRYPOINT [ "./jury" ]
52 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Jury
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
16 |
17 |
18 |
19 |
20 |
21 |
31 |
32 |
--------------------------------------------------------------------------------
/docs/docs/reference/envs.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 3
3 | ---
4 |
5 | # Environmental Variables
6 |
7 | This page will go over the specifics of the environmental variables you need to define when deploying locally.
8 |
9 | ```
10 | JURY_NAME=
11 | JURY_ADMIN_PASSWORD=
12 |
13 | MONGODB_URI=
14 |
15 | EMAIL_HOST=
16 | EMAIL_PORT=
17 | EMAIL_FROM=
18 | EMAIL_FROM_NAME=
19 | EMAIL_PASSWORD=
20 | SENDGRID_API_KEY=
21 |
22 | PORT=
23 | ```
24 |
25 | The `JURY_NAME` and `JURY_ADMIN_PASSWORD` are simply the name of the app and the admin password that you are using for local development. For development, the values here don't matter that much, but you should remember your admin password to log in (I personally use the classic `admin` password).
26 |
27 | If you are using MongoDB Atlas, you should fill in the `MONGODB_URI` field using the [instructions on the MongoDB Atlas starter guide](https://www.mongodb.com/docs/atlas/getting-started/). This is the recommended way to run the application locally. If you are instead running MongoDB locally, you will need to define the `MONGODB_USER` and `MONGODB_PASS` fields -- see the [contributing page](/docs/contributing#with-docker--local-database) for more details.
28 |
29 | For all email fields, refer to the information in the ["Deploying for your Hackathon"](/docs/usage/deploy#email-hosting) page.
30 |
31 | Finally, the definition of the `PORT` variable is optional -- specify this if you wish to connect to your app on a different port.
32 |
--------------------------------------------------------------------------------
/docs/docs/details/backend/structure.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 1
3 | title: File Structure
4 | description: How the backend code files are structured
5 | ---
6 |
7 | # File Structure
8 |
9 | The backend consists of multiple [modules](https://go.dev/blog/using-go-modules), each one in its own directory. Here is an overview of the modules:
10 |
11 | ### config
12 |
13 | This small module simply contains functions for checking and getting environmental variables.
14 |
15 | ### database
16 |
17 | This module contains function to interact with the database.
18 |
19 | ### funcs
20 |
21 | Specialized functionality such as CSV parsing and email sending.
22 |
23 | ### judging
24 |
25 | Complex functions used for the main judging flow. This includes aggregating judging scores, picking the next project, and maintaining the project comparisons matrix.
26 |
27 | ### logging
28 |
29 | The logging module contains all functionality to write to the admin log.
30 |
31 | ### models
32 |
33 | All the models (structs) that is used to represent the data in Jury. This includes judges, projects, and options.
34 |
35 | ### public
36 |
37 | A dummy directory for the frontend. In development, this is used to serve a dummy page. In production, the statically built frontend will be copied to this directory before the Go build process.
38 |
39 | ### router
40 |
41 | The main code for the API and route handler functions.
42 |
43 | ### util
44 |
45 | Utility functions used across the backend app.
46 |
--------------------------------------------------------------------------------
/server/database/util.go:
--------------------------------------------------------------------------------
1 | package database
2 |
3 | import (
4 | "context"
5 |
6 | "go.mongodb.org/mongo-driver/mongo"
7 | "go.mongodb.org/mongo-driver/mongo/options"
8 | "go.mongodb.org/mongo-driver/mongo/writeconcern"
9 | )
10 |
11 | // WithTransaction runs a function with a transaction,
12 | // returning only an error if the transaction fails.
13 | func WithTransaction(db *mongo.Database, fn func(mongo.SessionContext) error) error {
14 | wc := writeconcern.Majority()
15 | txnOptions := options.Transaction().SetWriteConcern(wc)
16 |
17 | session, err := db.Client().StartSession()
18 | if err != nil {
19 | return err
20 | }
21 | defer session.EndSession(context.Background())
22 |
23 | wrappedFn := func(sc mongo.SessionContext) (interface{}, error) {
24 | return nil, fn(sc)
25 | }
26 |
27 | _, err = session.WithTransaction(context.Background(), wrappedFn, txnOptions)
28 | return err
29 | }
30 |
31 | // WithTransactionItem runs a function with a transaction, returning the result of the function
32 | // and an error if the transaction fails.
33 | func WithTransactionItem(db *mongo.Database, fn func(mongo.SessionContext) (any, error)) (any, error) {
34 | wc := writeconcern.Majority()
35 | txnOptions := options.Transaction().SetWriteConcern(wc)
36 |
37 | session, err := db.Client().StartSession()
38 | if err != nil {
39 | return nil, err
40 | }
41 | defer session.EndSession(context.Background())
42 |
43 | return session.WithTransaction(context.Background(), fn, txnOptions)
44 | }
45 |
--------------------------------------------------------------------------------
/server/models/clock.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "sync"
5 | "time"
6 | )
7 |
8 | type ClockState struct {
9 | StartTime int64 `json:"start_time" bson:"start_time"`
10 | PauseTime int64 `json:"pause_time" bson:"pause_time"`
11 | Running bool `json:"running" bson:"running"`
12 | }
13 |
14 | func NewClockState() *ClockState {
15 | return &ClockState{
16 | StartTime: 0,
17 | PauseTime: 0,
18 | Running: false,
19 | }
20 | }
21 |
22 | // Gets the current clock time in milliseconds
23 | func GetCurrTime() int64 {
24 | return int64(time.Now().UnixNano() / 1000000)
25 | }
26 |
27 | func (c *ClockState) Pause() {
28 | if !c.Running {
29 | return
30 | }
31 | c.Running = false
32 | c.PauseTime = c.PauseTime + GetCurrTime() - c.StartTime
33 | }
34 |
35 | func (c *ClockState) Resume() {
36 | if c.Running {
37 | return
38 | }
39 | c.Running = true
40 | c.StartTime = GetCurrTime()
41 | }
42 |
43 | func (c *ClockState) Reset() {
44 | c.StartTime = 0
45 | c.PauseTime = 0
46 | c.Running = false
47 | }
48 |
49 | func (c *ClockState) GetDuration() int64 {
50 | if !c.Running {
51 | return c.PauseTime
52 | }
53 | return c.PauseTime + GetCurrTime() - c.StartTime
54 | }
55 |
56 | // SafeClock wraps ClockState in a mutex so it can be used safely across threads
57 | type SafeClock struct {
58 | Mutex sync.Mutex
59 | State ClockState
60 | }
61 |
62 | func NewSafeClock(clock *ClockState) *SafeClock {
63 | return &SafeClock{
64 | Mutex: sync.Mutex{},
65 | State: *clock,
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "docs",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "docusaurus": "docusaurus",
7 | "start": "docusaurus start",
8 | "build": "docusaurus build",
9 | "swizzle": "docusaurus swizzle",
10 | "deploy": "docusaurus deploy",
11 | "clear": "docusaurus clear",
12 | "serve": "docusaurus serve",
13 | "write-translations": "docusaurus write-translations",
14 | "write-heading-ids": "docusaurus write-heading-ids",
15 | "typecheck": "tsc"
16 | },
17 | "dependencies": {
18 | "@docusaurus/core": "^3.8.1",
19 | "@docusaurus/preset-classic": "^3.8.1",
20 | "@mdx-js/react": "^3.0.0",
21 | "clsx": "^2.0.0",
22 | "prism-react-renderer": "^2.3.0",
23 | "react": "^18.0.0",
24 | "react-dom": "^18.0.0"
25 | },
26 | "devDependencies": {
27 | "@docusaurus/module-type-aliases": "^3.8.1",
28 | "@docusaurus/tsconfig": "^3.8.1",
29 | "@docusaurus/types": "^3.8.1",
30 | "typescript": "~5.2.2"
31 | },
32 | "browserslist": {
33 | "production": [
34 | ">0.5%",
35 | "not dead",
36 | "not op_mini all"
37 | ],
38 | "development": [
39 | "last 3 chrome version",
40 | "last 3 firefox version",
41 | "last 5 safari version"
42 | ]
43 | },
44 | "engines": {
45 | "node": ">=18.0"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: Deploy Test Application
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | workflow_dispatch:
8 | jobs:
9 | deploy:
10 | runs-on: ubuntu-latest
11 | steps:
12 | # Checkout code
13 | - name: Checkout code
14 | uses: actions/checkout@v4
15 |
16 | # Build Docker image
17 | - name: Build image
18 | run: |
19 | JURY_NAME="Test Hackathon Judging" docker compose build
20 |
21 | # Log into Github Container Registry
22 | - name: Log in to registry
23 | run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
24 |
25 | # Push Image to Github Container Registry
26 | - name: Push image
27 | run: |
28 | IMAGE_ID=ghcr.io/${{ github.repository_owner }}/jury
29 | IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') # Convert to lowercase
30 | docker tag jury-main $IMAGE_ID:latest
31 | docker push $IMAGE_ID:latest
32 |
33 | # Connect to server
34 | - name: Restart docker container on server
35 | uses: appleboy/ssh-action@v1
36 | with:
37 | host: ${{ secrets.SSH_HOST }}
38 | username: ${{ secrets.SSH_USERNAME }}
39 | key: ${{ secrets.SSH_KEY }}
40 | script: |
41 | docker pull ghcr.io/hackutd/jury:latest
42 | docker stop jury-main && sleep 2 && docker run --rm -d --name jury-main --env-file ./jury.env -p 8083:8080 ghcr.io/hackutd/jury:latest
43 |
--------------------------------------------------------------------------------
/tests/src/logging.go:
--------------------------------------------------------------------------------
1 | package src
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | "strings"
8 | )
9 |
10 | type LogLevel int
11 |
12 | const (
13 | Error LogLevel = iota
14 | Warn
15 | Info
16 | Verbose
17 | )
18 |
19 | type Logger struct {
20 | OutFile *os.File
21 | }
22 |
23 | // NewLogger will create a new logger with the defined output sources
24 | func NewLogger(outFile string) *Logger {
25 | // Open the file for writing
26 | file, err := os.OpenFile(outFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
27 | if err != nil {
28 | fmt.Printf("Error opening log file: %s\n", err.Error())
29 | }
30 |
31 | return &Logger{OutFile: file}
32 | }
33 |
34 | // Log will log the message to the defined input sources
35 | func (l *Logger) Log(level LogLevel, format string, a ...any) {
36 | if logLevelToInt(GetEnv("LOG_LEVEL")) < int(level) {
37 | return
38 | }
39 |
40 | message := fmt.Sprintf(format, a...)
41 |
42 | l.OutFile.WriteString(message)
43 | fmt.Println(message)
44 | }
45 |
46 | // LogLn will log the message to the defined input sources with a newline character
47 | func (l *Logger) LogLn(level LogLevel, message string) {
48 | l.Log(level, "%s\n", message)
49 | }
50 |
51 | func logLevelToInt(level string) int {
52 | switch strings.ToLower(level) {
53 | case "error":
54 | return int(Error)
55 | case "warn":
56 | return int(Warn)
57 | case "info":
58 | return int(Info)
59 | case "verbose":
60 | return int(Verbose)
61 | default:
62 | log.Println("Invalid log level, defaulting to INFO")
63 | return int(Info)
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/client/src/components/admin/AdminHeader.tsx:
--------------------------------------------------------------------------------
1 | import { useNavigate } from 'react-router-dom';
2 | import { useState } from 'react';
3 | import Button from '../Button';
4 | import PauseButton from './PauseButton';
5 | import ActionsPopup from './ActionsPopup';
6 |
7 | const AdminHeader = () => {
8 | const navigate = useNavigate();
9 | const [actionsPopup, setActionsPopup] = useState(false);
10 |
11 | return (
12 | <>
13 |
37 | );
38 | };
39 |
40 | export default AppHub;
41 |
--------------------------------------------------------------------------------
/server/config/env.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "log"
5 | "os"
6 | "server/util"
7 | )
8 |
9 | var requiredEnvs = [...]string{"JURY_ADMIN_PASSWORD", "EMAIL_FROM"}
10 | var smtpEnvs = []string{"EMAIL_HOST", "EMAIL_USERNAME", "EMAIL_PASSWORD"}
11 | var sendgridEnvs = []string{"SENDGRID_API_KEY", "EMAIL_FROM_NAME"}
12 |
13 | // Checks to see if all required environmental variables are defined
14 | func CheckEnv() {
15 | for _, v := range requiredEnvs {
16 | if !hasEnv(v) {
17 | log.Fatalf("ERROR: %s environmental variable not defined\n", v)
18 | }
19 | }
20 |
21 | // Check to see if either all smtp envs are defined or all sendgrid envs are defined
22 | if !util.All(util.Map(smtpEnvs, hasEnv)) && !util.All(util.Map(sendgridEnvs, hasEnv)) {
23 | log.Fatalf("ERROR: either all envs for smtp or sendgrid must be defined (one of these sets): %v OR %v\n", smtpEnvs, sendgridEnvs)
24 | }
25 | }
26 |
27 | // hasEnv returns true if the environmental variable is defined and not empty
28 | func hasEnv(key string) bool {
29 | val, ok := os.LookupEnv(key)
30 | if !ok {
31 | return false
32 | }
33 | return val != ""
34 | }
35 |
36 | // GetEnv returns the value of the environmental variable or panics if it does not exist
37 | func GetEnv(key string) string {
38 | val, ok := os.LookupEnv(key)
39 | if !ok {
40 | log.Fatalf("ERROR: %s environmental variable not defined\n", key)
41 | return ""
42 | }
43 | return val
44 | }
45 |
46 | // GetOptEnv returns the value of the environmental variable or the default value if it does not exist
47 | func GetOptEnv(key string, defaultVal string) string {
48 | val, ok := os.LookupEnv(key)
49 | if !ok {
50 | return defaultVal
51 | }
52 | return val
53 | }
54 |
--------------------------------------------------------------------------------
/docs/docs/usage/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 1
3 | title: Using Jury
4 | description: Instructions on how to use Jury
5 | ---
6 |
7 | import CalloutButton from '@site/src/components/CalloutButton';
8 |
9 | # Welcome!
10 |
11 | Whether you're interested in using Jury at your own hackathon or wanting to help develop and improve the software, we're glad you're here 💙. Continue reading for information to get Jury up and running in no time for your event. If you instead want to help develop Jury, go to the [Contributing information](/docs/contributing).
12 |
13 | :::info[How does it work?]
14 | For a walkthrough of the judging process using Jury, check out our [Judging Overview](/docs/usage/overview) page. For more technical details, read about how Jury works [here](/docs/details)!
15 | :::
16 |
17 | Jury is a stand-alone application used solely for the judging of hackathon projects. Follow the guide below to set up your own instance of Jury:
18 |
19 |
20 |
21 | For other information regarding the usage of Jury, refer to these following pages:
22 |
23 | - [Physical Judging Setup](/docs/usage/judging-setup) - How to set up the judging space for using Jury
24 | - [Judging Overview](/docs/usage/overview) - A quick runthrough of how judging will look using Jury
25 | - **Jury "How-to" Guide**:
26 | - [Jury Admin](/docs/usage/admin) - Information on how to use the Jury Admin interface
27 | - [Jury Judge Interface](/docs/usage/judge) - Information on how to use the Jury Judging interface
28 | - [Jury Project Expo](/docs/usage/expo) - Information on how to use the Jury Project Expo interface
29 | - [Tips and Contingencies](/docs/usage/tips) - Tips for using Jury and possible contingency plans
30 |
--------------------------------------------------------------------------------
/client/src/components/RadioButton.tsx:
--------------------------------------------------------------------------------
1 | import { twMerge } from 'tailwind-merge';
2 | import Button from './Button';
3 |
4 | export interface RadioOption {
5 | /* Internal value of radio option */
6 | value: string;
7 |
8 | /* Display text of radio option */
9 | title: string;
10 |
11 | /* Subtitle display text of radio option */
12 | subtitle?: string;
13 | }
14 |
15 | interface RadioButtonProps {
16 | /* Radio button content */
17 | option: RadioOption;
18 |
19 | /* State variable for selection */
20 | selected: boolean;
21 |
22 | /* Function to run on click */
23 | onClick: (e: React.MouseEvent) => void;
24 |
25 | /* Color of the popup -- use colors defined in tailwind config */
26 | color: string;
27 | }
28 |
29 | /**
30 | * Component used in a RadioSelect component. The design of this button is so that
31 | * we can have an array of values (hence onClick instead of the state setter function).
32 | * See the RadioSelect component; this component shouldn't be directly used.
33 | */
34 | const RadioButton = (props: RadioButtonProps) => {
35 | return (
36 |
50 | );
51 | };
52 |
53 | export default RadioButton;
54 |
--------------------------------------------------------------------------------
/client/src/components/ConfirmPopup.tsx:
--------------------------------------------------------------------------------
1 | import Button from './Button';
2 | import Popup from './Popup';
3 |
4 | interface PopupProps {
5 | /* State variable for open/closed */
6 | enabled: boolean;
7 |
8 | /* Function to modify the popup state variable */
9 | setEnabled: React.Dispatch>;
10 |
11 | /* Title Text */
12 | title: string;
13 |
14 | /* Submit Text */
15 | submitText: string;
16 |
17 | /* On submit function */
18 | onSubmit: () => void;
19 |
20 | /* React children, corresponds to the body content */
21 | children?: React.ReactNode;
22 |
23 | /* If true, submit button is red */
24 | red?: boolean;
25 |
26 | /* Disable the submit button */
27 | disabledSubmit?: boolean;
28 | }
29 |
30 | const ConfirmPopup = (props: PopupProps) => {
31 | if (!props.enabled) {
32 | return null;
33 | }
34 |
35 | return (
36 |
37 |
51 | );
52 | };
53 |
54 | export default SelectionButton;
55 |
--------------------------------------------------------------------------------
/docs/src/pages/discord.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
--------------------------------------------------------------------------------
/docs/docs/details/frontend/styling.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 3
3 | title: Styling
4 | description: How to style frontend components with Tailwind.
5 | ---
6 |
7 | # Styling
8 |
9 | All elements are to be styled with [Tailwind](https://tailwindcss.com). Tailwind lets us style components without using CSS, instead relying on classNames defined on each element.
10 |
11 | ## Configuration
12 |
13 | Tailwind uses a `tailwind.config.js` file, located in the root directory. In this file, we define the colors, fonts, animations, and plugins that we use with Tailwind. The custom colors and fonts are standardized across Jury and should be used almost everywhere. All colors and fonts can also be viewed on the [Figma board](https://www.figma.com/file/qwBWs4i7pJMpFbcjMffDZU/Jury-(Gavel-Plus)?node-id=8%3A100&t=xYwfPwRAUeJw9jNr-1).
14 |
15 | ## Usage
16 |
17 | We will refer to another example to demonstrate the usage of Tailwind. Here we will look at the `client/src/components/Back.tsx` component:
18 |
19 | ```jsx
20 |
24 | {'<'} Back
25 |
26 | ```
27 |
28 | Notice we are using the `twMerge` function from the [tailwind merge](https://www.npmjs.com/package/tailwind-merge) library. This is used throughout the app to combine styles defined at different levels, such as `props.className` passed in through the component properties. `twMerge` will merge functions, replacing classes that **come later in the parameter list**. For example, if I have `twMerge('text-gold', 'text-primary')` as the className, the function will resolve to `'text-primary'`. The format above where we define the default styling and then merge it with `props.className` is very common among our user-defined components.
29 |
30 | There are a ton of tailwind classes, with most pretty similar to their CSS attribute counterpart. The Tailwind website is extremely useful to finding these classes--though you can just google the CSS attribute + tailwind and the page should show up as well.
31 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: Bug Report
2 | description: Report a bug or something that is wrong
3 | title: "[Bug] "
4 | labels: ["bug"]
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: Thanks for filling out this bug report! Please let us know what went wrong below.
9 | - type: textarea
10 | id: what-happened
11 | attributes:
12 | label: What Happened?
13 | description: What is happening that doesn't seem right, in as much detail as possible.
14 | placeholder: ex. The text box clips out of the edge of the screen
15 | validations:
16 | required: true
17 | - type: textarea
18 | id: reproduce
19 | attributes:
20 | label: Steps to Reproduce
21 | description: Give clear steps on how to reproduce the bug you found
22 | placeholder: 1. \n 2. \n 3. \n 4. \n
23 | validations:
24 | required: true
25 | - type: dropdown
26 | id: device
27 | attributes:
28 | label: What device(s) are you seeing this issue on?
29 | multiple: true
30 | options:
31 | - Linux Computer
32 | - Windows Computer
33 | - Mac Computer
34 | - Android Mobile Device
35 | - iOS Mobile Device
36 | - Other Device (list below)
37 | - type: dropdown
38 | id: browser
39 | attributes:
40 | label: What browser are you seeing this issue on?
41 | multiple: true
42 | options:
43 | - Firefox Desktop
44 | - Chromium Desktop (Chrome, Brave, Edge, Arc, Opera)
45 | - Safari Desktop
46 | - Firefox Mobile
47 | - Chromium Mobile
48 | - Safari Mobile
49 | - Other Browser (list below)
50 | - type: textarea
51 | id: solutions
52 | attributes:
53 | label: Possible Solutions
54 | description: List possible solutions to the bug, if any
55 | - type: textarea
56 | id: additional-info
57 | attributes:
58 | label: Additional Info
59 | description: Please attach any additional information not included above to help us better address this issue; this can be images, videos, or logs (optional)
60 |
--------------------------------------------------------------------------------
/client/src/components/admin/CSVPreview.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { fileToString, pad, parseCSV } from '../../util';
3 |
4 | interface CSVPreviewProps {
5 | /* The data of the CSV form */
6 | data: CSVFormState;
7 | }
8 |
9 | const CSVPreview = (props: CSVPreviewProps) => {
10 | const [csvData, setCsvData] = useState([]);
11 |
12 | useEffect(() => {
13 | if (!props.data.file) return;
14 |
15 | async function loadData() {
16 | // TODO: We assume the delimiter is a comma, but we should allow the user to change it
17 | const rawData = await fileToString(props.data.file as File);
18 | setCsvData(parseCSV(rawData, ','));
19 | console.log(parseCSV(rawData, ','));
20 | }
21 |
22 | loadData();
23 | }, [props.data.file]);
24 |
25 | if (!props.data.file) return null;
26 |
27 | return (
28 |
60 | );
61 | };
62 |
63 | export default ProjectDisplay;
64 |
--------------------------------------------------------------------------------
/docs/docs/details/frontend/components.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 2
3 | title: Components
4 | description: The component system and how to organize the frontend.
5 | ---
6 |
7 | # Components
8 |
9 | All components should be placed in the `client/src/components` directory. In that directory, the components not in a subdirectory are common shared components, such as `Button` and `Card`. These are used across the application.
10 |
11 | Components specific to a page are placed in subdirectories, such as `client/src/components/admin/AdminToolbar.tsx`.
12 |
13 | ## Making a component
14 |
15 | To create a component, refer to the common components. For example, here is the `Card` component:
16 |
17 | ```jsx
18 | import React from 'react';
19 | import { twMerge } from 'tailwind-merge';
20 |
21 | interface ContainerProps {
22 | /* Element children */
23 | children?: React.ReactNode;
24 |
25 | /* Tailwind classes, will override defaults */
26 | className?: string;
27 |
28 | /* If true, don't justify center the entire page */
29 | noCenter?: boolean;
30 | }
31 |
32 | /**
33 | * This is the main container for the site, which is optimized for mobile.
34 | * On desktop devices, a set width will be imposed to mimic mobile displays.
35 | */
36 | const Container = (props: ContainerProps) => {
37 | return (
38 |
45 | {props.children}
46 |
47 | );
48 | };
49 |
50 | export default Container;
51 | ```
52 |
53 | Props should be defined in a separate interface, with doc comments for each prop. If you are passing in children to the component, use `React.ReactNode` as the type. If you are passing in a Tailwind `className`, make sure to make it an optional string. Note that all props that are NOT optional will be required to be used otherwise an error will be thrown.
54 |
55 | ## Using Components
56 |
57 | Refer to the official React docs on [how to use a component](https://react.dev/learn/your-first-component#using-a-component) if you don't know how to.
58 |
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Create React App
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `yarn start`
10 |
11 | Runs the app in the development mode.\
12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
13 |
14 | The page will reload if you make edits.\
15 | You will also see any lint errors in the console.
16 |
17 | ### `yarn test`
18 |
19 | Launches the test runner in the interactive watch mode.\
20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
21 |
22 | ### `yarn build`
23 |
24 | Builds the app for production to the `build` folder.\
25 | It correctly bundles React in production mode and optimizes the build for the best performance.
26 |
27 | The build is minified and the filenames include the hashes.\
28 | Your app is ready to be deployed!
29 |
30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
31 |
32 | ### `yarn eject`
33 |
34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
35 |
36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37 |
38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
39 |
40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
41 |
42 | ## Learn More
43 |
44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
45 |
46 | To learn React, check out the [React documentation](https://reactjs.org/).
47 |
--------------------------------------------------------------------------------
/server/go.mod:
--------------------------------------------------------------------------------
1 | module server
2 |
3 | go 1.23.0
4 |
5 | toolchain go1.23.1
6 |
7 | require (
8 | github.com/gin-contrib/cors v1.7.5
9 | github.com/gin-contrib/static v1.1.5
10 | github.com/gin-gonic/gin v1.10.0
11 | github.com/joho/godotenv v1.5.1
12 | github.com/sendgrid/sendgrid-go v3.16.0+incompatible
13 | go.mongodb.org/mongo-driver v1.17.3
14 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0
15 | )
16 |
17 | require (
18 | github.com/bytedance/sonic v1.13.2 // indirect
19 | github.com/bytedance/sonic/loader v0.2.4 // indirect
20 | github.com/cloudwego/base64x v0.1.5 // indirect
21 | github.com/gabriel-vasile/mimetype v1.4.9 // indirect
22 | github.com/gin-contrib/sse v1.1.0 // indirect
23 | github.com/go-playground/locales v0.14.1 // indirect
24 | github.com/go-playground/universal-translator v0.18.1 // indirect
25 | github.com/go-playground/validator/v10 v10.26.0 // indirect
26 | github.com/goccy/go-json v0.10.5 // indirect
27 | github.com/golang/snappy v1.0.0 // indirect
28 | github.com/json-iterator/go v1.1.12 // indirect
29 | github.com/klauspost/compress v1.18.0 // indirect
30 | github.com/klauspost/cpuid/v2 v2.2.10 // indirect
31 | github.com/kr/text v0.2.0 // indirect
32 | github.com/leodido/go-urn v1.4.0 // indirect
33 | github.com/mattn/go-isatty v0.0.20 // indirect
34 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
35 | github.com/modern-go/reflect2 v1.0.2 // indirect
36 | github.com/montanaflynn/stats v0.7.1 // indirect
37 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect
38 | github.com/sendgrid/rest v2.6.9+incompatible // indirect
39 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
40 | github.com/ugorji/go/codec v1.2.12 // indirect
41 | github.com/xdg-go/pbkdf2 v1.0.0 // indirect
42 | github.com/xdg-go/scram v1.1.2 // indirect
43 | github.com/xdg-go/stringprep v1.0.4 // indirect
44 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
45 | golang.org/x/arch v0.16.0 // indirect
46 | golang.org/x/crypto v0.37.0 // indirect
47 | golang.org/x/net v0.39.0 // indirect
48 | golang.org/x/sync v0.13.0 // indirect
49 | golang.org/x/sys v0.32.0 // indirect
50 | golang.org/x/text v0.24.0 // indirect
51 | google.golang.org/protobuf v1.36.6 // indirect
52 | gopkg.in/yaml.v3 v3.0.1 // indirect
53 | )
54 |
--------------------------------------------------------------------------------
/client/src/components/admin/tables/EditJudgePopup.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { putRequest } from '../../../api';
3 | import { errorAlert } from '../../../util';
4 | import { useAdminStore } from '../../../store';
5 | import ConfirmPopup from '../../ConfirmPopup';
6 | import TextInput from '../../TextInput';
7 | import TextArea from '../../TextArea';
8 |
9 | interface EditJudgePopupProps {
10 | /* Judge to edit */
11 | judge: Judge;
12 |
13 | /* State variable for open/closed */
14 | enabled: boolean;
15 |
16 | /* Function to modify the popup state variable */
17 | setEnabled: React.Dispatch>;
18 | }
19 |
20 | const EditJudgePopup = (props: EditJudgePopupProps) => {
21 | const [isSubmitting, setIsSubmitting] = useState(false);
22 | const fetchJudges = useAdminStore((state) => state.fetchJudges);
23 | const [name, setName] = useState(props.judge.name);
24 | const [email, setEmail] = useState(props.judge.email);
25 | const [notes, setNotes] = useState(props.judge.notes);
26 |
27 | const onSubmit = async () => {
28 | setIsSubmitting(true);
29 |
30 | const data = { name, email, notes };
31 | const res = await putRequest(`/judge/${props.judge.id}`, 'admin', data);
32 | if (res.status !== 200) {
33 | errorAlert(res);
34 | return;
35 | }
36 |
37 | alert('Judge updated successfully!');
38 | setIsSubmitting(false);
39 | props.setEnabled(false);
40 | fetchJudges();
41 | };
42 |
43 | return (
44 |
52 |
{props.judge.name}
53 |
54 |
55 |
56 |
57 |
58 |
59 | );
60 | };
61 |
62 | export default EditJudgePopup;
63 |
--------------------------------------------------------------------------------
/docs/docs/usage/judging-setup.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 2
3 | title: Physical Judging Setup
4 | description: How to set up the physical judging space to use Jury
5 | ---
6 |
7 | # Physical Judging Setup
8 |
9 | Judging with Jury is done in an [**expo style**](https://www.millertanner.com/what-is-an-expo-event/). Think science fair or any expo-style conference that you've gone to. Essentially, all projects will get a table or booth and stay there for the duration of judging. In that time, judges will walk around and view projects by walking to them.
10 |
11 | The setup for an expo style event generally includes booths (most likely just tables or classrooms) where each team will be assigned a table to stand at. Here's an example of a map of tables at an event:
12 |
13 | 
14 |
15 | Jury will automatically assign projects to a table number, so you just need to make sure to have as many tables as projects. If you don't have enough tables, you can simply add more tables as you add projects.
16 |
17 | ## Judging Timeline
18 |
19 | | Time | Event | Description |
20 | | -------- | ---------------------- | ------------------------------------------------------------------------------------------------ |
21 | | 11:00 am | Judging Setup | Set up judging tables ahead of time |
22 | | 11:30 am | Judge Check-in | Give judges 30 mins to arrive and check in |
23 | | 12:00 pm | Projects Due | Hackers should submit projects by now! |
24 | | 12:00 pm | Judging Orientation | Walk through Jury and explain to judges how to use the software |
25 | | 12:00 pm | Add Projects to Jury | Add all projects to Jury, making sure everything looks right, then release project table numbers |
26 | | 12:30 pm | Add Judges to Jury | Add all judges to Jury and move them to the judging room |
27 | | 12:30 pm | Move hackers to tables | Send out the tables to hackers and make sure they are set up |
28 | | 1:00 pm | Judging! | This can last anywhere between 1-3 hours, depending on project to judge ratio. |
29 |
--------------------------------------------------------------------------------
/client/src/components/judge/popups/FinishPopup.tsx:
--------------------------------------------------------------------------------
1 | import Button from '../../Button';
2 | import Popup from '../../Popup';
3 | import Star from '../Star';
4 | import TextArea from '../../TextArea';
5 |
6 | interface FinishPopupProps {
7 | /* Function to modify the popup state variable */
8 | setEnabled: React.Dispatch>;
9 |
10 | /* Judge to vote on */
11 | judge: Judge;
12 |
13 | /* State variable for determining if popup is open */
14 | enabled: boolean;
15 |
16 | /* Callback function for flagging a project */
17 | callback: () => Promise;
18 |
19 | // TODO: Export all this to a global store for the judge
20 | /* Starred status of project */
21 | starred: boolean;
22 |
23 | /* Setter function for starred status */
24 | setStarred: React.Dispatch>;
25 |
26 | /* Notes for project */
27 | notes: string;
28 |
29 | /* Setter function for notes */
30 | setNotes: React.Dispatch>;
31 | }
32 |
33 | /**
34 | * Component to show when the user clicks the "Submit" button
35 | */
36 | const FinishPopup = (props: FinishPopupProps) => {
37 | if (!props.enabled) return null;
38 |
39 | const done = async () => {
40 | await props.callback();
41 | };
42 |
43 | return (
44 |
45 |
Judge Project
46 |
Finish judging this project
47 |
48 |
49 |
50 | Star projects you think should win the top places in the hackathon.
51 |
52 |
53 |
Personal Notes
54 |
60 |
63 |
64 | );
65 | };
66 |
67 | export default FinishPopup;
68 |
--------------------------------------------------------------------------------
/client/src/components/Popup.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { twMerge } from 'tailwind-merge';
3 |
4 | interface PopupProps {
5 | /* State variable for open/closed */
6 | enabled: boolean;
7 |
8 | /* Function to modify the popup state variable */
9 | setEnabled: React.Dispatch>;
10 |
11 | /* React children, corresponds to the body content */
12 | children?: React.ReactNode;
13 |
14 | /* Optional classname style to apply to outer div of popup */
15 | className?: string;
16 | }
17 |
18 | /**
19 | * Generic popup component with a backdrop and popup modal. Create a state variable and pass in the variable and setter function.
20 | * Clicking on the backdrop will close the popup.
21 | */
22 | const Popup = (props: PopupProps) => {
23 | useEffect(() => {
24 | // handle esc key press
25 | const handleKeyDown = (event: KeyboardEvent) => {
26 | if (event.key === 'Escape') {
27 | props.setEnabled(false);
28 | }
29 | };
30 |
31 | if (props.enabled) {
32 | window.addEventListener('keydown', handleKeyDown);
33 | }
34 |
35 | return () => window.removeEventListener('keydown', handleKeyDown);
36 | }, [props.enabled, props.setEnabled]);
37 |
38 | if (!props.enabled) {
39 | return null;
40 | }
41 |
42 | return (
43 | <>
44 |
props.setEnabled(false)}
47 | >
48 |
54 | {/* x to close the popup */}
55 |
62 | {props.children}
63 |
64 | >
65 | );
66 | };
67 |
68 | export default Popup;
69 |
--------------------------------------------------------------------------------
/server/funcs/proj_nums.go:
--------------------------------------------------------------------------------
1 | package funcs
2 |
3 | import (
4 | "errors"
5 | "server/database"
6 | "server/models"
7 | "sort"
8 |
9 | "go.mongodb.org/mongo-driver/mongo"
10 | )
11 |
12 | // ReassignNums assigns project numbers in order.
13 | func ReassignNums(db *mongo.Database) error {
14 | err := database.WithTransaction(db, func(sc mongo.SessionContext) error {
15 | // Get all the projects from the database
16 | projects, err := database.FindAllProjects(db, sc)
17 | if err != nil {
18 | return errors.New("error getting projects from database: " + err.Error())
19 | }
20 |
21 | // If projects is empty, send OK
22 | if len(projects) == 0 {
23 | return nil
24 | }
25 |
26 | // Sort projets by table num
27 | sort.Sort(models.ByTableNumber(projects))
28 |
29 | // Set init table num to 0
30 | tableNum := int64(0)
31 |
32 | // Loop through all projects
33 | for _, project := range projects {
34 | tableNum++
35 | project.Location = tableNum
36 | }
37 |
38 | // Update all projects in the database
39 | err = database.UpdateProjects(db, sc, projects)
40 | if err != nil {
41 | return errors.New("error updating projects in database: " + err.Error())
42 | }
43 | return nil
44 | })
45 |
46 | return err
47 | }
48 |
49 | // IncrementJudgeGroupNum increments every single judges' group number.
50 | func IncrementJudgeGroupNum(db *mongo.Database) error {
51 | return database.WithTransaction(db, func(sc mongo.SessionContext) error {
52 | // Get the options from the database
53 | options, err := database.GetOptions(db, sc)
54 | if err != nil {
55 | return errors.New("error getting options from database: " + err.Error())
56 | }
57 |
58 | // Get the judges from the database
59 | judges, err := database.FindAllJudges(db, sc)
60 | if err != nil {
61 | return errors.New("error getting judges from database: " + err.Error())
62 | }
63 |
64 | // Increment the group number for each judge
65 | for _, judge := range judges {
66 | judge.Group = (judge.Group + 1) % options.NumGroups
67 | }
68 |
69 | // Update all judges in the database
70 | err = database.UpdateJudgeGroups(db, sc, judges)
71 | if err != nil {
72 | return errors.New("error updating judges in database: " + err.Error())
73 | }
74 |
75 | // Increment the manual switch count in the database
76 | err = database.IncrementManualSwitches(db, sc)
77 | if err != nil {
78 | return errors.New("error incrementing manual switches in database: " + err.Error())
79 | }
80 |
81 | return nil
82 | })
83 | }
84 |
--------------------------------------------------------------------------------
/server/util/slices.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "sort"
5 | "strconv"
6 |
7 | "golang.org/x/exp/constraints"
8 | )
9 |
10 | // Map applies a function to each element of a slice and returns a new slice
11 | func Map[T, U interface{}](arr []T, fn func(T) U) []U {
12 | out := make([]U, len(arr))
13 | for i, v := range arr {
14 | out[i] = fn(v)
15 | }
16 | return out
17 | }
18 |
19 | // Any returns true if any element of the slice is true
20 | func Any[T bool](arr []T) bool {
21 | for _, v := range arr {
22 | if v {
23 | return true
24 | }
25 | }
26 | return false
27 | }
28 |
29 | // All returns true if all elements of the slice are true
30 | func All[T bool](arr []T) bool {
31 | for _, v := range arr {
32 | if !v {
33 | return false
34 | }
35 | }
36 | return true
37 | }
38 |
39 | // IndexFunc from golang slices library
40 | func IndexFunc[S ~[]E, E any](s S, f func(E) bool) int {
41 | for i := range s {
42 | if f(s[i]) {
43 | return i
44 | }
45 | }
46 | return -1
47 | }
48 |
49 | // ContainsFunc from golang slices library
50 | func ContainsFunc[S ~[]E, E any](s S, f func(E) bool) bool {
51 | return IndexFunc(s, f) >= 0
52 | }
53 |
54 | // Converts an int slice to string slice
55 | func IntToString[T constraints.Integer](arr []T) []string {
56 | out := make([]string, len(arr))
57 | for i, v := range arr {
58 | out[i] = strconv.Itoa(int(v))
59 | }
60 | return out
61 | }
62 |
63 | // SortMapByValue sorts a map by its values and returns the keys in descending order
64 | func SortMapByValue(m map[int64]int64, desc bool) []int64 {
65 | type kv struct {
66 | Key int64
67 | Value int64
68 | }
69 |
70 | var ss []kv
71 | for k, v := range m {
72 | ss = append(ss, kv{k, v})
73 | }
74 |
75 | // Sort by value
76 | sort.Slice(ss, func(i, j int) bool {
77 | if desc {
78 | return ss[i].Value > ss[j].Value
79 | } else {
80 | return ss[i].Value < ss[j].Value
81 | }
82 | })
83 |
84 | var keys []int64
85 | for _, kv := range ss {
86 | keys = append(keys, kv.Key)
87 | }
88 |
89 | return keys
90 | }
91 |
92 | // SetDiff returns the set difference between two slices
93 | func SetDiff[T comparable](a, b []T) []T {
94 | m := make(map[T]bool)
95 | for _, item := range b {
96 | m[item] = true
97 | }
98 |
99 | var diff []T
100 | for _, item := range a {
101 | if _, ok := m[item]; !ok {
102 | diff = append(diff, item)
103 | }
104 | }
105 |
106 | return diff
107 | }
108 |
--------------------------------------------------------------------------------
/docs/docs/usage/admin/add-projects.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 2
3 | title: Adding Projects
4 | description: How to import projects into Jury
5 | ---
6 |
7 | # Adding Projects
8 |
9 | Before any judging can be done, the projects need to be added to Jury. Go to the admin dashboard, switch to the projects tab, and click "Add Projects". You should be directed to a page that looks like the following.
10 |
11 | 
12 |
13 | There are currently 3 ways to add projects to Jury: adding a single project through the form, uploading a CSV, and uploading an export CSV from Devpost.
14 |
15 | ## Add Project Form
16 |
17 | This form is pretty straightforward and covers the required fields needed to add a project to Jury. You only need to provide a name, description, and project URL. All other fields are optional, but the challenge list is needed to enable Jury to do [track judging](/docs/usage/admin/tracks).
18 |
19 | ## CSV Upload
20 |
21 | You may also choose to upload a CSV of all projects. This is used if you have a project submission portal that is not Devpost. Each line should contain the following comma-separated values:
22 |
23 | - Name
24 | - Description
25 | - Project URL
26 | - "Try It" link
27 | - Video link
28 | - Comma separated challenge list (in quotes)
29 |
30 | When uploading CSVs, you will see a preview of the CSV with each row and column. Use this to confirm whether or not the CSV has a header (you can use the checkbox). Note that [Devpost uploads](#devpost-upload) will ignore this field as all Devpost CSVs will have a header.
31 |
32 | 
33 |
34 | ## Devpost Upload
35 |
36 | For hackathons that use Devpost, you may simply upload the CSV of projects exported from Devpost. To obtain this CSV, go to your hackathon’s **manage page** on Devpost. On that page, you should see a tab labeled **Metrics**. Click this tab and you should see an option to download the CSV. Make sure you do NOT include personal information as its unnecessary for Jury. See the image below for reference. Click the **Generate Report** button. Devpost will refresh the page and show a download link after a few moments (may take longer for bigger events).
37 |
38 | 
39 |
40 | Once you have the CSV downloaded, go back into Jury and upload the CSV. It should correctly import all projects into Jury!
41 |
42 | :::tip
43 | Devpost includes projects that are still drafts (haven't been submitted), but Jury automatically ignores them when importing the CSV.
44 | :::
45 |
--------------------------------------------------------------------------------
/client/src/components/ActionsDropdown.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react';
2 | import { twMerge } from 'tailwind-merge';
3 |
4 | interface ActionsDropdownProps {
5 | /* State variable to open or close the dropdown */
6 | open: boolean;
7 |
8 | /* Function to close the dropdown */
9 | setOpen: React.Dispatch>;
10 |
11 | /* List of actions to be displayed in the dropdown */
12 | actions: string[];
13 |
14 | /* List of functions for the action in the list */
15 | actionFunctions: (() => void)[];
16 |
17 | /* Indices to make red */
18 | redIndices?: number[];
19 |
20 | /* Large text */
21 | large?: boolean;
22 |
23 | /* Additional class names */
24 | className?: string;
25 | }
26 |
27 | const ActionsDropdown = (props: ActionsDropdownProps) => {
28 | const ref = useRef(null);
29 |
30 | useEffect(() => {
31 | function closeClick(event: MouseEvent) {
32 | if (ref && ref.current && !ref.current.contains(event.target as Node)) {
33 | props.setOpen(false);
34 | }
35 | }
36 |
37 | // Bind the event listener
38 | document.addEventListener('mousedown', closeClick);
39 | return () => {
40 | // Unbind the event listener on clean up
41 | document.removeEventListener('mousedown', closeClick);
42 | };
43 | }, [ref]);
44 |
45 | const handleClick = (index: number) => {
46 | props.actionFunctions[index]();
47 | props.setOpen(false);
48 | };
49 |
50 | if (!props.open) {
51 | return null;
52 | }
53 |
54 | return (
55 |
63 | {props.actions.map((action, index) => (
64 |
72 | {action}
73 |
74 | ))}
75 |
76 | );
77 | };
78 |
79 | export default ActionsDropdown;
80 |
--------------------------------------------------------------------------------
/docker-compose.test.yml:
--------------------------------------------------------------------------------
1 | services:
2 | mongo:
3 | build:
4 | context: './'
5 | dockerfile: mongo.Dockerfile
6 | restart: always
7 | environment:
8 | - MONGO_INITDB_ROOT_USERNAME=admin
9 | - MONGO_INITDB_ROOT_PASSWORD=admin
10 | - MONGO_INITDB_DATABASE=jury
11 | - MONGO_REPLICA_SET_NAME=rs0
12 | ports:
13 | - 27017:27017
14 | networks:
15 | - jury-dev-network
16 |
17 | mongorssetup:
18 | depends_on:
19 | - 'mongo'
20 | image: mongo:latest
21 | environment:
22 | - MONGODB_USER=admin
23 | - MONGODB_PASS=admin
24 | volumes:
25 | - .:/scripts
26 | restart: "no"
27 | entrypoint: [ 'bash', '/scripts/init-mongo-rs.sh' ]
28 | networks:
29 | - jury-dev-network
30 |
31 | go-dev:
32 | depends_on:
33 | - 'mongo'
34 | - 'mongorssetup'
35 | container_name: jury-dev-backend
36 | environment:
37 | - MONGODB_URI=mongodb://admin:admin@mongo:27017/
38 | - JURY_ADMIN_PASSWORD=${JURY_ADMIN_PASSWORD}
39 | - EMAIL_HOST=${EMAIL_HOST}
40 | - EMAIL_PORT=${EMAIL_PORT}
41 | - EMAIL_FROM=${EMAIL_FROM}
42 | - EMAIL_FROM_NAME=${EMAIL_FROM_NAME}
43 | - EMAIL_USERNAME=${EMAIL_USERNAME}
44 | - EMAIL_PASSWORD=${EMAIL_PASSWORD}
45 | - SENDGRID_API_KEY=${SENDGRID_API_KEY}
46 | - VITE_JURY_NAME=${JURY_NAME}
47 | - PORT=8000
48 | build:
49 | context: './'
50 | dockerfile: dev.Dockerfile
51 | ports:
52 | - 8000:8000
53 | volumes:
54 | - ./server:/jury
55 | networks:
56 | - jury-dev-network
57 |
58 | go-test:
59 | depends_on:
60 | - 'mongo'
61 | - 'mongorssetup'
62 | - 'go-dev'
63 | container_name: jury-testing
64 | environment:
65 | - MONGODB_URI=mongodb://admin:admin@mongo:27017/
66 | - LOG_LEVEL=${LOG_LEVEL:-info}
67 | - ADMIN_PASSWORD=${JURY_ADMIN_PASSWORD}
68 | - API_URL=http://go-dev:8000/api
69 | build:
70 | context: './'
71 | dockerfile: tests/Dockerfile
72 | ports:
73 | - 8001:8001
74 | volumes:
75 | - ./tests:/jury
76 | networks:
77 | - jury-dev-network
78 |
79 | networks:
80 | jury-dev-network:
81 | driver: bridge
82 |
--------------------------------------------------------------------------------
/docker-compose-mongo.dev.yml:
--------------------------------------------------------------------------------
1 | services:
2 | mongo:
3 | build:
4 | context: './'
5 | dockerfile: mongo.Dockerfile
6 | restart: always
7 | environment:
8 | - MONGO_INITDB_ROOT_USERNAME=${MONGODB_USER}
9 | - MONGO_INITDB_ROOT_PASSWORD=${MONGODB_PASS}
10 | - MONGO_INITDB_DATABASE=jury
11 | - MONGO_REPLICA_SET_NAME=rs0
12 | ports:
13 | - 27017:27017
14 | volumes:
15 | - './data:/data/db'
16 | networks:
17 | - jury-dev-network
18 |
19 | mongorssetup:
20 | depends_on:
21 | - 'mongo'
22 | image: mongo:latest
23 | environment:
24 | - MONGODB_USER=${MONGODB_USER}
25 | - MONGODB_PASS=${MONGODB_PASS}
26 | volumes:
27 | - .:/scripts
28 | restart: "no"
29 | entrypoint: [ 'bash', '/scripts/init-mongo-rs.sh' ]
30 | networks:
31 | - jury-dev-network
32 |
33 | go-dev:
34 | depends_on:
35 | - 'mongo'
36 | - 'mongorssetup'
37 | container_name: jury-dev-backend
38 | environment:
39 | - MONGODB_URI=mongodb://${MONGODB_USER}:${MONGODB_PASS}@mongo:27017/
40 | - JURY_ADMIN_PASSWORD=${JURY_ADMIN_PASSWORD}
41 | - EMAIL_HOST=${EMAIL_HOST}
42 | - EMAIL_PORT=${EMAIL_PORT}
43 | - EMAIL_FROM=${EMAIL_FROM}
44 | - EMAIL_FROM_NAME=${EMAIL_FROM_NAME}
45 | - EMAIL_USERNAME=${EMAIL_USERNAME}
46 | - EMAIL_PASSWORD=${EMAIL_PASSWORD}
47 | - SENDGRID_API_KEY=${SENDGRID_API_KEY}
48 | - VITE_JURY_NAME=${JURY_NAME}
49 | - PORT=8000
50 | build:
51 | context: './'
52 | dockerfile: dev.Dockerfile
53 | ports:
54 | - ${PORT:-8000}:8000
55 | volumes:
56 | - ./server:/jury
57 | networks:
58 | - jury-dev-network
59 |
60 | node-dev:
61 | depends_on:
62 | - 'go-dev'
63 | container_name: jury-dev-frontend
64 | environment:
65 | - VITE_JURY_NAME=${JURY_NAME}
66 | - VITE_JURY_URL=http://localhost:${PORT:-8000}/api
67 | - VITE_HUB=${HEHE:-}
68 | build:
69 | context: './'
70 | dockerfile: client/dev.Dockerfile
71 | ports:
72 | - 3000:3000
73 | volumes:
74 | - ./client:/client
75 | networks:
76 | - jury-dev-network
77 |
78 | networks:
79 | jury-dev-network:
80 | driver: bridge
81 |
--------------------------------------------------------------------------------
/docs/docs/usage/admin/scoring.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 4
3 | title: Score Aggregation
4 | description: How rankings are collected from judges and scores are aggregated.
5 | ---
6 |
7 | # Score Aggregation
8 |
9 | Jury's main feature revolves around its rank aggregation system. As judges view projects, they are asked to provide input through ranking and starring projects. These metrics will be aggregated to form a comprehensive ranking. In the following section, we go more in-depth on how this is done.
10 |
11 | ## Ranking
12 |
13 | The basis of Jury's algorithm is the [Copeland Counting method](https://en.wikipedia.org/wiki/Copeland's_method). A judge will rank as many projects as they can (ideally as many as possible). See the [Judging Interface](/docs/usage/judge) page for a visual on the judges' UI.
14 |
15 | The Copeland method works by breaking down rankings into pairwise comparisons. For example, if a judge has ranked projects in the order `A, B, C`, it would mean the same as the following comparisons: `(A, B), (B, C), (A, C)`, where we make `(winner, loser)` pairs. For every time a project is a "winner," it will get one point; every time it's a "loser," it will lose a point. These point values are then aggregated across all judges' rankings to form a final score for each project.
16 |
17 | This method was chosen for its simplicity while supporting uneven counts of partial rankings between different judges.
18 |
19 | ## Starring
20 |
21 | Projects can be starred from the finish judging popup:
22 |
23 | 
24 |
25 | Or from the ranking menu:
26 |
27 | 
28 |
29 | Stars are simply added up for each judge. We introduced stars as a way for judges to pick their absolute favorites out of the projects that they've seen. If they saw a project that they think should win the hackathon, we encourage judges to star those projects. There is no limit to how many projects a judge can star, but we recommend they star at most 3. The star system allows for judges to provide more information to the organizers and allows for organizers to get a better sense of which projects were most loved. We recommend using the score system mainly but considering the number of stars as a secondary metric.
30 |
31 | ## Final Deliberation
32 |
33 | Once all projects are judged, organizers will go into a room to do final deliberation. They will use the scores from Jury to determine the final ranking of projects, as well as the rankings among any tracks. To help in the final decision, it's a good idea to send organizers out before final scores are tallied and view the top ranked projects on Jury, especially if there are multiple that stand out among the best.
34 |
--------------------------------------------------------------------------------
/docs/docs/usage/tips.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 7
3 | title: Tips and Contingencies
4 | description: Tips for best ways to use Jury and possible Contingencies that can occur related to Jury
5 | ---
6 |
7 | # Tips and Contingencies
8 |
9 | ## Tips
10 |
11 | ### Hackathon Size
12 |
13 | Jury is designed to be used for **relatively larger hackathons**. If your hackathon has less than 20 projects, it might be a good idea to use a spreadsheet and manually assign judges to view projects. There just comes a point where the technical complexity of Jury outweights the benefits it provides. Any hackathons with more than 20 projects should be able to use Jury.
14 |
15 | The largest event that Jury has been used at is at [HackUTD 2024](https://ripple.hackutd.co), where we had 281 projects being judged using Jury. This worked decently well, but we did run into a couple of issues regarding size. The key metric when considering how many judges you need is the **judge to project ratio**. We've found that having a ratio of **at least 1 judge for every 2 projects** is generally requied for using Jury. Any less and there simply isn't enough data for judging. The more data the better, and events where that ratio was closer to 1:1 had a lot more data points and more accurate results.
16 |
17 | ### Overseeing Judging
18 |
19 | We generally assign one or two organizers to be in charge of the overall judging process. They will sit at the front and monitor the judging app. There are a couple of items that the manager should be looking at and delegating:
20 |
21 | 1. Keep an eye on the **average views per project**; we want to get that number to at least 3-5
22 | 2. Look for any projects that get flagged--if not flagged as absent, send an organizer to see if the flag is legit
23 | 3. Look for the **"hidden bc absent"** flags; send an organizer to make sure these are actually absent before dismissing the flag
24 | 4. Check the admin log occassionally to make sure nothing bad is happening (AHEM someone trying to hack into Jury)
25 | 5. Add judges and projects if needed
26 | 6. Monitor logins and disable if needed
27 |
28 | ## Contingencies
29 |
30 | ### Brute Force Attack
31 |
32 | At our event, we had an instance where someone tried to brute force login to judges. They weren't able to do any damage luckily, but we needed to create a method to fight this obviously. Our solution is a way to rate limit logins, as well as a way to disable logins completely. Any judges that are already logged in can keep judging with their login token. This can be done through the [admin settings](/docs/usage/admin/configuration#judge-login).
33 |
34 | You can detect these types of attacks by keeping an eye on the **audit log**. A brute force attack generally is easy to tell as it will show a lot of failed login attempts in a short or continuous amount of time.
35 |
--------------------------------------------------------------------------------
/server/router/middleware.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "encoding/base64"
5 | "fmt"
6 | "server/config"
7 | "server/database"
8 | "strings"
9 |
10 | "github.com/gin-gonic/gin"
11 | )
12 |
13 | // Authenticate Judge with Bearer token
14 | func AuthenticateJudge() gin.HandlerFunc {
15 | return func(ctx *gin.Context) {
16 | authHeader := ctx.GetHeader("Authorization")
17 | if authHeader == "" {
18 | no("Authorization header required with Bearer token", ctx)
19 | return
20 | }
21 |
22 | // Make sure the auth header starts with "Bearer "
23 | if len(authHeader) < 7 || authHeader[:7] != "Bearer " {
24 | no("Invalid Authorization header, illegal format detected", ctx)
25 | return
26 | }
27 |
28 | // Extract the token
29 | token := authHeader[7:]
30 |
31 | // Make sure the token is valid (check for judge in database)
32 | state := GetState(ctx)
33 | judge, err := database.FindJudgeByToken(state.Db, token)
34 | if err != nil {
35 | ctx.AbortWithStatusJSON(500, gin.H{"error": fmt.Sprintf("Error finding judge in database: %s", err.Error())})
36 | return
37 | }
38 | if judge == nil {
39 | no("Invalid Authorization header, no judge with provided token", ctx)
40 | return
41 | }
42 |
43 | // Success! - set judge in context
44 | ctx.Set("judge", judge)
45 | ctx.Next()
46 | }
47 | }
48 |
49 | // Authenticate Admin with Basic auth
50 | func AuthenticateAdmin() gin.HandlerFunc {
51 | return func(ctx *gin.Context) {
52 | authHeader := ctx.GetHeader("Authorization")
53 | if authHeader == "" {
54 | no("Authorization header required with Basic auth", ctx)
55 | return
56 | }
57 |
58 | // Make sure the auth header starts with "Basic "
59 | if len(authHeader) < 6 || authHeader[:6] != "Basic " {
60 | no("Invalid Authorization header, illegal format detected", ctx)
61 | return
62 | }
63 |
64 | // Extract the base64 encoded username:password and decode it
65 | userpassHash := authHeader[6:]
66 | userpass, err := base64.StdEncoding.DecodeString(userpassHash)
67 | if err != nil {
68 | no("Invalid Authorization header, invalid base64 encoding", ctx)
69 | return
70 | }
71 |
72 | // Split the username:password string
73 | authSplit := strings.Split(string(userpass), ":")
74 | if len(authSplit) != 2 {
75 | no("Invalid Authorization header, illegal format detected", ctx)
76 | return
77 | }
78 |
79 | // Username should be "admin" and password should be the admin password
80 | if authSplit[0] != "admin" || authSplit[1] != config.GetEnv("JURY_ADMIN_PASSWORD") {
81 | no("Invalid Authorization header, incorrect credentials", ctx)
82 | return
83 | }
84 | }
85 | }
86 |
87 | // When auth invalid, send a 401 error
88 | func no(msg string, ctx *gin.Context) {
89 | ctx.AbortWithStatusJSON(401, gin.H{"error": msg})
90 | }
91 |
--------------------------------------------------------------------------------
/client/src/components/admin/tables/HidePopup.tsx:
--------------------------------------------------------------------------------
1 | import { deleteRequest, postRequest } from '../../../api';
2 | import { useAdminStore } from '../../../store';
3 | import { errorAlert } from '../../../util';
4 |
5 | type HideElement = Project | Judge;
6 |
7 | interface DeletePopupProps {
8 | /* Element to delete */
9 | element: HideElement;
10 |
11 | /* Function to modify the popup state variable */
12 | close: React.Dispatch>;
13 | }
14 |
15 | function isProject(e: HideElement): e is Project {
16 | return 'mu' in e;
17 | }
18 |
19 | const DeletePopup = ({ element, close }: DeletePopupProps) => {
20 | const fetchStats = useAdminStore((state) => state.fetchStats);
21 | const fetchProjects = useAdminStore((state) => state.fetchProjects);
22 | const fetchJudges = useAdminStore((state) => state.fetchJudges);
23 |
24 | const hideElement = async () => {
25 | const resource = isProject(element) ? 'project' : 'judge';
26 | const res = await postRequest(`/${resource}/hide`, 'admin', { id: element.id });
27 | if (res.status === 200) {
28 | fetchStats();
29 | isProject(element) ? fetchProjects() : fetchJudges();
30 | alert(`${resource} hidden successfully!`);
31 | } else {
32 | errorAlert(res);
33 | }
34 |
35 | close(false);
36 | };
37 | return (
38 | <>
39 |
close(false)}
42 | >
43 |
44 |
Heads Up!
45 |
46 | Are you sure you want to delete{' '}
47 | {element.name}? This action is permanent
48 | and cannot be undone.
49 |
50 |
51 |
57 |
63 |
64 |
65 | >
66 | );
67 | };
68 |
69 | export default DeletePopup;
70 |
--------------------------------------------------------------------------------
/client/src/components/PasswordInput.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { twMerge } from 'tailwind-merge';
3 |
4 | interface PasswordInputProps {
5 | /* Set true if text input has errored */
6 | error?: boolean;
7 |
8 | /* Setter for error state; will reset if no longer errored */
9 | setError?: React.Dispatch>;
10 |
11 | /* Error message to show if text incorrect */
12 | errorMessage?: string;
13 |
14 | /* Max Length of text field; null for no max */
15 | maxLength?: number;
16 |
17 | /* Placeholder of text field */
18 | placeholder?: string;
19 |
20 | /* Label under the field */
21 | label: string;
22 |
23 | /* Classname styling */
24 | className?: string;
25 |
26 | /* Handler for text input onChange */
27 | onChange?: React.ChangeEventHandler;
28 |
29 | /* Handler for text input onKeyPress */
30 | onKeyPress?: React.KeyboardEventHandler;
31 |
32 | /* Set default value of input field */
33 | value?: string;
34 |
35 | /* True if it's a password field */
36 | isHidden?: boolean;
37 | }
38 |
39 | const PasswordInput = (props: PasswordInputProps) => {
40 | const [focused, setFocused] = useState(false);
41 |
42 | const handleChange = (e: React.KeyboardEvent) => {
43 | if (props.onKeyPress) props.onKeyPress(e);
44 | if (props.setError) props.setError(false);
45 | };
46 |
47 | return (
48 | <>
49 | {
61 | setFocused(true);
62 | }}
63 | onBlur={() => {
64 | setFocused(false);
65 | }}
66 | onChange={props.onChange}
67 | onKeyDown={handleChange}
68 | />
69 |
79 | >
80 | );
81 | };
82 |
83 | export default PasswordInput;
84 |
--------------------------------------------------------------------------------
/server/models/flag.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "time"
7 |
8 | "go.mongodb.org/mongo-driver/bson/primitive"
9 | )
10 |
11 | // List of valid reasons for skipping a project
12 | var validReasons = []string{"busy", "absent", "cannot-demo", "too-complex", "offensive", "hidden-absent"}
13 |
14 | // Defines an instance where the judge skips a project.
15 | // This can be one of these reasons:
16 | //
17 | // 1. busy: Busy (Being Judged)
18 | // 2. absent: Not Present
19 | // 3. cannot-demo: Cannot Demo Project
20 | // 4. too-complex: Too Complex
21 | // 5. offensive: Offensive Project
22 | // 6. hidden-absent: Hidden due to being absent 3 times
23 | //
24 | // With the exception of the 1st reason, all other reasons are grounds for
25 | // flagging, which is defined by the `flag` field.
26 | type Flag struct {
27 | Id primitive.ObjectID `json:"id" bson:"_id,omitempty"`
28 | ProjectId *primitive.ObjectID `json:"project_id" bson:"project_id"`
29 | JudgeId *primitive.ObjectID `json:"judge_id" bson:"judge_id"`
30 | Time primitive.DateTime `json:"time" bson:"time"`
31 | ProjectName string `json:"project_name" bson:"project_name"`
32 | ProjectLocation int64 `json:"project_location" bson:"project_location"`
33 | JudgeName string `json:"judge_name" bson:"judge_name"`
34 | Reason string `json:"reason" bson:"reason"`
35 | }
36 |
37 | func NewFlag(project *Project, judge *Judge, reason string) (*Flag, error) {
38 | // Check if the reason is valid
39 | valid := false
40 | for _, r := range validReasons {
41 | if r == reason {
42 | valid = true
43 | break
44 | }
45 | }
46 | if !valid {
47 | return nil, fmt.Errorf("reason field is invalid: %s", reason)
48 | }
49 |
50 | // Create the skip object
51 | return &Flag{
52 | ProjectId: &project.Id,
53 | JudgeId: &judge.Id,
54 | Time: primitive.NewDateTimeFromTime(time.Now()),
55 | ProjectName: project.Name,
56 | ProjectLocation: project.Location,
57 | JudgeName: judge.Name,
58 | Reason: reason,
59 | }, nil
60 | }
61 |
62 | // Create custom marshal function to change the format of the primitive.DateTime to a unix timestamp
63 | func (s *Flag) MarshalJSON() ([]byte, error) {
64 | type Alias Flag
65 | return json.Marshal(&struct {
66 | *Alias
67 | Time int64 `json:"time"`
68 | }{
69 | Alias: (*Alias)(s),
70 | Time: int64(s.Time),
71 | })
72 | }
73 |
74 | // Create custom unmarshal function to change the format of the primitive.DateTime from a unix timestamp
75 | func (s *Flag) UnmarshalJSON(data []byte) error {
76 | type Alias Flag
77 | aux := &struct {
78 | Time int64 `json:"time"`
79 | *Alias
80 | }{
81 | Alias: (*Alias)(s),
82 | }
83 | if err := json.Unmarshal(data, &aux); err != nil {
84 | return err
85 | }
86 | s.Time = primitive.DateTime(aux.Time)
87 | return nil
88 | }
89 |
--------------------------------------------------------------------------------
/client/src/components/admin/tables/MovePopup.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { putRequest } from '../../../api';
3 | import ConfirmPopup from '../../ConfirmPopup';
4 | import { errorAlert } from '../../../util';
5 | import { useAdminStore, useAdminTableStore } from '../../../store';
6 | import TextInput from '../../TextInput';
7 |
8 | interface MovePopupProps {
9 | /* State variable to open popup */
10 | enabled: boolean;
11 |
12 | /* Setter for open */
13 | setEnabled: React.Dispatch>;
14 |
15 | /* Item to move */
16 | item: Project;
17 | }
18 |
19 | const MovePopup = (props: MovePopupProps) => {
20 | const [newLocation, setNewLocation] = useState('');
21 | const fetchProjects = useAdminStore((state) => state.fetchProjects);
22 | const setSelected = useAdminTableStore((state) => state.setSelected);
23 | const selected = useAdminTableStore((state) => state.selected);
24 |
25 | const handleSubmit = () => {
26 | if (!props.item) {
27 | throw new Error('A project must be defined');
28 | }
29 |
30 | // Check to see if new location is valid
31 | const location = Number(newLocation);
32 | if (isNaN(location) || location < 0) {
33 | alert('Invalid location number');
34 | return;
35 | }
36 |
37 | move(location);
38 | };
39 |
40 | const move = async (location: number) => {
41 | const res = await putRequest(`/project/move/${props.item?.id}`, 'admin', {
42 | location,
43 | });
44 | if (res.status !== 200) {
45 | errorAlert(res);
46 | return;
47 | }
48 |
49 | alert(`Project moved to table ${location} successfully!`);
50 | fetchProjects();
51 | props.setEnabled(false);
52 | };
53 |
54 | useEffect(() => {
55 | if (!props.item) return;
56 |
57 | setNewLocation(String(props.item.location));
58 | }, [props.item]);
59 |
60 | return (
61 |
68 |
69 |
70 | Move the project
71 | {props.item?.name} to a new
72 | table. Enter the table number below.
73 |