Skip to content

Commit

Permalink
[v1.2.0] Merge pull request #31 from KageRyo/develop
Browse files Browse the repository at this point in the history
Update to RyoURL v1.2.0
  • Loading branch information
KageRyo authored Aug 5, 2024
2 parents 4aa8319 + fb25ed1 commit 3e8b83a
Show file tree
Hide file tree
Showing 12 changed files with 206 additions and 167 deletions.
40 changes: 32 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,58 @@
# RyoURL
RyoURL 是基於 Django 開發的短網址產生服務,使用者能夠創建短網址、查詢原始短網址及查看所有短網址。
能夠搭配 [RyoUrl-frontend](https://github.com/KageRyo/RyoURL-frontend) 使用。

- 能夠搭配 [RyoUrl-frontend](https://github.com/KageRyo/RyoURL-frontend) 使用。
- 能夠以 [RyoUrl-test](https://github.com/KageRyo/RyoURL-test) 進行單元測試。

## API
RyoURL 分別提供了一支 POST 及兩支 GET 的 API 可以使用,其 Schema 格式如下:
```python
orign_url : str # 原網址
short_string : str # 為了短網址生成的字符串
short_url : str # 短網址
create_date : datetime.datetime # 創建日期
orign_url : HttpUrl # 原網址
short_string : str # 為了短網址生成的字符串
short_url : HttpUrl # 短網址
create_date : datetime.datetime # 創建日期
expire_date: Optional[datetime.datetime] # 過期時間
visit_count: int # 瀏覽次數
```
### POST
- /api/register
- 提供使用者註冊帳號
- /api/login
- 提供使用者登入
- /api/logout
- 提供使用者登出
- /api/short-url
- 提供使用者創建新的短網址
- 創建邏輯為隨機生成 6 位數的英數亂碼,並檢查是否已經存在於資料庫,若無則建立其與原網址的關聯
- /api/custom-url/
- /api/custom-url
- 提供使用者自訂新的短網址
### GET
- /api/ (root)
- 可提供用於測試與 API 的連線狀態使用
- /api/orign-url/{short_string}
- 提供使用者以短網址查詢原網址
- /api/all-myurl
- 提供查詢目前自己建立的短網址
- /api/all-url
- 提供查詢目前所有已被建立的短網址
### DELETE
- /api/short-url/{short_string}
- 提供使用者刪除指定的短網址
- /api/expire-url
- 刪除過期的短網址


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

## 如何在本地架設 RyoURL 環境
1. 您必須先將此專案 Clone 到您的環境
```bash
Expand Down
2 changes: 2 additions & 0 deletions RyoURL/RyoURL/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@
# 密碼驗證
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators

AUTH_USER_MODEL = 'shortURL.User'

AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
Expand Down
1 change: 1 addition & 0 deletions RyoURL/shortURL/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
default_app_config = 'shortURL.apps.ShortURLConfig'
15 changes: 12 additions & 3 deletions RyoURL/shortURL/admin.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
from django.contrib import admin
from .models import Url
from django.contrib.auth.admin import UserAdmin
from .models import Url, User

class UrlAdmin(admin.ModelAdmin):
list_display = ('orign_url', 'short_string', 'short_url', 'create_date', 'expire_date', 'visit_count')
list_display = ('orign_url', 'short_string', 'short_url', 'create_date', 'expire_date', 'visit_count', 'user')

admin.site.register(Url, UrlAdmin)
class CustomUserAdmin(UserAdmin):
list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff', 'user_type')
list_filter = UserAdmin.list_filter + ('user_type',)
fieldsets = UserAdmin.fieldsets + (
('Additional Info', {'fields': ('user_type',)}),
)

admin.site.register(Url, UrlAdmin)
admin.site.register(User, CustomUserAdmin)
122 changes: 98 additions & 24 deletions RyoURL/shortURL/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
from django.shortcuts import get_object_or_404
from django.utils.crypto import get_random_string
from django.core.serializers.json import DjangoJSONEncoder
from django.contrib.auth import authenticate, login, logout
from functools import wraps

from .models import Url
from .models import Url, User

# 自定義 JSON 編碼器類別
class CustomJSONEncoder(DjangoJSONEncoder):
Expand Down Expand Up @@ -41,63 +43,124 @@ class UrlSchema(Schema):
class ErrorSchema(Schema):
message: str

# BASE62 編碼的函式
def base62_encode(num):
base62 = string.digits + string.ascii_letters
if num == 0:
return base62[0]
array = []
while num:
num, rem = divmod(num, 62)
array.append(base62[rem])
array.reverse()
return ''.join(array)
# 定義 User 的 Schema 類別
class UserSchema(Schema):
username: str
password: str

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

# 一般使用者的權限檢查裝飾器
def user_is_authenticated(func):
@wraps(func)
def wrapper(request, *args, **kwargs):
if not request.user.is_authenticated:
return api.create_response(request, {"message": "您必須登錄才能執行此操作。"}, status=403)
return func(request, *args, **kwargs)
return wrapper

# 管理員的權限檢查裝飾器
def user_is_admin(func):
@wraps(func)
def wrapper(request, *args, **kwargs):
if not request.user.is_authenticated or request.user.user_type != 2:
return api.create_response(request, {"message": "您必須是管理員才能執行此操作。"}, status=403)
return func(request, *args, **kwargs)
return wrapper

# 使用者是否可以編輯短網址的裝飾器
def user_can_edit_url(func):
@wraps(func)
def wrapper(request, short_string, *args, **kwargs):
url = Url.objects.filter(short_string=short_string).first()
if not url:
return api.create_response(request, {"message": "找不到此短網址。"}, status=404)
if url.user != request.user and request.user.user_type != 2:
return api.create_response(request, {"message": "您沒有權限編輯此短網址。"}, status=403)
return func(request, short_string, *args, **kwargs)
return wrapper

# 產生隨機短網址的函式
def generator_short_url(orign_url: str, length = 6):
hash_value = abs(hash(orign_url)) # 取得原網址的 hash 值
encode = base62_encode(hash_value) # 將 hash 值轉換為 BASE62 編碼
if len(encode) < length:
encode += get_random_string(length - len(encode), string.ascii_letters + string.digits)
return encode
return encode[:length]
def generator_short_url(length = 6):
char = string.ascii_letters + string.digits
while True:
short_url = ''.join(random.choices(char, k=length))
if not Url.objects.filter(short_url=short_url).exists():
return short_url # 如果短網址不存在 DB 中,則回傳此短網址

# 處理短網址域名的函式
def handle_domain(request, short_string):
domain = request.build_absolute_uri('/')[:-1].strip('/')
return f'{domain}/{short_string}'

# 建立短網址物件的函式
def create_url_entry(orign_url: HttpUrl, short_string: str, short_url: HttpUrl, expire_date: Optional[datetime.datetime] = None) -> Url:
def create_url_entry(orign_url: HttpUrl, short_string: str, short_url: HttpUrl, expire_date: Optional[datetime.datetime] = None, user=None) -> Url:
return Url.objects.create(
orign_url = str(orign_url),
short_string = short_string,
short_url = str(short_url),
create_date = datetime.datetime.now(),
expire_date = expire_date
expire_date = expire_date,
user = user
)

# GET : 首頁 API /
@api.get("/", response={200: ErrorSchema})
def index(request):
return 200, {"message": "已與 RyoURL 建立連線。"}

# POST : 註冊 API /register
@api.post("register", response={200: UserResponseSchema, 400: ErrorSchema})
def register_user(request, user_data: UserSchema):
try:
user = User.objects.create_user(
username=user_data.username,
password=user_data.password
)
return 200, {"username": user.username}
except:
return 400, {"message": "註冊失敗"}

# POST : 登入 API /login
@api.post("login", response={200: UserResponseSchema, 400: ErrorSchema})
def login_user(request, user_data: UserSchema):
user = authenticate(
username=user_data.username,
password=user_data.password
)
if user:
login(request, user)
return 200, {"username": user.username}
else:
return 400, {"message": "登入失敗"}

# POST : 登出 API /logout
@api.post("logout", response={200: ErrorSchema})
@user_is_authenticated
def logout_user(request):
logout(request)
return 200, {"message": "登出成功"}

# POST : 新增短網址 API /short_url
@api.post("short-url", response={200: UrlSchema, 404: ErrorSchema})
def create_short_url(request, orign_url: HttpUrl, expire_date: Optional[datetime.datetime] = None):
short_string = generator_short_url(orign_url)
short_string = generator_short_url()
short_url = HttpUrl(handle_domain(request, short_string))
url = create_url_entry(orign_url, short_string, short_url, expire_date)
user = request.user if request.user.is_authenticated else None
url = create_url_entry(orign_url, short_string, short_url, expire_date, user=user)
return 200, url

# POST : 新增自訂短網址 API /custom_url
@api.post("custom-url", response={200: UrlSchema, 403: ErrorSchema})
@user_is_authenticated
def create_custom_url(request, orign_url: HttpUrl, short_string: str, expire_date: Optional[datetime.datetime] = None):
short_url = HttpUrl(handle_domain(request, short_string))
if Url.objects.filter(short_url=str(short_url)).exists():
return 403, {"message": "自訂短網址已存在,請更換其他短網址。"}
else:
url = create_url_entry(orign_url, short_string, short_url, expire_date)
url = create_url_entry(orign_url, short_string, short_url, expire_date, user=request.user)
return 200, url

# GET : 以縮短網址字符查詢原網址 API /orign_url/{short_string}
Expand All @@ -106,21 +169,32 @@ def get_short_url(request, short_string: str):
url = get_object_or_404(Url, short_string=short_string)
return 200, url

# GET : 查詢自己所有短網址 API /all_myurl
@api.get('all-myurl', response=List[UrlSchema])
@user_is_authenticated
def get_all_myurl(request):
url = Url.objects.filter(user=request.user)
return url

# GET : 查詢所有短網址 API /all_url
@api.get('all-url', response=List[UrlSchema])
@user_is_admin
def get_all_url(request):
url = Url.objects.all()
return url

# DELETE : 刪除短網址 API /short_url/{short_string}
@api.delete('short-url/{short_string}', response={200: ErrorSchema})
@user_is_authenticated
@user_can_edit_url
def delete_short_url(request, short_string: str):
url = get_object_or_404(Url, short_string=short_string)
url.delete()
return 200, {"message": "成功刪除!"}

# DELETE : 刪除過期短網址 API /expire_url
@api.delete('expire-url', response={200: ErrorSchema})
@user_is_admin
def delete_expire_url(request):
url = Url.objects.filter(expire_date__lt=datetime.datetime.now())
url.delete()
Expand Down
46 changes: 42 additions & 4 deletions RyoURL/shortURL/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,61 @@
# Generated by Django 4.2 on 2024-07-18 07:46
# Generated by Django 4.2 on 2024-08-05 06:06

import datetime
from django.conf import settings
import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone


class Migration(migrations.Migration):

initial = True

dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]

operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('user_type', models.IntegerField(choices=[(1, '一般使用者'), (2, '管理員')], default=1)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='Url',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('oriUrl', models.CharField(max_length=200)),
('srtUrl', models.CharField(max_length=200)),
('creDate', models.DateTimeField(verbose_name='創建日期')),
('orign_url', models.URLField()),
('short_string', models.CharField(default='NULL', max_length=10, unique=True)),
('short_url', models.URLField()),
('create_date', models.DateTimeField(default=datetime.datetime.now)),
('expire_date', models.DateTimeField(blank=True, null=True)),
('visit_count', models.IntegerField(default=0)),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

This file was deleted.

Loading

0 comments on commit 3e8b83a

Please sign in to comment.