Compare commits

..

No commits in common. "master" and "0.1.1" have entirely different histories.

9 changed files with 127 additions and 136 deletions

2
.gitignore vendored
View file

@ -6,8 +6,6 @@ __pycache__/
.env .env
!.env.example !.env.example
secret_token.txt
load.txt load.txt
.DS_Store .DS_Store

View file

@ -1,17 +0,0 @@
FROM python:3.11-alpine3.20
WORKDIR /app
RUN apk update \
&& apk add --no-cache postgresql-dev gcc musl-dev make py3-uvloop \
&& pip install -U pip setuptools wheel Cython
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]

View file

@ -1,7 +1,5 @@
# Balance Application # Balance Application
- FastAPI로 제작한 가계부 시스템 입니다. (Backend) - FastAPI로 제작한 가계부 시스템 입니다. (Backend)
- 클라이언트
- [Balance Client](https://github.com/devproje/balance-client)
## Requirement ## Requirement
- Python 3.12 - Python 3.12
@ -41,7 +39,12 @@ DB_USERNAME=user
DB_PASSWORD=sample1234! DB_PASSWORD=sample1234!
``` ```
5. fastapi 명령어를 이용하여 서비스를 실행 해줍니다. 5. generate.py를 실행하여 테이블 및 계정을 생성 해줍니다.
```bash
python generate.py
```
6. fastapi 명령어를 이용하여 서비스를 실행 해줍니다.
```bash ```bash
fastapi run app.py fastapi run app.py
``` ```
@ -49,6 +52,3 @@ fastapi run app.py
```bash ```bash
fastapi run app.py --port 3000 fastapi run app.py --port 3000
``` ```
## License
본 프로젝트는 [MIT License](https://github.com/devproje/balance-application/blob/master/LICENSE)를 따릅니다.

24
app.py
View file

@ -1,29 +1,9 @@
import psycopg2
from generate import on_load
from fastapi import FastAPI, Response from fastapi import FastAPI, Response
from routes.auth import router as auth from routes.auth import router as auth
from contextlib import asynccontextmanager
from util.config import conn_param, db_url
from routes.balance import router as balance from routes.balance import router as balance
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
@asynccontextmanager app = FastAPI()
async def lifespan(app: FastAPI):
conn = psycopg2.connect(conn_param)
cur = conn.cursor()
try:
print("loading database for: %s" % db_url())
on_load(conn, cur)
except:
print("[warn] error occurred while creating table. aborted")
finally:
cur.close()
conn.close()
yield
app = FastAPI(lifespan=lifespan)
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
@ -34,7 +14,7 @@ app.add_middleware(
) )
@app.get("/") @app.get("/")
async def index(resp: Response): def index(resp: Response):
resp.headers.setdefault("Content-Type", "text") resp.headers.setdefault("Content-Type", "text")
return "Hello, World!" return "Hello, World!"

View file

@ -1,61 +1,81 @@
import psycopg2, os
import random, string import random, string
from getpass import getpass from getpass import getpass
from util.auth_lib import hash from util.auth_lib import hash
from util.config import conn_param
from service.auth_service import AuthData, AuthService from service.auth_service import AuthData, AuthService
def gen_salt(length = 20): def gen_salt(length = 20):
letters = string.ascii_lowercase + string.digits + string.punctuation letters = string.ascii_lowercase + string.digits + string.punctuation
return "".join(random.choice(letters) for i in range(length)) return ''.join(random.choice(letters) for i in range(length))
def _new_account(): def __main__():
name = input("input your display name: ") conn = psycopg2.connect(conn_param)
username = input("input your username: ") cur = conn.cursor()
password = getpass("input your password: ")
passchk = getpass("type password one more time: ")
salt = gen_salt()
if password != passchk: try:
return f = open("./load.txt", "r")
if f.read().split("=")[1] == "false":
raise ValueError("value not true")
hashed_password = hash(password, salt) print("server already initialized")
packed = AuthData( except:
name=name, cur.execute(
username=username, """
password=hashed_password, create table if not exists account(
salt=salt name varchar(25),
) username varchar(25) not null,
password varchar(100) not null,
salt varchar(50),
primary key(username)
);
"""
)
service = AuthService() cur.execute(
service.create(data=packed) """
create table if not exists balset(
id serial primary key,
uid varchar(25) not null,
name varchar(50),
date bigint,
price bigint,
buy boolean,
memo varchar(300),
constraint FK_Account_ID
foreign key (uid)
references account(username)
on delete CASCADE
);
"""
)
def on_load(conn, cur): conn.commit()
cur.execute(
""" cur.close()
create table account( conn.close()
name varchar(25),
username varchar(25) not null, name = input("input your display name: ")
password varchar(100) not null, username = input("input your username: ")
salt varchar(50), password = getpass("input your password: ")
primary key(username) passchk = getpass("type password one more time: ")
); salt = gen_salt()
"""
) if password != passchk:
cur.execute( return
"""
create table balset( hashed_password = hash(password, salt)
id serial primary key, packed = AuthData(
uid varchar(25) not null, name=name,
name varchar(50), username=username,
date bigint, password=hashed_password,
price bigint, salt=salt
buy boolean, )
memo varchar(300),
constraint FK_Account_ID service = AuthService()
foreign key (uid) service.create(data=packed)
references account(username)
on delete CASCADE f = open("load.txt", "w")
); f.write("init=true")
"""
) __main__()
conn.commit()
_new_account()

View file

@ -1,26 +1,20 @@
annotated-types==0.7.0 annotated-types==0.7.0
anyio==4.4.0 anyio==4.4.0
asgiref==3.8.1
certifi==2024.8.30 certifi==2024.8.30
click==8.1.7 click==8.1.7
Deprecated==1.2.14
dnspython==2.6.1 dnspython==2.6.1
email_validator==2.2.0 email_validator==2.2.0
fastapi==0.115.2 fastapi==0.114.0
fastapi-cli==0.0.5 fastapi-cli==0.0.5
googleapis-common-protos==1.65.0
grpcio==1.66.2
h11==0.14.0 h11==0.14.0
httpcore==1.0.5 httpcore==1.0.5
httptools==0.6.1 httptools==0.6.1
httpx==0.27.2 httpx==0.27.2
idna==3.8 idna==3.8
importlib_metadata==8.4.0
Jinja2==3.1.4 Jinja2==3.1.4
markdown-it-py==3.0.0 markdown-it-py==3.0.0
MarkupSafe==2.1.5 MarkupSafe==2.1.5
mdurl==0.1.2 mdurl==0.1.2
protobuf==4.25.5
psycopg2-binary==2.9.9 psycopg2-binary==2.9.9
pydantic==2.9.1 pydantic==2.9.1
pydantic_core==2.23.3 pydantic_core==2.23.3
@ -29,15 +23,12 @@ python-dotenv==1.0.1
python-multipart==0.0.9 python-multipart==0.0.9
PyYAML==6.0.2 PyYAML==6.0.2
rich==13.8.0 rich==13.8.0
setuptools==75.1.0
shellingham==1.5.4 shellingham==1.5.4
sniffio==1.3.1 sniffio==1.3.1
starlette==0.40.0 starlette==0.38.5
typer==0.12.5 typer==0.12.5
typing_extensions==4.12.2 typing_extensions==4.12.2
uvicorn==0.30.6 uvicorn==0.30.6
uvloop==0.20.0 uvloop==0.20.0
watchfiles==0.24.0 watchfiles==0.24.0
websockets==13.0.1 websockets==13.0.1
wrapt==1.16.0
zipp==3.20.2

View file

@ -87,10 +87,12 @@ def find(id, req: Request, resp: Response):
"respond_time": "{}ms".format(round((datetime.now().microsecond / 1000) - started)) "respond_time": "{}ms".format(round((datetime.now().microsecond / 1000) - started))
} }
@router.put("/balance/{id}") @router.patch("/balance/{action}/{id}")
def update(id, balance: UpdateForm, req: Request, resp: Response): def update(action, id, balance: UpdateForm, req: Request, resp: Response):
started = datetime.now().microsecond / 1000 started = datetime.now().microsecond / 1000
auth = AuthService() auth = AuthService()
print(auth.check_auth(req))
if not auth.check_auth(req): if not auth.check_auth(req):
resp.status_code = 403 resp.status_code = 403
return { return {
@ -99,7 +101,43 @@ def update(id, balance: UpdateForm, req: Request, resp: Response):
} }
service = BalanceService() service = BalanceService()
ok = service.update(int(id), balance) if action != "name" and action != "date" and action != "price" and action != "buy" and action != "memo":
print(action)
print(id)
resp.status_code = 400
return {"ok": 0, "errno": "action must be to name, date, price or memo"}
if action == "name" and balance.name == "":
resp.status_code = 400
return {"ok": 0, "action": action, "errno": "name value cannot be empty"}
if action == "date" and balance.date <= 0:
resp.status_code = 400
return {"ok": 0, "action": action, "errno": "date value cannot be 0 or minus"}
if action == "price" and balance.price <= 0:
resp.status_code = 400
return {"ok": 0, "action": action, "errno": "price value cannot be 0 or minus"}
if action == "memo" and len(balance.memo) > 300:
resp.status_code = 400
return {
"ok": 0,
"action": action,
"errno": "memo value size is too long: (maximum size: 300 bytes, your size: {} bytes)".format(len(balance.memo))
}
ok = service.update(
int(id),
action,
{
"name": balance.name,
"date": balance.date,
"price": balance.price,
"buy": balance.buy,
"memo": balance.memo
}
)
if not ok == 1: if not ok == 1:
resp.status_code = 500 resp.status_code = 500
@ -111,6 +149,7 @@ def update(id, balance: UpdateForm, req: Request, resp: Response):
return { return {
"ok": 1, "ok": 1,
"id": int(id), "id": int(id),
"action": action,
"respond_time": "{}ms".format(round((datetime.now().microsecond / 1000) - started)) "respond_time": "{}ms".format(round((datetime.now().microsecond / 1000) - started))
} }

View file

@ -82,18 +82,11 @@ class BalanceService:
"memo": data[5] "memo": data[5]
} }
def update(self, id: int, balance: UpdateForm): def update(self, id: int, act: str, balance: UpdateForm):
ok = True ok = True
cur = self._conn.cursor() cur = self._conn.cursor()
try: try:
cur.execute("update balset set name = %s, date = %s, price = %s, buy = %s, memo = %s where id = %s;", ( cur.execute(f"update balset set {act} = %s where id = %s;", (balance[act], id))
balance.name,
balance.date,
balance.price,
balance.buy,
balance.memo,
id
))
self._conn.commit() self._conn.commit()
except: except:
self._conn.rollback() self._conn.rollback()
@ -108,7 +101,7 @@ class BalanceService:
ok = True ok = True
cur = self._conn.cursor() cur = self._conn.cursor()
try: try:
cur.execute("delete from balset where id = %s;", (str(id))) cur.execute("delete from balset where id = %s;", (id))
self._conn.commit() self._conn.commit()
except: except:
self._conn.rollback() self._conn.rollback()

View file

@ -1,18 +1,7 @@
import os import os
from dotenv import load_dotenv from dotenv import load_dotenv
load_dotenv(verbose=True, override=True) load_dotenv()
def _load_secret():
try:
tok = open("./secret_token.txt", "r").read()
except:
return ""
return tok
def db_url():
return os.getenv("DB_URL")
conn_param = "host=%s port=%s dbname=%s user=%s password=%s" % ( conn_param = "host=%s port=%s dbname=%s user=%s password=%s" % (
os.getenv("DB_URL"), os.getenv("DB_URL"),
@ -21,5 +10,3 @@ conn_param = "host=%s port=%s dbname=%s user=%s password=%s" % (
os.getenv("DB_USERNAME"), os.getenv("DB_USERNAME"),
os.getenv("DB_PASSWORD") os.getenv("DB_PASSWORD")
) )
secret = _load_secret()