From 537476857205753916e02875e97f500508b63d42 Mon Sep 17 00:00:00 2001 From: EnLiao Date: Thu, 2 Oct 2025 22:08:42 +0800 Subject: [PATCH 01/15] feat: add user models --- user/models.py | 46 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/user/models.py b/user/models.py index 71a8362..80cb8d6 100644 --- a/user/models.py +++ b/user/models.py @@ -1,3 +1,47 @@ +import uuid from django.db import models +from django.contrib.auth.models import AbstractUser +from django.utils import timezone +from django.conf import settings -# Create your models here. +class User(AbstractUser): + """ + 自訂使用者: + - 主鍵改成 UUID + - 保留 username(你表格有 username) + - 新增 real_name、identity + - email 設為 unique(和你需求一致) + """ + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + + # Django AbstractUser 已有: username, password, email(非唯一), first_name, last_name, is_staff... + email = models.EmailField(max_length=254, unique=True) + real_name = models.CharField(max_length=150) + + class Identity(models.TextChoices): + TEACHER = 'teacher', 'Teacher' + ADMIN = 'admin', 'Admin' + STUDENT = 'student', 'Student' + identity = models.CharField(max_length=16, choices=Identity.choices, default=Identity.STUDENT) + + # 對齊你的欄位:date_joined & last_login 其實 AbstractUser 已內建 + # date_joined = models.DateTimeField(default=timezone.now) + # last_login = models.DateTimeField(blank=True, null=True) + + def __str__(self): + return f"{self.username} ({self.identity})" + + +class UserProfile(models.Model): + """ + 對應 user_profiles 表 + """ + user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, primary_key=True) + student_id = models.CharField(max_length=50, unique=True, null=True, blank=True) + bio = models.TextField(blank=True) + avatar = models.ImageField(upload_to='avatars/', null=True, blank=True) # 需要 Pillow + email_verified = models.BooleanField(default=False) + updated_at = models.DateTimeField(default=timezone.now) + + def __str__(self): + return f"Profile<{self.user.username}>" \ No newline at end of file From 9fdb60309934bed55f47fdf9e46615ca65fb65f1 Mon Sep 17 00:00:00 2001 From: EnLiao Date: Fri, 3 Oct 2025 10:45:31 +0800 Subject: [PATCH 02/15] feat: add some url at setting.py --- back_end/settings.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/back_end/settings.py b/back_end/settings.py index e0ff5af..591a75f 100644 --- a/back_end/settings.py +++ b/back_end/settings.py @@ -39,6 +39,13 @@ 'django.contrib.staticfiles', ] +AUTH_USER_MODEL = 'user.User' +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ) +} + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', From 5509c44c80b5d9aa779542fc473bb60b048a8c24 Mon Sep 17 00:00:00 2001 From: EnLiao Date: Fri, 3 Oct 2025 10:59:08 +0800 Subject: [PATCH 03/15] feat: add user/admin.py --- user/admin.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/user/admin.py b/user/admin.py index 8c38f3f..79bc766 100644 --- a/user/admin.py +++ b/user/admin.py @@ -1,3 +1,25 @@ from django.contrib import admin +from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin +from .models import User, UserProfile -# Register your models here. +@admin.register(User) +class UserAdmin(DjangoUserAdmin): + fieldsets = ( + (None, {'fields': ('username', 'password')}), + ('Personal info', {'fields': ('real_name', 'email')}), + ('Roles', {'fields': ('identity',)}), + ('Permissions', {'fields': ('is_active','is_staff','is_superuser','groups','user_permissions')}), + ('Important dates', {'fields': ('last_login','date_joined')}), + ) + add_fieldsets = ( + (None, {'classes': ('wide',), + 'fields': ('username','email','real_name','identity','password1','password2')}), + ) + list_display = ('username','email','real_name','identity','is_staff') + search_fields = ('username','email','real_name') + ordering = ('date_joined',) + +@admin.register(UserProfile) +class UserProfileAdmin(admin.ModelAdmin): + list_display = ('user','student_id','email_verified','updated_at') + search_fields = ('user__username','student_id') \ No newline at end of file From 614723704bbac50d5e6d821ac526a0c76d998c6b Mon Sep 17 00:00:00 2001 From: EnLiao Date: Fri, 3 Oct 2025 11:19:54 +0800 Subject: [PATCH 04/15] feat: add installed app at setting.py --- back_end/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/back_end/settings.py b/back_end/settings.py index 591a75f..7aea768 100644 --- a/back_end/settings.py +++ b/back_end/settings.py @@ -37,6 +37,7 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'user.apps.UserConfig', ] AUTH_USER_MODEL = 'user.User' From cfdec313d79425516f4874fdc2d1d89e855e8b36 Mon Sep 17 00:00:00 2001 From: EnLiao Date: Fri, 3 Oct 2025 11:24:16 +0800 Subject: [PATCH 05/15] feat: add migration and make migration/_pycache_ into gitignore --- .gitignore | 1 + back_end/__pycache__/settings.cpython-310.pyc | Bin 2234 -> 2411 bytes user/migrations/0001_initial.py | 60 ++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 user/migrations/0001_initial.py diff --git a/.gitignore b/.gitignore index 3c8a5fc..fa94ace 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ env/ db.sqlite3 media/ staticfiles/ +*/migrations/__pycache__/ # Tools/IDE .vscode/ diff --git a/back_end/__pycache__/settings.cpython-310.pyc b/back_end/__pycache__/settings.cpython-310.pyc index 63e83be9599902b2d106df3f477eca7c0e9ad281..bdc33ed655aeaa9ceafe8f1863b2406656e4f81d 100644 GIT binary patch delta 392 zcmdlb_*#fBpO=@50SFd3+|N+royaG{xM`xcd3_376n6@H3P&bm6i+%s3g;rmDBcvV zD83YKAde*lB*wdlF_STcZw^a3V-$Z1f0RIqK#E{0TZ)i0LyGVmCZJBi6p<8Bpc;`B zF)%3(CM7Z%(^;Z~QY523CqQdpuyfmkd>I!Zi6CQ2ejHkd(^QEub4Rz^nl$uUgR z>Z?Rbi&KmA5(^57^+JIx=ls01%=9Wwuqa62mYj>Ln`5X?NW5cch=;3Rh^Mn-h^N0_ zyt9vEaIkBzCS#RlQEG8Xd|FXrZfbdcQFeTBW^O@FYF2rPUSerUMrvM3W^!UlW`3TY zS9pjciiqas&rBzn7`Zp^VRd6+`}OW*jmijk3Rat711 z%{9y?nHae?yRo@3GKx>`WzU~1%dv`03Fzb^rO9VGyhOZ!Ocn+n9sy<+b{1w9CJt5( J&dG+HRsdvoG7ta& diff --git a/user/migrations/0001_initial.py b/user/migrations/0001_initial.py new file mode 100644 index 0000000..e986ccd --- /dev/null +++ b/user/migrations/0001_initial.py @@ -0,0 +1,60 @@ +# Generated by Django 5.2.6 on 2025-10-03 03:21 + +import django.contrib.auth.models +import django.contrib.auth.validators +import django.db.models.deletion +import django.utils.timezone +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('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')), + ('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')), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('email', models.EmailField(max_length=254, unique=True)), + ('real_name', models.CharField(max_length=150)), + ('identity', models.CharField(choices=[('teacher', 'Teacher'), ('admin', 'Admin'), ('student', 'Student')], default='student', max_length=16)), + ('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='UserProfile', + fields=[ + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), + ('student_id', models.CharField(blank=True, max_length=50, null=True, unique=True)), + ('bio', models.TextField(blank=True)), + ('avatar', models.ImageField(blank=True, null=True, upload_to='avatars/')), + ('email_verified', models.BooleanField(default=False)), + ('updated_at', models.DateTimeField(default=django.utils.timezone.now)), + ], + ), + ] From 77db3b1e55454e5157677baf8eabe3d1b268b4dc Mon Sep 17 00:00:00 2001 From: EnLiao Date: Fri, 3 Oct 2025 11:26:04 +0800 Subject: [PATCH 06/15] feat: add user/serializers.py --- user/serializers.py | 56 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 user/serializers.py diff --git a/user/serializers.py b/user/serializers.py new file mode 100644 index 0000000..ec556d0 --- /dev/null +++ b/user/serializers.py @@ -0,0 +1,56 @@ +from rest_framework import serializers +from django.contrib.auth import get_user_model + +User = get_user_model() + +class RegisterSerializer(serializers.ModelSerializer): + student_id = serializers.CharField(required=False, allow_blank=True, allow_null=True) + bio = serializers.CharField(required=False, allow_blank=True) + + class Meta: + model = User + fields = ['id','username','email','password','real_name','identity','student_id','bio'] + extra_kwargs = {'password': {'write_only': True}} + + def create(self, validated_data): + student_id = validated_data.pop('student_id', None) + bio = validated_data.pop('bio', '') + password = validated_data.pop('password') + + user = User(**validated_data) + user.set_password(password) + user.save() + + # 透過 signals 已有空 profile + profile = user.userprofile + if student_id is not None: + profile.student_id = student_id + if bio: + profile.bio = bio + profile.save() + return user + + +class MeSerializer(serializers.ModelSerializer): + class ProfileSerializer(serializers.Serializer): + student_id = serializers.CharField() + bio = serializers.CharField() + avatar = serializers.ImageField() + email_verified = serializers.BooleanField() + updated_at = serializers.DateTimeField() + + profile = serializers.SerializerMethodField() + + class Meta: + model = User + fields = ['id','username','email','real_name','identity','date_joined','last_login','profile'] + + def get_profile(self, obj): + p = obj.userprofile + return { + "student_id": p.student_id, + "bio": p.bio, + "avatar": p.avatar.url if p.avatar else None, + "email_verified": p.email_verified, + "updated_at": p.updated_at, + } \ No newline at end of file From 25b49c37bfd6eb10d3781bb56686fe655050f241 Mon Sep 17 00:00:00 2001 From: EnLiao Date: Fri, 3 Oct 2025 11:27:02 +0800 Subject: [PATCH 07/15] feat: add user/views.py --- user/views.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/user/views.py b/user/views.py index 91ea44a..2a8d12c 100644 --- a/user/views.py +++ b/user/views.py @@ -1,3 +1,18 @@ from django.shortcuts import render +from rest_framework import generics, permissions +from rest_framework.response import Response +from rest_framework.views import APIView +from django.contrib.auth import get_user_model +from .serializers import RegisterSerializer, MeSerializer -# Create your views here. +User = get_user_model() + +class RegisterView(generics.CreateAPIView): + queryset = User.objects.all() + serializer_class = RegisterSerializer + permission_classes = [permissions.AllowAny] + +class MeView(APIView): + permission_classes = [permissions.IsAuthenticated] + def get(self, request): + return Response(MeSerializer(request.user).data) From 4454553b80eab743a5c5f96b10c7a477f928545c Mon Sep 17 00:00:00 2001 From: EnLiao Date: Fri, 3 Oct 2025 12:07:54 +0800 Subject: [PATCH 08/15] feat: add user/urls and backend/urls and some media setting --- back_end/settings.py | 2 ++ back_end/urls.py | 8 +++++++- user/urls.py | 10 ++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 user/urls.py diff --git a/back_end/settings.py b/back_end/settings.py index 7aea768..df71348 100644 --- a/back_end/settings.py +++ b/back_end/settings.py @@ -123,6 +123,8 @@ # https://docs.djangoproject.com/en/5.2/howto/static-files/ STATIC_URL = 'static/' +MEDIA_URL = '/media/' +MEDIA_ROOT = BASE_DIR / 'media' # Default primary key field type # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field diff --git a/back_end/urls.py b/back_end/urls.py index 9020c65..1447f73 100644 --- a/back_end/urls.py +++ b/back_end/urls.py @@ -15,8 +15,14 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path +from django.urls import path, include +from django.conf import settings +from django.conf.urls.static import static urlpatterns = [ path('admin/', admin.site.urls), + path('user/', include('user.urls')), ] + +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/user/urls.py b/user/urls.py new file mode 100644 index 0000000..0a7e28a --- /dev/null +++ b/user/urls.py @@ -0,0 +1,10 @@ +from django.urls import path +from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView +from .views import RegisterView, MeView + +urlpatterns = [ + path('register/', RegisterView.as_view(), name='register'), + path('login/', TokenObtainPairView.as_view(), name='token_obtain_pair'), + path('refresh/', TokenRefreshView.as_view(), name='token_refresh'), + path('me/', MeView.as_view(), name='me'), +] From bcf43393e2c9d958b9adf6ed0df2eeca68852625 Mon Sep 17 00:00:00 2001 From: EnLiao Date: Fri, 3 Oct 2025 14:55:52 +0800 Subject: [PATCH 09/15] feat: add user/signals.py --- back_end/__pycache__/settings.cpython-310.pyc | Bin 2411 -> 2466 bytes user/apps.py | 3 +++ user/signals.py | 9 +++++++++ 3 files changed, 12 insertions(+) create mode 100644 user/signals.py diff --git a/back_end/__pycache__/settings.cpython-310.pyc b/back_end/__pycache__/settings.cpython-310.pyc index bdc33ed655aeaa9ceafe8f1863b2406656e4f81d..0738f60f5e08d845dcba468e69aa1b8546e5ec04 100644 GIT binary patch delta 127 zcmaDYv`CmYpO=@50SG>L-_MAc$ScdZZ=&`_p;WdMS!sqS$rQOLsTBEO22F*HkGk0S z+4XZ%Q!*3vZ?S^u$=U3RjMAHX*;N?DIDK7RJRRdhgM4mrL6|}Q{vne$aEP;M0F5uw Wn0%JQi!o%f1ZN-{3nK?32P*&p>LKU= delta 89 zcmZ1^{91@NpO=@50SFd3+|N*&$ScdZX`=Qf^GKp-2^|yGV5srx# Date: Fri, 3 Oct 2025 23:20:35 +0800 Subject: [PATCH 10/15] feat: update URL paths for user authentication endpoints --- back_end/urls.py | 2 +- user/urls.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/back_end/urls.py b/back_end/urls.py index 1447f73..fc6279f 100644 --- a/back_end/urls.py +++ b/back_end/urls.py @@ -21,7 +21,7 @@ urlpatterns = [ path('admin/', admin.site.urls), - path('user/', include('user.urls')), + path('auth/', include('user.urls')), ] if settings.DEBUG: diff --git a/user/urls.py b/user/urls.py index 0a7e28a..d785bd7 100644 --- a/user/urls.py +++ b/user/urls.py @@ -3,8 +3,8 @@ from .views import RegisterView, MeView urlpatterns = [ - path('register/', RegisterView.as_view(), name='register'), - path('login/', TokenObtainPairView.as_view(), name='token_obtain_pair'), + path('signup/', RegisterView.as_view(), name='register'), + path('session/', TokenObtainPairView.as_view(), name='token_obtain_pair'), path('refresh/', TokenRefreshView.as_view(), name='token_refresh'), path('me/', MeView.as_view(), name='me'), ] From 8213c9f229adbc8e43ab55f36c5ac140a1831952 Mon Sep 17 00:00:00 2001 From: EnLiao Date: Fri, 3 Oct 2025 23:59:45 +0800 Subject: [PATCH 11/15] docs: update README and add frontend API documentation --- README.md | 3 +- docs/frontend.MD | 229 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 docs/frontend.MD diff --git a/README.md b/README.md index f3a5de6..016a5ff 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ # NOJ new back_end -[README for developers](docs/developers.MD) \ No newline at end of file +[README for backend_developers](docs/developers.MD) +[README for frontend](docs/frontend.MD) \ No newline at end of file diff --git a/docs/frontend.MD b/docs/frontend.MD new file mode 100644 index 0000000..0961612 --- /dev/null +++ b/docs/frontend.MD @@ -0,0 +1,229 @@ +# 後端 API 說明(提供給前端) + +本專案後端使用 **Django + Django REST Framework + Simple JWT**,所有 API 採 **JSON** 傳輸。 + +> 本文件僅涵蓋「使用者認證」端點;**目前不啟用 reCAPTCHA**。 + +--- + +## 使用者註冊 API + +**路徑**:`POST /auth/signup/` + +**說明**:建立新使用者帳號。 + +### 請求格式(JSON) + +| 欄位名 | 型別 | 必填 | 說明 | +| ------------ | ------ | :-: | ----------------------------------------------------------- | +| `username` | string | ✅ | 最多 150 字元,**唯一** | +| `password` | string | ✅ | 使用者密碼(後端會雜湊儲存) | +| `email` | string | ✅ | 有效 Email,**唯一** | +| `real_name` | string | ✅ | 最多 150 字元 | +| `identity` | string | ✅ | 身分:`teacher` / `admin` / `student`(**必填**;模型預設仍為 `student`) | +| `student_id` | string | ❌ | 最多 50 字元(唯一)。若無可省略 | +| `bio` | string | ❌ | 自我介紹 | + +**請求範例**: + +```json +{ + "username": "momo", + "email": "momo@example.com", + "password": "abc12345", + "real_name": "小桃", + "identity": "student", + "student_id": "1160001", + "bio": "我愛娃娃" +} +``` + +**成功回應(201, JSON)** *回傳欄位可能依後端序列化調整,`password` 不會回傳: + +```json +{ + "id": "b1b6c3f4-6e9b-4bfa-9a78-9e1d1e6b8a21", + "username": "momo", + "email": "momo@example.com", + "real_name": "小桃", + "identity": "student", + "date_joined": "2025-10-03T07:15:14.123Z", + "last_login": null +} +``` + +**失敗回應(400, JSON)** + +* 已存在: + + ```json + {"username":["這個 username 在 user 已經存在。"],"email":["這個 email 在 user 已經存在。"]} + ``` +* `username` 未填: + + ```json + {"username":["此為必需欄位。"]} + ``` +* Email 格式錯誤: + + ```json + {"email":["請輸入有效的電子郵件地址。"]} + ``` +* `identity` 未填或值錯誤: + + ```json + {"identity":["此為必需欄位。或值必須為 teacher/admin/student 之一"]} + ``` + +**cURL 測試**: + +```bash +curl -X POST http://127.0.0.1:8000/auth/signup/ \ + -H "Content-Type: application/json" \ + -d '{ + "username":"momo", + "email":"momo@example.com", + "password":"abc12345", + "real_name":"小桃", + "identity":"student" + }' +``` + +--- + +## 使用者登入 API + +**路徑**:`POST /auth/login/` + +**說明**:以帳號密碼登入,取得 JWT。 + +### 請求格式(JSON) + +| 欄位名 | 型別 | 必填 | 說明 | +| ---------- | ------ | :-: | --------------------- | +| `username` | string | ✅ | 登入帳號(目前以 username 登入) | +| `password` | string | ✅ | 密碼 | + +**成功請求範例**: + +```json +{ "username": "momo", "password": "abc12345" } +``` + +**成功回應(200, JSON)** + +```json +{ "refresh": "", "access": "" } +``` + +**失敗回應** + +* 認證失敗(401): + + ```json + { "detail": "No active account found with the given credentials" } + ``` +* 缺少欄位(400): + + ```json + { "username": ["This field is required."] } + ``` + +**cURL 測試**: + +```bash +curl -X POST http://127.0.0.1:8000/auth/login/ \ + -H "Content-Type: application/json" \ + -d '{"username":"momo","password":"abc12345"}' +``` + +> 欄位說明: +> +> * `access`:短效 token,之後的 API 需帶 `Authorization: Bearer `。 +> * `refresh`:用於在 access 過期時換取新 access。 + +--- + +## 取得新 access token(refresh 機制) + +**路徑**:`POST /auth/refresh` + +**請求(JSON)** + +```json +{ "refresh": "<你的 refresh token>" } +``` + +**成功回應(200, JSON)** + +```json +{ "access": "" } +``` + +**失敗(refresh 無效/過期)**: + +```json +{ "detail": "Token is invalid or expired", "code": "token_not_valid" } +``` + +**cURL 測試**: + +```bash +curl -X POST http://127.0.0.1:8000/auth/refresh \ + -H "Content-Type: application/json" \ + -d '{"refresh":"<你的 refresh token>"}' +``` + +--- + +## 取得自己的資料 + +**路徑**:`GET /auth/me` + +**Headers**:`Authorization: Bearer ` + +**成功回應(200, JSON)**(示例) + +```json +{ + "id": "b1b6c3f4-6e9b-4bfa-9a78-9e1d1e6b8a21", + "username": "momo", + "email": "momo@example.com", + "real_name": "小桃", + "identity": "student", + "date_joined": "2025-10-03T07:15:14.123Z", + "last_login": "2025-10-03T07:20:18.000Z", + "profile": { + "student_id": "1160001", + "bio": "我愛娃娃", + "avatar": null, + "email_verified": false, + "updated_at": "2025-10-03T07:15:14.123Z" + } +} +``` + +**失敗回應(401)**: + +```json +{ "detail": "Authentication credentials were not provided." } +``` + +**cURL 測試**: + +```bash +curl http://127.0.0.1:8000/auth/me -H "Authorization: Bearer " +``` + +--- + +## 附錄:常見錯誤總表 + +| 錯誤情境 | HTTP 狀態 | 回應訊息 | +| ------------------- | :-----: | ----------------------------------------------------------------------------- | +| 帳密錯誤(登入) | 401 | `{"detail":"No active account found with the given credentials"}` | +| 未帶 access(/auth/me) | 401 | `{"detail":"Authentication credentials were not provided."}` | +| 帳號/信箱已存在(註冊) | 400 | `{"username":["這個 username 在 user 已經存在。"],"email":["這個 email 在 user 已經存在。"]}` | +| Email 格式錯誤(註冊) | 400 | `{"email":["請輸入有效的電子郵件地址。"]}` | +| 欄位缺漏(註冊/登入) | 400 | `{"username":["This field is required."]}` | +| `identity` 缺漏或值錯誤 | 400 | `{"identity":["此為必需欄位。或值必須為 teacher/admin/student 之一"]}` | From 4825cc016c617a32722c35af2cd97c73168c810a Mon Sep 17 00:00:00 2001 From: EnLiao Date: Sat, 4 Oct 2025 00:09:59 +0800 Subject: [PATCH 12/15] feat: update login endpoint path and enhance RegisterSerializer with identity field --- docs/frontend.MD | 4 ++-- user/serializers.py | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/frontend.MD b/docs/frontend.MD index 0961612..16f2964 100644 --- a/docs/frontend.MD +++ b/docs/frontend.MD @@ -93,7 +93,7 @@ curl -X POST http://127.0.0.1:8000/auth/signup/ \ ## 使用者登入 API -**路徑**:`POST /auth/login/` +**路徑**:`POST /auth/session/` **說明**:以帳號密碼登入,取得 JWT。 @@ -132,7 +132,7 @@ curl -X POST http://127.0.0.1:8000/auth/signup/ \ **cURL 測試**: ```bash -curl -X POST http://127.0.0.1:8000/auth/login/ \ +curl -X POST http://127.0.0.1:8000/auth/session/ \ -H "Content-Type: application/json" \ -d '{"username":"momo","password":"abc12345"}' ``` diff --git a/user/serializers.py b/user/serializers.py index ec556d0..c151410 100644 --- a/user/serializers.py +++ b/user/serializers.py @@ -1,9 +1,14 @@ from rest_framework import serializers from django.contrib.auth import get_user_model +from .models import UserProfile User = get_user_model() class RegisterSerializer(serializers.ModelSerializer): + identity = serializers.ChoiceField( + choices=[c.value for c in User.Identity], # ['teacher','admin','student'] + required=True + ) student_id = serializers.CharField(required=False, allow_blank=True, allow_null=True) bio = serializers.CharField(required=False, allow_blank=True) @@ -21,7 +26,6 @@ def create(self, validated_data): user.set_password(password) user.save() - # 透過 signals 已有空 profile profile = user.userprofile if student_id is not None: profile.student_id = student_id From d4abf011d9d6d50b751b34f5413078b99b4fd31d Mon Sep 17 00:00:00 2001 From: EnLiao Date: Sat, 4 Oct 2025 00:17:46 +0800 Subject: [PATCH 13/15] chore: delete pycache --- back_end/__pycache__/__init__.cpython-310.pyc | Bin 147 -> 0 bytes back_end/__pycache__/settings.cpython-310.pyc | Bin 2466 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 back_end/__pycache__/__init__.cpython-310.pyc delete mode 100644 back_end/__pycache__/settings.cpython-310.pyc diff --git a/back_end/__pycache__/__init__.cpython-310.pyc b/back_end/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index a0a683351ca97eceeac05eda5788501137068d36..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 147 zcmd1j<>g`kf}0+-^ch;zp);+4NRzgtEdwQrt)wP?WGuNUTA-1AukGvV;p)ms zqbG_$HC{1{a<^}^BnhE{4o7V z!^1XQ^cOe~XL5)~g2TIlDexQ?@Z1mlkK#{UKh;l}X*6eMkZ5MRoH^g+Q0fhD=1|(a zfHEjccyj@8^MK2voVkcDz>|n#T>cGDc(nLh?DFOkS~8c>GP;PV};fz-0wFD&dKX%P5*+z#bjZV!7nutSWb)`@fnB|;lEa)~F= z&tmWT)XT{|ScC&4f55wJ)c{5d#)bGujL1r#cwKOk+YOgvUb>cbwEIzz{i9703M@0>+sLm)sUfpla$LrnjV ztpInX4E+vO=L$R%5A3iP?ToWPJc`@H0B=0J)$bp-5OKF2w!n$))&uM0_NLoAvLC(r z`snUv@AirP^61{J?fZ{63!n7K_t+?8qP3A4x$S#lKw5H=)M#}UI|PgCGssv3dtrDo zz%;rtL!_8eNb`Au3k16h64z%XLMTi-E(eX)h7<>~Js42A2~g4ZI;0!TC)gOlZkDuN zwOKJNwP`%nYDT%J8s&P;Dppio*YpA(Jq|DptxjOO_}C9#Sd_Q}2ltOd*@j@iZ_-X0 zLH_p8P~X8oRyM}Ixdb5xAKL+zlPr_)MAqYU15iDsMGs~}83!*<$?GHBlvLro5$H45 z_vB)7`wvzcbmSr&&j>!AVtqK{OC0UAAMeIGB{&LogXLaJ^XP3qx&<=r^$Jbz=T#1S!PEq?rXIY z6HDcW&a&#kft5g+`a#^UdUWkH5684i;+BW(C5_242zTtE6P`{AYMrHpzH~^4??*X6 zj^Tp9(pqh=T+^6XQ>$8V0ro}objI_f9|kt1FcrvxokY-R4&u2|!#EYg5bMMxjvcr8 zvk`|ML{C0cQz!FfRjN)QMY=xzgdvs%L1~TjV2s>2XAzc_7f0tkf)e7q@V_EQ$uq>7tz$$h~ zD-a;nENfZu`ik+ee0=8!v-ALVhZnN!j;d>M=~+(KiVe-M_O4vr2fEpjx8~ZF*Etg6a?NDuK zY@t!F8&Pa z48%ILY`&t__L}ORW);CUC@fbs%dA6>Wx*q>y!oic(lKP15Sjs7xENb!^Hr@>RueEc z0%OP6%GBPjTf1egQu>(-?n3sGiSb$1*!O(7WpinP5ldgy-NxJ From ce2c4427ae59a755a4f65088e0156907e952c37c Mon Sep 17 00:00:00 2001 From: RokuSennyou <167313316+RokuSennyou@users.noreply.github.com> Date: Sat, 4 Oct 2025 00:23:18 +0800 Subject: [PATCH 14/15] Update user/serializers.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- user/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user/serializers.py b/user/serializers.py index c151410..b5b0670 100644 --- a/user/serializers.py +++ b/user/serializers.py @@ -26,7 +26,7 @@ def create(self, validated_data): user.set_password(password) user.save() - profile = user.userprofile + profile, _ = UserProfile.objects.get_or_create(user=user) if student_id is not None: profile.student_id = student_id if bio: From 1d304d51152de792caa3fff6df1af76bfe3fdb2c Mon Sep 17 00:00:00 2001 From: RokuSennyou <167313316+RokuSennyou@users.noreply.github.com> Date: Sat, 4 Oct 2025 00:23:47 +0800 Subject: [PATCH 15/15] Update user/serializers.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- user/serializers.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/user/serializers.py b/user/serializers.py index b5b0670..d4d0ba5 100644 --- a/user/serializers.py +++ b/user/serializers.py @@ -50,11 +50,20 @@ class Meta: fields = ['id','username','email','real_name','identity','date_joined','last_login','profile'] def get_profile(self, obj): - p = obj.userprofile - return { - "student_id": p.student_id, - "bio": p.bio, - "avatar": p.avatar.url if p.avatar else None, - "email_verified": p.email_verified, - "updated_at": p.updated_at, - } \ No newline at end of file + try: + p = obj.userprofile + return { + "student_id": p.student_id, + "bio": p.bio, + "avatar": p.avatar.url if p.avatar else None, + "email_verified": p.email_verified, + "updated_at": p.updated_at, + } + except UserProfile.DoesNotExist: + return { + "student_id": "", + "bio": "", + "avatar": None, + "email_verified": False, + "updated_at": None, + } \ No newline at end of file