├── .gitignore ├── .pre-commit-config.yaml ├── Dockerfile ├── LICENSE ├── README.md ├── backend ├── .dockerignore ├── .env ├── Dockerfile ├── api │ ├── __init__.py │ ├── auth.py │ ├── me.py │ └── user.py ├── app.py ├── auth │ ├── __init__.py │ ├── action.py │ └── utils.py ├── crud │ ├── __init__.py │ ├── dependencies.py │ └── user.py ├── database │ ├── __init__.py │ └── config.py ├── models │ ├── __init__.py │ └── user.py ├── production.env ├── pyproject.toml ├── requirements.txt ├── run.py ├── schemas │ ├── __init__.py │ ├── token.py │ └── user.py └── setting │ └── config.py ├── docker-compose.yml ├── docs ├── README_zh.md ├── banner.png └── demo.png ├── frontend ├── .env ├── .env.production ├── .gitignore ├── Dockerfile ├── README.md ├── index.html ├── package.json ├── public │ └── oauth.png ├── src │ ├── App.vue │ ├── api │ │ ├── auth.js │ │ ├── me.js │ │ ├── req.js │ │ └── user.js │ ├── assets │ │ └── vue.svg │ ├── components │ │ ├── Dialog.vue │ │ ├── Loading.vue │ │ └── NavBar.vue │ ├── main.js │ ├── router │ │ └── index.js │ ├── store │ │ ├── auth.js │ │ ├── dialog.js │ │ ├── loading.js │ │ ├── me.js │ │ └── user.js │ ├── style.css │ └── views │ │ ├── HomeView.vue │ │ ├── LoginView.vue │ │ ├── LogoutView.vue │ │ ├── ProfileView.vue │ │ ├── RefreshView.vue │ │ └── RegisterView.vue ├── vite.config.js └── yarn.lock ├── nginx └── nginx.conf └── template.env /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | __pycache__ 3 | poetry.lock 4 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | - repo: https://github.com/astral-sh/ruff-pre-commit 2 | # Ruff version. 3 | rev: v0.1.5 4 | hooks: 5 | # Run the linter. 6 | - id: ruff 7 | args: [ --fix ] 8 | # Run the formatter. 9 | - id: ruff-format -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile for the frontend 2 | # nginx is used to serve the frontend and forward requests to the backend 3 | FROM node:lts-alpine as build-stage 4 | WORKDIR /app 5 | ENV PATH /app/node_modules/.bin:$PATH 6 | COPY ./frontend/ . 7 | RUN yarn install 8 | RUN yarn build 9 | 10 | # copy the build output to nginx 11 | FROM nginx:stable-alpine as production-stage 12 | WORKDIR /app 13 | COPY --from=build-stage /app/dist /usr/share/nginx/html 14 | COPY ./nginx/* /etc/nginx/conf.d/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 LIU ZHE YOU 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FastAPI Vue OAuth2 Boilerplate 2 | 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 | [![DB : postgresql](https://img.shields.io/badge/DB-postgresql-blue.svg)](https://www.postgresql.org/) 5 | [![Backend : FastAPI](https://img.shields.io/badge/Backend-FastAPI-blue.svg)](https://fastapi.tiangolo.com/) 6 | [![Frontend : Vue](https://img.shields.io/badge/Frontend-Vue-green.svg)](https://v3.vuejs.org/) 7 | 8 | banner 9 | 10 | [中文說明](https://github.com/jason810496/FastAPI-Vue-OAuth2/blob/main/docs/README_zh.md) 11 | 12 | This boilerplate is a starting point for building a FastAPI backend using PostgreSQL with a Vue3 frontend.
13 | It includes OAuth2 authentication with JWT tokens, and a simple user CRUD. 14 | 15 | > **Note :** For `Vue: Option API` + `VueX` version, please check out [archived-2023-11-22](https://github.com/jason810496/FastAPI-Vue-OAuth2/tree/archived-2023-11-22) branch 16 | 17 | ## Demo 18 | `localhost` for frontend
19 | `localhost:5001/docs` for backend swagger docs 20 | 21 | 22 | demo 23 | 24 | 25 | Click image to watch demo video on YouTube ☝️ 26 | 27 | 28 | ## Features 29 | - FastAPI backend with PostgreSQL database 30 | - SQLAlchemy CRUD with async support 31 | - Simple User CRUD 32 | - OAuth2 authentication with JWT tokens 33 | - Store refresh token in `httpOnly` cookie, access token in memory ( Pinia store ) 34 | - Vue3 frontend with Pinia store 35 | - Docker Compose for development and production 36 | 37 | ## Project Structure & Details 38 | ### Backend 39 | - `app.py` FastAPI application files 40 | - `/api` API endpoints 41 | - `/auth` 42 | - OAuth2 authentication 43 | - `get_current_user` dependency 44 | - `/crud` 45 | - user related CRUD utilities 46 | - database session dependency 47 | - `/database` Database configuration files 48 | - `/models` SQLAlchemy models using `declarative_base` 49 | - `/schemas` Pydantic schemas 50 | 51 | ### Database 52 | - `PostgreSQL 15.1` image from Docker Hub 53 | - exposed on port `5432` 54 | - volume `postgres_data` for persistent data 55 | 56 | ### Frontend 57 | - `Vite` Frontend build tool 58 | - `/views` Frontend page views 59 | - use `RefreshView.vue` as middleware to refresh JWT tokens 60 | - `/store` Pinia store ( using `Data Provider Patten` ) 61 | - `/router` Vue router 62 | - `/api` API endpoints 63 | - `req.js` 64 | - `axios` request wrapper , handle `401` unauthorized error to refresh JWT tokens 65 | - use `import.meta.env.VITE_APP_API_URL` to load API url from `.env` file 66 | 67 | ## Environment Variables 68 | - `.env` for postgres database 69 | - `POSTGRES_USER` 70 | - `POSTGRES_PASSWORD` 71 | - `POSTGRES_DB` 72 | - `backend/.env` for backend 73 | - `PORT` 74 | - `RELOAD` 75 | - `DATABASE_URL` **Should be same as above setting dot file** 76 | - `JWT_ALGORITHM` 77 | - `ACCESS_TOKEN_SECRET` 78 | - `REFRESH_TOKEN_SECRET` 79 | - `ACCESS_TOKEN_EXPIRE_MINUTES` 80 | - `REFRESH_TOKEN_EXPIRE_MINUTES` 81 | 82 | - `nginx/nginx.conf` for nginx server 83 | - **Note :** backend hostname should be same as `docker-compose.yml` service name 84 | - `frontend/.env` for development API url 85 | - `frontend/.env.production` for production API url 86 | 87 | 88 | ## Deployment 89 | 90 | ### Containerization 91 | - `docker-compose.yml` Docker Compose configuration file 92 | - `Dockerfile` Dockerfile for frontend nginx server with production build 93 | - `backend/Dockerfile` Dockerfile for backend with hot reload 94 | 95 | ### Production 96 | - `docker-compose up -d --build` 97 | 98 | ## Development 99 | - Database 100 | ``` 101 | docker run --name fastapi_vue_oauth2_postgresql -e POSTGRES_USER=fastapi_vue_user -e POSTGRES_PASSWORD=fastapi_vue_password -e POSTGRES_DB=fastapi_vue_dev -p 5432:5432 -d -v postgres_data_dev:/var/lib/postgresql/data postgres:15.1 102 | ``` 103 | - Backend 104 |
105 | **Note** : shuold change in change `DATABASE_URL` to `DEV_DATABASE_URL` in `backend/.env`
106 | - Poetry 107 | ``` 108 | cd backend 109 | 110 | poetry install 111 | poetry shell 112 | 113 | python3 run.py 114 | ``` 115 | - Create virtual environment 116 | ``` 117 | cd backend 118 | 119 | python3 -m venv venv 120 | source venv/bin/activate 121 | pip3 install -r requirements.txt 122 | 123 | python3 run.py 124 | ``` 125 | - Frontend 126 | ``` 127 | cd frontend 128 | 129 | yarn dev 130 | ``` 131 | 132 | ### Advanced : Kubernetes 133 | 134 | ``` 135 | Still working on it on features/k8s branch ! 136 | ``` 137 | 138 | ## Issues & PR 139 | Feel free to open an issue ! 140 | 141 | Pull requests are welcome.
142 | Any contributions you make are **greatly appreciated**. 143 | 144 | -------------------------------------------------------------------------------- /backend/.dockerignore: -------------------------------------------------------------------------------- 1 | *cache* 2 | venv 3 | Dockerfile 4 | .dockerignore 5 | poetry.lock 6 | pyproject.toml 7 | *.env -------------------------------------------------------------------------------- /backend/.env: -------------------------------------------------------------------------------- 1 | # the setting should be same as the one in `.env` file in the root directory 2 | PORT=5001 3 | RELOAD=True 4 | # database url for sqlalchemy 5 | # the `db` hostname should be the same as the service name in docker-compose.yml 6 | # DATABASE_URL=postgresql+asyncpg://fastapi_vue_user:fastapi_vue_password@db:5432/fastapi_vue_prod 7 | 8 | # for local development 9 | DATABASE_URL=postgresql+asyncpg://fastapi_vue_user:fastapi_vue_password@localhost:5432/fastapi_vue_dev 10 | 11 | # use `openssl rand -hex 32` to generate a random secret 12 | ACCESS_TOKEN_SECRET=YOUR_ACCESS_TOKEN_SECRET 13 | REFRESH_TOKEN_SECRET=YOUR_REFRESH_TOKEN_SECRET 14 | # ACCESS_TOKEN_EXPIRE_MINUTES=60 * 3 # three hours 15 | # REFRESH_TOKEN_EXPIRE_MINUTES=60 * 24 * 3 # three days 16 | # ACCESS_TOKEN_EXPIRE_MINUTES=180 17 | # REFRESH_TOKEN_EXPIRE_MINUTES=4320 18 | 19 | ACCESS_TOKEN_EXPIRE_MINUTES=1 20 | REFRESH_TOKEN_EXPIRE_MINUTES=1 21 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | # pull official base image 2 | FROM python:3.11.1-slim 3 | 4 | # set work directory 5 | WORKDIR /usr/backend 6 | 7 | # set environment variables 8 | ENV PYTHONDONTWRITEBYTECODE 1 9 | ENV PYTHONUNBUFFERED 1 10 | 11 | # copy requirements file 12 | COPY ./requirements.txt /usr/backend/requirements.txt 13 | 14 | # install dependencies 15 | # RUN set -eux \ 16 | # && apk add --no-cache --virtual .build-deps build-base \ 17 | # libressl-dev libffi-dev gcc musl-dev python3-dev \ 18 | # postgresql-dev \ 19 | # && pip install --upgrade pip setuptools wheel \ 20 | # && pip install -r /usr/server/requirements.txt \ 21 | # && rm -rf /root/.cache/pip 22 | RUN pip install -r requirements.txt 23 | 24 | # copy project 25 | COPY . /usr/backend/ -------------------------------------------------------------------------------- /backend/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jason810496/FastAPI-Vue-OAuth2/ecd50b03e0bef21c1fd34f7a6b0c5e1ff279fd82/backend/api/__init__.py -------------------------------------------------------------------------------- /backend/api/auth.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta, timezone 2 | 3 | from jose import jwt 4 | from fastapi import APIRouter, Depends, HTTPException, status, Cookie, Response, Request 5 | from fastapi.security import OAuth2PasswordRequestForm 6 | 7 | from auth.action import validate_user 8 | from auth.utils import create_access_token, create_refresh_token 9 | from crud.dependencies import get_user_crud 10 | from crud.user import UserCRUD 11 | from schemas.token import Token 12 | from setting.config import get_settings 13 | 14 | settings = get_settings() 15 | router = APIRouter(prefix="/auth", tags=["auth"]) 16 | 17 | 18 | @router.post("/login", response_model=Token) 19 | async def login( 20 | response: Response, 21 | form_data: OAuth2PasswordRequestForm = Depends(), 22 | db: UserCRUD = Depends(get_user_crud), 23 | ): 24 | user = await validate_user(form_data.username, form_data.password) 25 | if not user: 26 | raise HTTPException( 27 | status_code=status.HTTP_401_UNAUTHORIZED, 28 | detail="Incorrect username or password", 29 | headers={"WWW-Authenticate": "Bearer"}, 30 | ) 31 | 32 | access_token = await create_access_token(data={"username": form_data.username}) 33 | refresh_token = await create_refresh_token(data={"username": form_data.username}) 34 | 35 | await db.update_user_login(username=form_data.username) 36 | expired_time = ( 37 | int(datetime.now(tz=timezone.utc).timestamp() * 1000) 38 | + timedelta(minutes=settings.access_token_expire_minutes).seconds * 1000 39 | ) 40 | 41 | response.set_cookie( 42 | "refresh_token", 43 | refresh_token, 44 | httponly=True, 45 | samesite="strict", 46 | secure=False, 47 | expires=timedelta(settings.refresh_token_expire_minutes), 48 | ) 49 | 50 | return Token( 51 | access_token=access_token, 52 | expires_in=expired_time, 53 | token_type="Bearer", 54 | ) 55 | 56 | 57 | @router.post("/refresh", response_model=Token) 58 | async def refresh( 59 | request: Request, 60 | response: Response, 61 | db: UserCRUD = Depends(get_user_crud), 62 | ): 63 | credentials_exception = HTTPException( 64 | status_code=401, 65 | detail="Could not validate credentials", 66 | headers={"WWW-Authenticate": "Bearer"}, 67 | ) 68 | try: 69 | refresh_token = request.cookies.get("refresh_token") 70 | if not refresh_token: 71 | raise credentials_exception 72 | 73 | payload = jwt.decode( 74 | refresh_token, 75 | settings.refresh_token_secret, 76 | algorithms=["HS256"], 77 | ) 78 | 79 | username: str = payload.get("username") 80 | if username is None: 81 | raise credentials_exception 82 | 83 | except Exception as e: 84 | credentials_exception.detail = str(e) 85 | raise credentials_exception 86 | 87 | access_token = await create_access_token(data={"username": username}) 88 | refresh_token = await create_refresh_token(data={"username": username}) 89 | 90 | db.update_user_login(username) 91 | expired_time = ( 92 | int(datetime.now(tz=timezone.utc).timestamp() * 1000) 93 | + timedelta(minutes=settings.access_token_expire_minutes).seconds * 1000 94 | ) 95 | 96 | response.set_cookie( 97 | "refresh_token", 98 | refresh_token, 99 | httponly=True, 100 | samesite="strict", 101 | secure=False, 102 | expires=timedelta(settings.refresh_token_expire_minutes), 103 | ) 104 | 105 | return Token( 106 | access_token=access_token, 107 | expires_in=expired_time, 108 | token_type="Bearer", 109 | ) 110 | 111 | 112 | @router.post("/logout") 113 | async def logout(response: Response): 114 | response.delete_cookie("refresh_token") 115 | return {"message": "Logout successfully"} 116 | -------------------------------------------------------------------------------- /backend/api/me.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends 2 | 3 | from auth.action import get_current_user 4 | from crud.user import UserCRUD 5 | from crud.dependencies import get_user_crud 6 | import schemas.user as user_schema 7 | 8 | router = APIRouter(prefix="/me", tags=["me"]) 9 | 10 | 11 | @router.delete("") 12 | async def delete_user( 13 | current_user: user_schema.Base = Depends(get_current_user), 14 | db: UserCRUD = Depends(get_user_crud), 15 | ): 16 | return await db.delete_user(username=current_user.username) 17 | 18 | 19 | @router.put("/password") 20 | async def update_password( 21 | request: user_schema.Password, 22 | current_user: user_schema.Base = Depends(get_current_user), 23 | db: UserCRUD = Depends(get_user_crud), 24 | ): 25 | return await db.update_password( 26 | username=current_user.username, password=request.password 27 | ) 28 | 29 | 30 | @router.put("/birthday") 31 | async def update_birthday( 32 | request: user_schema.Birthday, 33 | current_user: user_schema.Base = Depends(get_current_user), 34 | db: UserCRUD = Depends(get_user_crud), 35 | ): 36 | return await db.update_birthday( 37 | username=current_user.username, birthday=request.birthday 38 | ) 39 | 40 | 41 | @router.get("", response_model=user_schema.Base) 42 | async def protected(current_user: user_schema.Base = Depends(get_current_user)): 43 | return current_user 44 | -------------------------------------------------------------------------------- /backend/api/user.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends, HTTPException, status 2 | from typing import List 3 | 4 | from auth.action import get_current_user 5 | from crud.user import UserCRUD 6 | from crud.dependencies import get_user_crud 7 | import schemas.user as user_schema 8 | 9 | router = APIRouter(prefix="/users", tags=["users"]) 10 | 11 | 12 | @router.get("", response_model=List[user_schema.Base]) 13 | async def get_users(db: UserCRUD = Depends(get_user_crud)): 14 | return await db.get_users() 15 | 16 | 17 | @router.post("") 18 | async def register( 19 | new_user: user_schema.Register, db: UserCRUD = Depends(get_user_crud) 20 | ): 21 | db_user = await db.get_user_by_username(username=new_user.username) 22 | if db_user: 23 | raise HTTPException(status_code=409, detail="Username already registered") 24 | await db.create_user(new_user) 25 | return status.HTTP_201_CREATED 26 | 27 | 28 | @router.delete("", deprecated=True) 29 | async def delete_user( 30 | current_user: user_schema.Base = Depends(get_current_user), 31 | db: UserCRUD = Depends(get_user_crud), 32 | ): 33 | # return await db.delete_user(username=current_user.username) 34 | return "deprecated" 35 | 36 | 37 | @router.put("/password", deprecated=True) 38 | async def update_password( 39 | request: user_schema.Password, 40 | current_user: user_schema.Base = Depends(get_current_user), 41 | db: UserCRUD = Depends(get_user_crud), 42 | ): 43 | # return await db.update_password( username=current_user.username , password=request.password ) 44 | return "deprecated" 45 | 46 | 47 | @router.put("/birthday", deprecated=True) 48 | async def update_birthday( 49 | request: user_schema.Birthday, 50 | current_user: user_schema.Base = Depends(get_current_user), 51 | db: UserCRUD = Depends(get_user_crud), 52 | ): 53 | # return await db.update_birthday( username=current_user.username ,birthday=request.birthday ) 54 | return "deprecated" 55 | 56 | 57 | @router.get("/me", response_model=user_schema.Base, deprecated=True) 58 | async def protected(current_user: user_schema.Base = Depends(get_current_user)): 59 | # return current_user 60 | return "deprecated" 61 | -------------------------------------------------------------------------------- /backend/app.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from fastapi.middleware.cors import CORSMiddleware 3 | 4 | from api import user, auth, me 5 | from database.config import engine, database, Base 6 | 7 | 8 | app = FastAPI() 9 | app.include_router(auth.router, prefix="/api") 10 | app.include_router(user.router, prefix="/api") 11 | app.include_router(me.router, prefix="/api") 12 | 13 | origins = [ 14 | "http://localhost:5173", 15 | ] 16 | 17 | methods = [ 18 | "DELETE", 19 | "GET", 20 | "POST", 21 | "PUT", 22 | ] 23 | 24 | app.add_middleware( 25 | CORSMiddleware, 26 | allow_origins=origins, 27 | allow_credentials=True, 28 | allow_methods=methods, 29 | allow_headers=["*"], 30 | ) 31 | 32 | 33 | @app.on_event("startup") 34 | async def startup(): 35 | await database.connect() 36 | async with engine.begin() as conn: 37 | await conn.run_sync(Base.metadata.create_all) 38 | 39 | 40 | @app.on_event("shutdown") 41 | async def shutdown(): 42 | if database.is_connected: 43 | await database.disconnect() 44 | -------------------------------------------------------------------------------- /backend/auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jason810496/FastAPI-Vue-OAuth2/ecd50b03e0bef21c1fd34f7a6b0c5e1ff279fd82/backend/auth/__init__.py -------------------------------------------------------------------------------- /backend/auth/action.py: -------------------------------------------------------------------------------- 1 | from fastapi import Depends, HTTPException 2 | from jose import jwt 3 | from typing import Annotated 4 | 5 | from database.config import async_session 6 | from crud.user import UserCRUD 7 | from .utils import verify_password, oauth2_scheme 8 | from setting.config import get_settings 9 | 10 | settings = get_settings() 11 | 12 | 13 | async def validate_user(username: str, password: str): 14 | async with async_session() as session: 15 | async with session.begin(): 16 | db = UserCRUD(session) 17 | user = await db.get_user_by_username(username=username) 18 | if not user: 19 | return False 20 | 21 | if not verify_password(password, user.password): 22 | return False 23 | return user 24 | 25 | 26 | async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): 27 | credentials_exception = HTTPException( 28 | status_code=401, 29 | detail="Could not validate credentials", 30 | headers={"WWW-Authenticate": "Bearer"}, 31 | ) 32 | try: 33 | payload = jwt.decode( 34 | token, 35 | settings.access_token_secret, 36 | algorithms=["HS256"], 37 | ) 38 | username: str = payload.get("username") 39 | if username is None: 40 | raise credentials_exception 41 | 42 | except Exception as e: 43 | credentials_exception.detail = str(e) 44 | raise credentials_exception 45 | 46 | async with async_session() as session: 47 | async with session.begin(): 48 | db = UserCRUD(session) 49 | user = await db.get_user_by_username(username=username) 50 | if user is None: 51 | raise credentials_exception 52 | return user 53 | -------------------------------------------------------------------------------- /backend/auth/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | 3 | from fastapi.security import OAuth2PasswordBearer 4 | from jose import jwt 5 | from passlib.context import CryptContext 6 | from setting.config import get_settings 7 | 8 | settings = get_settings() 9 | 10 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 11 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login") 12 | 13 | 14 | def verify_password(plain_password, hashed_password): 15 | return pwd_context.verify(plain_password, hashed_password) 16 | 17 | 18 | def get_password_hash(password): 19 | return pwd_context.hash(password) 20 | 21 | 22 | async def create_access_token(data: dict): 23 | to_encode = data.copy() 24 | expire = datetime.utcnow() + timedelta(minutes=settings.access_token_expire_minutes) 25 | 26 | to_encode.update({"exp": expire}) 27 | encoded_jwt = jwt.encode( 28 | to_encode, 29 | settings.access_token_secret, 30 | algorithm="HS256", 31 | ) 32 | return encoded_jwt 33 | 34 | 35 | async def create_refresh_token(data: dict): 36 | to_encode = data.copy() 37 | expire = datetime.utcnow() + timedelta(minutes=settings.access_token_expire_minutes) 38 | 39 | to_encode.update({"exp": expire}) 40 | encoded_jwt = jwt.encode( 41 | to_encode, 42 | settings.refresh_token_secret, 43 | algorithm="HS256", 44 | ) 45 | return encoded_jwt 46 | -------------------------------------------------------------------------------- /backend/crud/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jason810496/FastAPI-Vue-OAuth2/ecd50b03e0bef21c1fd34f7a6b0c5e1ff279fd82/backend/crud/__init__.py -------------------------------------------------------------------------------- /backend/crud/dependencies.py: -------------------------------------------------------------------------------- 1 | from typing import Generator 2 | 3 | from database.config import async_session 4 | from crud.user import UserCRUD 5 | 6 | 7 | async def get_db() -> Generator: 8 | async with async_session() as session: 9 | async with session.begin(): 10 | yield session 11 | 12 | 13 | async def get_user_crud() -> Generator: 14 | async with async_session() as session: 15 | async with session.begin(): 16 | yield UserCRUD(session) 17 | -------------------------------------------------------------------------------- /backend/crud/user.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from sqlalchemy import select, update, delete 4 | from sqlalchemy.ext.asyncio import AsyncSession 5 | from typing import List 6 | 7 | from auth.utils import get_password_hash 8 | from models.user import UserModels 9 | import schemas.user as user_schema 10 | 11 | 12 | class UserCRUD: 13 | db_session = None 14 | 15 | def __init__(self, db_session: AsyncSession = None): 16 | self.db_session = db_session 17 | 18 | async def get_user_by_username(self, username: str): 19 | stmt = select(UserModels).where(UserModels.username == username) 20 | result = await self.db_session.execute(stmt) 21 | user = result.scalars().first() 22 | return user 23 | 24 | async def get_users(self) -> List[user_schema.Base]: 25 | stmt = select(UserModels) 26 | result = await self.db_session.execute(stmt) 27 | users = result.scalars().all() 28 | return users 29 | 30 | async def create_user(self, user: user_schema.Register) -> user_schema.Base: 31 | db_user = UserModels( 32 | username=user.username, 33 | password=get_password_hash(user.password), 34 | birthday=user.birthday, 35 | ) 36 | self.db_session.add(db_user) 37 | await self.db_session.commit() 38 | return db_user 39 | 40 | async def update_user_login(self, username: str): 41 | db_user = await self.get_user_by_username(username) 42 | db_user.last_login = datetime.utcnow() 43 | await self.db_session.refresh(db_user) 44 | return db_user 45 | 46 | async def update_birthday(self, username: str, birthday: datetime): 47 | stmt = ( 48 | update(UserModels) 49 | .where(UserModels.username == username) 50 | .values(birthday=birthday) 51 | ) 52 | stmt.execution_options(synchronize_session="fetch") 53 | await self.db_session.execute(stmt) 54 | 55 | async def update_password(self, username: str, password: str): 56 | stmt = ( 57 | update(UserModels) 58 | .where(UserModels.username == username) 59 | .values(password=get_password_hash(password)) 60 | ) 61 | stmt.execution_options(synchronize_session="fetch") 62 | await self.db_session.execute(stmt) 63 | 64 | async def delete_user(self, username: str): 65 | stmt = delete(UserModels).where(UserModels.username == username) 66 | stmt.execution_options(synchronize_session="fetch") 67 | await self.db_session.execute(stmt) 68 | -------------------------------------------------------------------------------- /backend/database/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jason810496/FastAPI-Vue-OAuth2/ecd50b03e0bef21c1fd34f7a6b0c5e1ff279fd82/backend/database/__init__.py -------------------------------------------------------------------------------- /backend/database/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | from databases import Database 5 | from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession 6 | from sqlalchemy.ext.declarative import declarative_base 7 | from sqlalchemy.orm import sessionmaker 8 | 9 | from setting.config import get_settings 10 | 11 | settings = get_settings() 12 | 13 | # Create engine 14 | engine = create_async_engine(settings.database_url, echo=True) 15 | 16 | # Create session 17 | async_session = sessionmaker( 18 | engine, expire_on_commit=False, autocommit=False, class_=AsyncSession 19 | ) 20 | 21 | Base = declarative_base() 22 | 23 | database = Database(settings.database_url) 24 | -------------------------------------------------------------------------------- /backend/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jason810496/FastAPI-Vue-OAuth2/ecd50b03e0bef21c1fd34f7a6b0c5e1ff279fd82/backend/models/__init__.py -------------------------------------------------------------------------------- /backend/models/user.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from sqlalchemy import Column, VARCHAR, DATE, DateTime 4 | 5 | from database.config import Base 6 | 7 | 8 | # Create User class 9 | class UserModels(Base): 10 | __tablename__ = "users" 11 | username = Column(VARCHAR, unique=True, primary_key=True) 12 | password = Column(VARCHAR) 13 | birthday = Column(DATE) 14 | create_time = Column(DateTime, default=datetime.utcnow()) 15 | last_login = Column(DateTime, default=datetime.utcnow()) 16 | 17 | def __init__(self, username: str, password: str, birthday: datetime): 18 | self.username = username 19 | self.password = password 20 | self.birthday = birthday 21 | 22 | def __repr__(self) -> str: 23 | return f"" 24 | -------------------------------------------------------------------------------- /backend/production.env: -------------------------------------------------------------------------------- 1 | # the setting should be same as the one in `.env` file in the root directory 2 | PORT=5001 3 | RELOAD=False 4 | # database url for sqlalchemy 5 | # the `db` hostname should be the same as the service name in docker-compose.yml 6 | DATABASE_URL=postgresql+asyncpg://fastapi_vue_user:fastapi_vue_password@db:5432/fastapi_vue_prod 7 | 8 | # for local development 9 | # DATABASE_URL=postgresql+asyncpg://fastapi_vue_user:fastapi_vue_password@localhost:5432/fastapi_vue_dev 10 | 11 | 12 | # jwt settings 13 | JWT_ALGORITHM=HS256 14 | # use `openssl rand -hex 32` to generate a random secret 15 | ACCESS_TOKEN_SECRET=YOUR_ACCESS_TOKEN_SECRET 16 | REFRESH_TOKEN_SECRET=YOUR_REFRESH_TOKEN_SECRET 17 | # ACCESS_TOKEN_EXPIRE_MINUTES=60 * 3 # three hours 18 | # REFRESH_TOKEN_EXPIRE_MINUTES=60 * 24 * 3 # three days 19 | ACCESS_TOKEN_EXPIRE_MINUTES=180 20 | REFRESH_TOKEN_EXPIRE_MINUTES=4320 21 | -------------------------------------------------------------------------------- /backend/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "fastapi-vue-oauth2-backend" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["jason810496 <810496@email.wlsh.tyc.edu.tw>"] 6 | license = "MIT" 7 | readme = "README.md" 8 | packages = [{include = "fastapi_vue_oauth2_backend"}] 9 | 10 | [tool.poetry.dependencies] 11 | python = "^3.9" 12 | python-dotenv = "1.0.0" 13 | uvicorn = "0.23.1" 14 | fastapi = "0.100.1" 15 | sqlalchemy = "1.4.49" 16 | databases = "0.7.0" 17 | asyncpg = "0.28.0" 18 | greenlet = "2.0.2" 19 | python-multipart = "0.0.6" 20 | bcrypt = "4.0.1" 21 | passlib = "1.7.4" 22 | python-jose = "3.3.0" 23 | ruff = "^0.1.5" 24 | 25 | 26 | [build-system] 27 | requires = ["poetry-core"] 28 | build-backend = "poetry.core.masonry.api" 29 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | python-dotenv==1.0.0 2 | uvicorn==0.23.1 3 | fastapi==0.100.1 4 | SQLAlchemy==1.4.49 5 | databases==0.7.0 6 | asyncpg==0.28.0 7 | greenlet==2.0.2 8 | python-multipart==0.0.6 9 | bcrypt==4.0.1 10 | passlib==1.7.4 11 | python-jose==3.3.0 12 | 13 | # alembic==1.11.3 14 | # annotated-types==0.5.0 15 | # anyio==3.7.1 16 | # asyncio==3.4.3 17 | # asyncpg==0.28.0 18 | # bcrypt==4.0.1 19 | # click==8.1.6 20 | # databases==0.7.0 21 | # ecdsa==0.18.0 22 | # exceptiongroup==1.1.2 23 | # fastapi==0.100.1 24 | # greenlet==2.0.2 25 | # h11==0.14.0 26 | # idna==3.4 27 | # Mako==1.2.4 28 | # MarkupSafe==2.1.3 29 | # passlib==1.7.4 30 | # psycopg2-binary==2.9.6 31 | # pyasn1==0.5.0 32 | # pydantic==2.1.1 33 | # pydantic_core==2.4.0 34 | # python-dotenv==1.0.0 35 | # python-jose==3.3.0 36 | # python-multipart==0.0.6 37 | # rsa==4.9 38 | # six==1.16.0 39 | # sniffio==1.3.0 40 | # SQLAlchemy==1.4.49 41 | # starlette==0.27.0 42 | # typing_extensions==4.7.1 43 | # uvicorn==0.23.1 44 | -------------------------------------------------------------------------------- /backend/run.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | 4 | from dotenv import load_dotenv 5 | import uvicorn 6 | 7 | if __name__ == "__main__": 8 | parser = argparse.ArgumentParser(description="Run the server in different modes.") 9 | 10 | app_mode = parser.add_argument_group( 11 | title="App Mode", description="Run the server in different modes." 12 | ) 13 | app_mode.add_argument( 14 | "--prod", action="store_true", help="Run the server in production mode." 15 | ) 16 | app_mode.add_argument( 17 | "--dev", action="store_true", help="Run the server in development mode." 18 | ) 19 | 20 | args = parser.parse_args() 21 | print("args", args.__dict__) 22 | 23 | if args.prod: 24 | # for production mode 25 | # the secret key is set in the environment variable 26 | pass 27 | else: 28 | # check out setting/config.py for `ENV_PREFIX` use case 29 | load_dotenv(".env") 30 | 31 | uvicorn.run( 32 | "app:app", 33 | host="0.0.0.0", 34 | port=int(os.getenv("PORT")), 35 | reload=bool(os.getenv("RELOAD")), 36 | ) 37 | -------------------------------------------------------------------------------- /backend/schemas/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jason810496/FastAPI-Vue-OAuth2/ecd50b03e0bef21c1fd34f7a6b0c5e1ff279fd82/backend/schemas/__init__.py -------------------------------------------------------------------------------- /backend/schemas/token.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | # `refresh_token` should be stored in HttpOnly cookie 4 | 5 | 6 | class Token(BaseModel): 7 | access_token: str 8 | expires_in: int 9 | token_type: str 10 | # refresh_token: str 11 | 12 | 13 | # class RefreshToken(BaseModel): 14 | # refresh_token: str 15 | -------------------------------------------------------------------------------- /backend/schemas/user.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from pydantic import BaseModel 4 | 5 | # User Schema 6 | 7 | 8 | class Base(BaseModel): 9 | username: str 10 | birthday: date 11 | 12 | 13 | class Register(Base): 14 | password: str 15 | 16 | 17 | class Password(BaseModel): 18 | password: str 19 | 20 | 21 | class Birthday(BaseModel): 22 | birthday: date 23 | -------------------------------------------------------------------------------- /backend/setting/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from functools import lru_cache 3 | 4 | 5 | class Settings: 6 | app_name: str = "FastAPI Vue3 OAuth2" 7 | author: str = "Jason Liu" 8 | 9 | database_url: str = os.getenv("DATABASE_URL") 10 | 11 | access_token_secret: str = os.getenv("ACCESS_TOKEN_SECRET") 12 | access_token_expire_minutes: int = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES")) 13 | 14 | refresh_token_secret: str = os.getenv("REFRESH_TOKEN_SECRET") 15 | refresh_token_expire_minutes: int = int(os.getenv("REFRESH_TOKEN_EXPIRE_MINUTES")) 16 | 17 | 18 | @lru_cache() 19 | def get_settings(): 20 | return Settings() 21 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '1.0' 2 | 3 | services: 4 | db: 5 | image: postgres:15.1 6 | expose: 7 | - 5432 8 | env_file: 9 | - .env 10 | restart: always 11 | volumes: 12 | - postgres_data:/var/lib/postgresql/data/ 13 | 14 | backend: 15 | build: ./backend 16 | # image: jasonbigcow/oauth2-fastapi 17 | ports: 18 | - 5001:5001 19 | volumes: 20 | - ./backend/:/usr/backend/ 21 | env_file: 22 | - ./backend/production.env 23 | command: python3 run.py --prod 24 | restart: always 25 | depends_on: 26 | - db 27 | 28 | nginx: 29 | build: ./ 30 | # image: jasonbigcow/oauth2-vue3-nginx 31 | ports: 32 | - 80:8080 33 | restart: always 34 | depends_on: 35 | - backend 36 | volumes: 37 | - ./nginx:/etc/nginx/conf.d 38 | 39 | volumes: 40 | postgres_data: 41 | 42 | networks: 43 | default: 44 | name: fastapi_vue_network -------------------------------------------------------------------------------- /docs/README_zh.md: -------------------------------------------------------------------------------- 1 | # FastAPI Vue OAuth2 Boilerplate 2 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 3 | [![DB : postgresql](https://img.shields.io/badge/DB-postgresql-blue.svg)](https://www.postgresql.org/) 4 | [![Backend : FastAPI](https://img.shields.io/badge/Backend-FastAPI-blue.svg)](https://fastapi.tiangolo.com/) 5 | [![Frontend : Vue](https://img.shields.io/badge/Frontend-Vue-green.svg)](https://v3.vuejs.org/) 6 | 7 | [English description](https://github.com/jason810496/FastAPI-Vue-OAuth2) 8 | 9 | 這個模板使用 FastAPI 與 PostgreSQL 的後端以及 Vue3 作為前端。
10 | 專案包含 OAuth2 認證與 JWT tokens,以及簡單的使用者 CRUD。 11 | 12 | ## Demo 13 | `localhost` 瀏覽前端
14 | `localhost:5001/docs` 瀏覽後端 swagger 15 | 16 | 17 | demo 18 | 19 | 20 | 點擊圖片觀看 YouTube Demo ☝️ 21 | 22 | 23 | ## 功能 24 | - FastAPI 後端與 PostgreSQL 資料庫 25 | - SQLAlchemy CRUD 支援 async 26 | - 簡單的使用者 CRUD 27 | - OAuth2 認證與 JWT tokens 28 | - Vue3 前端與 Vuex store 29 | - Docker Compose 開發與正式環境 30 | 31 | 32 | ## 專案結構與細節 33 | ### 後端 34 | - `app.py` FastAPI 進入點 35 | - `/api` API endpoints 36 | - `/auth` 37 | - OAuth2 認證 38 | - `get_current_user` 依賴 39 | - `/crud` 40 | - 使用者相關 CRUD 41 | - 資料庫 session 依賴 42 | - `/database` 資料庫設定檔案 43 | - `/models` SQLAlchemy models ,使用 `declarative_base` 44 | - `/schemas` Pydantic schemas 45 | 46 | ### 資料庫 47 | - `PostgreSQL 15.1` Docker Hub 的 image 48 | - 開放在 `5432` port 49 | - 設定 `postgres_data` volume 做資料持久化 50 | 51 | ### 前端 52 | - `Vite` 前端建置工具 53 | - `/views` 前端頁面 54 | - 使用 `RefreshView.vue` 作為 middleware 來刷新 JWT tokens 55 | - `/store` Vuex store 56 | - `/modules` Vuex modules ,包含 `auth.js` 與 `user.js` 57 | - `/router` Vue router 58 | - `/api` API endpoints 59 | - `req.js` 60 | - `axios` 請求攔截器 ,處理 `401` 未授權錯誤來刷新 JWT tokens 61 | - 使用 `import.meta.env.VITE_APP_API_URL` 從 `.env` 檔案載入 API url 62 | 63 | ## 環境變數 64 | - `.env` 資料庫設定 65 | - `POSTGRES_USER` 66 | - `POSTGRES_PASSWORD` 67 | - `POSTGRES_DB` 68 | - `backend/.env` 後端設定 69 | - `DATABASE_URL` **要與上面的 .env 設定為一樣** 70 | - `JWT_ALGORITHM` 71 | - `ACCESS_TOKEN_SECRET` 72 | - `REFRESH_TOKEN_SECRET` 73 | - `ACCESS_TOKEN_EXPIRE_MINUTES` 74 | - `REFRESH_TOKEN_EXPIRE_MINUTES` 75 | - `nginx/nginx.conf` nginx server 設定 76 | - **注意 :** 後端的 hostname 要與 `docker-compose.yml` 的 service name 一樣 77 | - `frontend/.env` 開發環境 API url 78 | - `frontend/.env.production` 正式環境 API url 79 | 80 | ## 部署 81 | 82 | ### 容器化 83 | - `docker-compose.yml` Docker Compose 設定檔案 84 | - `Dockerfile` 前端 nginx server 的 Dockerfile ,使用 production build 85 | - `backend/Dockerfile` 後端的 Dockerfile ,有提供 hot reload 86 | 87 | ### 正式環境 88 | - `docker-compose up -d --build` 89 | 90 | ### 開發環境 91 | - 資料庫 92 | ``` 93 | docker run --name fastapi_vue_oauth2_postgresql -e POSTGRES_USER=fastapi_vue_user -e POSTGRES_PASSWORD=fastapi_vue_password -e POSTGRES_DB=fastapi_vue_dev -p 5432:5432 -d -v postgres_data_dev:/var/lib/postgresql/data postgres:15.1 94 | ``` 95 | - 後端 96 |
97 | **注意** : 需要在 `backend/.env` 中把 `DATABASE_URL` 換成 `DEV_DATABASE_URL`
98 | - Poetry 99 | ``` 100 | cd backend 101 | 102 | poetry install 103 | poetry shell 104 | uvicorn app:app uvicorn app:app --reload --host 0.0.0.0 --port 5001 105 | ``` 106 | - Create virtual environment 107 | ``` 108 | cd backend 109 | 110 | python3 -m venv venv 111 | source venv/bin/activate 112 | pip3 install -r requirements.txt 113 | 114 | python3 -m uvicorn app:app --reload --host 0.0.0.0 --port 5001 115 | ``` 116 | - 前端 117 | ``` 118 | cd frontend 119 | 120 | yarn dev 121 | ``` 122 | 123 | ## Issues & PR 124 | 125 | 有任何問題歡迎開 issue ! 126 | 127 | 歡迎發送 pull requests 。 128 | 任何貢獻都很感謝。 -------------------------------------------------------------------------------- /docs/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jason810496/FastAPI-Vue-OAuth2/ecd50b03e0bef21c1fd34f7a6b0c5e1ff279fd82/docs/banner.png -------------------------------------------------------------------------------- /docs/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jason810496/FastAPI-Vue-OAuth2/ecd50b03e0bef21c1fd34f7a6b0c5e1ff279fd82/docs/demo.png -------------------------------------------------------------------------------- /frontend/.env: -------------------------------------------------------------------------------- 1 | VITE_APP_API_URL=http://localhost:5001/api 2 | -------------------------------------------------------------------------------- /frontend/.env.production: -------------------------------------------------------------------------------- 1 | VITE_APP_API_URL=http://localhost/api -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | # deveploment docker file 2 | # in production, we will use nginx to serve the static files 3 | FROM node:lts-alpine 4 | 5 | WORKDIR /app 6 | 7 | ENV PATH /app/node_modules/.bin:$PATH 8 | 9 | COPY package.json . 10 | COPY yarn.lock . 11 | RUN yarn install 12 | 13 | CMD ["yarn", "preview" , "--port" , "8080" , "--host"] 14 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Vue 3 + Vite 2 | 3 | This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 ` 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "axios": "^1.6.2", 13 | "pinia": "^2.1.7", 14 | "qs": "^6.11.2", 15 | "tailwindcss": "^3.3.3", 16 | "vue": "^3.2.47", 17 | "vue-router": "^4.2.1", 18 | "vuex-persistedstate": "^4.1.0" 19 | }, 20 | "devDependencies": { 21 | "@vitejs/plugin-vue": "^4.1.0", 22 | "vite": "^4.3.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /frontend/public/oauth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jason810496/FastAPI-Vue-OAuth2/ecd50b03e0bef21c1fd34f7a6b0c5e1ff279fd82/frontend/public/oauth.png -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | 17 | 25 | 26 | 29 | -------------------------------------------------------------------------------- /frontend/src/api/auth.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import qs from 'qs'; 3 | 4 | 5 | export const apiLogin = form => axios.post('/auth/login', qs.stringify(form) , { headers: { 'content-type': 'application/x-www-form-urlencoded' } } ); 6 | export const apiRefresh = () => axios.post('/auth/refresh'); 7 | export const apiLogout = () => axios.post('/auth/logout'); 8 | -------------------------------------------------------------------------------- /frontend/src/api/me.js: -------------------------------------------------------------------------------- 1 | import request from './req'; 2 | 3 | 4 | export const apiGetMyself = () => request('GET', '/me'); 5 | export const apiUpdatePass = data => request('PUT', '/me/password', data); 6 | export const apiUpdateBirth = data => request('PUT', '/me/birthday', data); 7 | export const apiDelateAccount = () => request('DELETE', '/me'); -------------------------------------------------------------------------------- /frontend/src/api/req.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import router from "../router"; 3 | import { useAuthStore } from "../store/auth"; 4 | 5 | const errorHandler = (state, msg) => { 6 | switch (state) { 7 | case 400: 8 | console.log("login fail" + msg); 9 | break; 10 | case 401: 11 | console.log("axios errorHandler : 401 Auth Fail"); 12 | console.log("axios errorHandler : " + msg); 13 | router.push({ name: "Refresh" }); 14 | break; 15 | case 403: 16 | console.log("unauthorized"); 17 | break; 18 | case 404: 19 | console.log("not found"); 20 | break; 21 | default: 22 | console.log("undefined error" + msg); 23 | } 24 | }; 25 | 26 | console.log(import.meta.env.VITE_APP_API_URL); 27 | 28 | var instance = axios.create({ 29 | baseURL: 30 | (import.meta.env.VITE_APP_API_URL ) , 31 | }); 32 | 33 | 34 | instance.interceptors.request.use( 35 | (config) => { 36 | const store = useAuthStore(); 37 | store.get_access_token && (config.headers.Authorization = `Bearer ${store.get_access_token }`); 38 | return config; 39 | }, 40 | (error) => { 41 | return Promise.reject(error); 42 | } 43 | ); 44 | 45 | instance.interceptors.response.use( 46 | (response) => { 47 | return response; 48 | }, 49 | (error) => { 50 | const { response } = error; 51 | if (response) { 52 | errorHandler(response.status, response.data); 53 | return Promise.reject(error); 54 | } else { 55 | if (!window.navigator.onLine) { 56 | console.log("offline"); 57 | } else { 58 | return Promise.reject(error); 59 | } 60 | } 61 | } 62 | ); 63 | 64 | export default function (method, url, data = null , headers = null) { 65 | method = method.toUpperCase(); 66 | 67 | if( headers ){ 68 | instance.defaults.headers.common = headers; 69 | } 70 | 71 | switch (method) { 72 | case "GET": 73 | let composeUrl = `${url}${data && Object.values(data)[0] ? "/" + Object.values(data)[0] : ""}`; 74 | let params = {}; 75 | if (data && Object.keys(data).length > 1) { 76 | let i = 0; 77 | for (let j in data) if (i++ > 0) params[j] = data[j]; 78 | } else { 79 | params = null; 80 | } 81 | return instance.get(composeUrl, { ...params } ); 82 | case "POST": 83 | return instance.post(url, data); 84 | case "PUT": 85 | return instance.put(url, data); 86 | case "DELETE": 87 | return instance.delete(url, data); 88 | case "PATCH": 89 | return instance.patch(url, data); 90 | default: 91 | console.log("unknow methods" + method); 92 | return false; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /frontend/src/api/user.js: -------------------------------------------------------------------------------- 1 | import request from './req'; 2 | import axios from 'axios'; 3 | 4 | export const apiRegister = data => axios.post('/users', data); 5 | export const apiGetUserList = () => axios.get('/users'); -------------------------------------------------------------------------------- /frontend/src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/components/Dialog.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | -------------------------------------------------------------------------------- /frontend/src/components/Loading.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | -------------------------------------------------------------------------------- /frontend/src/components/NavBar.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 40 | 41 | 46 | -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import axios from 'axios'; 3 | import { createPinia } from 'pinia'; 4 | 5 | import App from './App.vue'; 6 | import router from './router'; 7 | 8 | import './style.css'; 9 | 10 | 11 | axios.defaults.withCredentials = true; 12 | axios.defaults.baseURL = import.meta.env.VITE_APP_API_URL || 'http://localhost:5001/api'; // the FastAPI backend 13 | 14 | const pinia = createPinia() 15 | 16 | createApp(App) 17 | .use(pinia) 18 | .use(router) 19 | .mount("#app"); -------------------------------------------------------------------------------- /frontend/src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from 'vue-router' 2 | import HomeView from '../views/HomeView.vue' 3 | import ProfileView from '../views/ProfileView.vue' 4 | import LoginView from '../views/LoginView.vue' 5 | import RegisterView from '../views/RegisterView.vue' 6 | import LogoutView from '../views/LogoutView.vue' 7 | import RefreshView from '../views/RefreshView.vue' 8 | import { useAuthStore } from '../store/auth' 9 | 10 | const routes = [ 11 | { 12 | path: '/', 13 | name: 'Home', 14 | component: HomeView, 15 | }, 16 | { 17 | path: '/register', 18 | name: 'Register', 19 | component: RegisterView, 20 | }, 21 | { 22 | path: '/login', 23 | name: 'Login', 24 | component: LoginView, 25 | }, 26 | { 27 | path: '/profile', 28 | name: 'Profile', 29 | component: ProfileView, 30 | meta: { requiresAuth: true }, 31 | }, 32 | { 33 | path: '/logout', 34 | name: 'Logout', 35 | component: LogoutView, 36 | }, 37 | { 38 | path: '/refresh', 39 | name: 'Refresh', 40 | component: RefreshView, 41 | }, 42 | ] 43 | 44 | const router = createRouter({ 45 | history: createWebHashHistory(), 46 | routes, // short for `routes: routes` 47 | }) 48 | 49 | router.beforeEach((to, from , next) => { 50 | const auth = useAuthStore(); 51 | if (to.matched.some((record) => record.meta.requiresAuth)) { 52 | if (auth.isAuthenticated) { 53 | next(); 54 | return; 55 | } 56 | next("/login"); 57 | } else { 58 | next(); 59 | } 60 | 61 | }) 62 | 63 | export default router; -------------------------------------------------------------------------------- /frontend/src/store/auth.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref, computed } from 'vue' 3 | import { apiLogin, apiRefresh, apiLogout } from '../api/auth' 4 | import { useLoadingStore } from './loading'; 5 | import { useDialogStore } from './dialog'; 6 | import router from '../router'; 7 | 8 | export const useAuthStore = defineStore('auth', () => { 9 | const access_token = ref(null); 10 | const expires_in = ref(null); 11 | 12 | const loadingStore = useLoadingStore(); 13 | const dialogStore = useDialogStore(); 14 | 15 | const isAuthenticated = computed(() => access_token.value && expires_in.value && expires_in.value > Date.now()); 16 | const get_access_token = computed(() => access_token.value); 17 | const get_expires_in = computed(() => expires_in.value); 18 | 19 | async function login(form) { 20 | access_token.value = null; 21 | expires_in.value = null; 22 | 23 | loadingStore.setLoading(); 24 | 25 | apiLogin(form) 26 | .then(res => { 27 | access_token.value = res.data.access_token; 28 | expires_in.value = res.data.expires_in; 29 | 30 | dialogStore.setSuccess({ 31 | title: 'Login Success', 32 | firstLine: 'You can login now', 33 | secondLine: 'This dialog will close in 1 seconds' 34 | }); 35 | }) 36 | .catch(err => { 37 | dialogStore.setError({ 38 | title: 'Login Failed', 39 | firstLine: 'Please check your input', 40 | secondLine: 'This dialog will close in 1 seconds' 41 | }); 42 | access_token.value = null; 43 | expires_in.value = null; 44 | }) 45 | .finally( () => { 46 | loadingStore.clearLoading(); 47 | setTimeout(() => { 48 | dialogStore.reset(); 49 | 50 | console.log(isAuthenticated.value); 51 | console.log(access_token.value); 52 | console.log(expires_in.value); 53 | console.log(Date.now()); 54 | 55 | if (isAuthenticated.value){ 56 | router.push('/profile'); 57 | console.log('pushed to profile'); 58 | } 59 | 60 | }, 1000); 61 | }) 62 | 63 | } 64 | 65 | function logout() { 66 | apiLogout() 67 | .then(res => { 68 | access_token.value = null; 69 | expires_in.value = null; 70 | 71 | dialogStore.setSuccess({ 72 | title: 'Logout Success', 73 | firstLine: 'Redirecting to login page 1 second', 74 | secondLine: '' 75 | }); 76 | 77 | setTimeout(() => { 78 | dialogStore.reset(); 79 | router.push('/login'); 80 | },1000); 81 | }); 82 | } 83 | 84 | function refresh() { 85 | loadingStore.setLoading(); 86 | 87 | apiRefresh() 88 | .then(res => { 89 | access_token.value = res.data.access_token; 90 | expires_in.value = res.data.expires_in; 91 | 92 | dialogStore.setSuccess({ 93 | title: 'Refresh Success', 94 | firstLine: 'Redirecting to profile page', 95 | secondLine: 'This dialog will close in 1 seconds' 96 | }); 97 | }) 98 | .catch(err => { 99 | console.log(err); 100 | access_token.value = null; 101 | expires_in.value = null; 102 | 103 | dialogStore.setError({ 104 | title: 'Refresh Failed', 105 | firstLine: 'Please login again', 106 | secondLine: 'This dialog will close in 1 seconds' 107 | }); 108 | }) 109 | .finally( () => { 110 | setTimeout(() => { 111 | if (isAuthenticated.value){ 112 | router.push('/profile'); 113 | console.log('pushed to profile'); 114 | } 115 | else{ 116 | router.push('/login'); 117 | console.log('pushed to login'); 118 | } 119 | 120 | dialogStore.reset(); 121 | loadingStore.clearLoading(); 122 | 123 | }, 1000); 124 | }) 125 | } 126 | 127 | function refreshForLogin() { 128 | loadingStore.setLoading(); 129 | 130 | apiRefresh() 131 | .then(res => { 132 | access_token.value = res.data.access_token; 133 | expires_in.value = res.data.expires_in; 134 | }) 135 | .catch(err => { 136 | console.log(err); 137 | access_token.value = null; 138 | expires_in.value = null; 139 | }) 140 | .finally( () => { 141 | 142 | if (isAuthenticated.value){ 143 | router.push('/profile'); 144 | console.log('pushed to profile'); 145 | } 146 | else{ 147 | router.push('/login'); 148 | console.log('pushed to login'); 149 | } 150 | 151 | loadingStore.clearLoading(); 152 | }) 153 | } 154 | 155 | return { 156 | access_token, 157 | get_access_token, 158 | get_expires_in, 159 | isAuthenticated, 160 | login, 161 | logout, 162 | refresh, 163 | refreshForLogin 164 | } 165 | }) -------------------------------------------------------------------------------- /frontend/src/store/dialog.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref, computed } from 'vue' 3 | 4 | export const useDialogStore = defineStore('dialog', () => { 5 | const type = ref(''); // 'success' or 'danger' , for bootstrap alert 6 | const show = ref(false); 7 | const content = ref({ 8 | 'title': '', 9 | 'firstLine': '', 10 | 'secondLine': '', 11 | }) 12 | 13 | const isShow = computed(() => show.value); 14 | const dialogContent = computed(() => content.value); 15 | const dialogType = computed(() => type.value); 16 | 17 | 18 | async function setSuccess(newContent) { 19 | show.value = true; 20 | type.value = 'success'; 21 | content.value = newContent; 22 | } 23 | 24 | async function setError(newContent) { 25 | show.value = true; 26 | type.value = 'danger'; 27 | content.value = newContent; 28 | } 29 | 30 | async function reset() { 31 | show.value = false; 32 | } 33 | 34 | return { 35 | dialogContent, 36 | isShow, 37 | dialogType, 38 | setSuccess, 39 | setError, 40 | reset 41 | } 42 | }) -------------------------------------------------------------------------------- /frontend/src/store/loading.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref, computed } from 'vue' 3 | 4 | export const useLoadingStore = defineStore('loading', () => { 5 | const loading = ref(false); 6 | 7 | const isLoading = computed(() => loading.value); 8 | 9 | async function setLoading(isLoading) { 10 | loading.value = isLoading; 11 | } 12 | 13 | async function setLoading() { 14 | loading.value = true; 15 | } 16 | 17 | async function clearLoading() { 18 | loading.value = false; 19 | } 20 | 21 | return { 22 | loading, 23 | isLoading, 24 | setLoading, 25 | clearLoading 26 | } 27 | }) -------------------------------------------------------------------------------- /frontend/src/store/me.js: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { apiGetMyself, apiUpdatePass, apiUpdateBirth, apiDelateAccount } from '../api/me' 3 | import { useLoadingStore } from './loading'; 4 | import { useDialogStore } from './dialog'; 5 | import { useAuthStore } from './auth'; 6 | 7 | function useProfile(){ 8 | const loadingStore = useLoadingStore(); 9 | const dialogStore = useDialogStore(); 10 | const authStore = useAuthStore(); 11 | 12 | // can't use `reactive` here 13 | // because `reactive` can only be used with `Array` ,`Map` or `Set` 14 | const me = ref({ 15 | username: '', 16 | birthday: '', 17 | password: '', 18 | }); 19 | 20 | const fetchMe = async () => { 21 | loadingStore.setLoading(); 22 | 23 | try { 24 | const res = await apiGetMyself(); 25 | me.value = res.data; 26 | console.log(me.value); 27 | } 28 | catch (err) { 29 | console.log(err); 30 | } 31 | finally { 32 | loadingStore.clearLoading(); 33 | } 34 | } 35 | 36 | const reloadData = async () => { 37 | loadingStore.setLoading(); 38 | 39 | apiGetMyself() 40 | .then(res => { 41 | me.value = res.data; 42 | dialogStore.setSuccess({ 43 | title: 'Load Data Success', 44 | firstLine: 'Successfully loaded your data', 45 | secondLine: 'This dialog will close in 1 seconds' 46 | }); 47 | console.log(res.data); 48 | console.log(me.value); 49 | }) 50 | .catch(err => { 51 | console.log(err); 52 | dialogStore.setError({ 53 | title: 'Failed to fetch data', 54 | firstLine: 'Please check your internet connection', 55 | secondLine: 'This dialog will close in 1 seconds' 56 | }); 57 | }) 58 | .finally(() => { 59 | loadingStore.clearLoading(); 60 | setTimeout(() => { 61 | dialogStore.reset(); 62 | }, 1000); 63 | }); 64 | } 65 | 66 | const updatePassword = async () => { 67 | loadingStore.setLoading(); 68 | apiUpdatePass({password: me.value.password}) 69 | .then(res => { 70 | dialogStore.setSuccess({ 71 | title: 'Update Success', 72 | firstLine: 'Update password successfully', 73 | secondLine: 'This dialog will close in 1 seconds' 74 | }); 75 | }) 76 | .catch(err => { 77 | console.log(err); 78 | }) 79 | .finally(() => { 80 | loadingStore.clearLoading(); 81 | setTimeout(() => { 82 | dialogStore.reset(); 83 | }, 1000); 84 | }); 85 | } 86 | 87 | const updateBirthday = async () => { 88 | loadingStore.setLoading(); 89 | apiUpdateBirth({birthday: me.value.birthday}) 90 | .then(res => { 91 | dialogStore.setSuccess({ 92 | title: 'Update Success', 93 | firstLine: 'Update birthday successfully', 94 | secondLine: 'This dialog will close in 1 seconds' 95 | }); 96 | }) 97 | .catch(err => { 98 | console.log(err); 99 | }) 100 | .finally(() => { 101 | loadingStore.clearLoading(); 102 | setTimeout(() => { 103 | dialogStore.reset(); 104 | }, 1000); 105 | }); 106 | } 107 | 108 | const changeAccessToken = () => { 109 | authStore.access_token = null; 110 | 111 | dialogStore.setSuccess({ 112 | title: 'Delete Access Token Success', 113 | firstLine: 'The access token has been deleted.', 114 | secondLine: 'Click "Reload\" to try refresh token.', 115 | }); 116 | setTimeout(() => { 117 | dialogStore.reset(); 118 | }, 1000); 119 | } 120 | 121 | // load data before component mounted 122 | fetchMe(); 123 | 124 | return { 125 | me, 126 | reloadData, 127 | updatePassword, 128 | updateBirthday, 129 | changeAccessToken 130 | } 131 | } 132 | 133 | export { useProfile } -------------------------------------------------------------------------------- /frontend/src/store/user.js: -------------------------------------------------------------------------------- 1 | import { reactive } from "vue"; 2 | import { apiGetUserList, apiRegister } from "../api/user"; 3 | import { useLoadingStore } from "./loading"; 4 | import { useDialogStore } from "./dialog"; 5 | import router from "../router"; 6 | 7 | // data provider pattern : 8 | // https://www.patterns.dev/vue/data-provider 9 | 10 | function useFetchUser() { 11 | const userList = reactive([ 12 | {username: "",birthday: "",} 13 | ]); 14 | 15 | const fetchUsers = async () => { 16 | const loadingStore = useLoadingStore(); 17 | loadingStore.setLoading(); 18 | 19 | try { 20 | const res = await apiGetUserList(); 21 | userList.value = res.data; 22 | } catch (err) { 23 | console.log(err); 24 | } finally { 25 | // for loading test 26 | // setTimeout(() => { 27 | // loadingStore.clearLoading(); 28 | // }, 1000); 29 | loadingStore.clearLoading(); 30 | } 31 | }; 32 | 33 | fetchUsers(); 34 | 35 | return { userList }; 36 | } 37 | 38 | async function registerUser(form){ 39 | const loadingStore = useLoadingStore(); 40 | const dialogStore = useDialogStore(); 41 | loadingStore.setLoading(); 42 | 43 | apiRegister(form) 44 | .then((res) => { 45 | console.log(res); 46 | dialogStore.setSuccess({ 47 | title: "Register Success", 48 | firstLine: "You can login now", 49 | secondLine: "This dialog will close in 2 seconds", 50 | }); 51 | setTimeout(() => { 52 | router.push("/login"); 53 | },2010); 54 | }) 55 | .catch((err) => { 56 | console.log(err); 57 | dialogStore.setError({ 58 | title: "Register Failed", 59 | firstLine: "Please check your input", 60 | secondLine: "This dialog will close in 2 seconds", 61 | }); 62 | }) 63 | .finally(() => { 64 | loadingStore.clearLoading(); 65 | setTimeout(() => { 66 | dialogStore.reset(); 67 | }, 2000); 68 | }); 69 | } 70 | 71 | async function loginUser(form){ 72 | const loadingStore = useLoadingStore(); 73 | const dialogStore = useDialogStore(); 74 | loadingStore.setLoading(); 75 | 76 | apiRegister(form) 77 | .then((res) => { 78 | console.log(res); 79 | dialogStore.setSuccess({ 80 | title: "Login Success", 81 | firstLine: "You can login now", 82 | secondLine: "This dialog will close in 2 seconds", 83 | }); 84 | }) 85 | .catch((err) => { 86 | console.log(err); 87 | dialogStore.setError({ 88 | title: "Login Failed", 89 | firstLine: "Please check your input", 90 | secondLine: "This dialog will close in 2 seconds", 91 | }); 92 | }) 93 | .finally(() => { 94 | loadingStore.clearLoading(); 95 | setTimeout(() => { 96 | dialogStore.reset(); 97 | }, 2000); 98 | }); 99 | } 100 | 101 | 102 | export { useFetchUser, registerUser, loginUser }; 103 | -------------------------------------------------------------------------------- /frontend/src/style.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /frontend/src/views/HomeView.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | -------------------------------------------------------------------------------- /frontend/src/views/LoginView.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | -------------------------------------------------------------------------------- /frontend/src/views/LogoutView.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /frontend/src/views/ProfileView.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | -------------------------------------------------------------------------------- /frontend/src/views/RefreshView.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | -------------------------------------------------------------------------------- /frontend/src/views/RegisterView.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | -------------------------------------------------------------------------------- /frontend/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [vue()], 7 | }) 8 | -------------------------------------------------------------------------------- /frontend/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@alloc/quick-lru@^5.2.0": 6 | version "5.2.0" 7 | resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30" 8 | integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== 9 | 10 | "@babel/parser@^7.20.15", "@babel/parser@^7.21.3": 11 | version "7.21.9" 12 | resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.9.tgz#ab18ea3b85b4bc33ba98a8d4c2032c557d23cf14" 13 | integrity sha512-q5PNg/Bi1OpGgx5jYlvWZwAorZepEudDMCLtj967aeS7WMont7dUZI46M2XwcIQqvUlMxWfdLFu4S/qSxeUu5g== 14 | 15 | "@esbuild/android-arm64@0.17.19": 16 | version "0.17.19" 17 | resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz#bafb75234a5d3d1b690e7c2956a599345e84a2fd" 18 | integrity sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA== 19 | 20 | "@esbuild/android-arm@0.17.19": 21 | version "0.17.19" 22 | resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.17.19.tgz#5898f7832c2298bc7d0ab53701c57beb74d78b4d" 23 | integrity sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A== 24 | 25 | "@esbuild/android-x64@0.17.19": 26 | version "0.17.19" 27 | resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.17.19.tgz#658368ef92067866d95fb268719f98f363d13ae1" 28 | integrity sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww== 29 | 30 | "@esbuild/darwin-arm64@0.17.19": 31 | version "0.17.19" 32 | resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz#584c34c5991b95d4d48d333300b1a4e2ff7be276" 33 | integrity sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg== 34 | 35 | "@esbuild/darwin-x64@0.17.19": 36 | version "0.17.19" 37 | resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz#7751d236dfe6ce136cce343dce69f52d76b7f6cb" 38 | integrity sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw== 39 | 40 | "@esbuild/freebsd-arm64@0.17.19": 41 | version "0.17.19" 42 | resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz#cacd171665dd1d500f45c167d50c6b7e539d5fd2" 43 | integrity sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ== 44 | 45 | "@esbuild/freebsd-x64@0.17.19": 46 | version "0.17.19" 47 | resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz#0769456eee2a08b8d925d7c00b79e861cb3162e4" 48 | integrity sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ== 49 | 50 | "@esbuild/linux-arm64@0.17.19": 51 | version "0.17.19" 52 | resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz#38e162ecb723862c6be1c27d6389f48960b68edb" 53 | integrity sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg== 54 | 55 | "@esbuild/linux-arm@0.17.19": 56 | version "0.17.19" 57 | resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz#1a2cd399c50040184a805174a6d89097d9d1559a" 58 | integrity sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA== 59 | 60 | "@esbuild/linux-ia32@0.17.19": 61 | version "0.17.19" 62 | resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz#e28c25266b036ce1cabca3c30155222841dc035a" 63 | integrity sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ== 64 | 65 | "@esbuild/linux-loong64@0.17.19": 66 | version "0.17.19" 67 | resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz#0f887b8bb3f90658d1a0117283e55dbd4c9dcf72" 68 | integrity sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ== 69 | 70 | "@esbuild/linux-mips64el@0.17.19": 71 | version "0.17.19" 72 | resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz#f5d2a0b8047ea9a5d9f592a178ea054053a70289" 73 | integrity sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A== 74 | 75 | "@esbuild/linux-ppc64@0.17.19": 76 | version "0.17.19" 77 | resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz#876590e3acbd9fa7f57a2c7d86f83717dbbac8c7" 78 | integrity sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg== 79 | 80 | "@esbuild/linux-riscv64@0.17.19": 81 | version "0.17.19" 82 | resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz#7f49373df463cd9f41dc34f9b2262d771688bf09" 83 | integrity sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA== 84 | 85 | "@esbuild/linux-s390x@0.17.19": 86 | version "0.17.19" 87 | resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz#e2afd1afcaf63afe2c7d9ceacd28ec57c77f8829" 88 | integrity sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q== 89 | 90 | "@esbuild/linux-x64@0.17.19": 91 | version "0.17.19" 92 | resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz#8a0e9738b1635f0c53389e515ae83826dec22aa4" 93 | integrity sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw== 94 | 95 | "@esbuild/netbsd-x64@0.17.19": 96 | version "0.17.19" 97 | resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz#c29fb2453c6b7ddef9a35e2c18b37bda1ae5c462" 98 | integrity sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q== 99 | 100 | "@esbuild/openbsd-x64@0.17.19": 101 | version "0.17.19" 102 | resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz#95e75a391403cb10297280d524d66ce04c920691" 103 | integrity sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g== 104 | 105 | "@esbuild/sunos-x64@0.17.19": 106 | version "0.17.19" 107 | resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz#722eaf057b83c2575937d3ffe5aeb16540da7273" 108 | integrity sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg== 109 | 110 | "@esbuild/win32-arm64@0.17.19": 111 | version "0.17.19" 112 | resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz#9aa9dc074399288bdcdd283443e9aeb6b9552b6f" 113 | integrity sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag== 114 | 115 | "@esbuild/win32-ia32@0.17.19": 116 | version "0.17.19" 117 | resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz#95ad43c62ad62485e210f6299c7b2571e48d2b03" 118 | integrity sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw== 119 | 120 | "@esbuild/win32-x64@0.17.19": 121 | version "0.17.19" 122 | resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz#8cfaf2ff603e9aabb910e9c0558c26cf32744061" 123 | integrity sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA== 124 | 125 | "@jridgewell/gen-mapping@^0.3.2": 126 | version "0.3.3" 127 | resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" 128 | integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== 129 | dependencies: 130 | "@jridgewell/set-array" "^1.0.1" 131 | "@jridgewell/sourcemap-codec" "^1.4.10" 132 | "@jridgewell/trace-mapping" "^0.3.9" 133 | 134 | "@jridgewell/resolve-uri@3.1.0": 135 | version "3.1.0" 136 | resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" 137 | integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== 138 | 139 | "@jridgewell/set-array@^1.0.1": 140 | version "1.1.2" 141 | resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" 142 | integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== 143 | 144 | "@jridgewell/sourcemap-codec@1.4.14": 145 | version "1.4.14" 146 | resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" 147 | integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== 148 | 149 | "@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.13": 150 | version "1.4.15" 151 | resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" 152 | integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== 153 | 154 | "@jridgewell/trace-mapping@^0.3.9": 155 | version "0.3.18" 156 | resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6" 157 | integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA== 158 | dependencies: 159 | "@jridgewell/resolve-uri" "3.1.0" 160 | "@jridgewell/sourcemap-codec" "1.4.14" 161 | 162 | "@nodelib/fs.scandir@2.1.5": 163 | version "2.1.5" 164 | resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" 165 | integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== 166 | dependencies: 167 | "@nodelib/fs.stat" "2.0.5" 168 | run-parallel "^1.1.9" 169 | 170 | "@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": 171 | version "2.0.5" 172 | resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" 173 | integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== 174 | 175 | "@nodelib/fs.walk@^1.2.3": 176 | version "1.2.8" 177 | resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" 178 | integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== 179 | dependencies: 180 | "@nodelib/fs.scandir" "2.1.5" 181 | fastq "^1.6.0" 182 | 183 | "@vitejs/plugin-vue@^4.1.0": 184 | version "4.2.3" 185 | resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-4.2.3.tgz#ee0b6dfcc62fe65364e6395bf38fa2ba10bb44b6" 186 | integrity sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw== 187 | 188 | "@vue/compiler-core@3.3.4": 189 | version "3.3.4" 190 | resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.3.4.tgz#7fbf591c1c19e1acd28ffd284526e98b4f581128" 191 | integrity sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g== 192 | dependencies: 193 | "@babel/parser" "^7.21.3" 194 | "@vue/shared" "3.3.4" 195 | estree-walker "^2.0.2" 196 | source-map-js "^1.0.2" 197 | 198 | "@vue/compiler-dom@3.3.4": 199 | version "3.3.4" 200 | resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz#f56e09b5f4d7dc350f981784de9713d823341151" 201 | integrity sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w== 202 | dependencies: 203 | "@vue/compiler-core" "3.3.4" 204 | "@vue/shared" "3.3.4" 205 | 206 | "@vue/compiler-sfc@3.3.4": 207 | version "3.3.4" 208 | resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz#b19d942c71938893535b46226d602720593001df" 209 | integrity sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ== 210 | dependencies: 211 | "@babel/parser" "^7.20.15" 212 | "@vue/compiler-core" "3.3.4" 213 | "@vue/compiler-dom" "3.3.4" 214 | "@vue/compiler-ssr" "3.3.4" 215 | "@vue/reactivity-transform" "3.3.4" 216 | "@vue/shared" "3.3.4" 217 | estree-walker "^2.0.2" 218 | magic-string "^0.30.0" 219 | postcss "^8.1.10" 220 | source-map-js "^1.0.2" 221 | 222 | "@vue/compiler-ssr@3.3.4": 223 | version "3.3.4" 224 | resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz#9d1379abffa4f2b0cd844174ceec4a9721138777" 225 | integrity sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ== 226 | dependencies: 227 | "@vue/compiler-dom" "3.3.4" 228 | "@vue/shared" "3.3.4" 229 | 230 | "@vue/devtools-api@^6.5.0": 231 | version "6.5.0" 232 | resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.5.0.tgz#98b99425edee70b4c992692628fa1ea2c1e57d07" 233 | integrity sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q== 234 | 235 | "@vue/reactivity-transform@3.3.4": 236 | version "3.3.4" 237 | resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz#52908476e34d6a65c6c21cd2722d41ed8ae51929" 238 | integrity sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw== 239 | dependencies: 240 | "@babel/parser" "^7.20.15" 241 | "@vue/compiler-core" "3.3.4" 242 | "@vue/shared" "3.3.4" 243 | estree-walker "^2.0.2" 244 | magic-string "^0.30.0" 245 | 246 | "@vue/reactivity@3.3.4": 247 | version "3.3.4" 248 | resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.3.4.tgz#a27a29c6cd17faba5a0e99fbb86ee951653e2253" 249 | integrity sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ== 250 | dependencies: 251 | "@vue/shared" "3.3.4" 252 | 253 | "@vue/runtime-core@3.3.4": 254 | version "3.3.4" 255 | resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.3.4.tgz#4bb33872bbb583721b340f3088888394195967d1" 256 | integrity sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA== 257 | dependencies: 258 | "@vue/reactivity" "3.3.4" 259 | "@vue/shared" "3.3.4" 260 | 261 | "@vue/runtime-dom@3.3.4": 262 | version "3.3.4" 263 | resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.3.4.tgz#992f2579d0ed6ce961f47bbe9bfe4b6791251566" 264 | integrity sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ== 265 | dependencies: 266 | "@vue/runtime-core" "3.3.4" 267 | "@vue/shared" "3.3.4" 268 | csstype "^3.1.1" 269 | 270 | "@vue/server-renderer@3.3.4": 271 | version "3.3.4" 272 | resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.3.4.tgz#ea46594b795d1536f29bc592dd0f6655f7ea4c4c" 273 | integrity sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ== 274 | dependencies: 275 | "@vue/compiler-ssr" "3.3.4" 276 | "@vue/shared" "3.3.4" 277 | 278 | "@vue/shared@3.3.4": 279 | version "3.3.4" 280 | resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.3.4.tgz#06e83c5027f464eef861c329be81454bc8b70780" 281 | integrity sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ== 282 | 283 | any-promise@^1.0.0: 284 | version "1.3.0" 285 | resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" 286 | integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== 287 | 288 | anymatch@~3.1.2: 289 | version "3.1.3" 290 | resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" 291 | integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== 292 | dependencies: 293 | normalize-path "^3.0.0" 294 | picomatch "^2.0.4" 295 | 296 | arg@^5.0.2: 297 | version "5.0.2" 298 | resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" 299 | integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== 300 | 301 | asynckit@^0.4.0: 302 | version "0.4.0" 303 | resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 304 | integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== 305 | 306 | axios@^1.6.2: 307 | version "1.6.2" 308 | resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.2.tgz#de67d42c755b571d3e698df1b6504cde9b0ee9f2" 309 | integrity sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A== 310 | dependencies: 311 | follow-redirects "^1.15.0" 312 | form-data "^4.0.0" 313 | proxy-from-env "^1.1.0" 314 | 315 | balanced-match@^1.0.0: 316 | version "1.0.2" 317 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" 318 | integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== 319 | 320 | binary-extensions@^2.0.0: 321 | version "2.2.0" 322 | resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" 323 | integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== 324 | 325 | brace-expansion@^1.1.7: 326 | version "1.1.11" 327 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 328 | integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== 329 | dependencies: 330 | balanced-match "^1.0.0" 331 | concat-map "0.0.1" 332 | 333 | braces@^3.0.2, braces@~3.0.2: 334 | version "3.0.2" 335 | resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" 336 | integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== 337 | dependencies: 338 | fill-range "^7.0.1" 339 | 340 | call-bind@^1.0.0: 341 | version "1.0.2" 342 | resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" 343 | integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== 344 | dependencies: 345 | function-bind "^1.1.1" 346 | get-intrinsic "^1.0.2" 347 | 348 | camelcase-css@^2.0.1: 349 | version "2.0.1" 350 | resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" 351 | integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== 352 | 353 | chokidar@^3.5.3: 354 | version "3.5.3" 355 | resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" 356 | integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== 357 | dependencies: 358 | anymatch "~3.1.2" 359 | braces "~3.0.2" 360 | glob-parent "~5.1.2" 361 | is-binary-path "~2.1.0" 362 | is-glob "~4.0.1" 363 | normalize-path "~3.0.0" 364 | readdirp "~3.6.0" 365 | optionalDependencies: 366 | fsevents "~2.3.2" 367 | 368 | combined-stream@^1.0.8: 369 | version "1.0.8" 370 | resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" 371 | integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== 372 | dependencies: 373 | delayed-stream "~1.0.0" 374 | 375 | commander@^4.0.0: 376 | version "4.1.1" 377 | resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" 378 | integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== 379 | 380 | concat-map@0.0.1: 381 | version "0.0.1" 382 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 383 | integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== 384 | 385 | cssesc@^3.0.0: 386 | version "3.0.0" 387 | resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" 388 | integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== 389 | 390 | csstype@^3.1.1: 391 | version "3.1.2" 392 | resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" 393 | integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== 394 | 395 | deepmerge@^4.2.2: 396 | version "4.3.1" 397 | resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" 398 | integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== 399 | 400 | delayed-stream@~1.0.0: 401 | version "1.0.0" 402 | resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" 403 | integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== 404 | 405 | didyoumean@^1.2.2: 406 | version "1.2.2" 407 | resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" 408 | integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== 409 | 410 | dlv@^1.1.3: 411 | version "1.1.3" 412 | resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" 413 | integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== 414 | 415 | esbuild@^0.17.5: 416 | version "0.17.19" 417 | resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.19.tgz#087a727e98299f0462a3d0bcdd9cd7ff100bd955" 418 | integrity sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw== 419 | optionalDependencies: 420 | "@esbuild/android-arm" "0.17.19" 421 | "@esbuild/android-arm64" "0.17.19" 422 | "@esbuild/android-x64" "0.17.19" 423 | "@esbuild/darwin-arm64" "0.17.19" 424 | "@esbuild/darwin-x64" "0.17.19" 425 | "@esbuild/freebsd-arm64" "0.17.19" 426 | "@esbuild/freebsd-x64" "0.17.19" 427 | "@esbuild/linux-arm" "0.17.19" 428 | "@esbuild/linux-arm64" "0.17.19" 429 | "@esbuild/linux-ia32" "0.17.19" 430 | "@esbuild/linux-loong64" "0.17.19" 431 | "@esbuild/linux-mips64el" "0.17.19" 432 | "@esbuild/linux-ppc64" "0.17.19" 433 | "@esbuild/linux-riscv64" "0.17.19" 434 | "@esbuild/linux-s390x" "0.17.19" 435 | "@esbuild/linux-x64" "0.17.19" 436 | "@esbuild/netbsd-x64" "0.17.19" 437 | "@esbuild/openbsd-x64" "0.17.19" 438 | "@esbuild/sunos-x64" "0.17.19" 439 | "@esbuild/win32-arm64" "0.17.19" 440 | "@esbuild/win32-ia32" "0.17.19" 441 | "@esbuild/win32-x64" "0.17.19" 442 | 443 | estree-walker@^2.0.2: 444 | version "2.0.2" 445 | resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" 446 | integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== 447 | 448 | fast-glob@^3.2.12: 449 | version "3.3.1" 450 | resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" 451 | integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== 452 | dependencies: 453 | "@nodelib/fs.stat" "^2.0.2" 454 | "@nodelib/fs.walk" "^1.2.3" 455 | glob-parent "^5.1.2" 456 | merge2 "^1.3.0" 457 | micromatch "^4.0.4" 458 | 459 | fastq@^1.6.0: 460 | version "1.15.0" 461 | resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" 462 | integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== 463 | dependencies: 464 | reusify "^1.0.4" 465 | 466 | fill-range@^7.0.1: 467 | version "7.0.1" 468 | resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" 469 | integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== 470 | dependencies: 471 | to-regex-range "^5.0.1" 472 | 473 | follow-redirects@^1.15.0: 474 | version "1.15.2" 475 | resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" 476 | integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== 477 | 478 | form-data@^4.0.0: 479 | version "4.0.0" 480 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" 481 | integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== 482 | dependencies: 483 | asynckit "^0.4.0" 484 | combined-stream "^1.0.8" 485 | mime-types "^2.1.12" 486 | 487 | fs.realpath@^1.0.0: 488 | version "1.0.0" 489 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 490 | integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== 491 | 492 | fsevents@~2.3.2: 493 | version "2.3.2" 494 | resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" 495 | integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== 496 | 497 | function-bind@^1.1.1: 498 | version "1.1.1" 499 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" 500 | integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== 501 | 502 | get-intrinsic@^1.0.2: 503 | version "1.2.1" 504 | resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" 505 | integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== 506 | dependencies: 507 | function-bind "^1.1.1" 508 | has "^1.0.3" 509 | has-proto "^1.0.1" 510 | has-symbols "^1.0.3" 511 | 512 | glob-parent@^5.1.2, glob-parent@~5.1.2: 513 | version "5.1.2" 514 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" 515 | integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== 516 | dependencies: 517 | is-glob "^4.0.1" 518 | 519 | glob-parent@^6.0.2: 520 | version "6.0.2" 521 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" 522 | integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== 523 | dependencies: 524 | is-glob "^4.0.3" 525 | 526 | glob@7.1.6: 527 | version "7.1.6" 528 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" 529 | integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== 530 | dependencies: 531 | fs.realpath "^1.0.0" 532 | inflight "^1.0.4" 533 | inherits "2" 534 | minimatch "^3.0.4" 535 | once "^1.3.0" 536 | path-is-absolute "^1.0.0" 537 | 538 | has-proto@^1.0.1: 539 | version "1.0.1" 540 | resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" 541 | integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== 542 | 543 | has-symbols@^1.0.3: 544 | version "1.0.3" 545 | resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" 546 | integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== 547 | 548 | has@^1.0.3: 549 | version "1.0.3" 550 | resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" 551 | integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== 552 | dependencies: 553 | function-bind "^1.1.1" 554 | 555 | inflight@^1.0.4: 556 | version "1.0.6" 557 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 558 | integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== 559 | dependencies: 560 | once "^1.3.0" 561 | wrappy "1" 562 | 563 | inherits@2: 564 | version "2.0.4" 565 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 566 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 567 | 568 | is-binary-path@~2.1.0: 569 | version "2.1.0" 570 | resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" 571 | integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== 572 | dependencies: 573 | binary-extensions "^2.0.0" 574 | 575 | is-core-module@^2.11.0: 576 | version "2.12.1" 577 | resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.12.1.tgz#0c0b6885b6f80011c71541ce15c8d66cf5a4f9fd" 578 | integrity sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg== 579 | dependencies: 580 | has "^1.0.3" 581 | 582 | is-extglob@^2.1.1: 583 | version "2.1.1" 584 | resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" 585 | integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== 586 | 587 | is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: 588 | version "4.0.3" 589 | resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" 590 | integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== 591 | dependencies: 592 | is-extglob "^2.1.1" 593 | 594 | is-number@^7.0.0: 595 | version "7.0.0" 596 | resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" 597 | integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== 598 | 599 | jiti@^1.18.2: 600 | version "1.19.1" 601 | resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.19.1.tgz#fa99e4b76a23053e0e7cde098efe1704a14c16f1" 602 | integrity sha512-oVhqoRDaBXf7sjkll95LHVS6Myyyb1zaunVwk4Z0+WPSW4gjS0pl01zYKHScTuyEhQsFxV5L4DR5r+YqSyqyyg== 603 | 604 | lilconfig@^2.0.5, lilconfig@^2.1.0: 605 | version "2.1.0" 606 | resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" 607 | integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== 608 | 609 | lines-and-columns@^1.1.6: 610 | version "1.2.4" 611 | resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" 612 | integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== 613 | 614 | magic-string@^0.30.0: 615 | version "0.30.0" 616 | resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.0.tgz#fd58a4748c5c4547338a424e90fa5dd17f4de529" 617 | integrity sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ== 618 | dependencies: 619 | "@jridgewell/sourcemap-codec" "^1.4.13" 620 | 621 | merge2@^1.3.0: 622 | version "1.4.1" 623 | resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" 624 | integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== 625 | 626 | micromatch@^4.0.4, micromatch@^4.0.5: 627 | version "4.0.5" 628 | resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" 629 | integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== 630 | dependencies: 631 | braces "^3.0.2" 632 | picomatch "^2.3.1" 633 | 634 | mime-db@1.52.0: 635 | version "1.52.0" 636 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" 637 | integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== 638 | 639 | mime-types@^2.1.12: 640 | version "2.1.35" 641 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" 642 | integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== 643 | dependencies: 644 | mime-db "1.52.0" 645 | 646 | minimatch@^3.0.4: 647 | version "3.1.2" 648 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" 649 | integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== 650 | dependencies: 651 | brace-expansion "^1.1.7" 652 | 653 | mz@^2.7.0: 654 | version "2.7.0" 655 | resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" 656 | integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== 657 | dependencies: 658 | any-promise "^1.0.0" 659 | object-assign "^4.0.1" 660 | thenify-all "^1.0.0" 661 | 662 | nanoid@^3.3.6: 663 | version "3.3.6" 664 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" 665 | integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== 666 | 667 | normalize-path@^3.0.0, normalize-path@~3.0.0: 668 | version "3.0.0" 669 | resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" 670 | integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== 671 | 672 | object-assign@^4.0.1: 673 | version "4.1.1" 674 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 675 | integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== 676 | 677 | object-hash@^3.0.0: 678 | version "3.0.0" 679 | resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" 680 | integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== 681 | 682 | object-inspect@^1.9.0: 683 | version "1.12.3" 684 | resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" 685 | integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== 686 | 687 | once@^1.3.0: 688 | version "1.4.0" 689 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 690 | integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== 691 | dependencies: 692 | wrappy "1" 693 | 694 | path-is-absolute@^1.0.0: 695 | version "1.0.1" 696 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 697 | integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== 698 | 699 | path-parse@^1.0.7: 700 | version "1.0.7" 701 | resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" 702 | integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== 703 | 704 | picocolors@^1.0.0: 705 | version "1.0.0" 706 | resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" 707 | integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== 708 | 709 | picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: 710 | version "2.3.1" 711 | resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" 712 | integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== 713 | 714 | pify@^2.3.0: 715 | version "2.3.0" 716 | resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" 717 | integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== 718 | 719 | pinia@^2.1.7: 720 | version "2.1.7" 721 | resolved "https://registry.yarnpkg.com/pinia/-/pinia-2.1.7.tgz#4cf5420d9324ca00b7b4984d3fbf693222115bbc" 722 | integrity sha512-+C2AHFtcFqjPih0zpYuvof37SFxMQ7OEG2zV9jRI12i9BOy3YQVAHwdKtyyc8pDcDyIc33WCIsZaCFWU7WWxGQ== 723 | dependencies: 724 | "@vue/devtools-api" "^6.5.0" 725 | vue-demi ">=0.14.5" 726 | 727 | pirates@^4.0.1: 728 | version "4.0.6" 729 | resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" 730 | integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== 731 | 732 | postcss-import@^15.1.0: 733 | version "15.1.0" 734 | resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-15.1.0.tgz#41c64ed8cc0e23735a9698b3249ffdbf704adc70" 735 | integrity sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew== 736 | dependencies: 737 | postcss-value-parser "^4.0.0" 738 | read-cache "^1.0.0" 739 | resolve "^1.1.7" 740 | 741 | postcss-js@^4.0.1: 742 | version "4.0.1" 743 | resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.1.tgz#61598186f3703bab052f1c4f7d805f3991bee9d2" 744 | integrity sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw== 745 | dependencies: 746 | camelcase-css "^2.0.1" 747 | 748 | postcss-load-config@^4.0.1: 749 | version "4.0.1" 750 | resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-4.0.1.tgz#152383f481c2758274404e4962743191d73875bd" 751 | integrity sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA== 752 | dependencies: 753 | lilconfig "^2.0.5" 754 | yaml "^2.1.1" 755 | 756 | postcss-nested@^6.0.1: 757 | version "6.0.1" 758 | resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.0.1.tgz#f83dc9846ca16d2f4fa864f16e9d9f7d0961662c" 759 | integrity sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ== 760 | dependencies: 761 | postcss-selector-parser "^6.0.11" 762 | 763 | postcss-selector-parser@^6.0.11: 764 | version "6.0.13" 765 | resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz#d05d8d76b1e8e173257ef9d60b706a8e5e99bf1b" 766 | integrity sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ== 767 | dependencies: 768 | cssesc "^3.0.0" 769 | util-deprecate "^1.0.2" 770 | 771 | postcss-value-parser@^4.0.0: 772 | version "4.2.0" 773 | resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" 774 | integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== 775 | 776 | postcss@^8.1.10, postcss@^8.4.23: 777 | version "8.4.23" 778 | resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.23.tgz#df0aee9ac7c5e53e1075c24a3613496f9e6552ab" 779 | integrity sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA== 780 | dependencies: 781 | nanoid "^3.3.6" 782 | picocolors "^1.0.0" 783 | source-map-js "^1.0.2" 784 | 785 | proxy-from-env@^1.1.0: 786 | version "1.1.0" 787 | resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" 788 | integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== 789 | 790 | qs@^6.11.2: 791 | version "6.11.2" 792 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" 793 | integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== 794 | dependencies: 795 | side-channel "^1.0.4" 796 | 797 | queue-microtask@^1.2.2: 798 | version "1.2.3" 799 | resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" 800 | integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== 801 | 802 | read-cache@^1.0.0: 803 | version "1.0.0" 804 | resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" 805 | integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA== 806 | dependencies: 807 | pify "^2.3.0" 808 | 809 | readdirp@~3.6.0: 810 | version "3.6.0" 811 | resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" 812 | integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== 813 | dependencies: 814 | picomatch "^2.2.1" 815 | 816 | resolve@^1.1.7, resolve@^1.22.2: 817 | version "1.22.2" 818 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.2.tgz#0ed0943d4e301867955766c9f3e1ae6d01c6845f" 819 | integrity sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g== 820 | dependencies: 821 | is-core-module "^2.11.0" 822 | path-parse "^1.0.7" 823 | supports-preserve-symlinks-flag "^1.0.0" 824 | 825 | reusify@^1.0.4: 826 | version "1.0.4" 827 | resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" 828 | integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== 829 | 830 | rollup@^3.21.0: 831 | version "3.23.0" 832 | resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.23.0.tgz#b8d6146dac4bf058ee817f92820988e9b358b564" 833 | integrity sha512-h31UlwEi7FHihLe1zbk+3Q7z1k/84rb9BSwmBSr/XjOCEaBJ2YyedQDuM0t/kfOS0IxM+vk1/zI9XxYj9V+NJQ== 834 | optionalDependencies: 835 | fsevents "~2.3.2" 836 | 837 | run-parallel@^1.1.9: 838 | version "1.2.0" 839 | resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" 840 | integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== 841 | dependencies: 842 | queue-microtask "^1.2.2" 843 | 844 | shvl@^2.0.3: 845 | version "2.0.3" 846 | resolved "https://registry.yarnpkg.com/shvl/-/shvl-2.0.3.tgz#eb4bd37644f5684bba1fc52c3010c96fb5e6afd1" 847 | integrity sha512-V7C6S9Hlol6SzOJPnQ7qzOVEWUQImt3BNmmzh40wObhla3XOYMe4gGiYzLrJd5TFa+cI2f9LKIRJTTKZSTbWgw== 848 | 849 | side-channel@^1.0.4: 850 | version "1.0.4" 851 | resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" 852 | integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== 853 | dependencies: 854 | call-bind "^1.0.0" 855 | get-intrinsic "^1.0.2" 856 | object-inspect "^1.9.0" 857 | 858 | source-map-js@^1.0.2: 859 | version "1.0.2" 860 | resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" 861 | integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== 862 | 863 | sucrase@^3.32.0: 864 | version "3.34.0" 865 | resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.34.0.tgz#1e0e2d8fcf07f8b9c3569067d92fbd8690fb576f" 866 | integrity sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw== 867 | dependencies: 868 | "@jridgewell/gen-mapping" "^0.3.2" 869 | commander "^4.0.0" 870 | glob "7.1.6" 871 | lines-and-columns "^1.1.6" 872 | mz "^2.7.0" 873 | pirates "^4.0.1" 874 | ts-interface-checker "^0.1.9" 875 | 876 | supports-preserve-symlinks-flag@^1.0.0: 877 | version "1.0.0" 878 | resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" 879 | integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== 880 | 881 | tailwindcss@^3.3.3: 882 | version "3.3.3" 883 | resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.3.3.tgz#90da807393a2859189e48e9e7000e6880a736daf" 884 | integrity sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w== 885 | dependencies: 886 | "@alloc/quick-lru" "^5.2.0" 887 | arg "^5.0.2" 888 | chokidar "^3.5.3" 889 | didyoumean "^1.2.2" 890 | dlv "^1.1.3" 891 | fast-glob "^3.2.12" 892 | glob-parent "^6.0.2" 893 | is-glob "^4.0.3" 894 | jiti "^1.18.2" 895 | lilconfig "^2.1.0" 896 | micromatch "^4.0.5" 897 | normalize-path "^3.0.0" 898 | object-hash "^3.0.0" 899 | picocolors "^1.0.0" 900 | postcss "^8.4.23" 901 | postcss-import "^15.1.0" 902 | postcss-js "^4.0.1" 903 | postcss-load-config "^4.0.1" 904 | postcss-nested "^6.0.1" 905 | postcss-selector-parser "^6.0.11" 906 | resolve "^1.22.2" 907 | sucrase "^3.32.0" 908 | 909 | thenify-all@^1.0.0: 910 | version "1.6.0" 911 | resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" 912 | integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA== 913 | dependencies: 914 | thenify ">= 3.1.0 < 4" 915 | 916 | "thenify@>= 3.1.0 < 4": 917 | version "3.3.1" 918 | resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f" 919 | integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== 920 | dependencies: 921 | any-promise "^1.0.0" 922 | 923 | to-regex-range@^5.0.1: 924 | version "5.0.1" 925 | resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" 926 | integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== 927 | dependencies: 928 | is-number "^7.0.0" 929 | 930 | ts-interface-checker@^0.1.9: 931 | version "0.1.13" 932 | resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" 933 | integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== 934 | 935 | util-deprecate@^1.0.2: 936 | version "1.0.2" 937 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 938 | integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== 939 | 940 | vite@^4.3.2: 941 | version "4.3.8" 942 | resolved "https://registry.yarnpkg.com/vite/-/vite-4.3.8.tgz#70cd6a294ab52d7fb8f37f5bc63d117dd19e9918" 943 | integrity sha512-uYB8PwN7hbMrf4j1xzGDk/lqjsZvCDbt/JC5dyfxc19Pg8kRm14LinK/uq+HSLNswZEoKmweGdtpbnxRtrAXiQ== 944 | dependencies: 945 | esbuild "^0.17.5" 946 | postcss "^8.4.23" 947 | rollup "^3.21.0" 948 | optionalDependencies: 949 | fsevents "~2.3.2" 950 | 951 | vue-demi@>=0.14.5: 952 | version "0.14.6" 953 | resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.6.tgz#dc706582851dc1cdc17a0054f4fec2eb6df74c92" 954 | integrity sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w== 955 | 956 | vue-router@^4.2.1: 957 | version "4.2.1" 958 | resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.2.1.tgz#f8ab85c89e74682cad71519480fdf2b855e8c9e0" 959 | integrity sha512-nW28EeifEp8Abc5AfmAShy5ZKGsGzjcnZ3L1yc2DYUo+MqbBClrRP9yda3dIekM4I50/KnEwo1wkBLf7kHH5Cw== 960 | dependencies: 961 | "@vue/devtools-api" "^6.5.0" 962 | 963 | vue@^3.2.47: 964 | version "3.3.4" 965 | resolved "https://registry.yarnpkg.com/vue/-/vue-3.3.4.tgz#8ed945d3873667df1d0fcf3b2463ada028f88bd6" 966 | integrity sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw== 967 | dependencies: 968 | "@vue/compiler-dom" "3.3.4" 969 | "@vue/compiler-sfc" "3.3.4" 970 | "@vue/runtime-dom" "3.3.4" 971 | "@vue/server-renderer" "3.3.4" 972 | "@vue/shared" "3.3.4" 973 | 974 | vuex-persistedstate@^4.1.0: 975 | version "4.1.0" 976 | resolved "https://registry.yarnpkg.com/vuex-persistedstate/-/vuex-persistedstate-4.1.0.tgz#127165f85f5b4534fb3170a5d3a8be9811bd2a53" 977 | integrity sha512-3SkEj4NqwM69ikJdFVw6gObeB0NHyspRYMYkR/EbhR0hbvAKyR5gksVhtAfY1UYuWUOCCA0QNGwv9pOwdj+XUQ== 978 | dependencies: 979 | deepmerge "^4.2.2" 980 | shvl "^2.0.3" 981 | 982 | wrappy@1: 983 | version "1.0.2" 984 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 985 | integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== 986 | 987 | yaml@^2.1.1: 988 | version "2.3.1" 989 | resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b" 990 | integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ== 991 | -------------------------------------------------------------------------------- /nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 8080; 3 | listen [::]:8080; 4 | server_name fastapi.vue.oauth2; 5 | 6 | # the backend hostname should be the same in docker-compose.yml 7 | location /api { 8 | proxy_pass http://backend:5001; 9 | proxy_set_header Host $host; 10 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 11 | } 12 | 13 | # the `root` should be the same as the one in your Dockerfile 14 | location / { 15 | root /usr/share/nginx/html; 16 | try_files $uri $uri/ /index.html =404; 17 | } 18 | 19 | 20 | } -------------------------------------------------------------------------------- /template.env: -------------------------------------------------------------------------------- 1 | # postgres settings 2 | POSTGRES_PASSWORD=fastapi_vue_password 3 | POSTGRES_USER=fastapi_vue_user 4 | POSTGRES_DB=fastapi_vue_prod 5 | 6 | # for development 7 | # POSTGRES_DB=fastapi_vue_dev 8 | --------------------------------------------------------------------------------