add everything
parent
ec2b1aaa9b
commit
0d9a248b4d
35
README.md
35
README.md
|
@ -1,2 +1,35 @@
|
|||
# gemastik-xvii-final
|
||||
gemastik-xvii-final public repository
|
||||
this repository provides the Docker environments for the gemastik-xvii final services.
|
||||
|
||||
## warmup-challenges
|
||||
| challenges | author | category | writeup |
|
||||
| ---------- | ------ | -------- |---------|
|
||||
| bit-canvas | vidner | pwn | [contribute](https://github.com/vidner/gemastik-xvii-final/pulls) |
|
||||
| gift-card | deomkicer | crypto | [contribute](https://github.com/vidner/gemastik-xvii-final/pulls) |
|
||||
| go-green | vidner | rev | [contribute](https://github.com/vidner/gemastik-xvii-final/pulls) |
|
||||
| more-less | vidner | web | [contribute](https://github.com/vidner/gemastik-xvii-final/pulls) |
|
||||
|
||||
|
||||
## final-challenges
|
||||
| challenges | author | category | writeup |
|
||||
| ---------- | ------ | -------- |---------|
|
||||
| anti-alchemy | deomkicer | web | [contribute](https://github.com/vidner/gemastik-xvii-final/pulls) |
|
||||
| asmr | circleous | pwn | [contribute](https://github.com/vidner/gemastik-xvii-final/pulls) |
|
||||
| fjb | circleous | web-pwn | [contribute](https://github.com/vidner/gemastik-xvii-final/pulls) |
|
||||
| gift-voucher | deomkicer | crypto | [contribute](https://github.com/vidner/gemastik-xvii-final/pulls) |
|
||||
| gleam-drive | vidner | web-crypto | [contribute](https://github.com/vidner/gemastik-xvii-final/pulls) |
|
||||
| kode-viewer | vidner | web | [contribute](https://github.com/vidner/gemastik-xvii-final/pulls) |
|
||||
| tempest-poc | dummklloun | web | [contribute](https://github.com/vidner/gemastik-xvii-final/pulls) |
|
||||
| ticketer | merricx | crypto | [contribute](https://github.com/vidner/gemastik-xvii-final/pulls) |
|
||||
|
||||
## how-to-contribute
|
||||
contribute by sharing a link to your write-up. since services may have multiple vulnerabilities, different approaches are welcome.
|
||||
|
||||
### pull-request
|
||||
```diff
|
||||
- [contribute](https://github.com/vidner/gemastik-xvii-final/pulls)
|
||||
+ [by yourname](https://link-to-writeup)
|
||||
```
|
||||
|
||||
### discord
|
||||
dm @vidner with the link to your write-up.
|
|
@ -0,0 +1,24 @@
|
|||
FROM public.ecr.aws/docker/library/python:3.11-slim-buster
|
||||
|
||||
ARG PASSWORD
|
||||
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
|
||||
RUN echo root:${PASSWORD} | chpasswd
|
||||
RUN apt-get update && apt-get install -y openssh-server curl nano
|
||||
RUN echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config
|
||||
RUN echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
|
||||
RUN service ssh start
|
||||
RUN useradd --user-group --system --create-home --no-log-init --shell /bin/bash ctf
|
||||
|
||||
WORKDIR /home/ctf/app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install -r ./requirements.txt && rm ./requirements.txt
|
||||
|
||||
COPY src/ .
|
||||
|
||||
COPY entrypoint.sh .
|
||||
RUN chmod +x entrypoint.sh
|
||||
|
||||
ENTRYPOINT [ "./entrypoint.sh" ]
|
|
@ -0,0 +1,106 @@
|
|||
--
|
||||
-- PostgreSQL database dump
|
||||
--
|
||||
|
||||
-- Dumped from database version 16.3
|
||||
-- Dumped by pg_dump version 16.3
|
||||
|
||||
SET statement_timeout = 0;
|
||||
SET lock_timeout = 0;
|
||||
SET idle_in_transaction_session_timeout = 0;
|
||||
SET client_encoding = 'UTF8';
|
||||
SET standard_conforming_strings = on;
|
||||
SELECT pg_catalog.set_config('search_path', '', false);
|
||||
SET check_function_bodies = false;
|
||||
SET xmloption = content;
|
||||
SET client_min_messages = warning;
|
||||
SET row_security = off;
|
||||
|
||||
SET default_tablespace = '';
|
||||
|
||||
SET default_table_access_method = heap;
|
||||
|
||||
--
|
||||
-- Name: cwe; Type: TABLE; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
CREATE TABLE public.cwe (
|
||||
id INTEGER PRIMARY KEY,
|
||||
title TEXT,
|
||||
description TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE public.users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username VARCHAR(255),
|
||||
password VARCHAR(255)
|
||||
);
|
||||
|
||||
CREATE TABLE public.salt (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username VARCHAR(255),
|
||||
salt VARCHAR(255)
|
||||
);
|
||||
|
||||
--
|
||||
-- Data for Name: cwe; Type: TABLE DATA; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
INSERT INTO public.cwe (id,title,description) VALUES
|
||||
(20,'CWE-20: Improper Input Validation','The product receives input or data, but it does not validate or incorrectly validates that the input has the properties that are required to process the data safely and correctly.'),
|
||||
(22,'CWE-22: Improper Limitation of a Pathname to a Restricted Directory (''Path Traversal'')','The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.'),
|
||||
(73,'CWE-73: External Control of File Name or Path','The product allows user input to control or influence paths or file names that are used in filesystem operations.'),
|
||||
(77,'CWE-77: Improper Neutralization of Special Elements used in a Command (''Command Injection'')','The product constructs all or part of a command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended command when it is sent to a downstream component.'),
|
||||
(78,'CWE-78: Improper Neutralization of Special Elements used in an OS Command (''OS Command Injection'')','The product constructs all or part of an OS command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended OS command when it is sent to a downstream component.'),
|
||||
(79,'CWE-79: Improper Neutralization of Input During Web Page Generation (''Cross-site Scripting'')','The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.'),
|
||||
(88,'CWE-88: Improper Neutralization of Argument Delimiters in a Command (''Argument Injection'')','The product constructs a string for a command to be executed by a separate component in another control sphere, but it does not properly delimit the intended arguments, options, or switches within that command string.'),
|
||||
(89,'CWE-89: Improper Neutralization of Special Elements used in an SQL Command (''SQL Injection'')','The product constructs all or part of an SQL command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended SQL command when it is sent to a downstream component. Without sufficient removal or quoting of SQL syntax in user-controllable inputs, the generated SQL query can cause those inputs to be interpreted as SQL instead of ordinary user data.'),
|
||||
(94,'CWE-94: Improper Control of Generation of Code (''Code Injection'')','The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.'),
|
||||
(119,'CWE-119: Improper Restriction of Operations within the Bounds of a Memory Buffer','The product performs operations on a memory buffer, but it reads from or writes to a memory location outside the buffer''s intended boundary. This may result in read or write operations on unexpected memory locations that could be linked to other variables, data structures, or internal program data.');
|
||||
INSERT INTO public.cwe (id,title,description) VALUES
|
||||
(125,'CWE-125: Out-of-bounds Read','The product reads data past the end, or before the beginning, of the intended buffer.'),
|
||||
(190,'CWE-190: Integer Overflow or Wraparound','The product performs a calculation that can produce an integer overflow or wraparound when the logic assumes that the resulting value will always be larger than the original value. This occurs when an integer value is incremented to a value that is too large to store in the associated representation. When this occurs, the value may become a very small or negative number.'),
|
||||
(200,'CWE-200: Exposure of Sensitive Information to an Unauthorized Actor','The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.'),
|
||||
(250,'CWE-250: Execution with Unnecessary Privileges','The product performs an operation at a privilege level that is higher than the minimum level required, which creates new weaknesses or amplifies the consequences of other weaknesses.'),
|
||||
(266,'CWE-266: Incorrect Privilege Assignment','A product incorrectly assigns a privilege to a particular actor, creating an unintended sphere of control for that actor.'),
|
||||
(269,'CWE-269: Improper Privilege Management','The product does not properly assign, modify, track, or check privileges for an actor, creating an unintended sphere of control for that actor.'),
|
||||
(276,'CWE-276: Incorrect Default Permissions','During installation, installed file permissions are set to allow anyone to modify those files.'),
|
||||
(284,'CWE-284: Improper Access Control','The product does not restrict or incorrectly restricts access to a resource from an unauthorized actor.'),
|
||||
(285,'CWE-285: Improper Authorization','The product does not perform or incorrectly performs an authorization check when an actor attempts to access a resource or perform an action.'),
|
||||
(287,'CWE-287: Improper Authentication','When an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct.');
|
||||
INSERT INTO public.cwe (id,title,description) VALUES
|
||||
(295,'CWE-295: Improper Certificate Validation','The product does not validate, or incorrectly validates, a certificate.'),
|
||||
(306,'CWE-306: Missing Authentication for Critical Function','The product does not perform any authentication for functionality that requires a provable user identity or consumes a significant amount of resources.'),
|
||||
(307,'CWE-307: Improper Restriction of Excessive Authentication Attempts','The product does not implement sufficient measures to prevent multiple failed authentication attempts within a short time frame, making it more susceptible to brute force attacks.'),
|
||||
(311,'CWE-311: Missing Encryption of Sensitive Data','The product does not encrypt sensitive or critical information before storage or transmission.'),
|
||||
(327,'CWE-327: Use of a Broken or Risky Cryptographic Algorithm','The product uses a broken or risky cryptographic algorithm or protocol.'),
|
||||
(330,'CWE-330: Use of Insufficiently Random Values','The product uses insufficiently random numbers or values in a security context that depends on unpredictable numbers.'),
|
||||
(352,'CWE-352: Cross-Site Request Forgery (CSRF)','The web application does not, or can not, sufficiently verify whether a well-formed, valid, consistent request was intentionally provided by the user who submitted the request.'),
|
||||
(362,'CWE-362: Concurrent Execution using Shared Resource with Improper Synchronization (''Race Condition'')','The product contains a code sequence that can run concurrently with other code, and the code sequence requires temporary, exclusive access to a shared resource, but a timing window exists in which the shared resource can be modified by another code sequence that is operating concurrently.'),
|
||||
(400,'CWE-400: Uncontrolled Resource Consumption','The product does not properly control the allocation and maintenance of a limited resource, thereby enabling an actor to influence the amount of resources consumed, eventually leading to the exhaustion of available resources.'),
|
||||
(416,'CWE-416: Use After Free','The product reuses or references memory after it has been freed. At some point afterward, the memory may be allocated again and saved in another pointer, while the original pointer references a location somewhere within the new allocation. Any operations using the original pointer are no longer valid because the memory "belongs" to the code that operates on the new pointer.');
|
||||
INSERT INTO public.cwe (id,title,description) VALUES
|
||||
(426,'CWE-426: Untrusted Search Path','The product searches for critical resources using an externally-supplied search path that can point to resources that are not under the product''s direct control.'),
|
||||
(434,'CWE-434: Unrestricted Upload of File with Dangerous Type','The product allows the upload or transfer of dangerous file types that are automatically processed within its environment.'),
|
||||
(476,'CWE-476: NULL Pointer Dereference','The product dereferences a pointer that it expects to be valid but is NULL.'),
|
||||
(502,'CWE-502: Deserialization of Untrusted Data','The product deserializes untrusted data without sufficiently verifying that the resulting data will be valid.'),
|
||||
(522,'CWE-522: Insufficiently Protected Credentials','The product transmits or stores authentication credentials, but it uses an insecure method that is susceptible to unauthorized interception and/or retrieval.'),
|
||||
(611,'CWE-611: Improper Restriction of XML External Entity Reference','The product processes an XML document that can contain XML entities with URIs that resolve to documents outside of the intended sphere of control, causing the product to embed incorrect documents into its output.'),
|
||||
(732,'CWE-732: Incorrect Permission Assignment for Critical Resource','The product specifies permissions for a security-critical resource in a way that allows that resource to be read or modified by unintended actors.'),
|
||||
(754,'CWE-754: Improper Check for Unusual or Exceptional Conditions','The product does not check or incorrectly checks for unusual or exceptional conditions that are not expected to occur frequently during day to day operation of the product.'),
|
||||
(772,'CWE-772: Missing Release of Resource after Effective Lifetime','The product does not release a resource after its effective lifetime has ended, i.e., after the resource is no longer needed.'),
|
||||
(787,'CWE-787: Out-of-bounds Write','The product writes data past the end, or before the beginning, of the intended buffer.');
|
||||
INSERT INTO public.cwe (id,title,description) VALUES
|
||||
(798,'CWE-798: Use of Hard-coded Credentials','The product contains hard-coded credentials, such as a password or cryptographic key.'),
|
||||
(862,'CWE-862: Missing Authorization','The product does not perform an authorization check when an actor attempts to access a resource or perform an action.'),
|
||||
(863,'CWE-863: Incorrect Authorization','The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. This allows attackers to bypass intended access restrictions.'),
|
||||
(918,'CWE-918: Server-Side Request Forgery (SSRF)','The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.');
|
||||
|
||||
|
||||
--
|
||||
-- Name: cwe cwe_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
--
|
||||
-- PostgreSQL database dump complete
|
||||
--
|
|
@ -0,0 +1,28 @@
|
|||
services:
|
||||
anti-alchemy:
|
||||
hostname: anti-alchemy
|
||||
restart: always
|
||||
build:
|
||||
args:
|
||||
- PASSWORD=PASSWORD_11000
|
||||
volumes:
|
||||
- ./flag.txt:/flag.txt:ro
|
||||
ports:
|
||||
- "11000:5000"
|
||||
- "11022:22"
|
||||
depends_on:
|
||||
- anti-alchemy-db
|
||||
environment:
|
||||
- DB_NAME=postgres
|
||||
- DB_USER=postgres
|
||||
- DB_PASS=password
|
||||
- DB_HOST=anti-alchemy-db
|
||||
- DB_PORT=5432
|
||||
- SECRET_KEY=PASSWORD_11000
|
||||
anti-alchemy-db:
|
||||
image: public.ecr.aws/docker/library/postgres:16.3-alpine
|
||||
environment:
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=password
|
||||
volumes:
|
||||
- ./db/dump.sql:/docker-entrypoint-initdb.d/init.sql
|
|
@ -0,0 +1,6 @@
|
|||
#!/bin/bash
|
||||
|
||||
/usr/sbin/sshd -D &
|
||||
sleep 3
|
||||
python3 ./misc/init_users.py
|
||||
su ctf -c "gunicorn --bind 0.0.0.0:5000 --timeout 60 --workers 6 app:app"
|
|
@ -0,0 +1 @@
|
|||
PLACEHOLDER
|
|
@ -0,0 +1,3 @@
|
|||
Flask
|
||||
gunicorn
|
||||
psycopg2-binary
|
|
@ -0,0 +1,83 @@
|
|||
class ClauseBuilder:
|
||||
def __init__(self) -> None:
|
||||
self._queries = []
|
||||
self._chars_to_sanitize = ["'", '"']
|
||||
self._is_where_called = False
|
||||
|
||||
def _sanitize(self, q) -> str:
|
||||
for c in self._chars_to_sanitize:
|
||||
if c in q:
|
||||
q = q.replace(c, c * 2)
|
||||
return str(q).strip()
|
||||
|
||||
def _where(self, column, value, op) -> map:
|
||||
if not self._is_where_called:
|
||||
op = "WHERE"
|
||||
self._is_where_called = True
|
||||
return map(self._sanitize, [column, value, op])
|
||||
|
||||
def final(self) -> str:
|
||||
q = " ".join(self._queries)
|
||||
self.__init__()
|
||||
return str(q).strip()
|
||||
|
||||
def order_by(self, column, value) -> None:
|
||||
column, value = map(self._sanitize, [column, value])
|
||||
self._queries.append("ORDER BY")
|
||||
self._queries.append('"' + column + '"')
|
||||
self._queries.append(value)
|
||||
|
||||
def select(self, table) -> None:
|
||||
table = self._sanitize(table)
|
||||
self._queries.append("SELECT")
|
||||
self._queries.append("*")
|
||||
self._queries.append("FROM")
|
||||
self._queries.append('"' + table + '"')
|
||||
|
||||
def where(self, column, value, cmp="ILIKE", op="AND") -> None:
|
||||
column, value, op = self._where(column, value, op)
|
||||
self._queries.append(op)
|
||||
self._queries.append('"' + column + '"')
|
||||
if column == "id":
|
||||
self._queries.append("=")
|
||||
self._queries.append(str(int(value)))
|
||||
elif cmp == "EQ":
|
||||
self._queries.append("=")
|
||||
self._queries.append("'" + value + "'")
|
||||
else:
|
||||
self._queries.append("ILIKE")
|
||||
self._queries.append("'%" + value + "%'")
|
||||
|
||||
|
||||
class QueryBuilder:
|
||||
def __init__(self) -> None:
|
||||
self._cb = ClauseBuilder()
|
||||
self._obj = {}
|
||||
self._defined_keys = ["table", "columns"]
|
||||
self._sorting_values = ["asc", "desc"]
|
||||
self._login_keys = ["username"]
|
||||
|
||||
def _order_by(self) -> None:
|
||||
for value, column in self._obj.items():
|
||||
if value in self._sorting_values:
|
||||
self._cb.order_by(column, value)
|
||||
break
|
||||
|
||||
def _select(self) -> None:
|
||||
self._cb.select(self._obj["table"])
|
||||
|
||||
def _where(self) -> None:
|
||||
for column, value in self._obj.items():
|
||||
if column in self._defined_keys + self._sorting_values:
|
||||
continue
|
||||
elif column in self._login_keys:
|
||||
self._cb.where(column, value, "EQ")
|
||||
else:
|
||||
self._cb.where(column, value)
|
||||
|
||||
def generate(self, obj) -> str:
|
||||
self._obj = obj
|
||||
self._select()
|
||||
self._where()
|
||||
self._order_by()
|
||||
return self._cb.final()
|
|
@ -0,0 +1,119 @@
|
|||
from flask import Flask, render_template, session, redirect, url_for
|
||||
from os import environ
|
||||
|
||||
from antialchemy import *
|
||||
from helper import *
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config["SECRET_KEY"] = environ.get("SECRET_KEY", "SECRET_KEY")
|
||||
app.config["PERMANENT_SESSION_LIFETIME"] = 3 * 60
|
||||
qb = QueryBuilder()
|
||||
|
||||
|
||||
@app.get("/api/flag")
|
||||
@check_login_status
|
||||
def flag():
|
||||
if session["user"] == "admin":
|
||||
try:
|
||||
return make_resp(200, open("/flag.txt").read())
|
||||
except:
|
||||
return make_resp(500, "Flag not found, please contact problem setter")
|
||||
|
||||
return make_resp(401, "You need to log in as admin to get the flag")
|
||||
|
||||
|
||||
@app.post("/api/login")
|
||||
@check_request_body
|
||||
def login(*args, **kwargs):
|
||||
payload = kwargs.copy()
|
||||
|
||||
try:
|
||||
conn = create_db_conn()
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
qb.generate(
|
||||
{
|
||||
"table": "users",
|
||||
"username": payload["username"],
|
||||
}
|
||||
)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
return make_resp(401, "Invalid username/password")
|
||||
|
||||
_, username, password_hash = row
|
||||
password = payload.pop("password")
|
||||
|
||||
cur.execute(
|
||||
qb.generate(
|
||||
{
|
||||
"table": "salt",
|
||||
"username": username,
|
||||
}
|
||||
)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
return make_resp(401, "Invalid username/password")
|
||||
|
||||
_, _, salt = row
|
||||
|
||||
if not check_password_hash(password, salt, password_hash):
|
||||
return make_resp(401, "Invalid username/password")
|
||||
|
||||
session.clear()
|
||||
session["user"] = username
|
||||
return redirect(url_for("index"))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Exception: {e}")
|
||||
return make_resp(400, "Bad Request")
|
||||
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@app.get("/api/logout")
|
||||
@check_login_status
|
||||
def logout():
|
||||
session.clear()
|
||||
return redirect(url_for("index"))
|
||||
|
||||
|
||||
@app.post("/api/view")
|
||||
@check_login_status
|
||||
@check_request_body
|
||||
def view(*args, **kwargs):
|
||||
payload = kwargs.copy()
|
||||
|
||||
try:
|
||||
conn = create_db_conn()
|
||||
cur = conn.cursor()
|
||||
cur.execute(qb.generate(payload | {"table": "cwe"}))
|
||||
rows = cur.fetchall()
|
||||
return make_resp(200, "OK", {"rows": rows})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Exception: {e}")
|
||||
return make_resp(400, "Bad Request")
|
||||
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def index():
|
||||
try:
|
||||
if session["user"]:
|
||||
return render_template("dashboard.html")
|
||||
except:
|
||||
pass
|
||||
|
||||
return render_template("login.html")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=True)
|
|
@ -0,0 +1,73 @@
|
|||
from flask import request, session
|
||||
from functools import wraps
|
||||
from hashlib import sha1
|
||||
from json import dumps
|
||||
from os import environ
|
||||
from psycopg2 import connect
|
||||
from string import printable
|
||||
from time import sleep
|
||||
|
||||
def create_db_conn():
|
||||
while True:
|
||||
try:
|
||||
return connect(
|
||||
database=environ.get("DB_NAME", "postgres"),
|
||||
user=environ.get("DB_USER", "postgres"),
|
||||
password=environ.get("DB_PASS", "password"),
|
||||
host=environ.get("DB_HOST", "localhost"),
|
||||
port=environ.get("DB_PORT", "5432"),
|
||||
)
|
||||
except:
|
||||
sleep(1)
|
||||
|
||||
|
||||
def make_resp(status, message, data=None):
|
||||
return {"status": status, "message": message, "data": data}
|
||||
|
||||
|
||||
def generate_password_hash(password, salt):
|
||||
return sha1((salt + password).encode()).hexdigest()
|
||||
|
||||
|
||||
def check_password_hash(password, salt, password_hash):
|
||||
return generate_password_hash(password, salt) == password_hash
|
||||
|
||||
|
||||
def check_login_status(f):
|
||||
@wraps(f)
|
||||
def inner(*args, **kwargs):
|
||||
try:
|
||||
session["user"]
|
||||
return f(*args, **kwargs)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Exception: {e}")
|
||||
return make_resp(401, "Unauthorized")
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
def check_request_body(f):
|
||||
check_blist = lambda s: all(x not in s.lower() for x in ["pg_"])
|
||||
check_wlist = lambda s: all(c in printable for c in s)
|
||||
|
||||
@wraps(f)
|
||||
def inner(*args, **kwargs):
|
||||
try:
|
||||
data = request.get_json()
|
||||
data = {k: v for k, v in data.items() if data.get(k)}
|
||||
|
||||
dd = dumps(data, separators=(",", ":"))
|
||||
assert len(dd) < 256 and check_blist(dd), "Bad payload"
|
||||
|
||||
for item in data.items():
|
||||
assert all(map(check_wlist, item)), "Bad payload"
|
||||
|
||||
kwargs.update(data)
|
||||
return f(*args, **kwargs)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Exception: {e}")
|
||||
return make_resp(400, "Bad Request")
|
||||
|
||||
return inner
|
|
@ -0,0 +1,28 @@
|
|||
import sys
|
||||
sys.path += [".", ".."]
|
||||
|
||||
from helper import create_db_conn
|
||||
from os import environ
|
||||
|
||||
print("Getting environment variables...")
|
||||
print(environ.get("SECRET_KEY", "SECRET_KEY"))
|
||||
print()
|
||||
|
||||
print("Getting rows of users and salt...")
|
||||
conn = create_db_conn()
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute("SELECT * FROM users")
|
||||
rows = cur.fetchall()
|
||||
for row in rows:
|
||||
print(row)
|
||||
|
||||
cur.execute("SELECT * FROM salt")
|
||||
rows = cur.fetchall()
|
||||
for row in rows:
|
||||
print(row)
|
||||
print()
|
||||
|
||||
print("Done")
|
||||
cur.close()
|
||||
conn.close()
|
|
@ -0,0 +1,25 @@
|
|||
import sys
|
||||
sys.path += [".", ".."]
|
||||
|
||||
from helper import create_db_conn, generate_password_hash
|
||||
from os import environ, urandom
|
||||
|
||||
conn = create_db_conn()
|
||||
cur = conn.cursor()
|
||||
|
||||
users = [
|
||||
("admin", environ.get("SECRET_KEY", "SECRET_KEY"), urandom(8).hex()),
|
||||
("gemastik", "P@ssw0rd", urandom(8).hex()),
|
||||
]
|
||||
|
||||
for username, password, salt in users:
|
||||
cur.execute(
|
||||
"INSERT INTO users (username, password) VALUES (%s, %s)",
|
||||
(username, generate_password_hash(password, salt)),
|
||||
)
|
||||
cur.execute("INSERT INTO salt (username, salt) VALUES (%s, %s)", (username, salt))
|
||||
|
||||
conn.commit()
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
|
@ -0,0 +1 @@
|
|||
.sortable-column,button,strong{cursor:pointer}a,strong{color:#44509e}#pagination,#search{border:var(--debug);gap:4px;width:300px;padding:4px}*{box-sizing:border-box}body{font-family:Arial,Helvetica,sans-serif}table{width:100%;border-collapse:collapse}td,th{padding:.5rem;text-align:left;border-bottom:1px solid #ddd}td{height:6rem}a{text-decoration:none;font-weight:700}button:disabled{cursor:not-allowed}.container{height:inherit;display:flex;flex-direction:column;justify-content:space-between;border:var(--debug)}#search label,.atas{flex-direction:row;display:flex}.sortable th:first-child{width:48px}.sortable th:nth-child(2){width:96px}.sortable th:nth-child(3){width:25%}.sortable-column:hover{background-color:#eee}.sortable-column.no-sort{pointer-events:none}.sortable-column::after,.sortable-column::before{transition:color .2s ease-in-out;font-size:1.2rem;color:transparent}.sortable-column:hover::after{color:inherit}.sortable-column::after{content:"\025B8"}.sortable-column.dir-d::after{content:"\025BE"}.sortable-column.dir-u::after{content:"\025B4"}.atas{gap:1rem;justify-content:space-between;border:var(--debug)}.bawah{margin-top:5px;padding:5px;border:var(--debug);height:auto}@media (max-width:600px){.atas{flex-direction:column;align-items:center}#search{margin-top:1rem}}#search{display:flex;flex-direction:column}#search label{justify-content:flex-end;gap:8px}#search input{width:200px}#search button{align-self:flex-end;width:200px}#pagination{display:flex;flex-direction:row;justify-content:space-between;align-content:end}#pagination button{width:80px;height:1.5rem;margin-top:auto}#pagination p{margin:auto 0 0;padding:2px}
|
|
@ -0,0 +1 @@
|
|||
const step=10;var rawResp={},currentData=[],totalData=[],currentPage=0,totalPage=Math.floor((rawResp.length+9)/10),pload={};function fillBody(e,t){let a=document.getElementById("fillable-body");a.innerHTML="",e.forEach((e,n)=>{let r=document.createElement("tr"),l=document.createElement("td"),i=document.createElement("td"),c=document.createElement("td"),o=document.createElement("td"),s=document.createElement("a");l.textContent=n+t,s.href=`https://cwe.mitre.org/data/definitions/${e[0]}.html`,s.textContent=e[0],c.textContent=e[1],o.textContent=e[2],i.appendChild(s),r.appendChild(l),r.appendChild(i),r.appendChild(c),r.appendChild(o),a.appendChild(r)})}function fillPage(e,t){let a=document.getElementById("page");a.innerHTML=`Page ${e}/${t}`}async function sendReq(e,t,a){try{let n=await fetch(t,{method:e,headers:{"Content-Type":"application/json"},body:a});var{status:r,message:l,data:i}=rawResp=await n.json();200===r?(rawResp=i.rows,totalPage=Math.floor((rawResp.length+9)/10),currentPage=1,currentData=rawResp.slice((currentPage-1)*10,10*currentPage),fillPage(currentPage,totalPage),fillBody(currentData,(currentPage-1)*10+1),toggleButton()):(console.error("Failed to fetch data:",l),alert(l))}catch(c){console.error("Error fetching data:",c)}}async function search(){let e=document.getElementById("cwe-id").value,t=document.getElementById("title").value,a=document.getElementById("description").value;try{await sendReq("POST","/api/view",JSON.stringify(pload={id:e,title:t,description:a}))}catch(n){console.log(n)}}async function sort(e){let t=document.getElementsByClassName("sortable-column"),a=["id","title","description"];removeNeighborClass(e),t[e].classList.contains("dir-d")?(t[e].classList.remove("dir-d"),t[e].classList.add("dir-u"),delete pload.asc,pload={...pload,desc:a[e]}):(t[e].classList.remove("dir-u"),t[e].classList.add("dir-d"),delete pload.desc,pload={...pload,asc:a[e]});try{await sendReq("POST","/api/view",JSON.stringify(pload))}catch(n){console.log(n)}}function toggleButton(){document.getElementById("prev-page").disabled=1===currentPage,document.getElementById("next-page").disabled=currentPage===totalPage}function removeNeighborClass(e){let t=document.getElementsByClassName("sortable-column");t[(e+1)%3].classList.remove("dir-d"),t[(e+1)%3].classList.remove("dir-u"),t[(e+2)%3].classList.remove("dir-d"),t[(e+2)%3].classList.remove("dir-u")}document.getElementById("prev-page").addEventListener("click",function(e){e.preventDefault(),currentPage--,totalPage=Math.floor((rawResp.length+9)/10),currentData=rawResp.slice((currentPage-1)*10,10*currentPage),fillPage(currentPage,totalPage),fillBody(currentData,(currentPage-1)*10+1),toggleButton()}),document.getElementById("next-page").addEventListener("click",function(e){e.preventDefault(),currentPage++,totalPage=Math.floor((rawResp.length+9)/10),currentData=rawResp.slice((currentPage-1)*10,10*currentPage),fillPage(currentPage,totalPage),fillBody(currentData,(currentPage-1)*10+1),toggleButton()}),document.getElementById("access-flag").addEventListener("click",async function(e){try{let t=await fetch("/api/flag"),a=await t.json();alert(a.message)}catch(n){console.log(n)}}),document.getElementById("logout").addEventListener("click",async function(e){try{await fetch("/api/logout"),window.location.reload()}catch(t){console.log(t)}}),async function(){try{await sendReq("POST","/api/view",JSON.stringify(pload))}catch(e){console.log(e)}}();
|
|
@ -0,0 +1 @@
|
|||
.form-group button{width:100%}.container{max-width:640px;position:absolute;top:50%;left:50%;-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}h1{text-align:center}
|
|
@ -0,0 +1 @@
|
|||
document.getElementById("form-submit").addEventListener("click",async function(e){e.preventDefault();let t=document.getElementById("form-username").value,a=document.getElementById("form-password").value;if(!t||!a){alert("Username/password cannot be empty");return}try{let n=await fetch("/api/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:t,password:a})}),o=await n.json();alert(o.message)}catch(r){console.log(r),window.location.reload()}});
|
|
@ -0,0 +1,61 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>List of Top CWE - Common Weakness Enumeration</title>
|
||||
<link rel="icon" href="data:," />
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='dashboard.min.css') }}" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="atas">
|
||||
<form action="javascript:search()" id="search">
|
||||
<label for="code">CWE ID<input type="number" name="CWE ID" id="cwe-id" min="0" autofocus /></label>
|
||||
<label for="title">Title<input type="text" name="Title" id="title" maxlength="255" /></label>
|
||||
<label for="description">Description<input type="text" name="Description" id="description"
|
||||
maxlength="255" /></label>
|
||||
<button type="submit">Search</button>
|
||||
</form>
|
||||
<div style="margin: auto; text-align: center;">
|
||||
<p>
|
||||
Welcome to <span style="font-weight: bold">List of Top CWE</span>,
|
||||
{{ session["user"] }}!
|
||||
</p>
|
||||
<p>
|
||||
Click here to access admin <strong id="access-flag">flag</strong> or
|
||||
<strong id="logout">logout</strong>.
|
||||
</p>
|
||||
</div>
|
||||
<form id="pagination">
|
||||
<button type="submit" id="prev-page">Prev</button>
|
||||
<p id="page"></p>
|
||||
<button type="submit" id="next-page">Next</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="bawah">
|
||||
<table class="sortable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th class="sortable-column" onclick="javascript:sort(0)">
|
||||
CWE ID
|
||||
</th>
|
||||
<th class="sortable-column" onclick="javascript:sort(1)">
|
||||
Title
|
||||
</th>
|
||||
<th class="sortable-column" onclick="javascript:sort(2)">
|
||||
Description
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="fillable-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script src="{{ url_for('static', filename='dashboard.min.js') }}"></script>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,34 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Login - Common Weakness Enumeration</title>
|
||||
<link rel="icon" href="data:," />
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='login.min.css') }}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Login</h1>
|
||||
<form>
|
||||
<div class="form-group pt-3">
|
||||
<input type="text" class="form-control" id="form-username" placeholder="Username" />
|
||||
</div>
|
||||
<div class="form-group pt-3">
|
||||
<input type="password" class="form-control" id="form-password" placeholder="Password" />
|
||||
</div>
|
||||
<div class="form-group pt-3">
|
||||
<button type="submit" class="btn btn-primary" id="form-submit">
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<script src="{{ url_for('static', filename='login.min.js') }}"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,40 @@
|
|||
FROM public.ecr.aws/docker/library/ubuntu:24.04@sha256:dfc10878be8d8fc9c61cbff33166cb1d1fe44391539243703c72766894fa834a
|
||||
|
||||
ARG PASSWORD
|
||||
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
|
||||
RUN echo root:${PASSWORD} | chpasswd
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
openssh-server \
|
||||
xinetd \
|
||||
curl \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-setuptools && \
|
||||
rm -rf /var/cache/apt/archives /var/lib/apt/lists
|
||||
|
||||
RUN pip3 install --break-system-packages unicorn
|
||||
|
||||
RUN echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config
|
||||
RUN echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
|
||||
RUN service ssh start
|
||||
|
||||
RUN useradd -m ctf
|
||||
|
||||
WORKDIR /ctf
|
||||
|
||||
RUN echo "Connection blocked" > /etc/banner_fail
|
||||
COPY ctf.xinetd /etc/xinetd.d/ctf
|
||||
COPY ./src/asmr.py ./
|
||||
COPY ./run.sh ./
|
||||
COPY ./start.sh ./
|
||||
|
||||
RUN touch /flag.txt
|
||||
RUN chmod -R 755 /ctf
|
||||
RUN chmod +x /ctf/start.sh
|
||||
|
||||
CMD ./start.sh
|
||||
|
||||
EXPOSE 8000
|
|
@ -0,0 +1,16 @@
|
|||
service ctf
|
||||
{
|
||||
disable = no
|
||||
socket_type = stream
|
||||
protocol = tcp
|
||||
wait = no
|
||||
user = root
|
||||
type = UNLISTED
|
||||
port = 8000
|
||||
bind = 0.0.0.0
|
||||
server = /bin/sh
|
||||
server_args = /ctf/run.sh
|
||||
banner_fail = /etc/banner_fail
|
||||
# safety options
|
||||
rlimit_cpu = 1 # the maximum number of CPU seconds that the service may use
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
services:
|
||||
asmr:
|
||||
hostname: asmr
|
||||
restart: always
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
- PASSWORD=root
|
||||
ports:
|
||||
- "15000:8000"
|
||||
- "15022:22"
|
|
@ -0,0 +1 @@
|
|||
su ctf -c "timeout 60s python3 -u /ctf/asmr.py"
|
|
@ -0,0 +1,145 @@
|
|||
import ctypes
|
||||
from ctypes.util import find_library
|
||||
from random import randbytes
|
||||
from typing import Any
|
||||
|
||||
from unicorn import Uc, UcError, x86_const
|
||||
from unicorn.unicorn_const import UC_ARCH_X86, UC_HOOK_INSN, UC_MODE_64
|
||||
|
||||
MAX_NOTES = 16
|
||||
|
||||
engine = Uc(UC_ARCH_X86, UC_MODE_64)
|
||||
notes = [0 for _ in range(MAX_NOTES)]
|
||||
libc = ctypes.CDLL(find_library("c"))
|
||||
|
||||
|
||||
def find_empty():
|
||||
for i in range(MAX_NOTES):
|
||||
if notes[i] == 0:
|
||||
return i
|
||||
return -1
|
||||
|
||||
|
||||
def syscall(uc: Uc, data: Any):
|
||||
rax = uc.reg_read(x86_const.UC_X86_REG_RAX)
|
||||
rdi = uc.reg_read(x86_const.UC_X86_REG_RDI)
|
||||
rsi = uc.reg_read(x86_const.UC_X86_REG_RSI)
|
||||
rdx = uc.reg_read(x86_const.UC_X86_REG_RDX)
|
||||
|
||||
def create(size):
|
||||
idx = find_empty()
|
||||
if idx == -1:
|
||||
uc.reg_write(x86_const.UC_X86_REG_RAX, -1)
|
||||
return
|
||||
|
||||
notes[idx] = libc.malloc(size)
|
||||
|
||||
uc.reg_write(x86_const.UC_X86_REG_RAX, idx)
|
||||
return
|
||||
|
||||
def edit(idx, buf, size):
|
||||
if notes[idx] == 0:
|
||||
uc.reg_write(x86_const.UC_X86_REG_RAX, -1)
|
||||
return
|
||||
|
||||
ubuf = uc.mem_read(buf, size)
|
||||
ctypes.memmove(
|
||||
notes[idx],
|
||||
ctypes.create_string_buffer(bytes(ubuf)),
|
||||
size,
|
||||
)
|
||||
|
||||
uc.reg_write(x86_const.UC_X86_REG_RAX, 0)
|
||||
return
|
||||
|
||||
def delete(idx):
|
||||
if notes[idx] == 0:
|
||||
uc.reg_write(x86_const.UC_X86_REG_RAX, -1)
|
||||
return
|
||||
|
||||
libc.free(notes[idx])
|
||||
notes[idx] = 0
|
||||
|
||||
uc.reg_write(x86_const.UC_X86_REG_RAX, 0)
|
||||
return
|
||||
|
||||
def view(idx, buf, size):
|
||||
if notes[idx] == 0:
|
||||
uc.reg_write(x86_const.UC_X86_REG_RAX, -1)
|
||||
return
|
||||
|
||||
libc.puts(notes[idx])
|
||||
|
||||
kbuf = ctypes.string_at(notes[idx], size)
|
||||
uc.mem_write(buf, kbuf)
|
||||
|
||||
uc.reg_write(x86_const.UC_X86_REG_RAX, 0)
|
||||
return
|
||||
|
||||
def dbg():
|
||||
print(f"RAX {uc.reg_read(x86_const.UC_X86_REG_RAX):#x}")
|
||||
print(f"RBX {uc.reg_read(x86_const.UC_X86_REG_RBX):#x}")
|
||||
print(f"RCX {uc.reg_read(x86_const.UC_X86_REG_RCX):#x}")
|
||||
print(f"RDX {uc.reg_read(x86_const.UC_X86_REG_RDX):#x}")
|
||||
print(f"RDI {uc.reg_read(x86_const.UC_X86_REG_RDI):#x}")
|
||||
print(f"RSI {uc.reg_read(x86_const.UC_X86_REG_RSI):#x}")
|
||||
print(f"R8 {uc.reg_read(x86_const.UC_X86_REG_R8):#x}")
|
||||
print(f"R9 {uc.reg_read(x86_const.UC_X86_REG_R9):#x}")
|
||||
print(f"R10 {uc.reg_read(x86_const.UC_X86_REG_R10):#x}")
|
||||
print(f"R11 {uc.reg_read(x86_const.UC_X86_REG_R11):#x}")
|
||||
print(f"R12 {uc.reg_read(x86_const.UC_X86_REG_R12):#x}")
|
||||
print(f"R13 {uc.reg_read(x86_const.UC_X86_REG_R13):#x}")
|
||||
print(f"R14 {uc.reg_read(x86_const.UC_X86_REG_R14):#x}")
|
||||
print(f"R15 {uc.reg_read(x86_const.UC_X86_REG_R15):#x}")
|
||||
print(f"RBP {uc.reg_read(x86_const.UC_X86_REG_RBP):#x}")
|
||||
print(f"RSP {uc.reg_read(x86_const.UC_X86_REG_RSP):#x}")
|
||||
print(f"RIP {uc.reg_read(x86_const.UC_X86_REG_RIP):#x}")
|
||||
|
||||
if rax == 1:
|
||||
create(rdi)
|
||||
elif rax == 2:
|
||||
edit(rdi, rsi, rdx)
|
||||
elif rax == 3:
|
||||
delete(rdi)
|
||||
elif rax == 4:
|
||||
view(rdi, rsi, rdx)
|
||||
elif rax == 5:
|
||||
dbg()
|
||||
else:
|
||||
uc.emu_stop()
|
||||
raise ValueError("invalid syscall number")
|
||||
|
||||
|
||||
def main():
|
||||
code_sz = 8 << 20 # 8MB
|
||||
base = 0x400000
|
||||
engine.mem_map(base, code_sz)
|
||||
|
||||
stack_sz = 2 << 20 # 2MB
|
||||
stack = int.from_bytes(randbytes(5), "big") & ~0xFFF
|
||||
stack = 0x7FF0_00000000 | stack
|
||||
engine.mem_map(stack, stack_sz)
|
||||
|
||||
engine.hook_add(UC_HOOK_INSN, syscall, None, 1, 0,
|
||||
x86_const.UC_X86_INS_SYSCALL)
|
||||
|
||||
while True:
|
||||
try:
|
||||
code = input("Code (in hex): ")
|
||||
sc = bytes.fromhex(code)[:code_sz]
|
||||
break
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
engine.reg_write(x86_const.UC_X86_REG_RSP, stack + stack_sz - 0x1000)
|
||||
|
||||
engine.mem_write(base, sc)
|
||||
|
||||
try:
|
||||
engine.emu_start(base, base + code_sz, 30_000) # 30s
|
||||
except (ValueError, UcError) as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
/usr/sbin/sshd -D &
|
||||
/usr/sbin/xinetd -dontfork
|
|
@ -0,0 +1,31 @@
|
|||
FROM public.ecr.aws/docker/library/ubuntu:22.04
|
||||
|
||||
ARG PASSWORD
|
||||
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
|
||||
RUN echo root:${PASSWORD} | chpasswd
|
||||
RUN apt-get update && apt-get install -y openssh-server xinetd cmake gcc curl
|
||||
RUN echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config
|
||||
RUN echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
|
||||
RUN service ssh start
|
||||
|
||||
RUN useradd -m ctf
|
||||
|
||||
WORKDIR /ctf
|
||||
|
||||
RUN echo "Connection blocked" > /etc/banner_fail
|
||||
COPY ctf.xinetd /etc/xinetd.d/ctf
|
||||
COPY ./src/main.c ./
|
||||
COPY ./run.sh ./
|
||||
COPY ./start.sh ./
|
||||
|
||||
RUN gcc /ctf/main.c -o /ctf/main
|
||||
|
||||
RUN touch /flag.txt
|
||||
RUN chmod -R 755 /ctf
|
||||
RUN chmod +x /ctf/start.sh
|
||||
|
||||
CMD ./start.sh
|
||||
|
||||
EXPOSE 8000
|
|
@ -0,0 +1,16 @@
|
|||
service ctf
|
||||
{
|
||||
disable = no
|
||||
socket_type = stream
|
||||
protocol = tcp
|
||||
wait = no
|
||||
user = root
|
||||
type = UNLISTED
|
||||
port = 8000
|
||||
bind = 0.0.0.0
|
||||
server = /bin/sh
|
||||
server_args = /ctf/run.sh
|
||||
banner_fail = /etc/banner_fail
|
||||
# safety options
|
||||
rlimit_cpu = 1 # the maximum number of CPU seconds that the service may use
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
services:
|
||||
bit-canvas:
|
||||
hostname: bit-canvas
|
||||
restart: always
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
- PASSWORD=root
|
||||
ports:
|
||||
- "20000:8000"
|
||||
- "20022:22"
|
|
@ -0,0 +1 @@
|
|||
su ctf -c "timeout 60s /ctf/main"
|
|
@ -0,0 +1,112 @@
|
|||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
|
||||
|
||||
int main() {
|
||||
setbuf(stdout, NULL);
|
||||
|
||||
int choice = 0;
|
||||
int debug = 0;
|
||||
int size = 16;
|
||||
int canvas[16] = {0};
|
||||
|
||||
puts("bit-canvas @ gemastik-xvii");
|
||||
puts("1. draw");
|
||||
puts("2. clear");
|
||||
puts("3. resize");
|
||||
puts("4. admin");
|
||||
puts("5. exit");
|
||||
|
||||
while (1) {
|
||||
printf("choice? ");
|
||||
scanf("%d", &choice);
|
||||
switch (choice) {
|
||||
case 1:
|
||||
int n = 0;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
|
||||
printf("how many bits? ");
|
||||
scanf("%d", &n);
|
||||
|
||||
if (n > (size * size * 2)) {
|
||||
puts("too many bits");
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
printf("where x y? ");
|
||||
scanf("%d %d", &x, &y);
|
||||
bool prev = 0;
|
||||
bool next = 0;
|
||||
prev = canvas[x] >> y & 1;
|
||||
next = prev ^ 1;
|
||||
canvas[x] ^= 1 << y;
|
||||
if (debug) printf("[DEBUG] x: %d, y: %d, prev: %d, next: %d\n", x, y, prev, next);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 2:
|
||||
for (int i = 0; i < size; i++) {
|
||||
for (int j = 0; j < size * 2; j++) {
|
||||
canvas[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 3:
|
||||
printf("size? ");
|
||||
scanf("%d", &size);
|
||||
|
||||
break;
|
||||
|
||||
case 4:
|
||||
|
||||
FILE *f = fopen("/flag.txt", "r");
|
||||
|
||||
if (f == NULL) {
|
||||
puts("flag not found");
|
||||
return 1;
|
||||
}
|
||||
|
||||
char flag[43];
|
||||
char buffer[43];
|
||||
|
||||
fgets(flag, sizeof(flag), f);
|
||||
fclose(f);
|
||||
|
||||
printf("password? ");
|
||||
scanf("%s", buffer);
|
||||
|
||||
if (!strcmp(buffer, flag)) {
|
||||
debug = 1;
|
||||
puts("debug mode enabled");
|
||||
} else {
|
||||
puts("invalid password");
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 5:
|
||||
puts("bye");
|
||||
return 0;
|
||||
|
||||
default:
|
||||
printf("invalid choice %d\n", choice);
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
for (int j = 0; j < size * 2; j++) {
|
||||
printf("%d", canvas[i] >> j & 1);
|
||||
}
|
||||
puts("");
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
/usr/sbin/sshd -D &
|
||||
/usr/sbin/xinetd -dontfork
|
|
@ -0,0 +1,4 @@
|
|||
**/node_modules/
|
||||
**/dist/
|
||||
**/*.so
|
||||
**/*.o
|
|
@ -0,0 +1,5 @@
|
|||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
|
@ -0,0 +1,67 @@
|
|||
FROM node:20-slim AS base
|
||||
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
|
||||
RUN corepack enable
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./frontend/package.json ./frontend/pnpm-lock.yaml /app/
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
FROM base AS build
|
||||
|
||||
COPY ./frontend/ /app/
|
||||
RUN pnpm run build
|
||||
|
||||
FROM ghcr.io/circleous/httpd:latest@sha256:8a353b1208a4871233845d9a84eb4f40c4581a7acaed505de4ccdd52c8d56615
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./kauth /app/kauth/
|
||||
|
||||
RUN set eux; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
openssh-server \
|
||||
pkg-config \
|
||||
gcc \
|
||||
curl \
|
||||
make \
|
||||
lua5.3 \
|
||||
liblua5.3-dev \
|
||||
libsodium-dev \
|
||||
libsqlite3-dev \
|
||||
luarocks \
|
||||
; \
|
||||
luarocks-5.3 install lua-cjson; \
|
||||
luarocks-5.3 install lsqlite3; \
|
||||
luarocks-5.3 install bcrypt; \
|
||||
cd kauth; \
|
||||
luarocks-5.3 make; \
|
||||
cd ..; \
|
||||
rm -rf kauth;\
|
||||
apt-get remove -y \
|
||||
pkg-config \
|
||||
gcc \
|
||||
make \
|
||||
lua5.3 \
|
||||
liblua5.3-dev; \
|
||||
echo root:${PASSWORD} | chpasswd \
|
||||
echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config; \
|
||||
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config; \
|
||||
service ssh start; \
|
||||
apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false;
|
||||
|
||||
COPY httpd-foreground /usr/local/bin/
|
||||
COPY ./httpd.conf /usr/local/apache2/conf/httpd.conf
|
||||
COPY --chown=www-data:www-data ./fjb.db /app/data/fjb.db
|
||||
COPY --from=build /app/dist /usr/local/apache2/htdocs
|
||||
COPY ./src/ /app/lua/
|
||||
|
||||
RUN sed -ri "s/SECRETSECRETSECRETSECRETSECRETSE/$(openssl rand -hex 16)/g" /app/lua/secret.lua && \
|
||||
chmod 644 /app/lua/secret.lua
|
||||
|
||||
EXPOSE 80
|
||||
CMD ["httpd-foreground"]
|
|
@ -0,0 +1,9 @@
|
|||
services:
|
||||
fjb:
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
- PASSWORD=root
|
||||
ports:
|
||||
- 17000:80
|
||||
- 17022:22
|
Binary file not shown.
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
|
@ -0,0 +1,8 @@
|
|||
# React + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": false,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import react from "eslint-plugin-react";
|
||||
import reactHooks from "eslint-plugin-react-hooks";
|
||||
import reactRefresh from "eslint-plugin-react-refresh";
|
||||
|
||||
export default [
|
||||
{ ignores: ["dist"] },
|
||||
{
|
||||
files: ["**/*.{js,jsx}"],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: "module",
|
||||
},
|
||||
},
|
||||
settings: { react: { version: "18.3" } },
|
||||
plugins: {
|
||||
react,
|
||||
"react-hooks": reactHooks,
|
||||
"react-refresh": reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...js.configs.recommended.rules,
|
||||
...react.configs.recommended.rules,
|
||||
...react.configs["jsx-runtime"].rules,
|
||||
...reactHooks.configs.recommended.rules,
|
||||
"react/jsx-no-target-blank": "off",
|
||||
"react-refresh/only-export-components": [
|
||||
"warn",
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
"react/prop-types": "off",
|
||||
},
|
||||
},
|
||||
];
|
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>FJB</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "^1.1.1",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-tabs": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.2",
|
||||
"@tanstack/react-query": "^5.56.2",
|
||||
"@tanstack/react-router": "^1.58.3",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"ky": "^1.7.2",
|
||||
"lucide-react": "^0.441.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.9.0",
|
||||
"@tanstack/router-devtools": "^1.58.3",
|
||||
"@tanstack/router-plugin": "^1.58.4",
|
||||
"@types/node": "^22.5.5",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.9.0",
|
||||
"eslint-plugin-react": "^7.35.0",
|
||||
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.9",
|
||||
"globals": "^15.9.0",
|
||||
"postcss": "^8.4.47",
|
||||
"tailwindcss": "^3.4.12",
|
||||
"vite": "^5.4.1"
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#f38ba8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-store"><path d="m2 7 4.41-4.41A2 2 0 0 1 7.83 2h8.34a2 2 0 0 1 1.42.59L22 7"/><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><path d="M15 22v-4a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2v4"/><path d="M2 7h20"/><path d="M22 7v3a2 2 0 0 1-2 2a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 16 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 12 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 8 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 4 12a2 2 0 0 1-2-2V7"/></svg>
|
After Width: | Height: | Size: 690 B |
|
@ -0,0 +1,238 @@
|
|||
import { useCallback, useState, useEffect } from "react";
|
||||
import {
|
||||
Loader2,
|
||||
ShoppingBag,
|
||||
ShoppingCart,
|
||||
LogOut,
|
||||
LogIn,
|
||||
Plus,
|
||||
Eye,
|
||||
EyeOff,
|
||||
} from "lucide-react";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Link, useNavigate, useLocation } from "@tanstack/react-router";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
import useAuth from "@/hooks/useAuth";
|
||||
import router from "@/lib/router";
|
||||
import httpClient from "@/lib/httpClient";
|
||||
|
||||
function AuthButton() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { isPending, authSuccess } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const logout = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
document.cookie = "session=";
|
||||
queryClient.invalidateQueries({ queryKey: ["user"] });
|
||||
router.invalidate();
|
||||
navigate({ to: "/login" });
|
||||
},
|
||||
[queryClient, navigate],
|
||||
);
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<Button className="flex items-center gap-2" disabled>
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
<span className="hidden sm:inline">Loading</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
if (authSuccess) {
|
||||
return (
|
||||
<Button className="flex items-center gap-2" onClick={logout}>
|
||||
<LogOut className="h-5 w-5" />
|
||||
<span className="hidden sm:inline">Logout</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link to="/login">
|
||||
<Button className="flex items-center gap-2">
|
||||
<LogIn className="h-5 w-5" />
|
||||
<span className="hidden sm:inline">Login</span>
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Navigation() {
|
||||
const [data, setData] = useState({
|
||||
name: "",
|
||||
description: "",
|
||||
valuable: "",
|
||||
price: "",
|
||||
});
|
||||
const [valueHidden, setValueHidden] = useState(true);
|
||||
const { pathname } = useLocation();
|
||||
const [dialogClass, setDialogClass] = useState(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const { isSuccess: authSuccess } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { isPending, mutate } = useMutation({
|
||||
mutationKey: ["post-add-item"],
|
||||
mutationFn: (data) => httpClient.post("/api/catalog", { json: data }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["items"] });
|
||||
setOpen(false);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (pathname === "/" && authSuccess) {
|
||||
setDialogClass("flex items-center gap-2");
|
||||
} else {
|
||||
setDialogClass("hidden");
|
||||
}
|
||||
}, [pathname, authSuccess]);
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { name, value } = event.target;
|
||||
setData((prev) => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
mutate(data);
|
||||
};
|
||||
|
||||
const toggleValuableVisibility = () => {
|
||||
setValueHidden((prev) => !prev);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex justify-between items-center mb-6 flex-wrap gap-4">
|
||||
<Link to="/">
|
||||
<h1 className="text-3xl font-bold">FJB</h1>
|
||||
</Link>
|
||||
<div className="flex gap-2">
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className={dialogClass}>
|
||||
<Plus className="h-5 w-5" />
|
||||
<span className="hidden sm:inline">Add Item</span>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add New Item</DialogTitle>
|
||||
<DialogDescription>
|
||||
Enter the details of the item you want to add to the
|
||||
marketplace.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="name" className="text-right">
|
||||
Name
|
||||
</Label>
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
value={data.name}
|
||||
onChange={handleChange}
|
||||
className="col-span-3"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="description" className="text-right">
|
||||
Description
|
||||
</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
name="description"
|
||||
value={data.description}
|
||||
onChange={handleChange}
|
||||
className="col-span-3"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="valuable" className="text-right">
|
||||
Valuable
|
||||
</Label>
|
||||
<div className="col-span-3 flex">
|
||||
<Input
|
||||
id="valuable"
|
||||
name="valuable"
|
||||
type={valueHidden ? "text" : "password"}
|
||||
value={data.valuable}
|
||||
onChange={handleChange}
|
||||
className="flex-grow"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={toggleValuableVisibility}
|
||||
className="ml-2"
|
||||
>
|
||||
{valueHidden ? (
|
||||
<EyeOff className="h-4 w-4" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="price" className="text-right">
|
||||
Price
|
||||
</Label>
|
||||
<Input
|
||||
id="price"
|
||||
name="price"
|
||||
value={data.price}
|
||||
onChange={handleChange}
|
||||
type="number"
|
||||
className="col-span-3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="submit" disabled={isPending}>
|
||||
{isPending && (
|
||||
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-primary border-r-transparent" />
|
||||
)}
|
||||
Submit
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Link to="/">
|
||||
<Button variant="outline" className="flex items-center gap-2">
|
||||
<ShoppingBag className="h-5 w-5" />
|
||||
<span className="hidden sm:inline">Marketplace</span>
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/cart">
|
||||
<Button variant="outline" className="flex items-center gap-2">
|
||||
<ShoppingCart className="h-5 w-5" />
|
||||
<span className="hidden sm:inline">Cart</span>
|
||||
</Button>
|
||||
</Link>
|
||||
<AuthButton />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import * as React from "react"
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}) {
|
||||
return (<div className={cn(badgeVariants({ variant }), className)} {...props} />);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
|
@ -0,0 +1,48 @@
|
|||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
(<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props} />)
|
||||
);
|
||||
})
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
|
@ -0,0 +1,50 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("rounded-xl border bg-card text-card-foreground shadow", className)}
|
||||
{...props} />
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props} />
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn("font-semibold leading-none tracking-tight", className)}
|
||||
{...props} />
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props} />
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props} />
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
|
@ -0,0 +1,94 @@
|
|||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { Cross2Icon } from "@radix-ui/react-icons"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
|
||||
const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
{children}
|
||||
<DialogPrimitive.Close
|
||||
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<Cross2Icon className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<div
|
||||
className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
|
||||
{...props} />
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<div
|
||||
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||
{...props} />
|
||||
)
|
||||
DialogFooter.displayName = "DialogFooter"
|
||||
|
||||
const DialogTitle = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
||||
{...props} />
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props} />
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Input = React.forwardRef(({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
(<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props} />)
|
||||
);
|
||||
})
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
|
@ -0,0 +1,16 @@
|
|||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
const Label = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
|
@ -0,0 +1,86 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Table = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props} />
|
||||
</div>
|
||||
))
|
||||
Table.displayName = "Table"
|
||||
|
||||
const TableHeader = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||
))
|
||||
TableHeader.displayName = "TableHeader"
|
||||
|
||||
const TableBody = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props} />
|
||||
))
|
||||
TableBody.displayName = "TableBody"
|
||||
|
||||
const TableFooter = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)}
|
||||
{...props} />
|
||||
))
|
||||
TableFooter.displayName = "TableFooter"
|
||||
|
||||
const TableRow = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
TableRow.displayName = "TableRow"
|
||||
|
||||
const TableHead = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
TableHead.displayName = "TableHead"
|
||||
|
||||
const TableCell = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
TableCell.displayName = "TableCell"
|
||||
|
||||
const TableCaption = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||
{...props} />
|
||||
))
|
||||
TableCaption.displayName = "TableCaption"
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Tabs = TabsPrimitive.Root
|
||||
|
||||
const TabsList = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
TabsList.displayName = TabsPrimitive.List.displayName
|
||||
|
||||
const TabsTrigger = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||
|
||||
const TabsContent = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
|
@ -0,0 +1,18 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Textarea = React.forwardRef(({ className, ...props }, ref) => {
|
||||
return (
|
||||
(<textarea
|
||||
className={cn(
|
||||
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props} />)
|
||||
);
|
||||
})
|
||||
Textarea.displayName = "Textarea"
|
||||
|
||||
export { Textarea }
|
|
@ -0,0 +1,24 @@
|
|||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
|
||||
const TooltipContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
|
@ -0,0 +1,9 @@
|
|||
const PAGE = {
|
||||
LOGIN: 0,
|
||||
MARKETPLACE: 1,
|
||||
CART: 2,
|
||||
};
|
||||
|
||||
const TOKEN_KEY = "TOKEN";
|
||||
|
||||
export { PAGE, TOKEN_KEY };
|
|
@ -0,0 +1,9 @@
|
|||
import httpClient from "@/lib/httpClient";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
export default function useAuth() {
|
||||
return useQuery({
|
||||
queryKey: ["user"],
|
||||
queryFn: () => httpClient.get("/api/user").json(),
|
||||
});
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
--radius: 0.5rem
|
||||
}
|
||||
.dark {
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%
|
||||
}
|
||||
}
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import ky from "ky";
|
||||
|
||||
export default ky.create({
|
||||
credentials: "include",
|
||||
retry: {
|
||||
limit: 1,
|
||||
statusCodes: [500, 504],
|
||||
},
|
||||
});
|
|
@ -0,0 +1,12 @@
|
|||
import { createRouter } from "@tanstack/react-router";
|
||||
import { routeTree } from "@/routeTree.gen";
|
||||
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
context: {
|
||||
auth: null,
|
||||
queryClient: null,
|
||||
},
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,6 @@
|
|||
import { clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
|
||||
import { RouterProvider } from "@tanstack/react-router";
|
||||
|
||||
import router from "@/lib/router";
|
||||
|
||||
import "./index.css";
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
mutations: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
createRoot(document.getElementById("root")).render(
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={router} />
|
||||
</QueryClientProvider>
|
||||
</StrictMode>,
|
||||
);
|
|
@ -0,0 +1,159 @@
|
|||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useCallback } from "react";
|
||||
import { Loader2, Trash2 } from "lucide-react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import httpClient from "@/lib/httpClient";
|
||||
import useAuth from "@/hooks/useAuth";
|
||||
|
||||
export default function CartCheckout() {
|
||||
const auth = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { isLoading, data: cart } = useQuery({
|
||||
queryKey: ["cart"],
|
||||
queryFn: () => httpClient("/api/cart").json(),
|
||||
enabled: auth.isSuccess,
|
||||
});
|
||||
|
||||
const { mutate } = useMutation({
|
||||
mutationKey: ["delete-cart"],
|
||||
mutationFn: (id) =>
|
||||
httpClient
|
||||
.delete("/api/cart", {
|
||||
searchParams: new URLSearchParams([["id", id]]),
|
||||
})
|
||||
.json(),
|
||||
onSuccess: async () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["cart"],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const calculateTotal = () => {
|
||||
return cart ? cart.items.reduce((total, item) => total + item.price, 0) : 0;
|
||||
};
|
||||
|
||||
const handleDelete = useCallback(
|
||||
(index) => {
|
||||
mutate(index);
|
||||
},
|
||||
[mutate],
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-background">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{!cart || !cart.items || cart.items.length === 0 ? (
|
||||
<div className="flex justify-center items-center">
|
||||
Your cart is empty
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<Card className="md:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle>Your Items</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Item</TableHead>
|
||||
<TableHead>Price</TableHead>
|
||||
<TableHead className="text-right">Action</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{cart.items.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>{item.name}</TableCell>
|
||||
<TableCell>${item.price.toFixed(2)}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => handleDelete(item.id)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="h-fit">
|
||||
<CardHeader>
|
||||
<CardTitle>Order Summary</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span>Subtotal</span>
|
||||
<span>${calculateTotal().toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Shipping</span>
|
||||
<span>Free</span>
|
||||
</div>
|
||||
<div className="flex justify-between font-bold">
|
||||
<span>Total</span>
|
||||
<span>${calculateTotal().toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="inline-block w-full">
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled
|
||||
className="cursor-not-allowed w-full"
|
||||
>
|
||||
{/* TODO: create checkout */}
|
||||
<span>Proceed to Payment</span>
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>This action is currently unavailable</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
import { useRef } from "react";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import httpClient from "@/lib/httpClient";
|
||||
|
||||
export default function LoginRegister() {
|
||||
const usernameRef = useRef();
|
||||
const passwordRef = useRef();
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const { isLoading, mutate } = useMutation({
|
||||
mutationKey: ["login"],
|
||||
mutationFn: (data) => httpClient.post("/api/login", { json: data }).json(),
|
||||
onSettled(data, err) {
|
||||
if (err || data?.error) {
|
||||
queryClient.setQueryData(["user"], null);
|
||||
return;
|
||||
}
|
||||
queryClient.setQueryData(["user"], data);
|
||||
navigate({ to: "/" });
|
||||
},
|
||||
retry: false,
|
||||
});
|
||||
|
||||
async function onSubmit(event) {
|
||||
event.preventDefault();
|
||||
const username = usernameRef.current.value;
|
||||
const password = passwordRef.current.value;
|
||||
mutate({ username, password });
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="w-[350px]">
|
||||
<CardHeader>
|
||||
<CardTitle>Account</CardTitle>
|
||||
<CardDescription>Login or create a new account.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs defaultValue="login" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="login">Login</TabsTrigger>
|
||||
<TabsTrigger value="register">Register</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="login">
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="grid gap-2">
|
||||
<div className="grid gap-1">
|
||||
<Label htmlFor="username">Username</Label>
|
||||
<Input
|
||||
id="username"
|
||||
placeholder=""
|
||||
type="username"
|
||||
autoCapitalize="none"
|
||||
autoComplete="username"
|
||||
autoCorrect="off"
|
||||
disabled={isLoading}
|
||||
ref={usernameRef}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-1">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
placeholder="********"
|
||||
type="password"
|
||||
autoCapitalize="none"
|
||||
autoComplete="current-password"
|
||||
disabled={isLoading}
|
||||
ref={passwordRef}
|
||||
/>
|
||||
</div>
|
||||
<Button disabled={isLoading}>
|
||||
{isLoading && (
|
||||
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-primary border-r-transparent" />
|
||||
)}
|
||||
Sign In
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</TabsContent>
|
||||
<TabsContent value="register">
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="grid gap-2">
|
||||
<div className="grid gap-1">
|
||||
<Label htmlFor="username">Username</Label>
|
||||
<Input
|
||||
id="username"
|
||||
placeholder=""
|
||||
type="username"
|
||||
autoCapitalize="none"
|
||||
autoComplete="username"
|
||||
autoCorrect="off"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-1">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
placeholder="********"
|
||||
type="password"
|
||||
autoCapitalize="none"
|
||||
autoComplete="new-password"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-1">
|
||||
<Label htmlFor="confirm-password">Confirm Password</Label>
|
||||
<Input
|
||||
id="confirm-password"
|
||||
placeholder="********"
|
||||
type="password"
|
||||
autoCapitalize="none"
|
||||
autoComplete="new-password"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<Button disabled={isLoading}>
|
||||
{isLoading && (
|
||||
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-primary border-r-transparent" />
|
||||
)}
|
||||
Create Account
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
import { useCallback } from "react";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { ShoppingBasket, Loader2 } from "lucide-react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import httpClient from "@/lib/httpClient";
|
||||
|
||||
export default function MarketplaceListing() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ["items"],
|
||||
queryFn: () => httpClient.get("/api/catalog").json(),
|
||||
});
|
||||
|
||||
const { mutate } = useMutation({
|
||||
mutationKey: ["post-cart"],
|
||||
mutationFn: (data) => httpClient.post("/api/cart", { json: data }).json(),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["cart"] });
|
||||
},
|
||||
});
|
||||
|
||||
const handleAddToCart = useCallback(
|
||||
(id) => {
|
||||
mutate({ item: id });
|
||||
},
|
||||
[mutate],
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-background">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-screen">
|
||||
No data available
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{data.items.map((item) => (
|
||||
<Card key={item.id} className="flex flex-col">
|
||||
<CardHeader>
|
||||
<CardTitle>{item.name}</CardTitle>
|
||||
<CardDescription>{item.description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-grow">
|
||||
<p className="text-2xl font-bold">${item.price.toFixed(2)}</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
Posted {formatDistanceToNow(new Date(item.date_posted))} ago
|
||||
</p>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-between items-center">
|
||||
<Badge variant="secondary">Owner ID: {item.owner}</Badge>
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
console.log(e.target);
|
||||
e.target.setAttribute("disabled", "true");
|
||||
handleAddToCart(item.id);
|
||||
setTimeout(() => {
|
||||
e.target.removeAttribute("disabled");
|
||||
}, 1000);
|
||||
}}
|
||||
>
|
||||
<ShoppingBasket className="h-5 w-5" />
|
||||
<span className="hidden sm:inline">Add to Cart</span>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
/* prettier-ignore-start */
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
|
||||
// This file is auto-generated by TanStack Router
|
||||
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
// Import Routes
|
||||
|
||||
import { Route as rootRoute } from './routes/__root'
|
||||
import { Route as CartImport } from './routes/cart'
|
||||
|
||||
// Create Virtual Routes
|
||||
|
||||
const LoginLazyImport = createFileRoute('/login')()
|
||||
const IndexLazyImport = createFileRoute('/')()
|
||||
|
||||
// Create/Update Routes
|
||||
|
||||
const LoginLazyRoute = LoginLazyImport.update({
|
||||
path: '/login',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() => import('./routes/login.lazy').then((d) => d.Route))
|
||||
|
||||
const CartRoute = CartImport.update({
|
||||
path: '/cart',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const IndexLazyRoute = IndexLazyImport.update({
|
||||
path: '/',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() => import('./routes/index.lazy').then((d) => d.Route))
|
||||
|
||||
// Populate the FileRoutesByPath interface
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof IndexLazyImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/cart': {
|
||||
id: '/cart'
|
||||
path: '/cart'
|
||||
fullPath: '/cart'
|
||||
preLoaderRoute: typeof CartImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/login': {
|
||||
id: '/login'
|
||||
path: '/login'
|
||||
fullPath: '/login'
|
||||
preLoaderRoute: typeof LoginLazyImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create and export the route tree
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexLazyRoute
|
||||
'/cart': typeof CartRoute
|
||||
'/login': typeof LoginLazyRoute
|
||||
}
|
||||
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexLazyRoute
|
||||
'/cart': typeof CartRoute
|
||||
'/login': typeof LoginLazyRoute
|
||||
}
|
||||
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRoute
|
||||
'/': typeof IndexLazyRoute
|
||||
'/cart': typeof CartRoute
|
||||
'/login': typeof LoginLazyRoute
|
||||
}
|
||||
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/' | '/cart' | '/login'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/' | '/cart' | '/login'
|
||||
id: '__root__' | '/' | '/cart' | '/login'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
|
||||
export interface RootRouteChildren {
|
||||
IndexLazyRoute: typeof IndexLazyRoute
|
||||
CartRoute: typeof CartRoute
|
||||
LoginLazyRoute: typeof LoginLazyRoute
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexLazyRoute: IndexLazyRoute,
|
||||
CartRoute: CartRoute,
|
||||
LoginLazyRoute: LoginLazyRoute,
|
||||
}
|
||||
|
||||
export const routeTree = rootRoute
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
|
||||
/* prettier-ignore-end */
|
||||
|
||||
/* ROUTE_MANIFEST_START
|
||||
{
|
||||
"routes": {
|
||||
"__root__": {
|
||||
"filePath": "__root.jsx",
|
||||
"children": [
|
||||
"/",
|
||||
"/cart",
|
||||
"/login"
|
||||
]
|
||||
},
|
||||
"/": {
|
||||
"filePath": "index.lazy.jsx"
|
||||
},
|
||||
"/cart": {
|
||||
"filePath": "cart.jsx"
|
||||
},
|
||||
"/login": {
|
||||
"filePath": "login.lazy.jsx"
|
||||
}
|
||||
}
|
||||
}
|
||||
ROUTE_MANIFEST_END */
|
|
@ -0,0 +1,15 @@
|
|||
import Navigation from "@/components/navigation";
|
||||
import { createRootRoute, Outlet } from "@tanstack/react-router";
|
||||
|
||||
function RootComponent() {
|
||||
return (
|
||||
<div className="container mx-auto py-8 px-4">
|
||||
<Navigation />
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Route = createRootRoute({
|
||||
component: () => <RootComponent />,
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
import CartCheckout from '@/pages/cart-checkout'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/cart')({
|
||||
component: () => <CartCheckout />,
|
||||
})
|
|
@ -0,0 +1,6 @@
|
|||
import MarketplaceListing from '@/pages/marketplace-lisiting'
|
||||
import { createLazyFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createLazyFileRoute('/')({
|
||||
component: () => <MarketplaceListing />,
|
||||
})
|
|
@ -0,0 +1,10 @@
|
|||
import LoginRegister from "@/pages/login-register";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createLazyFileRoute("/login")({
|
||||
component: () => (
|
||||
<div className="flex justify-center bg-background">
|
||||
<LoginRegister />
|
||||
</div>
|
||||
),
|
||||
});
|
|
@ -0,0 +1,57 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
darkMode: ["class"],
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
colors: {
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
chart: {
|
||||
1: "hsl(var(--chart-1))",
|
||||
2: "hsl(var(--chart-2))",
|
||||
3: "hsl(var(--chart-3))",
|
||||
4: "hsl(var(--chart-4))",
|
||||
5: "hsl(var(--chart-5))",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
import path from "path";
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [TanStackRouterVite(), react()],
|
||||
server: {
|
||||
proxy: {
|
||||
"/api": "http://localhost:17000",
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# start sshd
|
||||
/usr/sbin/sshd -D &
|
||||
|
||||
# Apache gets grumpy about PID files pre-existing
|
||||
rm -f /usr/local/apache2/logs/httpd.pid
|
||||
|
||||
exec httpd -DFOREGROUND "$@"
|
|
@ -0,0 +1,116 @@
|
|||
ServerRoot "/usr/local/apache2"
|
||||
|
||||
|
||||
Listen 80
|
||||
|
||||
LoadModule mpm_event_module modules/mod_mpm_event.so
|
||||
LoadModule authn_file_module modules/mod_authn_file.so
|
||||
#LoadModule authn_dbm_module modules/mod_authn_dbm.so
|
||||
#LoadModule authn_anon_module modules/mod_authn_anon.so
|
||||
#LoadModule authn_dbd_module modules/mod_authn_dbd.so
|
||||
#LoadModule authn_socache_module modules/mod_authn_socache.so
|
||||
LoadModule authn_core_module modules/mod_authn_core.so
|
||||
LoadModule authz_host_module modules/mod_authz_host.so
|
||||
LoadModule authz_groupfile_module modules/mod_authz_groupfile.so
|
||||
LoadModule authz_user_module modules/mod_authz_user.so
|
||||
#LoadModule authz_dbm_module modules/mod_authz_dbm.so
|
||||
#LoadModule authz_owner_module modules/mod_authz_owner.so
|
||||
#LoadModule authz_dbd_module modules/mod_authz_dbd.so
|
||||
LoadModule authz_core_module modules/mod_authz_core.so
|
||||
LoadModule access_compat_module modules/mod_access_compat.so
|
||||
LoadModule auth_basic_module modules/mod_auth_basic.so
|
||||
LoadModule dbd_module modules/mod_dbd.so
|
||||
LoadModule reqtimeout_module modules/mod_reqtimeout.so
|
||||
LoadModule filter_module modules/mod_filter.so
|
||||
LoadModule mime_module modules/mod_mime.so
|
||||
LoadModule log_config_module modules/mod_log_config.so
|
||||
LoadModule lua_module modules/mod_lua.so
|
||||
LoadModule env_module modules/mod_env.so
|
||||
LoadModule headers_module modules/mod_headers.so
|
||||
LoadModule setenvif_module modules/mod_setenvif.so
|
||||
LoadModule version_module modules/mod_version.so
|
||||
LoadModule unixd_module modules/mod_unixd.so
|
||||
LoadModule dir_module modules/mod_dir.so
|
||||
LoadModule alias_module modules/mod_alias.so
|
||||
LoadModule rewrite_module modules/mod_rewrite.so
|
||||
|
||||
<IfModule unixd_module>
|
||||
User www-data
|
||||
Group www-data
|
||||
</IfModule>
|
||||
|
||||
|
||||
ServerAdmin hermit@kaos.engineer
|
||||
ServerName fjb.kaos.engineer:80
|
||||
|
||||
<Directory />
|
||||
AllowOverride none
|
||||
Require all denied
|
||||
</Directory>
|
||||
|
||||
#
|
||||
# Note that from this point forward you must specifically allow
|
||||
# particular features to be enabled - so if something's not working as
|
||||
# you might expect, make sure that you have specifically enabled it
|
||||
# below.
|
||||
#
|
||||
|
||||
#
|
||||
# DocumentRoot: The directory out of which you will serve your
|
||||
# documents. By default, all requests are taken from this directory, but
|
||||
# symbolic links and aliases may be used to point to other locations.
|
||||
#
|
||||
DocumentRoot "/usr/local/apache2/htdocs"
|
||||
<Directory "/usr/local/apache2/htdocs">
|
||||
AllowOverride None
|
||||
Options Indexes FollowSymLinks
|
||||
Require all granted
|
||||
|
||||
RewriteEngine on
|
||||
# Don't rewrite files or directories
|
||||
RewriteCond %{REQUEST_FILENAME} -f [OR]
|
||||
RewriteCond %{REQUEST_FILENAME} -d
|
||||
RewriteRule ^ - [L]
|
||||
# Rewrite everything else to index.html to allow html5 state links
|
||||
RewriteRule ^ index.html [L]
|
||||
</Directory>
|
||||
|
||||
LuaMapHandler /api/(\w+) /app/lua/handler/api.lua
|
||||
LuaPackageCPath /usr/local/lib/lua/5.3/?.so
|
||||
LuaPackagePath /app/lua/?.lua
|
||||
|
||||
<IfModule dir_module>
|
||||
DirectoryIndex index.html
|
||||
</IfModule>
|
||||
|
||||
<Files ".ht*">
|
||||
Require all denied
|
||||
</Files>
|
||||
|
||||
ErrorLog /proc/self/fd/2
|
||||
LogLevel warn
|
||||
|
||||
<IfModule log_config_module>
|
||||
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
|
||||
LogFormat "%h %l %u %t \"%r\" %>s %b" common
|
||||
CustomLog /proc/self/fd/1 common
|
||||
</IfModule>
|
||||
|
||||
<IfModule alias_module>
|
||||
#
|
||||
</IfModule>
|
||||
|
||||
<IfModule headers_module>
|
||||
RequestHeader unset Proxy early
|
||||
</IfModule>
|
||||
|
||||
<IfModule mime_module>
|
||||
TypesConfig conf/mime.types
|
||||
|
||||
AddType application/x-compress .Z
|
||||
AddType application/x-gzip .gz .tgz
|
||||
</IfModule>
|
||||
|
||||
|
||||
# Various default settings
|
||||
#Include conf/extra/httpd-default.conf
|
|
@ -0,0 +1,2 @@
|
|||
*.o
|
||||
*.so
|
|
@ -0,0 +1,12 @@
|
|||
# CFLAGS += -c -O2 -fPIC $$(pkg-config --cflags lua-5.3 libsodium)
|
||||
# LDFLAGS += -shared -fPIC $$(pkg-config --libs libsodium lua-5.3)
|
||||
|
||||
all: default
|
||||
|
||||
kauth.so: kauth.c
|
||||
luarocks make --no-install
|
||||
|
||||
clean:
|
||||
rm kauth.o kauth.so
|
||||
|
||||
.PHONY: clean
|
|
@ -0,0 +1,26 @@
|
|||
package = "kauth"
|
||||
version = "1.0-1"
|
||||
source = {
|
||||
url = "https://kaos.engineer"
|
||||
}
|
||||
|
||||
dependencies = {
|
||||
"lua >= 5.3",
|
||||
}
|
||||
|
||||
local srcs = {
|
||||
"kauth.c",
|
||||
}
|
||||
|
||||
build = {
|
||||
type = "builtin",
|
||||
modules = {
|
||||
kauth = {
|
||||
sources = srcs,
|
||||
libraries = {"sodium"},
|
||||
},
|
||||
},
|
||||
platforms = {
|
||||
unix = { modules = { kauth = srcs } },
|
||||
},
|
||||
}
|
|
@ -0,0 +1,164 @@
|
|||
#include <lauxlib.h>
|
||||
#include <lua.h>
|
||||
#include <lualib.h>
|
||||
#include <sodium.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "kauth.h"
|
||||
|
||||
#define kauth_token_claim_BYTES \
|
||||
(sodium_base64_ENCODED_LEN(sizeof(struct kauth_user), \
|
||||
sodium_base64_VARIANT_URLSAFE_NO_PADDING))
|
||||
|
||||
#define kauth_token_sign_BYTES \
|
||||
(sodium_base64_ENCODED_LEN(crypto_auth_hmacsha256_BYTES, \
|
||||
sodium_base64_VARIANT_URLSAFE_NO_PADDING))
|
||||
|
||||
#define kauth_token_BYTES (kauth_token_claim_BYTES + kauth_token_sign_BYTES) - 1
|
||||
|
||||
static int encode(lua_State *L) {
|
||||
int typ;
|
||||
struct kauth_user claim;
|
||||
unsigned char h[crypto_auth_hmacsha256_BYTES];
|
||||
unsigned char token[kauth_token_BYTES];
|
||||
|
||||
memset(h, 0, crypto_auth_hmacsha256_BYTES);
|
||||
memset(token, 0, kauth_token_BYTES);
|
||||
memset(&claim, 0, sizeof(struct kauth_user));
|
||||
|
||||
size_t keylen = 0;
|
||||
const char *key = luaL_checklstring(L, 1, &keylen);
|
||||
if (keylen != crypto_auth_hmacsha256_KEYBYTES) {
|
||||
lua_pushstring(L, "invalid key len");
|
||||
return lua_error(L);
|
||||
}
|
||||
|
||||
luaL_checktype(L, 2, LUA_TTABLE);
|
||||
|
||||
if ((typ = lua_getfield(L, 2, "id")) != LUA_TNUMBER) {
|
||||
lua_pop(L, 1);
|
||||
lua_pushstring(L, "invalid type id (number expected)");
|
||||
return lua_error(L);
|
||||
}
|
||||
lua_Integer id = lua_tointeger(L, -1);
|
||||
lua_pop(L, 1);
|
||||
claim.id = id;
|
||||
|
||||
if ((typ = lua_getfield(L, 2, "name")) != LUA_TSTRING) {
|
||||
lua_pop(L, 1);
|
||||
lua_pushstring(L, "invalid type name (string expected)");
|
||||
return lua_error(L);
|
||||
}
|
||||
const char *name = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
strcpy((char *)claim.name, name);
|
||||
|
||||
claim.exp = time(0) + 1800; // 30 min
|
||||
|
||||
crypto_auth_hmacsha256(h, (const unsigned char *)&claim,
|
||||
sizeof(struct kauth_user), (const unsigned char *)key);
|
||||
|
||||
sodium_bin2base64((char *)token, kauth_token_claim_BYTES,
|
||||
(const unsigned char *)&claim, sizeof(struct kauth_user),
|
||||
sodium_base64_VARIANT_URLSAFE_NO_PADDING);
|
||||
|
||||
sodium_bin2base64((char *)(token + kauth_token_claim_BYTES),
|
||||
kauth_token_sign_BYTES, h, crypto_auth_hmacsha256_BYTES,
|
||||
sodium_base64_VARIANT_URLSAFE_NO_PADDING);
|
||||
|
||||
token[kauth_token_claim_BYTES - 1] = '.';
|
||||
|
||||
lua_pushlstring(L, (char *)token, kauth_token_BYTES);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int decode(lua_State *L) {
|
||||
struct kauth_user claim;
|
||||
const char name[129];
|
||||
const unsigned char h[crypto_auth_hmacsha256_BYTES];
|
||||
|
||||
memset(&claim, 0, sizeof(struct kauth_user));
|
||||
memset((void *)name, 0, sizeof(name));
|
||||
memset((void *)h, 0, sizeof(h));
|
||||
|
||||
size_t keylen = 0;
|
||||
const char *key = luaL_checklstring(L, 1, &keylen);
|
||||
if (keylen != crypto_auth_hmacsha256_BYTES) {
|
||||
lua_pushstring(L, "invalid key len");
|
||||
return lua_error(L);
|
||||
}
|
||||
|
||||
size_t tokenlen = 0;
|
||||
const char *token = luaL_checklstring(L, 2, &tokenlen);
|
||||
if (tokenlen != kauth_token_BYTES) {
|
||||
lua_pushstring(L, "invalid token len");
|
||||
return lua_error(L);
|
||||
}
|
||||
|
||||
if (token[kauth_token_claim_BYTES - 1] != '.') {
|
||||
lua_pushstring(L, "invalid token");
|
||||
return lua_error(L);
|
||||
}
|
||||
|
||||
size_t clen = 0;
|
||||
const char *b64_end = NULL;
|
||||
if (sodium_base642bin((unsigned char *)&claim, sizeof(struct kauth_user),
|
||||
token, kauth_token_claim_BYTES, NULL, &clen, &b64_end,
|
||||
sodium_base64_VARIANT_URLSAFE_NO_PADDING) != 0) {
|
||||
|
||||
lua_pushstring(L, "invalid token: failed to decode claim");
|
||||
return lua_error(L);
|
||||
}
|
||||
strncpy((char *)name, claim.name, sizeof(name));
|
||||
|
||||
clen = 0;
|
||||
b64_end = NULL;
|
||||
if (sodium_base642bin((unsigned char *)h, crypto_auth_hmacsha256_BYTES,
|
||||
token + kauth_token_claim_BYTES, kauth_token_sign_BYTES,
|
||||
NULL, &clen, &b64_end,
|
||||
sodium_base64_VARIANT_URLSAFE_NO_PADDING) != 0) {
|
||||
lua_pushstring(L, "invalid token: failed to decode hash");
|
||||
return lua_error(L);
|
||||
}
|
||||
|
||||
if (crypto_auth_hmacsha256_verify(h, (const unsigned char *)&claim,
|
||||
sizeof(struct kauth_user),
|
||||
(const unsigned char *)key) != 0) {
|
||||
lua_pushstring(L, "invalid token: failed to verify");
|
||||
return lua_error(L);
|
||||
}
|
||||
|
||||
if (claim.exp <= time(0)) {
|
||||
lua_pushstring(L, "invalid token: expired");
|
||||
return lua_error(L);
|
||||
}
|
||||
|
||||
lua_newtable(L);
|
||||
|
||||
lua_pushstring(L, name);
|
||||
lua_setfield(L, -2, "name");
|
||||
|
||||
lua_pushinteger(L, claim.id);
|
||||
lua_setfield(L, -2, "id");
|
||||
return 1;
|
||||
}
|
||||
|
||||
static const struct luaL_Reg luakauth_lib[] = {
|
||||
{"encode", encode},
|
||||
{"decode", decode},
|
||||
|
||||
{NULL, NULL},
|
||||
};
|
||||
|
||||
LUALIB_API int luaopen_kauth(lua_State *L) {
|
||||
if (sodium_init() == -1) {
|
||||
lua_pushstring(L, "crypto init failed");
|
||||
return lua_error(L);
|
||||
}
|
||||
|
||||
luaL_newlib(L, luakauth_lib);
|
||||
|
||||
return 1;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
struct kauth_user {
|
||||
const char name[128];
|
||||
unsigned long long id;
|
||||
unsigned long long exp;
|
||||
};
|
|
@ -0,0 +1,256 @@
|
|||
local sqlite3 = require "lsqlite3"
|
||||
local bcrypt = require "bcrypt"
|
||||
|
||||
local _M = {}
|
||||
|
||||
function _M.register(username, password)
|
||||
local user = nil
|
||||
local err = nil
|
||||
|
||||
local conn = sqlite3.open("/app/data/fjb.db", sqlite3.OPEN_READWRITE)
|
||||
local hashed_password = bcrypt.digest(password, 9)
|
||||
local stmt = conn:prepare("INSERT INTO users (username, password) VALUES (?, ?)")
|
||||
stmt:bind_values(username, hashed_password)
|
||||
local result = stmt:step()
|
||||
|
||||
if result == sqlite3.DONE then
|
||||
user = conn:last_insert_rowid()
|
||||
elseif result == sqlite3.CONSTRAINT then
|
||||
else
|
||||
err = conn:errmsg()
|
||||
end
|
||||
|
||||
conn:close()
|
||||
return user, err
|
||||
end
|
||||
|
||||
function _M.login(username, password)
|
||||
local user = nil
|
||||
local err = nil
|
||||
|
||||
local conn = sqlite3.open("/app/data/fjb.db")
|
||||
local stmt = conn:prepare("SELECT id, username, password FROM users WHERE username = ?")
|
||||
stmt:bind_values(username)
|
||||
local result = stmt:step()
|
||||
|
||||
if result == sqlite3.ROW then
|
||||
local hashed_password = stmt:get_value(2)
|
||||
if bcrypt.verify(password, hashed_password) then
|
||||
user = {
|
||||
name = stmt:get_value(1),
|
||||
id = stmt:get_value(0),
|
||||
}
|
||||
end
|
||||
elseif result == sqlite3.DONE then
|
||||
else
|
||||
err = conn:errmsg()
|
||||
end
|
||||
|
||||
conn:close()
|
||||
return user, err
|
||||
end
|
||||
|
||||
function _M.get_user(id)
|
||||
local user = nil
|
||||
local err = nil
|
||||
|
||||
local conn = sqlite3.open("/app/data/fjb.db")
|
||||
local stmt = conn:prepare("SELECT username FROM users WHERE id = ?")
|
||||
stmt:bind_values(id)
|
||||
local result = stmt:step()
|
||||
|
||||
if result == sqlite3.ROW then
|
||||
user = stmt:get_value(0)
|
||||
else
|
||||
err = conn:errmsg()
|
||||
end
|
||||
|
||||
conn:close()
|
||||
return user, err
|
||||
end
|
||||
|
||||
function _M.get_wallet(id)
|
||||
local wallet = nil
|
||||
local err = nil
|
||||
|
||||
local conn = sqlite3.open("/app/data/fjb.db")
|
||||
local stmt = conn:prepare("SELECT wallet FROM users WHERE id = ?")
|
||||
stmt:bind_values(id)
|
||||
local result = stmt:step()
|
||||
|
||||
if result == sqlite3.ROW then
|
||||
wallet = stmt:get_value(0)
|
||||
else
|
||||
err = conn:errmsg()
|
||||
end
|
||||
|
||||
conn:close()
|
||||
return wallet, err
|
||||
end
|
||||
|
||||
function _M.add_item(uid, item_name, description, valuable, price)
|
||||
local id = nil
|
||||
local err = nil
|
||||
|
||||
local conn = sqlite3.open("/app/data/fjb.db", sqlite3.OPEN_READWRITE)
|
||||
local created_at = os.date("!%Y-%m-%dT%H:%M:%S+00:00")
|
||||
local stmt = conn:prepare [[
|
||||
INSERT INTO catalog
|
||||
(user_id, item_name, description, value, price, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
]]
|
||||
stmt:bind_values(uid, item_name, description, valuable, price, created_at)
|
||||
local result = stmt:step()
|
||||
if result == sqlite3.DONE then
|
||||
id = conn:last_insert_rowid()
|
||||
else
|
||||
err = conn:errmsg()
|
||||
end
|
||||
conn:close()
|
||||
return id, err
|
||||
end
|
||||
|
||||
function _M.get_all_items(uid)
|
||||
local conn = sqlite3.open("/app/data/fjb.db")
|
||||
local query = "SELECT * FROM catalog"
|
||||
if uid then
|
||||
query = query .. " WHERE user_id = ?"
|
||||
end
|
||||
local stmt = conn:prepare(query)
|
||||
if uid then
|
||||
stmt:bind_values(uid)
|
||||
end
|
||||
local items = {}
|
||||
local result = stmt:step()
|
||||
while result == sqlite3.ROW do
|
||||
local id = stmt:get_value(0)
|
||||
local user_id = stmt:get_value(1)
|
||||
local item_name = stmt:get_value(2)
|
||||
local description = stmt:get_value(3)
|
||||
local value = stmt:get_value(4)
|
||||
local price = stmt:get_value(5)
|
||||
local date_posted = stmt:get_value(6)
|
||||
table.insert(items, {
|
||||
id = id,
|
||||
name = item_name,
|
||||
description = description,
|
||||
owner = user_id,
|
||||
value = uid == user_id and value or nil,
|
||||
price = price,
|
||||
date_posted = date_posted,
|
||||
})
|
||||
result = stmt:step()
|
||||
end
|
||||
conn:close()
|
||||
return items
|
||||
end
|
||||
|
||||
function _M.get_item(uid, item_id)
|
||||
local conn = sqlite3.open("/app/data/fjb.db")
|
||||
local stmt = conn:prepare("SELECT * FROM catalog WHERE id = ?")
|
||||
stmt:bind_values(item_id)
|
||||
local result = stmt:step()
|
||||
if result ~= sqlite3.ROW then
|
||||
return nil
|
||||
end
|
||||
local user_id = stmt:get_value(1)
|
||||
if user_id ~= uid then
|
||||
return nil
|
||||
end
|
||||
local id = stmt:get_value(0)
|
||||
local item_name = stmt:get_value(2)
|
||||
local description = stmt:get_value(3)
|
||||
local value = stmt:get_value(4)
|
||||
local price = stmt:get_value(5)
|
||||
local date_posted = stmt:get_value(6)
|
||||
local item = {
|
||||
id = id,
|
||||
name = item_name,
|
||||
description = description,
|
||||
value = value,
|
||||
price = price,
|
||||
date_posted = date_posted,
|
||||
}
|
||||
conn:close()
|
||||
return item
|
||||
end
|
||||
|
||||
function _M.add_to_cart(uid, item_id)
|
||||
local id = nil
|
||||
local err = nil
|
||||
|
||||
local conn = sqlite3.open("/app/data/fjb.db", sqlite3.OPEN_READWRITE)
|
||||
|
||||
local stmt = conn:prepare("SELECT user_id FROM catalog WHERE id = ?")
|
||||
stmt:bind_values(item_id)
|
||||
local result = stmt:step()
|
||||
if result ~= sqlite3.ROW then
|
||||
return nil, "invalid item_id"
|
||||
end
|
||||
|
||||
if stmt:get_value(0) == uid then
|
||||
return nil, "cant add your own item to cart"
|
||||
end
|
||||
|
||||
stmt = conn:prepare("INSERT INTO cart (user_id, item_id) VALUES (?, ?)")
|
||||
stmt:bind_values(uid, item_id)
|
||||
local result = stmt:step()
|
||||
if result == sqlite3.DONE then
|
||||
id = conn:last_insert_rowid()
|
||||
else
|
||||
err = conn:errmsg()
|
||||
end
|
||||
conn:close()
|
||||
return id, err
|
||||
end
|
||||
|
||||
function _M.get_items_from_cart(uid, checkout)
|
||||
local pay = checkout ~= nil
|
||||
local conn = sqlite3.open("/app/data/fjb.db")
|
||||
local items = {}
|
||||
local stmt = conn:prepare [[
|
||||
SELECT car.id, cat.id, cat.item_name, cat.price, cat.value
|
||||
FROM cart car
|
||||
JOIN catalog cat ON car.item_id = cat.id
|
||||
WHERE car.user_id = ?
|
||||
]]
|
||||
stmt:bind_values(uid)
|
||||
local result = stmt:step()
|
||||
while result == sqlite3.ROW do
|
||||
local id = stmt:get_value(0)
|
||||
local item_id = stmt:get_value(1)
|
||||
local name = stmt:get_value(2)
|
||||
local price = stmt:get_value(3)
|
||||
local valuable = stmt:get_value(4)
|
||||
table.insert(items, {
|
||||
id = id,
|
||||
item_id = item_id,
|
||||
name = name,
|
||||
price = price,
|
||||
valuable = pay and valuable or nil,
|
||||
})
|
||||
result = stmt:step()
|
||||
end
|
||||
conn:close()
|
||||
return items
|
||||
end
|
||||
|
||||
function _M.delete_item_from_cart(uid, id)
|
||||
local err = nil
|
||||
local conn = sqlite3.open("/app/data/fjb.db")
|
||||
local stmt = conn:prepare [[
|
||||
DELETE FROM
|
||||
cart
|
||||
WHERE
|
||||
user_id = ? AND id = ?
|
||||
]]
|
||||
stmt:bind_values(uid, id)
|
||||
local result = stmt:step()
|
||||
if result ~= sqlite3.DONE then
|
||||
err = conn:errmsg()
|
||||
end
|
||||
conn:close()
|
||||
return err
|
||||
end
|
||||
|
||||
return _M
|
|
@ -0,0 +1,40 @@
|
|||
local cjson = require "cjson"
|
||||
|
||||
local middleware = require "middleware"
|
||||
local login = require "handler.login"
|
||||
local register = require "handler.register"
|
||||
local user = require "handler.user"
|
||||
local catalog = require "handler.catalog"
|
||||
local cart = require "handler.cart"
|
||||
local checkout = require "handler.checkout"
|
||||
|
||||
function handle(r)
|
||||
if r.uri == "/api/login" and r.method == "POST" then
|
||||
return login.handler(r)
|
||||
elseif r.uri == "/api/register" and r.method == "POST" then
|
||||
return register.handler(r)
|
||||
elseif r.uri == "/api/user" and r.method == "GET" then
|
||||
return middleware.auth(r, user.handler)
|
||||
elseif r.uri == "/api/catalog" and r.method == "GET" then
|
||||
return catalog.get(r)
|
||||
elseif r.uri == "/api/catalog/me" and r.method == "GET" then
|
||||
return middleware.auth(r, catalog.get_me)
|
||||
elseif r.uri == "/api/catalog" and r.method == "POST" then
|
||||
return middleware.auth(r, catalog.post)
|
||||
elseif r.uri == "/api/cart" and r.method == "GET" then
|
||||
return middleware.auth(r, cart.get)
|
||||
elseif r.uri == "/api/cart" and r.method == "POST" then
|
||||
return middleware.auth(r, cart.post)
|
||||
elseif r.uri == "/api/cart" and r.method == "DELETE" then
|
||||
return middleware.auth(r, cart.delete)
|
||||
elseif r.uri == "/api/checkout" and r.method == "POST" then
|
||||
return middleware.auth(r, checkout.post)
|
||||
else
|
||||
r.status = 404
|
||||
r.content_type = "application/json"
|
||||
r:write(cjson.encode({
|
||||
error = "Not found"
|
||||
}))
|
||||
return apache2.OK
|
||||
end
|
||||
end
|
|
@ -0,0 +1,73 @@
|
|||
local cjson = require "cjson"
|
||||
cjson.encode_empty_table_as_object(false)
|
||||
|
||||
local db = require "database"
|
||||
local util = require "lib.utils"
|
||||
|
||||
local _M = {}
|
||||
|
||||
function _M.get(r)
|
||||
local items = db.get_items_from_cart(tonumber(r.notes.kauth_id))
|
||||
r.content_type = "application/json"
|
||||
r:write(cjson.encode({
|
||||
items = items,
|
||||
}))
|
||||
return apache2.OK
|
||||
end
|
||||
|
||||
function _M.delete(r)
|
||||
local spec = r:parseargs()
|
||||
if util.isempty(spec.id) then
|
||||
r.status = 400
|
||||
r:write(cjson.encode({
|
||||
error = "Bad Request: missing id"
|
||||
}))
|
||||
return apache2.OK
|
||||
end
|
||||
|
||||
r.content_type = "application/json"
|
||||
|
||||
local err = db.delete_item_from_cart(tonumber(r.notes.kauth_id), tonumber(spec.id))
|
||||
if err then
|
||||
r:write(cjson.encode({
|
||||
error = "Internal error: " .. err,
|
||||
}))
|
||||
return apache2.OK
|
||||
end
|
||||
|
||||
r:write(cjson.encode({
|
||||
message = "Successfully removed from cart",
|
||||
}))
|
||||
return apache2.OK
|
||||
end
|
||||
|
||||
|
||||
function _M.post(r)
|
||||
local input = r:requestbody()
|
||||
local spec = cjson.decode(input or "{}")
|
||||
|
||||
if util.isempty(spec.item) then
|
||||
r.status = 400
|
||||
r:write(cjson.encode({
|
||||
error = "Bad Request: missing name"
|
||||
}))
|
||||
return apache2.OK
|
||||
end
|
||||
|
||||
r.content_type = "application/json"
|
||||
|
||||
local _, err = db.add_to_cart(tonumber(r.notes.kauth_id), tonumber(spec.item))
|
||||
if err then
|
||||
r:write(cjson.encode({
|
||||
error = "Internal error: " .. err,
|
||||
}))
|
||||
return apache2.OK
|
||||
end
|
||||
|
||||
r:write(cjson.encode({
|
||||
message = "Successfully added to cart",
|
||||
}))
|
||||
return apache2.OK
|
||||
end
|
||||
|
||||
return _M
|
|
@ -0,0 +1,79 @@
|
|||
local cjson = require "cjson"
|
||||
cjson.encode_empty_table_as_object(false)
|
||||
|
||||
local db = require "database"
|
||||
local util = require "lib.utils"
|
||||
|
||||
local _M = {}
|
||||
|
||||
function _M.get(r)
|
||||
local items = db.get_all_items()
|
||||
r.content_type = "application/json"
|
||||
r:write(cjson.encode({
|
||||
items = items,
|
||||
}))
|
||||
return apache2.OK
|
||||
end
|
||||
|
||||
function _M.get_me(r)
|
||||
local items = db.get_all_items(tonumber(r.notes.kauth_id))
|
||||
r.content_type = "application/json"
|
||||
r:write(cjson.encode({
|
||||
items = items,
|
||||
}))
|
||||
return apache2.OK
|
||||
end
|
||||
|
||||
function _M.post(r)
|
||||
local input = r:requestbody()
|
||||
local spec = cjson.decode(input or "{}")
|
||||
|
||||
if util.isempty(spec.name) then
|
||||
r.status = 400
|
||||
r:write(cjson.encode({
|
||||
error = "Bad Request: missing name"
|
||||
}))
|
||||
return apache2.OK
|
||||
end
|
||||
|
||||
if util.isempty(spec.description) then
|
||||
r.status = 400
|
||||
r:write(cjson.encode({
|
||||
error = "Bad Request: missing description"
|
||||
}))
|
||||
return apache2.OK
|
||||
end
|
||||
|
||||
if util.isempty(spec.valuable) then
|
||||
r.status = 400
|
||||
r:write(cjson.encode({
|
||||
error = "Bad Request: missing valuable"
|
||||
}))
|
||||
return apache2.OK
|
||||
end
|
||||
|
||||
if util.isempty(spec.price) or tonumber(spec.price) <= 0 then
|
||||
r.status = 400
|
||||
r:write(cjson.encode({
|
||||
error = "Bad Request: missing price"
|
||||
}))
|
||||
return apache2.OK
|
||||
end
|
||||
|
||||
r.content_type = "application/json"
|
||||
|
||||
local id, err = db.add_item(tonumber(r.notes.kauth_id), spec.name, spec.description, spec.valuable, tonumber(spec.price))
|
||||
if err then
|
||||
r:write(cjson.encode({
|
||||
error = "Internal error: " .. err,
|
||||
}))
|
||||
return apache2.OK
|
||||
end
|
||||
|
||||
r:write(cjson.encode({
|
||||
message = "Successfully added to catalog",
|
||||
}))
|
||||
return apache2.OK
|
||||
end
|
||||
|
||||
return _M
|
|
@ -0,0 +1,40 @@
|
|||
local cjson = require "cjson"
|
||||
|
||||
local db = require "database"
|
||||
local util = require "lib.utils"
|
||||
|
||||
local _M = {}
|
||||
|
||||
function _M.post(r)
|
||||
local cost = 0x0
|
||||
r.content_type = "application/json"
|
||||
|
||||
local items = db.get_items_from_cart(tonumber(r.notes.kauth_id), true)
|
||||
for i = 1, #items do
|
||||
cost = cost + items[i].price
|
||||
end
|
||||
|
||||
local wallet, err = db.get_wallet(tonumber(r.notes.kauth_id))
|
||||
if err then
|
||||
r.status = 500
|
||||
r:write(cjson.encode({
|
||||
error = "Internal error: " .. err,
|
||||
}))
|
||||
return apache2.OK
|
||||
end
|
||||
|
||||
if wallet < cost then
|
||||
r.status = 401
|
||||
r:write(cjson.encode({
|
||||
error = "Unathorized: not enough balance",
|
||||
}))
|
||||
return apache2.OK
|
||||
end
|
||||
|
||||
r:write(cjson.encode({
|
||||
items = items,
|
||||
}))
|
||||
return apache2.OK
|
||||
end
|
||||
|
||||
return _M
|
|
@ -0,0 +1,65 @@
|
|||
local cjson = require "cjson"
|
||||
local kauth = require "kauth"
|
||||
|
||||
local utils = require "lib.utils"
|
||||
local db = require "database"
|
||||
local secret = require "secret"
|
||||
|
||||
local _M = {}
|
||||
|
||||
function _M.handler(r)
|
||||
local input = r:requestbody()
|
||||
local spec = cjson.decode(input or "{}")
|
||||
|
||||
r.content_type = "application/json"
|
||||
|
||||
if utils.isempty(spec.username) or utils.isempty(spec.password) then
|
||||
r.status = 400
|
||||
r:write(cjson.encode({
|
||||
error = "Bad Request: missing username or password"
|
||||
}))
|
||||
return apache2.OK
|
||||
end
|
||||
|
||||
local user, err = db.login(spec.username, spec.password)
|
||||
if err then
|
||||
r.status = 500
|
||||
r:write(cjson.encode({
|
||||
error = "Internal error: " .. err
|
||||
}))
|
||||
return apache2.OK
|
||||
end
|
||||
|
||||
if not user then
|
||||
r.status = 401
|
||||
r:write(cjson.encode({
|
||||
error = "Unathorized: invalid password or username"
|
||||
}))
|
||||
return apache2.OK
|
||||
end
|
||||
|
||||
local ok, token = pcall(function ()
|
||||
return kauth.encode(secret.key, user)
|
||||
end)
|
||||
if not ok then
|
||||
r.status = 500
|
||||
r:write(cjson.encode({
|
||||
error = "Internal error: failed to generate token"
|
||||
}))
|
||||
return apache2.OK
|
||||
end
|
||||
|
||||
r:setcookie({
|
||||
key = "session",
|
||||
value = token,
|
||||
expires = os.time() + 1800,
|
||||
})
|
||||
|
||||
r:write(cjson.encode({
|
||||
user = user,
|
||||
token = token,
|
||||
}))
|
||||
return apache2.OK
|
||||
end
|
||||
|
||||
return _M
|
|
@ -0,0 +1,45 @@
|
|||
local cjson = require "cjson"
|
||||
|
||||
local utils = require "lib.utils"
|
||||
local db = require "database"
|
||||
|
||||
local _M = {}
|
||||
|
||||
function _M.handler(r)
|
||||
local input = r:requestbody()
|
||||
local spec = cjson.decode(input or "{}")
|
||||
|
||||
r.content_type = "application/json"
|
||||
|
||||
if utils.isempty(spec.username) or utils.isempty(spec.password) then
|
||||
r.status = 400
|
||||
r:write(cjson.encode({
|
||||
error = "Bad Request: missing username or password"
|
||||
}))
|
||||
return apache2.OK
|
||||
end
|
||||
|
||||
local uid, err = db.register(spec.username, spec.password)
|
||||
if err then
|
||||
r.status = 500
|
||||
r:write(cjson.encode({
|
||||
error = "Internal error: " .. err
|
||||
}))
|
||||
return apache2.OK
|
||||
end
|
||||
|
||||
if uid == nil then
|
||||
r.status = 400
|
||||
r:write(cjson.encode({
|
||||
error = "Bad Request: invalid password or username"
|
||||
}))
|
||||
return apache2.OK
|
||||
end
|
||||
|
||||
r:write(cjson.encode({
|
||||
message = "Successfully registered"
|
||||
}))
|
||||
return apache2.OK
|
||||
end
|
||||
|
||||
return _M
|
|
@ -0,0 +1,16 @@
|
|||
local cjson = require "cjson"
|
||||
|
||||
local _M = {}
|
||||
|
||||
function _M.handler(r)
|
||||
r.content_type = "application/json"
|
||||
r:write(cjson.encode({
|
||||
user = {
|
||||
id = r.notes.kauth_id,
|
||||
name = r.notes.kauth_name,
|
||||
}
|
||||
}))
|
||||
return apache2.OK
|
||||
end
|
||||
|
||||
return _M
|
|
@ -0,0 +1,7 @@
|
|||
local _M = {}
|
||||
|
||||
function _M.isempty(s)
|
||||
return s == nil or s == ""
|
||||
end
|
||||
|
||||
return _M
|
|
@ -0,0 +1,47 @@
|
|||
local kauth = require "kauth"
|
||||
local cjson = require "cjson"
|
||||
|
||||
local database = require "database"
|
||||
local secret = require "secret"
|
||||
|
||||
local _M = {}
|
||||
|
||||
function _M.auth(r, fn)
|
||||
local session = r:getcookie("session")
|
||||
if not session then
|
||||
r.status = 401
|
||||
r.content_type = "application/json"
|
||||
r:write(cjson.encode({
|
||||
error = "Unathorized: invalid session"
|
||||
}))
|
||||
return apache2.OK
|
||||
end
|
||||
local ok, res = pcall(function ()
|
||||
return kauth.decode(secret.key, session)
|
||||
end)
|
||||
if not ok then
|
||||
r.status = 401
|
||||
r.content_type = "application/json"
|
||||
r:write(cjson.encode({
|
||||
error = "Unathorized: " .. res
|
||||
}))
|
||||
return apache2.OK
|
||||
end
|
||||
|
||||
local _, err = database.get_user(res.id)
|
||||
if err then
|
||||
r.status = 401
|
||||
r.content_type = "application/json"
|
||||
r:write(cjson.encode({
|
||||
error = "Unathorized: invalid session"
|
||||
}))
|
||||
return apache2.OK
|
||||
end
|
||||
|
||||
r.user = res.name
|
||||
r.notes.kauth_name = res.name
|
||||
r.notes.kauth_id = res.id
|
||||
return fn(r)
|
||||
end
|
||||
|
||||
return _M
|
|
@ -0,0 +1,5 @@
|
|||
local _M = {}
|
||||
|
||||
_M.key = "SECRETSECRETSECRETSECRETSECRETSE"
|
||||
|
||||
return _M
|
|
@ -0,0 +1,19 @@
|
|||
FROM public.ecr.aws/docker/library/python:3.11-slim-buster
|
||||
|
||||
ARG PASSWORD
|
||||
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
|
||||
RUN echo root:${PASSWORD} | chpasswd
|
||||
RUN apt-get update && apt-get install -y openssh-server curl socat nano
|
||||
RUN echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config
|
||||
RUN echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
|
||||
RUN service ssh start
|
||||
|
||||
WORKDIR /ctf/gift-card
|
||||
|
||||
COPY src/ src/
|
||||
COPY start.sh .
|
||||
|
||||
RUN chmod +x start.sh
|
||||
CMD ./start.sh
|
|
@ -0,0 +1,13 @@
|
|||
services:
|
||||
gift-card:
|
||||
hostname: gift-card
|
||||
restart: always
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
- PASSWORD=PASSWORD_21000
|
||||
ports:
|
||||
- "21000:5000"
|
||||
- "21022:22"
|
||||
volumes:
|
||||
- ./flag.txt:/ctf/gift-card/flag.txt:ro
|
|
@ -0,0 +1 @@
|
|||
PLACEHOLDER
|
|
@ -0,0 +1,126 @@
|
|||
import random
|
||||
import signal
|
||||
|
||||
FLAG = open("../flag.txt").read()
|
||||
|
||||
|
||||
class GiftShop:
|
||||
# TODO: design better giftcard generator and validator
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
self.mod = lambda x: x % 256
|
||||
self.Hm = self.mod(sum(name.encode()))
|
||||
|
||||
def generate_giftcard(self) -> str:
|
||||
r = random.randbytes(15)
|
||||
s = int.to_bytes(self.mod(self.Hm - self.mod(sum(r))))
|
||||
signature = r + s
|
||||
return signature.hex()
|
||||
|
||||
def validate_giftcard(self, giftcard: str) -> bool:
|
||||
signature = bytes.fromhex(giftcard)
|
||||
return len(signature) == 16 and self.mod(sum(signature)) == self.Hm
|
||||
|
||||
|
||||
class Challenge:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.balance = 100
|
||||
self.items = {
|
||||
"bread": {"price": 33, "callable": self.__get_bread},
|
||||
"giftcard": {"price": 50, "callable": self.__get_giftcard},
|
||||
"flag": {"price": 420, "callable": self.__get_flag},
|
||||
}
|
||||
self.redeemed_giftcard = set()
|
||||
self.gift_shop = GiftShop(self.name)
|
||||
|
||||
def greet(self):
|
||||
msg = ""
|
||||
msg += f"\nWelcome, {self.name}!"
|
||||
msg += f"\nOption:"
|
||||
msg += f"\n [1] Check available items"
|
||||
msg += f"\n [2] Buy item"
|
||||
msg += f"\n [3] Redeem giftcard"
|
||||
msg += f"\n [9] Input admin code"
|
||||
print(msg)
|
||||
|
||||
def __get_bread(self):
|
||||
return chr(0x1F35E)
|
||||
|
||||
def __get_giftcard(self):
|
||||
return self.gift_shop.generate_giftcard()
|
||||
|
||||
def __get_flag(self):
|
||||
return FLAG
|
||||
|
||||
def check_available(self):
|
||||
msg = ""
|
||||
msg += f"\nAvailable items:"
|
||||
for item_name, item_info in self.items.items():
|
||||
item_price = item_info["price"]
|
||||
msg += f"\n [+] {item_name} (${item_price})"
|
||||
return msg
|
||||
|
||||
def buy_item(self, item):
|
||||
if item not in self.items.keys():
|
||||
return f"Item {item} is not available"
|
||||
|
||||
if self.balance < self.items[item]["price"]:
|
||||
return f"Insufficient balance"
|
||||
|
||||
self.balance -= self.items[item]["price"]
|
||||
return self.items[item]["callable"]()
|
||||
|
||||
def redeem_giftcard(self, giftcard):
|
||||
if giftcard in self.redeemed_giftcard:
|
||||
return "Your giftcard is already used"
|
||||
|
||||
if not self.gift_shop.validate_giftcard(giftcard):
|
||||
return "Your giftcard is invalid"
|
||||
|
||||
value = random.randint(30, 60)
|
||||
self.balance += value
|
||||
self.redeemed_giftcard.add(giftcard)
|
||||
return f"You got ${value} from the giftcard"
|
||||
|
||||
def admin_code(self, code):
|
||||
if code != FLAG:
|
||||
return "Incorrect code"
|
||||
|
||||
self.balance += self.items["flag"]["price"]
|
||||
return "Correct code"
|
||||
|
||||
|
||||
def user_input(s):
|
||||
inp = input(s).strip()
|
||||
assert len(inp) < 256
|
||||
return inp
|
||||
|
||||
|
||||
def main():
|
||||
name = user_input("Name: ")
|
||||
challenge = Challenge(name)
|
||||
challenge.greet()
|
||||
|
||||
while True:
|
||||
print(f"\nCurrent balance: ${challenge.balance}")
|
||||
match int(user_input("> ")):
|
||||
case 1:
|
||||
print(challenge.check_available())
|
||||
case 2:
|
||||
item = user_input("Item: ")
|
||||
print(challenge.buy_item(item))
|
||||
case 3:
|
||||
giftcard = user_input("Giftcard: ")
|
||||
print(challenge.redeem_giftcard(giftcard))
|
||||
case 9:
|
||||
code = user_input("Code: ")
|
||||
print(challenge.admin_code(code))
|
||||
case _:
|
||||
print("Invalid option")
|
||||
break
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
signal.alarm(60)
|
||||
main()
|
|
@ -0,0 +1,5 @@
|
|||
#!/bin/sh
|
||||
/usr/sbin/sshd -D &
|
||||
|
||||
cd src
|
||||
socat TCP-LISTEN:5000,reuseaddr,fork EXEC:"python3 -u main.py"
|
|
@ -0,0 +1,21 @@
|
|||
FROM python:3.11-slim-buster
|
||||
|
||||
ARG PASSWORD
|
||||
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
|
||||
RUN echo root:${PASSWORD} | chpasswd
|
||||
RUN apt-get update && apt-get install -y openssh-server curl socat nano python3 python3-pip
|
||||
RUN echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config
|
||||
RUN echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
|
||||
RUN service ssh start
|
||||
|
||||
WORKDIR /ctf/gift-voucher
|
||||
|
||||
COPY src/ src/
|
||||
COPY start.sh .
|
||||
|
||||
RUN pip install pycryptodome
|
||||
|
||||
RUN chmod +x start.sh
|
||||
CMD ./start.sh
|
|
@ -0,0 +1,13 @@
|
|||
services:
|
||||
gift-voucher:
|
||||
hostname: gift-voucher
|
||||
restart: always
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
- PASSWORD=PASSWORD_16000
|
||||
ports:
|
||||
- "16000:5000"
|
||||
- "16022:22"
|
||||
volumes:
|
||||
- ./flag.txt:/ctf/gift-voucher/flag.txt:ro
|
|
@ -0,0 +1 @@
|
|||
PLACEHOLDER
|
|
@ -0,0 +1,134 @@
|
|||
from Crypto.Hash import SHA256
|
||||
from Crypto.PublicKey import RSA
|
||||
import os
|
||||
import random
|
||||
import signal
|
||||
import base64
|
||||
|
||||
FLAG = open("../flag.txt").read()
|
||||
|
||||
|
||||
class GiftShop:
|
||||
def __init__(self, name: str) -> None:
|
||||
self.key = RSA.generate(1024)
|
||||
self.Hm = SHA256.new(name.encode()).digest()
|
||||
self.prefix = b"\x00\x02"
|
||||
self.null = b"\x00"
|
||||
|
||||
def generate_voucher(self) -> str:
|
||||
message = self.prefix + os.urandom(93) + self.null + self.Hm
|
||||
voucher = pow(int.from_bytes(message, "big"), self.key.d, self.key.n)
|
||||
return base64.b64encode(int.to_bytes(voucher, 128, "big")).decode()
|
||||
|
||||
def validate_voucher(self, voucher: str) -> bool:
|
||||
voucher = int.from_bytes(base64.b64decode(voucher.encode()), "big")
|
||||
message = int.to_bytes(pow(voucher, self.key.e, self.key.n), 128, "big")
|
||||
return (
|
||||
voucher.bit_length() <= 1024
|
||||
and message.startswith(self.prefix)
|
||||
and message.endswith(self.null + self.Hm)
|
||||
)
|
||||
|
||||
|
||||
class Challenge:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.balance = 100
|
||||
self.items = {
|
||||
"bread": {"price": 33, "callable": self.__get_bread},
|
||||
"voucher": {"price": 50, "callable": self.__get_voucher},
|
||||
"flag": {"price": 420, "callable": self.__get_flag},
|
||||
}
|
||||
self.redeemed_voucher = set()
|
||||
self.gift_shop = GiftShop(self.name)
|
||||
|
||||
def greet(self):
|
||||
msg = ""
|
||||
msg += f"\nWelcome, {self.name}!"
|
||||
msg += f"\nOption:"
|
||||
msg += f"\n [1] Check available items"
|
||||
msg += f"\n [2] Buy item"
|
||||
msg += f"\n [3] Redeem voucher"
|
||||
msg += f"\n [9] Input admin code"
|
||||
print(msg)
|
||||
|
||||
def __get_bread(self):
|
||||
return self.gift_shop.key.public_key().export_key().decode().replace('-', chr(0x1F35E))
|
||||
|
||||
def __get_voucher(self):
|
||||
return self.gift_shop.generate_voucher()
|
||||
|
||||
def __get_flag(self):
|
||||
return FLAG
|
||||
|
||||
def check_available(self):
|
||||
msg = ""
|
||||
msg += f"\nAvailable items:"
|
||||
for item_name, item_info in self.items.items():
|
||||
item_price = item_info["price"]
|
||||
msg += f"\n [+] {item_name} (${item_price})"
|
||||
return msg
|
||||
|
||||
def buy_item(self, item):
|
||||
if item not in self.items.keys():
|
||||
return f"Item {item} is not available"
|
||||
|
||||
if self.balance < self.items[item]["price"]:
|
||||
return f"Insufficient balance"
|
||||
|
||||
self.balance -= self.items[item]["price"]
|
||||
return self.items[item]["callable"]()
|
||||
|
||||
def redeem_voucher(self, voucher):
|
||||
if voucher in self.redeemed_voucher:
|
||||
return "Your voucher is already used"
|
||||
|
||||
if not self.gift_shop.validate_voucher(voucher):
|
||||
return "Your voucher is invalid"
|
||||
|
||||
value = random.randint(30, 60)
|
||||
self.balance += value
|
||||
self.redeemed_voucher.add(voucher)
|
||||
return f"You got ${value} from the voucher"
|
||||
|
||||
def admin_code(self, code):
|
||||
if code != FLAG:
|
||||
return "Incorrect code"
|
||||
|
||||
self.balance += self.items["flag"]["price"]
|
||||
return "Correct code"
|
||||
|
||||
|
||||
def user_input(s):
|
||||
inp = input(s).strip()
|
||||
assert len(inp) < 1024
|
||||
return inp
|
||||
|
||||
|
||||
def main():
|
||||
name = user_input("Name: ")
|
||||
challenge = Challenge(name)
|
||||
challenge.greet()
|
||||
|
||||
while True:
|
||||
print(f"\nCurrent balance: ${challenge.balance}")
|
||||
match int(user_input("> ")):
|
||||
case 1:
|
||||
print(challenge.check_available())
|
||||
case 2:
|
||||
item = user_input("Item: ")
|
||||
print(challenge.buy_item(item))
|
||||
case 3:
|
||||
voucher = user_input("Voucher: ")
|
||||
print(challenge.redeem_voucher(voucher))
|
||||
case 9:
|
||||
code = user_input("Code: ")
|
||||
print(challenge.admin_code(code))
|
||||
case _:
|
||||
print("Invalid option")
|
||||
break
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
signal.alarm(60)
|
||||
main()
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue