mirror of
https://github.com/devproje/balance-application.git
synced 2024-10-21 00:11:21 +09:00
Compare commits
19 commits
Author | SHA1 | Date | |
---|---|---|---|
693efd988c | |||
b3e511b730 | |||
|
f2fe139d10 | ||
442448e71b | |||
f0e25f0740 | |||
b787891c27 | |||
70bc590b0c | |||
a943ff08fb | |||
a48644b381 | |||
e3e51091d3 | |||
f5413faa6d | |||
3e87cd3d14 | |||
51fcc3ffcb | |||
38113cc6a8 | |||
6ae4b78a98 | |||
4eb5ad04f1 | |||
d279776baa | |||
8e2518e4de | |||
6f8fef42ac |
9 changed files with 136 additions and 127 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -6,6 +6,8 @@ __pycache__/
|
||||||
.env
|
.env
|
||||||
!.env.example
|
!.env.example
|
||||||
|
|
||||||
|
secret_token.txt
|
||||||
|
|
||||||
load.txt
|
load.txt
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
17
Dockerfile
Normal file
17
Dockerfile
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
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"]
|
||||||
|
|
12
README.md
12
README.md
|
@ -1,5 +1,7 @@
|
||||||
# Balance Application
|
# Balance Application
|
||||||
- FastAPI로 제작한 가계부 시스템 입니다. (Backend)
|
- FastAPI로 제작한 가계부 시스템 입니다. (Backend)
|
||||||
|
- 클라이언트
|
||||||
|
- [Balance Client](https://github.com/devproje/balance-client)
|
||||||
|
|
||||||
## Requirement
|
## Requirement
|
||||||
- Python 3.12
|
- Python 3.12
|
||||||
|
@ -39,12 +41,7 @@ DB_USERNAME=user
|
||||||
DB_PASSWORD=sample1234!
|
DB_PASSWORD=sample1234!
|
||||||
```
|
```
|
||||||
|
|
||||||
5. generate.py를 실행하여 테이블 및 계정을 생성 해줍니다.
|
5. fastapi 명령어를 이용하여 서비스를 실행 해줍니다.
|
||||||
```bash
|
|
||||||
python generate.py
|
|
||||||
```
|
|
||||||
|
|
||||||
6. fastapi 명령어를 이용하여 서비스를 실행 해줍니다.
|
|
||||||
```bash
|
```bash
|
||||||
fastapi run app.py
|
fastapi run app.py
|
||||||
```
|
```
|
||||||
|
@ -52,3 +49,6 @@ 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
24
app.py
|
@ -1,9 +1,29 @@
|
||||||
|
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
|
||||||
|
|
||||||
app = FastAPI()
|
@asynccontextmanager
|
||||||
|
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,
|
||||||
|
@ -14,7 +34,7 @@ app.add_middleware(
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
def index(resp: Response):
|
async def index(resp: Response):
|
||||||
resp.headers.setdefault("Content-Type", "text")
|
resp.headers.setdefault("Content-Type", "text")
|
||||||
return "Hello, World!"
|
return "Hello, World!"
|
||||||
|
|
||||||
|
|
120
generate.py
120
generate.py
|
@ -1,81 +1,61 @@
|
||||||
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 __main__():
|
def _new_account():
|
||||||
conn = psycopg2.connect(conn_param)
|
name = input("input your display name: ")
|
||||||
cur = conn.cursor()
|
username = input("input your username: ")
|
||||||
|
password = getpass("input your password: ")
|
||||||
|
passchk = getpass("type password one more time: ")
|
||||||
|
salt = gen_salt()
|
||||||
|
|
||||||
try:
|
if password != passchk:
|
||||||
f = open("./load.txt", "r")
|
return
|
||||||
if f.read().split("=")[1] == "false":
|
|
||||||
raise ValueError("value not true")
|
|
||||||
|
|
||||||
print("server already initialized")
|
hashed_password = hash(password, salt)
|
||||||
except:
|
packed = AuthData(
|
||||||
cur.execute(
|
name=name,
|
||||||
"""
|
username=username,
|
||||||
create table if not exists account(
|
password=hashed_password,
|
||||||
name varchar(25),
|
salt=salt
|
||||||
username varchar(25) not null,
|
)
|
||||||
password varchar(100) not null,
|
|
||||||
salt varchar(50),
|
|
||||||
primary key(username)
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
cur.execute(
|
service = AuthService()
|
||||||
"""
|
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
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
conn.commit()
|
def on_load(conn, cur):
|
||||||
|
cur.execute(
|
||||||
cur.close()
|
"""
|
||||||
conn.close()
|
create table account(
|
||||||
|
name varchar(25),
|
||||||
name = input("input your display name: ")
|
username varchar(25) not null,
|
||||||
username = input("input your username: ")
|
password varchar(100) not null,
|
||||||
password = getpass("input your password: ")
|
salt varchar(50),
|
||||||
passchk = getpass("type password one more time: ")
|
primary key(username)
|
||||||
salt = gen_salt()
|
);
|
||||||
|
"""
|
||||||
if password != passchk:
|
)
|
||||||
return
|
cur.execute(
|
||||||
|
"""
|
||||||
hashed_password = hash(password, salt)
|
create table balset(
|
||||||
packed = AuthData(
|
id serial primary key,
|
||||||
name=name,
|
uid varchar(25) not null,
|
||||||
username=username,
|
name varchar(50),
|
||||||
password=hashed_password,
|
date bigint,
|
||||||
salt=salt
|
price bigint,
|
||||||
)
|
buy boolean,
|
||||||
|
memo varchar(300),
|
||||||
service = AuthService()
|
constraint FK_Account_ID
|
||||||
service.create(data=packed)
|
foreign key (uid)
|
||||||
|
references account(username)
|
||||||
f = open("load.txt", "w")
|
on delete CASCADE
|
||||||
f.write("init=true")
|
);
|
||||||
|
"""
|
||||||
__main__()
|
)
|
||||||
|
conn.commit()
|
||||||
|
_new_account()
|
||||||
|
|
|
@ -1,20 +1,26 @@
|
||||||
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.114.0
|
fastapi==0.115.2
|
||||||
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
|
||||||
|
@ -23,12 +29,15 @@ 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.38.5
|
starlette==0.40.0
|
||||||
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
|
||||||
|
|
|
@ -87,12 +87,10 @@ 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.patch("/balance/{action}/{id}")
|
@router.put("/balance/{id}")
|
||||||
def update(action, id, balance: UpdateForm, req: Request, resp: Response):
|
def update(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 {
|
||||||
|
@ -101,43 +99,7 @@ def update(action, id, balance: UpdateForm, req: Request, resp: Response):
|
||||||
}
|
}
|
||||||
|
|
||||||
service = BalanceService()
|
service = BalanceService()
|
||||||
if action != "name" and action != "date" and action != "price" and action != "buy" and action != "memo":
|
ok = service.update(int(id), balance)
|
||||||
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
|
||||||
|
@ -149,7 +111,6 @@ def update(action, 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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -82,11 +82,18 @@ class BalanceService:
|
||||||
"memo": data[5]
|
"memo": data[5]
|
||||||
}
|
}
|
||||||
|
|
||||||
def update(self, id: int, act: str, balance: UpdateForm):
|
def update(self, id: int, balance: UpdateForm):
|
||||||
ok = True
|
ok = True
|
||||||
cur = self._conn.cursor()
|
cur = self._conn.cursor()
|
||||||
try:
|
try:
|
||||||
cur.execute(f"update balset set {act} = %s where id = %s;", (balance[act], id))
|
cur.execute("update balset set name = %s, date = %s, price = %s, buy = %s, memo = %s where id = %s;", (
|
||||||
|
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()
|
||||||
|
@ -101,7 +108,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;", (id))
|
cur.execute("delete from balset where id = %s;", (str(id)))
|
||||||
self._conn.commit()
|
self._conn.commit()
|
||||||
except:
|
except:
|
||||||
self._conn.rollback()
|
self._conn.rollback()
|
||||||
|
|
|
@ -1,7 +1,18 @@
|
||||||
import os
|
import os
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv(verbose=True, override=True)
|
||||||
|
|
||||||
|
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"),
|
||||||
|
@ -10,3 +21,5 @@ 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()
|
||||||
|
|
Loading…
Reference in a new issue