Skip to content

Commit

Permalink
[v1.3.0] Merge pull request #41 from KageRyo/develop
Browse files Browse the repository at this point in the history
Update to RyoURL v1.3.0
  • Loading branch information
KageRyo authored Aug 16, 2024
2 parents 51914da + 4cf608d commit 31fb7e1
Show file tree
Hide file tree
Showing 16 changed files with 393 additions and 269 deletions.
Binary file removed .DS_Store
Binary file not shown.
107 changes: 57 additions & 50 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,63 +1,67 @@
# RyoURL

RyoURL 是基於 Django 開發的短網址產生服務,使用者能夠創建短網址、查詢原始短網址及查看所有短網址。
- 能夠搭配 [RyoUrl-frontend](https://github.com/KageRyo/RyoURL-frontend) 使用。
- 能夠以 [RyoUrl-test](https://github.com/KageRyo/RyoURL-test) 進行單元測試。

## API
RyoURL 分別提供了一支 POST 及兩支 GET 的 API 可以使用,其 Schema 格式如下:
```python
origin_url : HttpUrl # 原網址
short_string : str # 為了短網址生成的字符串
short_url : HttpUrl # 短網址
create_date : datetime.datetime # 創建日期
expire_date : Optional[datetime.datetime] # 過期時間
visit_count : int # 瀏覽次數
```
### 短網址
**POST**
- /api/short-url/short
- 提供使用者創建新的短網址
- 創建邏輯為隨機生成 6 位數的英數亂碼,並檢查是否已經存在於資料庫,若無則建立其與原網址的關聯
- /api/short-url/custom
- 提供使用者自訂新的短網址

**GET**
- /api/short-url/origin/{short_string}
- 提供使用者以短網址查詢原網址
- /api/short-url/all-my
- 提供查詢目前自己建立的短網址
- /api/short-url/all
- 提供查詢目前所有已被建立的短網址

**DELETE**
- /api/short-url/url/{short_string}
- 提供使用者刪除指定的短網址
- /api/short-url/expire
- 刪除過期的短網址

### 帳號管理
**POST**
- /api/auth/register
- 提供使用者註冊帳號
- /api/auth/login
- 提供使用者登入
- /api/auth/refresh-token
- 更新 TOKEN 權杖


RyoURL 提供了多種 API 端點,分為以下幾個類別:

### 短網址相關

#### 基本短網址功能 (/api/short-url/)

- **POST /short**
- 提供使用者創建新的隨機短網址
- 創建邏輯為隨機生成 6 位數的英數亂碼,並檢查是否已經存在於資料庫,若無則建立其與原網址的關聯
- **GET /origin/{short_string}**
- 提供使用者以短網址查詢原網址

#### 需要認證的短網址功能 (/api/auth-short-url/)

- **POST /custom**
- 提供使用者自訂新的短網址 (需要登入)
- **GET /all-my**
- 提供查詢目前自己建立的所有短網址 (需要登入)
- **DELETE /url/{short_string}**
- 提供使用者刪除指定的短網址 (需要登入)

### 認證相關 (/api/auth/)

- **POST /register**
- 提供使用者註冊帳號
- **POST /login**
- 提供使用者登入

### 用戶相關 (/api/user/)

- **GET /info**
- 獲取用戶資訊 (需要登入)
- **POST /refresh-token**
- 更新 TOKEN 權杖 (需要登入)

### 管理員功能 (/api/admin/)

- **GET /all-urls**
- 獲取所有 URL (需要管理員權限)
- **DELETE /expire-urls**
- 刪除過期 URL (需要管理員權限)
- **GET /users**
- 獲取所有用戶 (需要管理員權限)
- **PUT /user/{username}**
- 更新用戶類型 (需要管理員權限)
- **DELETE /user/{username}**
- 刪除用戶 (需要管理員權限)

## 權限管理

- 管理員 [2]
- 擁有完整權限
- 一般使用者 [1]
- 產生隨機短網址
- 產生自訂短網址
- 以短網址查詢原網址
- 查看自己產生的所有短網址
- 刪除自己產生的短網址
- 未登入的使用者 [0]
- 產生隨機短網址
- 以短網址查詢原網址


## 如何在本地架設 RyoURL 環境

1. 您必須先將此專案 Clone 到您的環境
```bash
git clone https://github.com/KageRyo/RyoURL.git
Expand All @@ -78,12 +82,15 @@ visit_count : int # 瀏覽次數
```

## 資料庫

此專案資料庫使用 PostgreSQL。當然,您能依照需求更換成其他關聯性資料庫,包含但不限於:MySQL、sqlite3 ......等,別忘了到 `settings.py` 中進行修改。

## 開源貢獻

歡迎對 RyoURL 做出任何形式的貢獻,您可以於 [Issues](https://github.com/KageRyo/RyoURL/issues) 提出問題或希望增加的功能,亦歡迎透過 [Pull Requests](https://github.com/KageRyo/RyoURL/pulls) 提交您的程式碼更動!

## LICENSE

此專案採用 [MIT License](License) 開源條款,
有任何問題也歡迎向我聯繫。
電子信箱:[kageryo@coderyo.com](mailto:kageryo@coderyo.com) 。
1 change: 1 addition & 0 deletions RyoURL/RyoURL/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
ALLOWED_HOSTS = [
'.ngrok-free.app',
'127.0.0.1',
'172.21.0.2',
'localhost',
'0.0.0.0'
]
Expand Down
21 changes: 12 additions & 9 deletions RyoURL/shortURL/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,26 @@

from pydantic import AnyUrl

from .apis.auth_api import auth_router, JWTAuth
from .apis.short_api import url_router
from .apis.auth import AdminJWTAuth, AnonymousAuth, JWTAuth
from .apis.auth_api import auth_router
from .apis.short_url_basic_api import short_url_router
from .apis.short_url_with_auth_api import auth_short_url_router
from .apis.user_api import user_router
from .apis.admin_api import admin_router

# 自定義 JSON 編碼器和渲染器
class CustomJSONEncoder(DjangoJSONEncoder):
def default(self, obj):
if isinstance(obj, AnyUrl):
return str(obj)
return super().default(obj)

# 自定義 JSON 渲染器類別
class CustomJSONRenderer(JSONRenderer):
encoder_class = CustomJSONEncoder

# 建立 API 實例,並設定自定義渲染器和 JWT 認證
api = NinjaAPI(renderer=CustomJSONRenderer(), auth=JWTAuth())
api = NinjaAPI(renderer=CustomJSONRenderer())

# 設定路由(API子路由)
api.add_router("/auth/", auth_router) # 帳號系統相關 API
api.add_router("/short-url/", url_router) # 短網址相關 API
api.add_router("/auth/", auth_router)
api.add_router("/short-url/", short_url_router, auth=[JWTAuth(), AnonymousAuth()])
api.add_router("/short-url-with-auth/", auth_short_url_router, auth=JWTAuth())
api.add_router("/user/", user_router, auth=JWTAuth())
api.add_router("/admin/", admin_router, auth=AdminJWTAuth())
5 changes: 4 additions & 1 deletion RyoURL/shortURL/apis/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
from .short_url_basic_api import short_url_router
from .short_url_with_auth_api import auth_short_url_router
from .auth_api import auth_router
from .short_api import url_router
from .user_api import user_router
from .admin_api import admin_router
39 changes: 39 additions & 0 deletions RyoURL/shortURL/apis/admin_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from http import HTTPStatus
from ninja import Router
from typing import List
import datetime
from django.shortcuts import get_object_or_404
from ninja.errors import HttpError

from ..models import Url, User
from .schemas import UrlSchema, ErrorSchema, UserInfoSchema

admin_router = Router()

@admin_router.get('all-urls', response={HTTPStatus.OK: List[UrlSchema], HTTPStatus.FORBIDDEN: ErrorSchema})
def get_all_url(request):
urls = Url.objects.all()
return HTTPStatus.OK, [UrlSchema.from_orm(url) for url in urls]

@admin_router.delete('expire-urls', response={HTTPStatus.NO_CONTENT: None, HTTPStatus.FORBIDDEN: ErrorSchema})
def delete_expire_url(request):
Url.objects.filter(expire_date__lt=datetime.datetime.now()).delete()
return HTTPStatus.NO_CONTENT, None

@admin_router.get('users', response={HTTPStatus.OK: List[UserInfoSchema], HTTPStatus.FORBIDDEN: ErrorSchema})
def get_all_users(request):
users = User.objects.all()
return HTTPStatus.OK, [UserInfoSchema(username=user.username, user_type=user.user_type) for user in users]

@admin_router.put('user/{username}', response={HTTPStatus.OK: UserInfoSchema, HTTPStatus.FORBIDDEN: ErrorSchema, HTTPStatus.NOT_FOUND: ErrorSchema})
def update_user_type(request, username: str, user_type: int):
user = get_object_or_404(User, username=username)
user.user_type = user_type
user.save()
return HTTPStatus.OK, UserInfoSchema(username=user.username, user_type=user.user_type)

@admin_router.delete('user/{username}', response={HTTPStatus.NO_CONTENT: None, HTTPStatus.FORBIDDEN: ErrorSchema, HTTPStatus.NOT_FOUND: ErrorSchema})
def delete_user(request, username: str):
user = get_object_or_404(User, username=username)
user.delete()
return HTTPStatus.NO_CONTENT, None
30 changes: 30 additions & 0 deletions RyoURL/shortURL/apis/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from ninja.security import HttpBearer
from rest_framework_simplejwt.tokens import AccessToken
from ninja.errors import HttpError
from http import HTTPStatus
from ..models import User

class JWTAuth(HttpBearer):
def authenticate(self, request, token):
if not token:
return None
try:
access_token = AccessToken(token)
user = User.objects.get(id=access_token['user_id'])
return {
'user': user,
'user_type': user.user_type
}
except Exception:
return None

class AnonymousAuth:
def __call__(self, request):
return {'user': None, 'user_type': 0}

class AdminJWTAuth(JWTAuth):
def authenticate(self, request, token):
auth = super().authenticate(request, token)
if not auth or auth['user_type'] != 2:
raise HttpError(HTTPStatus.FORBIDDEN, "需要管理員權限")
return auth
104 changes: 30 additions & 74 deletions RyoURL/shortURL/apis/auth_api.py
Original file line number Diff line number Diff line change
@@ -1,93 +1,49 @@
from ninja import Router, Schema
from ninja.security import HttpBearer
from http import HTTPStatus
from django.contrib.auth import authenticate

from ninja import Router
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework_simplejwt.exceptions import TokenError
from rest_framework_simplejwt.tokens import AccessToken, RefreshToken
from ninja.errors import HttpError
from django.db import IntegrityError

from .schemas import UserSchema, UserResponseSchema, ErrorSchema
from ..models import User
from .auth import JWTAuth

auth_router = Router(tags=["auth"])

# 定義 User 的 Schema 類別
class UserSchema(Schema):
username: str
password: str

# 定義 Token 的 Schema 類別
class TokenSchema(Schema):
refresh: str

# 定義 Token 回應的 Schema 類別
class TokenResponseSchema(Schema):
access: str

# 定義 User 註冊或登入回應的 Schema 類別
class UserResponseSchema(Schema):
username: str
access: str
refresh: str

# 定義錯誤回應的 Schema 類別
class ErrorSchema(Schema):
message: str

# JWT 認證類別
class JWTAuth(HttpBearer):
def authenticate(self, request, token):
try:
access_token = AccessToken(token)
user_id = access_token['user_id']
user = User.objects.get(id=user_id)
request.auth = {
'user': user,
'user_type': user.user_type
}
return request.auth
except Exception:
request.auth = None
return None

# POST : 註冊 API /api/auth/register
@auth_router.post("register", auth=None, response={201: UserResponseSchema, 400: ErrorSchema})
@auth_router.post("register", auth=None, response={HTTPStatus.CREATED: UserResponseSchema, HTTPStatus.BAD_REQUEST: ErrorSchema})
def register_user(request, user_data: UserSchema):
try:
user = User.objects.create_user(
username=user_data.username,
password=user_data.password
)
refresh = RefreshToken.for_user(user)
return 201, {
"username": user.username,
"access": str(refresh.access_token),
"refresh": str(refresh)
}
except:
return 400, {"message": "註冊失敗"}
except IntegrityError:
raise HttpError(HTTPStatus.BAD_REQUEST, "用戶名已存在")
except Exception as e:
raise HttpError(HTTPStatus.BAD_REQUEST, f"註冊失敗: {str(e)}")

refresh = RefreshToken.for_user(user)
return HTTPStatus.CREATED, UserResponseSchema(
username=user.username,
user_type=user.user_type,
access=str(refresh.access_token),
refresh=str(refresh)
)

# POST : 登入 API /api/auth/login
@auth_router.post("login", auth=None, response={200: UserResponseSchema, 400: ErrorSchema})
@auth_router.post("login", auth=None, response={HTTPStatus.OK: UserResponseSchema, HTTPStatus.BAD_REQUEST: ErrorSchema})
def login_user(request, user_data: UserSchema):
user = authenticate(
username=user_data.username,
password=user_data.password
)
if user:
refresh = RefreshToken.for_user(user)
return 200, {
"username": user.username,
"access": str(refresh.access_token),
"refresh": str(refresh)
}
else:
return 400, {"message": "登入失敗"}

# POST : 更新 TOKEN API /api/auth/refresh-token
@auth_router.post("refresh-token", auth=None, response={200: TokenResponseSchema, 400: ErrorSchema})
def refresh_token(request, token_data: TokenSchema):
try:
refresh = RefreshToken(token_data.refresh)
return 200, {"access": str(refresh.access_token)}
except TokenError:
return 400, {"message": "無效的更新權杖"}
if not user:
raise HttpError(HTTPStatus.BAD_REQUEST, "登入失敗")

refresh = RefreshToken.for_user(user)
return HTTPStatus.OK, UserResponseSchema(
username=user.username,
user_type=user.user_type,
access=str(refresh.access_token),
refresh=str(refresh)
)
Loading

0 comments on commit 31fb7e1

Please sign in to comment.