This commit is contained in:
2025-05-11 03:45:17 +08:00
commit 3d7660a24c
70 changed files with 13635 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,128 @@
<?php
namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\MemberProfile;
use App\Models\SocialAccount;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Laravel\Socialite\Facades\Socialite;
class SocialAuthController extends Controller
{
/**
* 重定向到 Google 登入頁面
*/
public function redirectToGoogle()
{
return Socialite::driver('google')
->scopes(['openid', 'profile', 'email'])
->with(['access_type' => 'offline', 'prompt' => 'consent']) // 這裡要求 prompt=consent 才能每次都獲取 refresh token
->stateless()
->redirect();
}
/**
* 處理 Google 回調
*/
public function handleGoogleCallback(Request $request)
{
try {
// 獲取 Google 用戶資訊
$googleUser = Socialite::driver('google')->stateless()->user();
// 查找相關的社交帳號
$socialAccount = SocialAccount::where('provider', 'google')
->where('provider_id', $googleUser->getId())
->first();
if ($socialAccount) {
// 已存在社交帳號,直接獲取用戶
$user = $socialAccount->user;
// 如果用戶不是會員,拒絕登入
if ($user->role !== 'member') {
return response()->json([
'status' => false,
'message' => '只有會員可以使用 Google 登入'
], 403);
}
} else {
// 檢查是否有相同 email 的用戶
$user = User::where('email', $googleUser->getEmail())->first();
if ($user) {
// 已存在用戶,但沒有連結社交帳號
// 檢查是否為會員
if ($user->role !== 'member') {
return response()->json([
'status' => false,
'message' => '只有會員可以使用 Google 登入'
], 403);
}
} else {
// 建立新用戶
$user = User::create([
'name' => $googleUser->getName(),
'email' => $googleUser->getEmail(),
'password' => Hash::make(Str::random(24)),
'role' => 'member', // 強制為會員角色
'is_active' => true,
]);
// 建立會員資料
try {
MemberProfile::create([
'user_id' => $user->id,
// 可以選擇性地從 Google 獲取更多資訊
]);
} catch (\Exception $e) {
// 記錄錯誤,但不中斷整個登入流程
\Log::error('Google 登入建立會員資料失敗: ' . $e->getMessage());
}
}
// 建立社交帳號連結
$socialAccountData = [
'user_id' => $user->id,
'provider' => 'google',
'provider_id' => $googleUser->getId(),
'provider_email' => $googleUser->getEmail(),
'access_token' => $googleUser->token,
'expires_in' => $googleUser->expiresIn ?? null,
];
// 確保如果有 refreshToken 就正確地儲存
if (!empty($googleUser->refreshToken)) {
$socialAccountData['refresh_token'] = $googleUser->refreshToken;
}
$socialAccount = SocialAccount::create($socialAccountData);
}
// 生成 Sanctum token
$token = $user->createToken('google-auth')->plainTextToken;
// 載入會員資料
$user->load('memberProfile');
return response()->json([
'status' => true,
'message' => 'Google 登入成功',
'data' => [
'user' => $user,
'token' => $token,
'token_type' => 'Bearer',
]
]);
} catch (\Exception $e) {
return response()->json([
'status' => false,
'message' => 'Google 登入失敗:' . $e->getMessage()
], 500);
}
}
}
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}
+132
View File
@@ -0,0 +1,132 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class AdminProfile extends Model
{
use HasFactory;
/**
* 與模型關聯的資料表
*
* @var string
*/
protected $table = 'admin_profiles';
/**
* 可以被批量賦值的屬性
*
* @var array
*/
protected $fillable = [
'user_id',
'position',
'department',
'permissions',
];
/**
* 應該被轉換的屬性
*
* @var array
*/
protected $casts = [
'permissions' => 'array',
];
/**
* 獲取擁有此管理員資料的用戶
*/
public function user()
{
return $this->belongsTo(User::class);
}
/**
* 檢查管理員是否擁有特定權限
*
* @param string $permission
* @return bool
*/
public function hasPermission($permission)
{
if (empty($this->permissions)) {
return false;
}
return in_array($permission, $this->permissions);
}
/**
* 添加權限給管理員
*
* @param string $permission
* @return void
*/
public function addPermission($permission)
{
$permissions = $this->permissions ?? [];
if (!in_array($permission, $permissions)) {
$permissions[] = $permission;
$this->permissions = $permissions;
$this->save();
}
}
/**
* 移除管理員的權限
*
* @param string $permission
* @return void
*/
public function removePermission($permission)
{
if (empty($this->permissions)) {
return;
}
$permissions = array_filter($this->permissions, function($p) use ($permission) {
return $p !== $permission;
});
$this->permissions = array_values($permissions);
$this->save();
}
/**
* 設定多個權限
*
* @param array $permissions
* @return void
*/
public function setPermissions(array $permissions)
{
$this->permissions = $permissions;
$this->save();
}
/**
* 清除所有權限
*
* @return void
*/
public function clearPermissions()
{
$this->permissions = [];
$this->save();
}
/**
* 獲取所有權限
*
* @return array
*/
public function getPermissions()
{
return $this->permissions ?? [];
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class CoachProfile extends Model
{
/**
* 可以批量分配的屬性
*
* @var array<int, string>
*/
protected $fillable = [
'user_id',
'bio',
'expertise',
'certification',
'experience',
'rating',
'availability'
];
/**
* 與用戶的關聯
*/
public function user()
{
return $this->belongsTo(User::class);
}
}
+133
View File
@@ -0,0 +1,133 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class MemberProfile extends Model
{
use HasFactory;
/**
* 與模型關聯的資料表
*
* @var string
*/
protected $table = 'member_profiles';
/**
* 可以被批量賦值的屬性
*
* @var array
*/
protected $fillable = [
'user_id', // 這個欄位必須包含在這裡
'birthday',
'gender',
'address',
'emergency_contact',
'emergency_phone',
];
/**
* 應該被轉換的屬性
*
* @var array
*/
protected $casts = [
'birthday' => 'date',
];
/**
* 獲取擁有此會員資料的用戶
*/
public function user()
{
return $this->belongsTo(User::class);
}
/**
* 獲取會員的訂閱記錄
*/
public function subscriptions()
{
return $this->hasMany(Subscription::class, 'user_id', 'user_id');
}
/**
* 獲取會員的活躍訂閱
*/
public function activeSubscription()
{
return $this->hasOne(Subscription::class, 'user_id', 'user_id')
->where('status', 'active')
->where('end_date', '>=', now())
->latest();
}
/**
* 獲取會員的教練
*/
public function coaches()
{
return $this->hasManyThrough(
CoachProfile::class,
'coach_member',
'member_id',
'user_id',
'user_id',
'coach_id'
);
}
/**
* 檢查會員是否有活躍訂閱
*
* @return bool
*/
public function hasActiveSubscription()
{
return $this->activeSubscription()->exists();
}
/**
* 取得會員年齡
*
* @return int|null
*/
public function getAge()
{
if (!$this->birthday) {
return null;
}
return now()->diffInYears($this->birthday);
}
/**
* 設定會員生日
*
* @param string|null $date
* @return void
*/
public function setBirthday($date)
{
$this->birthday = $date;
$this->save();
}
/**
* 更新會員緊急聯絡資訊
*
* @param string $contact
* @param string $phone
* @return void
*/
public function updateEmergencyContact($contact, $phone)
{
$this->emergency_contact = $contact;
$this->emergency_phone = $phone;
$this->save();
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Plan extends Model
{
//
}
+44
View File
@@ -0,0 +1,44 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class SocialAccount extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'user_id',
'provider',
'provider_id',
'provider_email',
'access_token',
'refresh_token',
'expires_in',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'access_token',
'refresh_token',
];
/**
* 取得此社交帳號的使用者
*/
public function user()
{
return $this->belongsTo(User::class);
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Subscription extends Model
{
//
}
+215
View File
@@ -0,0 +1,215 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
/**
* User 使用者模型
*
* 對應 users 資料表,並提供角色判斷、關聯資料取得等功能。
*/
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
/**
* 可批次賦值的欄位(對應 users 資料表)
* @var array<int, string>
*/
protected $fillable = [
'name', // 姓名
'email', // 電子郵件
'password', // 密碼
'phone', // 電話
'role', // 角色
'is_active', // 是否啟用
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
/**
* 隱藏於序列化時的欄位
* @var array<int, string>
*/
protected $hidden = [
'password', // 密碼
'remember_token', // 記住我 token
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
/**
* 欄位型別轉換
* @var array<string, string>
*/
protected $casts = [
'email_verified_at' => 'datetime', // 驗證時間
'password' => 'hashed', // 密碼雜湊
];
/**
* 判斷用戶是否為管理員
*/
/**
* 判斷用戶是否為管理員
* @return bool
*/
public function isAdmin()
{
return $this->role === 'admin';
}
/**
* 判斷用戶是否為教練
*/
/**
* 判斷用戶是否為教練
* @return bool
*/
public function isCoach()
{
return $this->role === 'coach';
}
/**
* 判斷用戶是否為一般會員
*/
/**
* 判斷用戶是否為一般會員
* @return bool
*/
public function isMember()
{
return $this->role === 'member';
}
/**
* 獲取用戶的管理員資料
*/
/**
* 取得用戶的管理員詳細資料(關聯 admin_profiles
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function adminProfile()
{
return $this->hasOne(AdminProfile::class);
}
/**
* 獲取用戶的教練資料
*/
/**
* 取得用戶的教練詳細資料(關聯 coach_profiles
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function coachProfile()
{
return $this->hasOne(CoachProfile::class);
}
/**
* 獲取用戶的會員資料
*/
/**
* 取得用戶的會員詳細資料(關聯 member_profiles
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function memberProfile()
{
return $this->hasOne(MemberProfile::class);
}
/**
* 獲取用戶的設定檔資料 (根據角色自動選擇)
*/
/**
* 取得用戶的詳細資料(依角色自動選擇對應 profile
* @return mixed
*/
public function profile()
{
if ($this->isAdmin()) {
return $this->adminProfile;
} elseif ($this->isCoach()) {
return $this->coachProfile;
} else {
return $this->memberProfile;
}
}
/**
* 獲取教練的會員 (僅適用於教練角色)
*/
/**
* 取得教練所帶的會員(僅教練角色適用)
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany|null
*/
public function members()
{
if (!$this->isCoach()) {
return null;
}
return $this->belongsToMany(User::class, 'coach_member', 'coach_id', 'member_id')
->where('role', 'member');
}
/**
* 獲取會員的教練 (僅適用於會員角色)
*/
/**
* 取得會員對應的教練(僅會員角色適用)
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany|null
*/
public function coaches()
{
if (!$this->isMember()) {
return null;
}
return $this->belongsToMany(User::class, 'coach_member', 'member_id', 'coach_id')
->where('role', 'coach');
}
/**
* 獲取用戶的訂閱
*/
/**
* 取得用戶的所有訂閱紀錄
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function subscriptions()
{
return $this->hasMany(Subscription::class);
}
/**
* 獲取用戶目前有效的訂閱
*/
/**
* 取得用戶目前有效的訂閱(狀態為 active 且未過期)
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function activeSubscription()
{
return $this->hasOne(Subscription::class)
->where('status', 'active')
->where('end_date', '>=', now())
->latest();
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
//
}
}