diff --git a/app/Http/Controllers/Web/AssistantController.php b/app/Http/Controllers/Web/AssistantController.php index 082ce5b..ab81b87 100644 --- a/app/Http/Controllers/Web/AssistantController.php +++ b/app/Http/Controllers/Web/AssistantController.php @@ -10,10 +10,7 @@ class AssistantController extends Controller /** * Display a listing of the resource. */ - public function index(Request $request) - { - - } + public function index(Request $request) {} /** * Store a newly created resource in storage. diff --git a/app/Http/Controllers/Web/AuthController.php b/app/Http/Controllers/Web/AuthController.php index dd2bb41..5e9a779 100644 --- a/app/Http/Controllers/Web/AuthController.php +++ b/app/Http/Controllers/Web/AuthController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Web; use App\Http\Controllers\Controller; +use App\Logic\OpenIDLogic; use App\Models\User; use GuzzleHttp\Client; use GuzzleHttp\Exception\ClientException; @@ -18,11 +19,7 @@ class AuthController extends Controller { public string $scopes = 'profile email realname openid'; - protected string $auth_url; - - protected string $token_url; - - protected string $user_url; + protected OpenIDLogic $openIDLogic; protected string $callback_url; @@ -33,24 +30,7 @@ class AuthController extends Controller */ public function __construct() { - $cache_key = 'oauth_discovery'; - - if (cache()->has($cache_key)) { - $oauth_discovery = cache()->get($cache_key); - } else { - // lock 防止重复请求 - $oauth_discovery = cache()->remember($cache_key, 3600, function () { - $client = new Client(); - $response = $client->get(config('oauth.discovery')); - - return json_decode($response->getBody(), true); - }); - } - - $this->auth_url = $oauth_discovery['authorization_endpoint']; - $this->token_url = $oauth_discovery['token_endpoint']; - $this->user_url = $oauth_discovery['userinfo_endpoint']; - $this->callback_url = route('oauth.callback'); + $this->openIDLogic = app(OpenIDLogic::class); } public function redirect(Request $request) @@ -65,7 +45,7 @@ public function redirect(Request $request) 'state' => $state, ]); - return redirect()->to($this->auth_url.'?'.$query); + return redirect()->to($this->openIDLogic->auth_url.'?'.$query); } /** @@ -87,7 +67,7 @@ public function callback(Request $request) $http = new Client; try { - $authorize = $http->post($this->token_url, [ + $authorize = $http->post($this->openIDLogic->token_url, [ 'form_params' => [ 'grant_type' => 'authorization_code', 'client_id' => config('oauth.client_id'), @@ -102,7 +82,7 @@ public function callback(Request $request) $authorize = json_decode($authorize->getBody()); - $oauth_user = $http->get($this->user_url, [ + $oauth_user = $http->get($this->openIDLogic->user_url, [ 'headers' => [ 'Accept' => 'application/json', 'Authorization' => 'Bearer '.$authorize->access_token, diff --git a/app/Http/Controllers/Web/ToolController.php b/app/Http/Controllers/Web/ToolController.php index 2be22d3..14dc294 100644 --- a/app/Http/Controllers/Web/ToolController.php +++ b/app/Http/Controllers/Web/ToolController.php @@ -8,9 +8,7 @@ use cebe\openapi\exceptions\TypeErrorException; use cebe\openapi\exceptions\UnresolvableReferenceException; use cebe\openapi\json\InvalidJsonPointerSyntaxException; -use cebe\openapi\Reader; use Illuminate\Http\Request; -use Illuminate\Support\Facades\Log; class ToolController extends Controller { @@ -28,18 +26,18 @@ public function index() * @throws UnresolvableReferenceException * @throws InvalidJsonPointerSyntaxException */ - public function getOpenAPI() { - $url = "http://127.0.0.1:8081/openapi.yml"; + public function getOpenAPI() + { + $url = 'http://127.0.0.1:8081/openapi.yml'; $file = file_get_contents($url); - -// $openAPI = new OpenAPI(); -// -// $document = $openAPI->parse($file); -// -// -// dd($document->openapi()); + // $openAPI = new OpenAPI(); + // + // $document = $openAPI->parse($file); + // + // + // dd($document->openapi()); } /** diff --git a/app/Http/Middleware/JWTMiddleware.php b/app/Http/Middleware/JWTMiddleware.php new file mode 100644 index 0000000..dc3dafd --- /dev/null +++ b/app/Http/Middleware/JWTMiddleware.php @@ -0,0 +1,60 @@ +getJwks(); + } catch (Exception $e) { + return response()->json(['error' => 'Failed to fetch JWKS data'], 500); + } + $keys = JWK::parseKeySet($jwks); + + $jwt = $request->bearerToken(); + + if (empty($jwt)) { + return response()->json(['error' => 'No token provided'], 401); + } + + $headers = new stdClass(); + + try { + // 解码并验证 JWT + $decoded = JWT::decode($jwt, $keys, $headers); + + if ($headers->typ != $token_type) { + return response()->json(['error' => 'Invalid token type, must be '.$token_type.', got '.$headers->typ], 401); + } + + $user = User::where('external_id', $decoded->sub)->firstOrCreate([ + 'external_id' => $decoded->sub, + 'name' => $decoded->name, + ]); + + Auth::guard('api')->loginUsingId($user->id, true); + } catch (Exception $e) { + return response()->json(['error' => 'Invalid token, '.$e->getMessage()], 401); + } + + return $next($request); + } +} diff --git a/app/LLM/Base.php b/app/LLM/Base.php index 7cb6273..9407b18 100644 --- a/app/LLM/Base.php +++ b/app/LLM/Base.php @@ -2,7 +2,4 @@ namespace App\LLM; -class Base -{ - -} +class Base {} diff --git a/app/Logic/OpenIDLogic.php b/app/Logic/OpenIDLogic.php new file mode 100644 index 0000000..d127f5f --- /dev/null +++ b/app/Logic/OpenIDLogic.php @@ -0,0 +1,56 @@ +has($cache_key)) { + $oauth_discovery = cache()->get($cache_key); + } else { + // lock 防止重复请求 + $oauth_discovery = cache()->remember($cache_key, 3600, function () use ($http) { + $response = $http->get(config('oauth.discovery')); + + return json_decode($response->getBody(), true); + }); + } + + $this->auth_url = $oauth_discovery['authorization_endpoint']; + $this->token_url = $oauth_discovery['token_endpoint']; + $this->user_url = $oauth_discovery['userinfo_endpoint']; + $this->jwks_url = $oauth_discovery['jwks_uri']; + + $cache_key = 'oauth_discovery_jwks'; + + if (cache()->has($cache_key)) { + $this->jwks = cache()->get($cache_key); + } else { + $this->jwks = cache()->remember($cache_key, 3600, function () use ($http) { + $response = $http->get($this->jwks_url); + + return json_decode($response->getBody(), true); + }); + } + + } +} diff --git a/app/Models/Assistant.php b/app/Models/Assistant.php index 72bc54a..1be4f78 100644 --- a/app/Models/Assistant.php +++ b/app/Models/Assistant.php @@ -2,10 +2,36 @@ namespace App\Models; -use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\HasMany; class Assistant extends Model { - use HasFactory; + protected $fillable = [ + 'name', + 'description', + 'user_id', + ]; + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function tools(): BelongsToMany + { + return $this->belongsToMany(Tool::class, 'assistant_tools'); + } + + public function chats(): HasMany + { + return $this->hasMany(Chat::class); + } + + public function chatHistories(): HasMany + { + return $this->hasMany(ChatHistory::class); + } } diff --git a/app/Models/AssistantTool.php b/app/Models/AssistantTool.php index 937516a..a522b74 100644 --- a/app/Models/AssistantTool.php +++ b/app/Models/AssistantTool.php @@ -2,10 +2,12 @@ namespace App\Models; -use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class AssistantTool extends Model { - use HasFactory; + protected $fillable = [ + 'assistant_id', + 'tool_id', + ]; } diff --git a/app/Models/Chat.php b/app/Models/Chat.php new file mode 100644 index 0000000..69dabbc --- /dev/null +++ b/app/Models/Chat.php @@ -0,0 +1,31 @@ +belongsTo(Assistant::class); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function histories(): HasMany + { + return $this->hasMany(ChatHistory::class); + } +} diff --git a/app/Models/ChatHistory.php b/app/Models/ChatHistory.php index 92d3495..46cb3d5 100644 --- a/app/Models/ChatHistory.php +++ b/app/Models/ChatHistory.php @@ -2,10 +2,22 @@ namespace App\Models; -use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; class ChatHistory extends Model { - use HasFactory; + protected $fillable = [ + 'chat_id', + 'content', + 'role', + 'input_tokens', + 'output_tokens', + 'total_tokens', + ]; + + public function chat(): BelongsTo + { + return $this->belongsTo(Chat::class); + } } diff --git a/app/Models/Tool.php b/app/Models/Tool.php index 729568a..a69c2f8 100644 --- a/app/Models/Tool.php +++ b/app/Models/Tool.php @@ -2,10 +2,41 @@ namespace App\Models; -use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Facades\Http; class Tool extends Model { - use HasFactory; + protected $fillable = [ + 'name', + 'description', + 'url', + 'api_key', + 'callback_url', + 'user_id', + ]; + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function functions(): HasMany + { + return $this->hasMany(ToolFunction::class); + } + + public function fetchFunctions(): void + { + $url = $this->url; + + $json = Http::get($url); + + $json = $json->json(); + + $this->callback_url = $json['callback_url']; + + } } diff --git a/app/Models/ToolFunction.php b/app/Models/ToolFunction.php index 7038aca..8f7818d 100644 --- a/app/Models/ToolFunction.php +++ b/app/Models/ToolFunction.php @@ -2,10 +2,41 @@ namespace App\Models; -use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; class ToolFunction extends Model { - use HasFactory; + protected $fillable = [ + 'tool_id', + 'name', + 'description', + 'parameters', + 'required', + ]; + + public function tool(): BelongsTo + { + return $this->belongsTo(Tool::class); + } + + public function getParametersAttribute($value) + { + return json_decode($value, true); + } + + public function getRequiredAttribute($value) + { + return json_decode($value, true); + } + + public function setParametersAttribute($value): void + { + $this->attributes['parameters'] = json_encode($value); + } + + public function setRequiredAttribute($value): void + { + $this->attributes['required'] = json_encode($value); + } } diff --git a/app/Models/User.php b/app/Models/User.php index def621f..0f126be 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -18,30 +18,31 @@ class User extends Authenticatable */ protected $fillable = [ 'name', - 'email', - 'password', + 'external_id', + // 'email', + // 'password', ]; - /** - * The attributes that should be hidden for serialization. - * - * @var array - */ - protected $hidden = [ - 'password', - 'remember_token', - ]; + // /** + // * The attributes that should be hidden for serialization. + // * + // * @var array + // */ + // protected $hidden = [ + // 'password', + // 'remember_token', + // ]; - /** - * Get the attributes that should be cast. - * - * @return array - */ - protected function casts(): array - { - return [ - 'email_verified_at' => 'datetime', - 'password' => 'hashed', - ]; - } + // /** + // * Get the attributes that should be cast. + // * + // * @return array + // */ + // protected function casts(): array + // { + // return [ + // 'email_verified_at' => 'datetime', + // 'password' => 'hashed', + // ]; + // } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 452e6b6..5a5aed8 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,7 +2,15 @@ namespace App\Providers; +use App\Logic\OpenIDLogic; +use App\Models\User; +use Exception; +use Firebase\JWT\JWK; +use Firebase\JWT\JWT; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\ServiceProvider; +use stdClass; class AppServiceProvider extends ServiceProvider { @@ -19,6 +27,43 @@ public function register(): void */ public function boot(): void { - // + $this->setJWTGuard(); + } + + private function setJWTGuard(): void + { + Auth::viaRequest('jwt', function (Request $request) { + $logic = app(OpenIDLogic::class); + + $keys = JWK::parseKeySet($logic->jwks); + + $jwt = $request->bearerToken(); + + if (empty($jwt)) { + return response()->json(['error' => 'No token provided'], 401); + } + + $headers = new stdClass(); + + try { + $decoded = JWT::decode($jwt, $keys, $headers); + $request->attributes->add(['token_type' => $headers->typ]); + } catch (Exception $e) { + return response()->json(['error' => 'Invalid token, '.$e->getMessage()], 401); + } + + if (! in_array($decoded->aud, config('oauth.trusted_aud'))) { + return response()->json(['error' => 'The application rejected the token, token aud is '.$decoded->aud.', app aud is '.config('oauth.client_id')], 401); + } + + if (config('oauth.force_aud') && $decoded->aud != config('oauth.client_id')) { + return response()->json(['error' => 'The token not match the application, '.' token aud is '.$decoded->aud.', app aud is '.config('oauth.client_id')], 401); + } + + return User::where('external_id', $decoded->sub)->firstOrCreate([ + 'external_id' => $decoded->sub, + 'name' => $decoded->name, + ]); + }); } } diff --git a/app/Repositories/Tool/Tool.php b/app/Repositories/Tool/Tool.php new file mode 100644 index 0000000..05dc276 --- /dev/null +++ b/app/Repositories/Tool/Tool.php @@ -0,0 +1,49 @@ +validate($data)) { + throw new Exception('Invalid data'); + } + + $this->name = $data['name']; + $this->url = $data['url']; + $this->description = $data['description']; + $this->api_key = $data['api_key']; + $this->user_id = $data['user_id']; + } + + public function validate(array $data): bool + { + // all fields are required + if (empty($data['name']) || + empty($data['url']) || + empty($data['description']) || + empty($data['api_key']) || + empty($data['user_id'])) { + return false; + } + + return true; + } +} diff --git a/app/Repositories/Tool/ToolFunction.php b/app/Repositories/Tool/ToolFunction.php new file mode 100644 index 0000000..476d721 --- /dev/null +++ b/app/Repositories/Tool/ToolFunction.php @@ -0,0 +1,14 @@ + explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( + '%s%s', + 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', + Sanctum::currentApplicationUrlWithPort() + ))), + + /* + |-------------------------------------------------------------------------- + | Sanctum Guards + |-------------------------------------------------------------------------- + | + | This array contains the authentication guards that will be checked when + | Sanctum is trying to authenticate a request. If none of these guards + | are able to authenticate the request, Sanctum will use the bearer + | token that's present on an incoming request for authentication. + | + */ + + 'guard' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Expiration Minutes + |-------------------------------------------------------------------------- + | + | This value controls the number of minutes until an issued token will be + | considered expired. This will override any values set in the token's + | "expires_at" attribute, but first-party sessions are not affected. + | + */ + + 'expiration' => null, + + /* + |-------------------------------------------------------------------------- + | Token Prefix + |-------------------------------------------------------------------------- + | + | Sanctum can prefix new tokens in order to take advantage of numerous + | security scanning initiatives maintained by open source platforms + | that notify developers if they commit tokens into repositories. + | + | See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning + | + */ + + 'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''), + + /* + |-------------------------------------------------------------------------- + | Sanctum Middleware + |-------------------------------------------------------------------------- + | + | When authenticating your first-party SPA with Sanctum you may need to + | customize some of the middleware Sanctum uses while processing the + | request. You may change the middleware listed below as required. + | + */ + + 'middleware' => [ + 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, + 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, + 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, + ], + +]; diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php index 05fb5d9..22c60d2 100644 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -14,27 +14,27 @@ public function up(): void Schema::create('users', function (Blueprint $table) { $table->id(); $table->string('name'); - $table->string('email')->unique(); - $table->timestamp('email_verified_at')->nullable(); - $table->string('password'); - $table->rememberToken(); + $table->unsignedBigInteger('external_id')->index(); + // $table->string('email')->unique(); + // $table->timestamp('email_verified_at')->nullable(); + // $table->rememberToken(); $table->timestamps(); }); - Schema::create('password_reset_tokens', function (Blueprint $table) { - $table->string('email')->primary(); - $table->string('token'); - $table->timestamp('created_at')->nullable(); - }); - - Schema::create('sessions', function (Blueprint $table) { - $table->string('id')->primary(); - $table->foreignId('user_id')->nullable()->index(); - $table->string('ip_address', 45)->nullable(); - $table->text('user_agent')->nullable(); - $table->longText('payload'); - $table->integer('last_activity')->index(); - }); + // Schema::create('password_reset_tokens', function (Blueprint $table) { + // $table->string('email')->primary(); + // $table->string('token'); + // $table->timestamp('created_at')->nullable(); + // }); + // + // Schema::create('sessions', function (Blueprint $table) { + // $table->string('id')->primary(); + // $table->foreignId('user_id')->nullable()->index(); + // $table->string('ip_address', 45)->nullable(); + // $table->text('user_agent')->nullable(); + // $table->longText('payload'); + // $table->integer('last_activity')->index(); + // }); } /** diff --git a/database/migrations/2024_07_23_093251_create_assistants_table.php b/database/migrations/2024_07_23_093251_create_assistants_table.php index 88e439e..1308324 100644 --- a/database/migrations/2024_07_23_093251_create_assistants_table.php +++ b/database/migrations/2024_07_23_093251_create_assistants_table.php @@ -21,7 +21,6 @@ public function up(): void $table->unsignedBigInteger('user_id')->index(); $table->foreign('user_id')->references('id')->on('users'); - $table->timestamps(); }); } diff --git a/database/migrations/2024_07_23_093301_create_chats_table.php b/database/migrations/2024_07_23_093301_create_chats_table.php index 2e36dfc..05cfc59 100644 --- a/database/migrations/2024_07_23_093301_create_chats_table.php +++ b/database/migrations/2024_07_23_093301_create_chats_table.php @@ -22,7 +22,6 @@ public function up(): void // index $table->index(['assistant_id', 'user_id']); - $table->timestamps(); }); } diff --git a/database/migrations/2024_07_23_151217_create_chat_histories_table.php b/database/migrations/2024_07_23_151217_create_chat_histories_table.php index e4eb410..addfff4 100644 --- a/database/migrations/2024_07_23_151217_create_chat_histories_table.php +++ b/database/migrations/2024_07_23_151217_create_chat_histories_table.php @@ -18,7 +18,7 @@ public function up(): void $table->text('content'); $table->string('role'); -// $table->string('model'); + // $table->string('model'); // input tokens $table->integer('input_tokens')->default(null); @@ -31,7 +31,6 @@ public function up(): void // index $table->index(['chat_id', 'role']); - $table->timestamps(); }); } diff --git a/database/migrations/2024_07_23_153355_add_callback_url_to_tools_table.php b/database/migrations/2024_07_23_153355_add_callback_url_to_tools_table.php new file mode 100644 index 0000000..2eabdd3 --- /dev/null +++ b/database/migrations/2024_07_23_153355_add_callback_url_to_tools_table.php @@ -0,0 +1,28 @@ +string('callback_url')->after('api_key'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('tools', function (Blueprint $table) { + $table->dropColumn('callback_url'); + }); + } +}; diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 0000000..f35f6f8 --- /dev/null +++ b/routes/api.php @@ -0,0 +1,8 @@ +user(); +})->middleware('auth:api');