Skip to content

Commit 3e8b83a

Browse files
authored
[v1.2.0] Merge pull request #31 from KageRyo/develop
Update to RyoURL v1.2.0
2 parents 4aa8319 + fb25ed1 commit 3e8b83a

12 files changed

Lines changed: 206 additions & 167 deletions

README.md

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,58 @@
11
# RyoURL
22
RyoURL 是基於 Django 開發的短網址產生服務,使用者能夠創建短網址、查詢原始短網址及查看所有短網址。
3-
能夠搭配 [RyoUrl-frontend](https://github.com/KageRyo/RyoURL-frontend) 使用。
4-
3+
- 能夠搭配 [RyoUrl-frontend](https://github.com/KageRyo/RyoURL-frontend) 使用。
4+
- 能夠以 [RyoUrl-test](https://github.com/KageRyo/RyoURL-test) 進行單元測試。
5+
56
## API
67
RyoURL 分別提供了一支 POST 及兩支 GET 的 API 可以使用,其 Schema 格式如下:
78
```python
8-
orign_url : str # 原網址
9-
short_string : str # 為了短網址生成的字符串
10-
short_url : str # 短網址
11-
create_date : datetime.datetime # 創建日期
9+
orign_url : HttpUrl # 原網址
10+
short_string : str # 為了短網址生成的字符串
11+
short_url : HttpUrl # 短網址
12+
create_date : datetime.datetime # 創建日期
13+
expire_date: Optional[datetime.datetime] # 過期時間
14+
visit_count: int # 瀏覽次數
1215
```
1316
### POST
17+
- /api/register
18+
- 提供使用者註冊帳號
19+
- /api/login
20+
- 提供使用者登入
21+
- /api/logout
22+
- 提供使用者登出
1423
- /api/short-url
1524
- 提供使用者創建新的短網址
1625
- 創建邏輯為隨機生成 6 位數的英數亂碼,並檢查是否已經存在於資料庫,若無則建立其與原網址的關聯
17-
- /api/custom-url/
26+
- /api/custom-url
1827
- 提供使用者自訂新的短網址
1928
### GET
2029
- /api/ (root)
2130
- 可提供用於測試與 API 的連線狀態使用
2231
- /api/orign-url/{short_string}
2332
- 提供使用者以短網址查詢原網址
33+
- /api/all-myurl
34+
- 提供查詢目前自己建立的短網址
2435
- /api/all-url
2536
- 提供查詢目前所有已被建立的短網址
2637
### DELETE
2738
- /api/short-url/{short_string}
2839
- 提供使用者刪除指定的短網址
2940
- /api/expire-url
3041
- 刪除過期的短網址
31-
42+
43+
## 權限管理
44+
- 管理員 [2]
45+
- 擁有完整權限
46+
- 一般使用者 [1]
47+
- 產生隨機短網址
48+
- 產生自訂短網址
49+
- 以短網址查詢原網址
50+
- 查看自己產生的所有短網址
51+
- 刪除自己產生的短網址
52+
- 未登入的使用者 [0]
53+
- 產生隨機短網址
54+
- 以短網址查詢原網址
55+
3256
## 如何在本地架設 RyoURL 環境
3357
1. 您必須先將此專案 Clone 到您的環境
3458
```bash

RyoURL/RyoURL/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@
133133
# 密碼驗證
134134
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
135135

136+
AUTH_USER_MODEL = 'shortURL.User'
137+
136138
AUTH_PASSWORD_VALIDATORS = [
137139
{
138140
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',

RyoURL/shortURL/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
default_app_config = 'shortURL.apps.ShortURLConfig'

RyoURL/shortURL/admin.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
from django.contrib import admin
2-
from .models import Url
2+
from django.contrib.auth.admin import UserAdmin
3+
from .models import Url, User
34

45
class UrlAdmin(admin.ModelAdmin):
5-
list_display = ('orign_url', 'short_string', 'short_url', 'create_date', 'expire_date', 'visit_count')
6+
list_display = ('orign_url', 'short_string', 'short_url', 'create_date', 'expire_date', 'visit_count', 'user')
67

7-
admin.site.register(Url, UrlAdmin)
8+
class CustomUserAdmin(UserAdmin):
9+
list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff', 'user_type')
10+
list_filter = UserAdmin.list_filter + ('user_type',)
11+
fieldsets = UserAdmin.fieldsets + (
12+
('Additional Info', {'fields': ('user_type',)}),
13+
)
14+
15+
admin.site.register(Url, UrlAdmin)
16+
admin.site.register(User, CustomUserAdmin)

RyoURL/shortURL/api.py

Lines changed: 98 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
from django.shortcuts import get_object_or_404
1111
from django.utils.crypto import get_random_string
1212
from django.core.serializers.json import DjangoJSONEncoder
13+
from django.contrib.auth import authenticate, login, logout
14+
from functools import wraps
1315

14-
from .models import Url
16+
from .models import Url, User
1517

1618
# 自定義 JSON 編碼器類別
1719
class CustomJSONEncoder(DjangoJSONEncoder):
@@ -41,63 +43,124 @@ class UrlSchema(Schema):
4143
class ErrorSchema(Schema):
4244
message: str
4345

44-
# BASE62 編碼的函式
45-
def base62_encode(num):
46-
base62 = string.digits + string.ascii_letters
47-
if num == 0:
48-
return base62[0]
49-
array = []
50-
while num:
51-
num, rem = divmod(num, 62)
52-
array.append(base62[rem])
53-
array.reverse()
54-
return ''.join(array)
46+
# 定義 User 的 Schema 類別
47+
class UserSchema(Schema):
48+
username: str
49+
password: str
50+
51+
# 定義 User 註冊或登入回應的 Schema 類別
52+
class UserResponseSchema(Schema):
53+
username: str
54+
55+
# 一般使用者的權限檢查裝飾器
56+
def user_is_authenticated(func):
57+
@wraps(func)
58+
def wrapper(request, *args, **kwargs):
59+
if not request.user.is_authenticated:
60+
return api.create_response(request, {"message": "您必須登錄才能執行此操作。"}, status=403)
61+
return func(request, *args, **kwargs)
62+
return wrapper
63+
64+
# 管理員的權限檢查裝飾器
65+
def user_is_admin(func):
66+
@wraps(func)
67+
def wrapper(request, *args, **kwargs):
68+
if not request.user.is_authenticated or request.user.user_type != 2:
69+
return api.create_response(request, {"message": "您必須是管理員才能執行此操作。"}, status=403)
70+
return func(request, *args, **kwargs)
71+
return wrapper
72+
73+
# 使用者是否可以編輯短網址的裝飾器
74+
def user_can_edit_url(func):
75+
@wraps(func)
76+
def wrapper(request, short_string, *args, **kwargs):
77+
url = Url.objects.filter(short_string=short_string).first()
78+
if not url:
79+
return api.create_response(request, {"message": "找不到此短網址。"}, status=404)
80+
if url.user != request.user and request.user.user_type != 2:
81+
return api.create_response(request, {"message": "您沒有權限編輯此短網址。"}, status=403)
82+
return func(request, short_string, *args, **kwargs)
83+
return wrapper
5584

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

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

7098
# 建立短網址物件的函式
71-
def create_url_entry(orign_url: HttpUrl, short_string: str, short_url: HttpUrl, expire_date: Optional[datetime.datetime] = None) -> Url:
99+
def create_url_entry(orign_url: HttpUrl, short_string: str, short_url: HttpUrl, expire_date: Optional[datetime.datetime] = None, user=None) -> Url:
72100
return Url.objects.create(
73101
orign_url = str(orign_url),
74102
short_string = short_string,
75103
short_url = str(short_url),
76104
create_date = datetime.datetime.now(),
77-
expire_date = expire_date
105+
expire_date = expire_date,
106+
user = user
78107
)
79108

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

114+
# POST : 註冊 API /register
115+
@api.post("register", response={200: UserResponseSchema, 400: ErrorSchema})
116+
def register_user(request, user_data: UserSchema):
117+
try:
118+
user = User.objects.create_user(
119+
username=user_data.username,
120+
password=user_data.password
121+
)
122+
return 200, {"username": user.username}
123+
except:
124+
return 400, {"message": "註冊失敗"}
125+
126+
# POST : 登入 API /login
127+
@api.post("login", response={200: UserResponseSchema, 400: ErrorSchema})
128+
def login_user(request, user_data: UserSchema):
129+
user = authenticate(
130+
username=user_data.username,
131+
password=user_data.password
132+
)
133+
if user:
134+
login(request, user)
135+
return 200, {"username": user.username}
136+
else:
137+
return 400, {"message": "登入失敗"}
138+
139+
# POST : 登出 API /logout
140+
@api.post("logout", response={200: ErrorSchema})
141+
@user_is_authenticated
142+
def logout_user(request):
143+
logout(request)
144+
return 200, {"message": "登出成功"}
145+
85146
# POST : 新增短網址 API /short_url
86147
@api.post("short-url", response={200: UrlSchema, 404: ErrorSchema})
87148
def create_short_url(request, orign_url: HttpUrl, expire_date: Optional[datetime.datetime] = None):
88-
short_string = generator_short_url(orign_url)
149+
short_string = generator_short_url()
89150
short_url = HttpUrl(handle_domain(request, short_string))
90-
url = create_url_entry(orign_url, short_string, short_url, expire_date)
151+
user = request.user if request.user.is_authenticated else None
152+
url = create_url_entry(orign_url, short_string, short_url, expire_date, user=user)
91153
return 200, url
92154

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

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

172+
# GET : 查詢自己所有短網址 API /all_myurl
173+
@api.get('all-myurl', response=List[UrlSchema])
174+
@user_is_authenticated
175+
def get_all_myurl(request):
176+
url = Url.objects.filter(user=request.user)
177+
return url
178+
109179
# GET : 查詢所有短網址 API /all_url
110180
@api.get('all-url', response=List[UrlSchema])
181+
@user_is_admin
111182
def get_all_url(request):
112183
url = Url.objects.all()
113184
return url
114185

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

122195
# DELETE : 刪除過期短網址 API /expire_url
123196
@api.delete('expire-url', response={200: ErrorSchema})
197+
@user_is_admin
124198
def delete_expire_url(request):
125199
url = Url.objects.filter(expire_date__lt=datetime.datetime.now())
126200
url.delete()
Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,61 @@
1-
# Generated by Django 4.2 on 2024-07-18 07:46
1+
# Generated by Django 4.2 on 2024-08-05 06:06
22

3+
import datetime
4+
from django.conf import settings
5+
import django.contrib.auth.models
6+
import django.contrib.auth.validators
37
from django.db import migrations, models
8+
import django.db.models.deletion
9+
import django.utils.timezone
410

511

612
class Migration(migrations.Migration):
713

814
initial = True
915

1016
dependencies = [
17+
('auth', '0012_alter_user_first_name_max_length'),
1118
]
1219

1320
operations = [
21+
migrations.CreateModel(
22+
name='User',
23+
fields=[
24+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
25+
('password', models.CharField(max_length=128, verbose_name='password')),
26+
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
27+
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
28+
('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')),
29+
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
30+
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
31+
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
32+
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
33+
('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')),
34+
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
35+
('user_type', models.IntegerField(choices=[(1, '一般使用者'), (2, '管理員')], default=1)),
36+
('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')),
37+
('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')),
38+
],
39+
options={
40+
'verbose_name': 'user',
41+
'verbose_name_plural': 'users',
42+
'abstract': False,
43+
},
44+
managers=[
45+
('objects', django.contrib.auth.models.UserManager()),
46+
],
47+
),
1448
migrations.CreateModel(
1549
name='Url',
1650
fields=[
1751
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18-
('oriUrl', models.CharField(max_length=200)),
19-
('srtUrl', models.CharField(max_length=200)),
20-
('creDate', models.DateTimeField(verbose_name='創建日期')),
52+
('orign_url', models.URLField()),
53+
('short_string', models.CharField(default='NULL', max_length=10, unique=True)),
54+
('short_url', models.URLField()),
55+
('create_date', models.DateTimeField(default=datetime.datetime.now)),
56+
('expire_date', models.DateTimeField(blank=True, null=True)),
57+
('visit_count', models.IntegerField(default=0)),
58+
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
2159
],
2260
),
2361
]

RyoURL/shortURL/migrations/0002_alter_url_credate_alter_url_oriurl_alter_url_srturl.py

Lines changed: 0 additions & 29 deletions
This file was deleted.

0 commit comments

Comments
 (0)