Compare commits
18 Commits
android1.0
...
main
Author | SHA1 | Date |
---|---|---|
Easy | b8fa724a9a | |
Easy | 4ce68572b9 | |
Easy | 16aec6c468 | |
Easy | bfadc52fef | |
Easy | d7f3279458 | |
Easy | f5841403c7 | |
Easy | be51654558 | |
Easy | aa9a2a9dfa | |
Easy | 1a9ad9e351 | |
Easy | 19011afdd0 | |
Easy | f9779893ec | |
Easy | 09acc7955e | |
Chi | 6053cf624a | |
Easy | d15892727a | |
Easy | b5e6605328 | |
Easy | 8e5292acfb | |
hext | ed61f49eca | |
Easy | 6927a94860 |
60
README.md
60
README.md
|
@ -1,9 +1,11 @@
|
|||
> ⚠️ 目前,官方架设的Android版本因接口权限停止无法使用,[详情请点击](https://github.com/easychen/pushdeer/issues/150)
|
||||
|
||||
> ⚠️ 自架版服务器端需每年更新推送证书,如果之前架设的服务突然无法收到推送,请尝试拉取部署最新代码,或者[手动更新证书](https://github.com/easychen/pushdeer/tree/main/push)
|
||||
|
||||
PushDeer是一个可以自行架设的无APP推送服务,同时也为因为某些原因无法使用无APP推送方案的同学提供有APP/自制设备方案。
|
||||
|
||||
[🐙🐱 GitHub仓库](https://github.com/easychen/pushdeer) [🔮 中国大陆镜像仓库@Gitee](https://gitee.com/easychen/pushdeer)
|
||||
|
||||
> [🤖 Android自架版求开发者参与](doc/Android%E7%89%88self-hosted%E7%9A%84%E8%AE%BE%E8%AE%A1%E5%92%8C%E5%AE%9E%E7%8E%B0.md)
|
||||
|
||||
本项目已经实现的方案/端包括:
|
||||
|
||||
- 无APP方案:
|
||||
|
@ -97,6 +99,7 @@ https://api2.pushdeer.com/message/push?pushkey=<key>&text=标题&desp=<markdown>
|
|||
|
||||
- [Python SDK](https://github.com/gaoliang/pypushdeer) by [Gao Liang](https://github.com/gaoliang)
|
||||
- [Go SDK](https://github.com/Luoxin/go-pushdeer-sdk) by [Luoxin](https://github.com/Luoxin)
|
||||
- [Rust SDK](https://github.com/abgelehnt/rupushdeer) by [Chi](https://github.com/abgelehnt)
|
||||
|
||||
PHP函数:
|
||||
|
||||
|
@ -158,20 +161,19 @@ function pushdeer_send($text, $desp = '', $type='text', $key = '[PUSHKEY]')
|
|||
|
||||
然后运行以下代码:
|
||||
|
||||
(大陆服务器使用)
|
||||
```
|
||||
git clone https://gitee.com/easychen/pushdeer.git
|
||||
cd pushdeer
|
||||
docker-compose -f docker-compose.self-hosted.yml up --build -d
|
||||
```
|
||||
|
||||
(海外服务器使用)
|
||||
```
|
||||
git clone https://github.com/easychen/pushdeer.git
|
||||
cd pushdeer
|
||||
docker-compose -f docker-compose.self-hosted.yml up --build -d
|
||||
```
|
||||
|
||||
如果你的服务器连接GitHub有困难,可以使用Gitee的代码,但需要核对是否为最新版本(有可能没同步)
|
||||
```
|
||||
git clone https://gitee.com/easychen/pushdeer.git
|
||||
cd pushdeer
|
||||
docker-compose -f docker-compose.self-hosted.yml up --build -d
|
||||
```
|
||||
|
||||
> 如提示docker服务未安装/找不到/未启动,可在 docker-compose 前加 sudo 再试
|
||||
|
||||
等待初始化完成后,访问 `$AAA(需替换为服务器端IP或域名):8800`,看到扫码提示和图片则说明容器已经启动。
|
||||
|
@ -180,6 +182,7 @@ docker-compose -f docker-compose.self-hosted.yml up --build -d
|
|||
|
||||
如果您在部署中遇到问题,可按[调试文档](/doc/调试文档.md)定位并发现错误信息。
|
||||
|
||||
<!--
|
||||
#### 单一容器部署方案
|
||||
|
||||
对于很多不能运行docker-compose的容器环境,可以直接使用 pushdeer 镜像。该镜像中已经包含了 redis 服务,但需要通过环境变量指定数据库等信息:
|
||||
|
@ -189,6 +192,7 @@ docker run -e DB_DATABASE=* -e DB_HOST=* -e DB_PORT=*28740* -e DB_USERNAME=* -e
|
|||
```
|
||||
|
||||
请将上述命令中的`*`替换为对应的数据库信息。
|
||||
-->
|
||||
|
||||
### 使用自架版客户端
|
||||
|
||||
|
@ -419,6 +423,38 @@ type 为 image 时,text 中为要发送图片的URL。
|
|||
|-|-|-|
|
||||
|token|认证token|
|
||||
|
||||
#### Simple token
|
||||
> 为了方便客户端永久保持登入状态,我们提供了一个永不失效的Token,即 Simple token
|
||||
|
||||
##### 获取 Simple token
|
||||
通过 上文中的「获得当前用户的基本信息」接口(`POST /user/info`) 得到
|
||||
|
||||
##### 通过 Simple token 登入
|
||||
|
||||
`POST /login/simple_token`
|
||||
|
||||
|参数|说明|备注|
|
||||
|-|-|-|
|
||||
|stoken|Simple token|
|
||||
|
||||
登入成功返回认证token。
|
||||
|
||||
##### 重置 Simple token
|
||||
|
||||
`POST /simple_token/regen`
|
||||
|
||||
|参数|说明|备注|
|
||||
|-|-|-|
|
||||
|token|认证token|
|
||||
|
||||
##### 清空 Simple token
|
||||
|
||||
`POST /simple_token/remove`
|
||||
|
||||
|参数|说明|备注|
|
||||
|-|-|-|
|
||||
|token|认证token|
|
||||
|
||||
|
||||
[更详细的请求和返回值可以参考这里](doc/api/PushDeerOS.md)
|
||||
|
||||
|
@ -521,7 +557,7 @@ PushDeer主要面向以下三类用户
|
|||
|
||||
# 授权
|
||||
|
||||
本项目禁止商用(包括但不限于搭建后挂广告或售卖会员、打包后上架商店销售等),对非商业用途采用 GPLV2 授权
|
||||
本项目禁止商用(包括但不限于搭建后挂广告或售卖会员、打包后上架商店销售等),在非商用的情况下遵循GPL v2,当两者冲突时,以非商用原则优先。
|
||||
|
||||
|
||||
# 相关项目
|
||||
|
@ -532,4 +568,4 @@ PushDeer主要面向以下三类用户
|
|||
- [API的Go实现](https://github.com/iepngs/pushdeer-backend-go) by [iepngs](https://github.com/iepngs)
|
||||
- [API的Node实现](https://github.com/xkrfer/pushdeer-node) by [DouDou](https://github.com/xkrfer)
|
||||
- [浏览器插件](https://github.com/xkrfer/pushdeer-crx) by [DouDou](https://github.com/xkrfer)
|
||||
|
||||
- [Rust SDK](https://github.com/abgelehnt/rupushdeer) by [Chi](https://github.com/abgelehnt)
|
||||
|
|
|
@ -39,6 +39,8 @@ class PushDeerUserController extends Controller
|
|||
$the_user['level'] = 1;
|
||||
|
||||
$pd_user = PushDeerUser::create($the_user);
|
||||
$pd_user['simple_token'] = 'SP'.$pd_user['id'].'P'.md5(uniqid(rand(), true));
|
||||
$pd_user->save();
|
||||
}
|
||||
|
||||
// 将数据写到session
|
||||
|
@ -47,6 +49,7 @@ class PushDeerUserController extends Controller
|
|||
$_SESSION['name'] = $pd_user['name'];
|
||||
$_SESSION['email'] = $pd_user['email'];
|
||||
$_SESSION['level'] = $pd_user['level'];
|
||||
$_SESSION['simple_token'] = $pd_user['simple_token'];
|
||||
|
||||
session_regenerate_id(true);
|
||||
$token = session_id();
|
||||
|
@ -56,6 +59,56 @@ class PushDeerUserController extends Controller
|
|||
return send_error('id_token解析错误', ErrorCode('ARGS'));
|
||||
}
|
||||
|
||||
public function loginBySimpleToken(Request $request)
|
||||
{
|
||||
$validated = $request->validate(
|
||||
[
|
||||
'stoken' => 'required|string',
|
||||
]
|
||||
);
|
||||
|
||||
if (!$pd_user = PushDeerUser::where('simple_token', $validated['stoken'])->get()->first()) {
|
||||
return send_error('stoken无效', ErrorCode('ARGS'));
|
||||
}
|
||||
|
||||
if ($pd_user['level']<1) {
|
||||
return send_error('账号已被禁用', ErrorCode('ARGS'));
|
||||
}
|
||||
|
||||
// 将数据写到session
|
||||
session_start();
|
||||
$_SESSION['uid'] = $pd_user['id'];
|
||||
$_SESSION['name'] = $pd_user['name'];
|
||||
$_SESSION['email'] = $pd_user['email'];
|
||||
$_SESSION['level'] = $pd_user['level'];
|
||||
|
||||
session_regenerate_id(true);
|
||||
$token = session_id();
|
||||
return http_result(['token'=>$token]);
|
||||
}
|
||||
|
||||
public function simpleTokenRegen(Request $request)
|
||||
{
|
||||
// get user by session
|
||||
if (!$pd_user = PushDeerUser::where('id', $_SESSION['uid'])->get()->first()) {
|
||||
return send_error('用户不存在', ErrorCode('ARGS'));
|
||||
}
|
||||
$pd_user['simple_token'] = 'SP'.$pd_user['id'].'P'.md5(uniqid(rand(), true));
|
||||
$pd_user->save();
|
||||
return http_result(['stoken'=>$pd_user['simple_token']]);
|
||||
}
|
||||
|
||||
public function simpleTokenRemove(Request $request)
|
||||
{
|
||||
// get user by session
|
||||
if (!$pd_user = PushDeerUser::where('id', $_SESSION['uid'])->get()->first()) {
|
||||
return send_error('用户不存在', ErrorCode('ARGS'));
|
||||
}
|
||||
$pd_user['simple_token'] = '';
|
||||
$pd_user->save();
|
||||
return http_result(['stoken'=>$pd_user['simple_token']]);
|
||||
}
|
||||
|
||||
public function wecode2unionid(Request $request)
|
||||
{
|
||||
$validated = $request->validate(
|
||||
|
@ -133,6 +186,8 @@ class PushDeerUserController extends Controller
|
|||
$the_user['level'] = 1;
|
||||
|
||||
$pd_user = PushDeerUser::create($the_user);
|
||||
$pd_user['simple_token'] = 'SP'.$pd_user['id'].'P'.md5(uniqid(rand(), true));
|
||||
$pd_user->save();
|
||||
}
|
||||
|
||||
// 将数据写到session
|
||||
|
@ -141,6 +196,7 @@ class PushDeerUserController extends Controller
|
|||
$_SESSION['name'] = $pd_user['name'];
|
||||
$_SESSION['email'] = $pd_user['email'];
|
||||
$_SESSION['level'] = $pd_user['level'];
|
||||
$_SESSION['simple_token'] = $pd_user['simple_token'];
|
||||
|
||||
session_regenerate_id(true);
|
||||
$token = session_id();
|
||||
|
@ -175,6 +231,8 @@ class PushDeerUserController extends Controller
|
|||
$the_user['level'] = 1;
|
||||
|
||||
$pd_user = PushDeerUser::create($the_user);
|
||||
$pd_user['simple_token'] = 'SP'.$pd_user['id'].'P'.md5(uniqid(rand(), true));
|
||||
$pd_user->save();
|
||||
}
|
||||
|
||||
// 将数据写到session
|
||||
|
@ -183,6 +241,7 @@ class PushDeerUserController extends Controller
|
|||
$_SESSION['name'] = $pd_user['name'];
|
||||
$_SESSION['email'] = $pd_user['email'];
|
||||
$_SESSION['level'] = $pd_user['level'];
|
||||
$_SESSION['simple_token'] = $pd_user['simple_token'];
|
||||
|
||||
session_regenerate_id(true);
|
||||
$token = session_id();
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddSimpleTokenToUserTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('push_deer_users', function (Blueprint $table) {
|
||||
$table->string('simple_token')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('push_deer_users', function (Blueprint $table) {
|
||||
$table->dropColumn('simple_token');
|
||||
});
|
||||
}
|
||||
}
|
|
@ -23,6 +23,9 @@ use Illuminate\Support\Facades\Route;
|
|||
// 假登入,用于测试使用
|
||||
Route::any('/login/fake', 'App\Http\Controllers\PushDeerUserController@fakeLogin');
|
||||
|
||||
// 通过 simple_token 登入
|
||||
Route::any('/login/simple_token', 'App\Http\Controllers\PushDeerUserController@loginBySimpleToken');
|
||||
|
||||
// 通过 apple 返回的 idtoken 登入
|
||||
Route::post('/login/idtoken', 'App\Http\Controllers\PushDeerUserController@login');
|
||||
|
||||
|
@ -59,6 +62,11 @@ Route::middleware('auto.login')->group(function () {
|
|||
// 删除一个key
|
||||
Route::post('/key/remove', 'App\Http\Controllers\PushDeerKeyController@remove');
|
||||
|
||||
// simple_token
|
||||
Route::post('/simple_token/regen', 'App\Http\Controllers\PushDeerUserController@simpleTokenRegen');
|
||||
|
||||
Route::post('/simple_token/remove', 'App\Http\Controllers\PushDeerUserController@simpleTokenRemove');
|
||||
|
||||
// 消息列表
|
||||
Route::post('/message/list', 'App\Http\Controllers\PushDeerMessageController@list');
|
||||
// 删除消息
|
||||
|
|
|
@ -11,8 +11,8 @@ services:
|
|||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=theVeryp@ssw0rd
|
||||
- MYSQL_DATABASE=pushdeer
|
||||
ports:
|
||||
- '3306:3306'
|
||||
# ports:
|
||||
# - '3306:3306'
|
||||
redis:
|
||||
image: 'bitnami/redis:6.0.16'
|
||||
healthcheck:
|
||||
|
@ -53,4 +53,4 @@ services:
|
|||
# - MQTT_PASSWORD=y0urp@ss
|
||||
# - MQTT_BASE_TOPIC=default
|
||||
volumes:
|
||||
mariadb_data:
|
||||
mariadb_data:
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
version: '2'
|
||||
services:
|
||||
app:
|
||||
image: 'ccr.ccs.tencentyun.com/ftqq/pushdeercore'
|
||||
ports:
|
||||
- '9000:9000'
|
||||
environment:
|
||||
- DB_HOST=host.docker.internal
|
||||
- DB_PORT=3306
|
||||
- DB_USERNAME=root
|
||||
- DB_DATABASE=pushdeer_local
|
||||
- DB_PASSWORD=
|
||||
- DB_TIMEZONE=+08:00
|
||||
- GO_PUSH_IOS_TOPIC=com.pushdeer.self.ios
|
||||
- GO_PUSH_IOS_CLIP_TOPIC=com.pushdeer.self.ios.Clip
|
||||
- APP_DEBUG=false
|
||||
- WEB_PHP_SOCKET=127.0.0.1:8000
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
# 已废弃
|
||||
# version: '2'
|
||||
# services:
|
||||
# app:
|
||||
# image: 'ccr.ccs.tencentyun.com/ftqq/pushdeercore'
|
||||
# ports:
|
||||
# - '9000:9000'
|
||||
# environment:
|
||||
# - DB_HOST=host.docker.internal
|
||||
# - DB_PORT=3306
|
||||
# - DB_USERNAME=root
|
||||
# - DB_DATABASE=pushdeer_local
|
||||
# - DB_PASSWORD=
|
||||
# - DB_TIMEZONE=+08:00
|
||||
# - GO_PUSH_IOS_TOPIC=com.pushdeer.self.ios
|
||||
# - GO_PUSH_IOS_CLIP_TOPIC=com.pushdeer.self.ios.Clip
|
||||
# - APP_DEBUG=false
|
||||
# - WEB_PHP_SOCKET=127.0.0.1:8000
|
||||
# extra_hosts:
|
||||
# - "host.docker.internal:host-gateway"
|
||||
|
|
|
@ -49,4 +49,4 @@ SPEC CHECKSUMS:
|
|||
|
||||
PODFILE CHECKSUM: 42e3d8abd976589c1043ff9f9e864c275a490160
|
||||
|
||||
COCOAPODS: 1.11.2
|
||||
COCOAPODS: 1.11.3
|
||||
|
|
|
@ -97,6 +97,9 @@
|
|||
52EB90B32778DA4E0048E0ED /* Line.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52EB90B22778DA4E0048E0ED /* Line.swift */; };
|
||||
52EED71E27C9394D0086A804 /* WXDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52EED71D27C9394D0086A804 /* WXDelegate.swift */; };
|
||||
52EED71F27C93B960086A804 /* WXDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52EED71D27C9394D0086A804 /* WXDelegate.swift */; };
|
||||
52EF0AFC28CE081A00C99E4F /* CommonUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52EF0AFB28CE081A00C99E4F /* CommonUtils.swift */; };
|
||||
52EF0AFD28CE081A00C99E4F /* CommonUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52EF0AFB28CE081A00C99E4F /* CommonUtils.swift */; };
|
||||
52EF0AFE28CE08EC00C99E4F /* CommonUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52EF0AFB28CE081A00C99E4F /* CommonUtils.swift */; };
|
||||
52F0243F277737470071D861 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52F0243E277737470071D861 /* LoginView.swift */; };
|
||||
52F2C223277961D7006F08DC /* SettingsItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52F2C222277961D7006F08DC /* SettingsItemView.swift */; };
|
||||
52F40D2F277CA05600766C24 /* MessageItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52F40D2E277CA05600766C24 /* MessageItemView.swift */; };
|
||||
|
@ -218,6 +221,7 @@
|
|||
52EB90AF2778D67F0048E0ED /* KeyItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyItemView.swift; sourceTree = "<group>"; };
|
||||
52EB90B22778DA4E0048E0ED /* Line.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Line.swift; sourceTree = "<group>"; };
|
||||
52EED71D27C9394D0086A804 /* WXDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WXDelegate.swift; sourceTree = "<group>"; };
|
||||
52EF0AFB28CE081A00C99E4F /* CommonUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonUtils.swift; sourceTree = "<group>"; };
|
||||
52F0243C277733CE0071D861 /* PushDeer.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PushDeer.entitlements; sourceTree = "<group>"; };
|
||||
52F0243E277737470071D861 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; };
|
||||
52F2C222277961D7006F08DC /* SettingsItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsItemView.swift; sourceTree = "<group>"; };
|
||||
|
@ -367,6 +371,7 @@
|
|||
527872C327CE0EE800AD79AD /* Info-SelfHosted.plist */,
|
||||
52E317DA279305BB000B8BB1 /* Localizable.strings */,
|
||||
52E317D7279305BB000B8BB1 /* InfoPlist.strings */,
|
||||
52EF0AFA28CE07A600C99E4F /* Tool */,
|
||||
52450F362784822C003652D8 /* Service */,
|
||||
52B8CF5D277DE5FF004CB680 /* Common */,
|
||||
52450F3D27849228003652D8 /* Model */,
|
||||
|
@ -447,6 +452,14 @@
|
|||
path = Common;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
52EF0AFA28CE07A600C99E4F /* Tool */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
52EF0AFB28CE081A00C99E4F /* CommonUtils.swift */,
|
||||
);
|
||||
path = Tool;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
52F0243D2777370F0071D861 /* View */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -776,6 +789,7 @@
|
|||
5206009E27CF76BC00188431 /* PushDeerApi.swift in Sources */,
|
||||
520600A627D0AE2300188431 /* Line.swift in Sources */,
|
||||
5206009D27CF74C100188431 /* HttpRequest.swift in Sources */,
|
||||
52EF0AFE28CE08EC00C99E4F /* CommonUtils.swift in Sources */,
|
||||
520600A127CF770600188431 /* Env.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -810,6 +824,7 @@
|
|||
52EED71E27C9394D0086A804 /* WXDelegate.swift in Sources */,
|
||||
52EB90AE2778AFD60048E0ED /* BaseNavigationView.swift in Sources */,
|
||||
524E99E627B3CD0F00292396 /* EndpointView.swift in Sources */,
|
||||
52EF0AFC28CE081A00C99E4F /* CommonUtils.swift in Sources */,
|
||||
52EB90AC2778ADF80048E0ED /* CardView.swift in Sources */,
|
||||
52EB90B02778D67F0048E0ED /* KeyItemView.swift in Sources */,
|
||||
52AC5C2827B7FE1D00EEB185 /* ViewExtension.swift in Sources */,
|
||||
|
@ -853,6 +868,7 @@
|
|||
52FBA09427874879003308C2 /* ContentView.swift in Sources */,
|
||||
52B8CF85277E0C12004CB680 /* Line.swift in Sources */,
|
||||
52AC5C2927B7FE1D00EEB185 /* ViewExtension.swift in Sources */,
|
||||
52EF0AFD28CE081A00C99E4F /* CommonUtils.swift in Sources */,
|
||||
52450F402784923D003652D8 /* Result.swift in Sources */,
|
||||
526A1E712791E00400BA2177 /* PushDeerData.xcdatamodeld in Sources */,
|
||||
52450F432784943F003652D8 /* HttpRequest.swift in Sources */,
|
||||
|
|
|
@ -29,6 +29,7 @@ struct UserInfoContent: Codable{
|
|||
let level: Int
|
||||
let created_at: String
|
||||
let updated_at: String
|
||||
let simple_token: String?
|
||||
}
|
||||
|
||||
struct DeviceItem: Codable, Identifiable{
|
||||
|
@ -78,6 +79,12 @@ struct ResultContent: Codable{
|
|||
let result: Array<String>
|
||||
}
|
||||
|
||||
|
||||
struct STokenContent: Codable{
|
||||
let stoken: String?
|
||||
}
|
||||
|
||||
|
||||
let dateFormatter = DateFormatter()
|
||||
|
||||
extension KeyItem {
|
||||
|
|
|
@ -125,6 +125,35 @@ class AppState: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
func loginAfter() {
|
||||
Task {
|
||||
// 查询 UserInfo
|
||||
self.userInfo = try await HttpRequest.getUserInfo()
|
||||
var stoken = self.userInfo?.simple_token
|
||||
if isEmpty(stoken) {
|
||||
// UserInfo 中的 stoken 为空, 就重新生成一个
|
||||
stoken = try await HttpRequest.stokenRegen().stoken
|
||||
}
|
||||
if isNotEmpty(stoken) {
|
||||
// 最后 stoken 不为空, 就保存到本地
|
||||
self.saveSTokenToLocal(stoken: stoken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func saveSTokenToLocal(stoken: String?) -> Void {
|
||||
let key = "stoken_\(self.api_endpoint)"
|
||||
getUserDefaults().set(stoken, forKey: key)
|
||||
}
|
||||
func getLocalSToken() -> String? {
|
||||
let key = "stoken_\(self.api_endpoint)"
|
||||
return getUserDefaults().string(forKey: key)
|
||||
}
|
||||
func deleteLocalSToken() -> Void {
|
||||
let key = "stoken_\(self.api_endpoint)"
|
||||
getUserDefaults().removeObject(forKey: key)
|
||||
}
|
||||
|
||||
func appleIdLogin(_ result: Result<ASAuthorization, Error>) async throws -> TokenContent {
|
||||
switch result {
|
||||
case let .success(authorization):
|
||||
|
|
|
@ -8,13 +8,63 @@
|
|||
import Foundation
|
||||
import Moya
|
||||
|
||||
struct TokenAuthorizationPlugin: PluginType {
|
||||
|
||||
func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {
|
||||
if
|
||||
let url = request.url,
|
||||
var components = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
||||
let queryItems = components.queryItems
|
||||
{
|
||||
let queryItems_new = queryItems.map { item -> URLQueryItem in
|
||||
if item.name == "token" {
|
||||
// 把请求参数中的 token 都换成最新的
|
||||
return URLQueryItem(name: "token", value: AppState.shared.token)
|
||||
}
|
||||
return item
|
||||
}
|
||||
components.queryItems = queryItems_new
|
||||
var request_mutable = request
|
||||
request_mutable.url = components.url
|
||||
return request_mutable
|
||||
}
|
||||
return request
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
struct HttpRequest {
|
||||
|
||||
static let provider = MoyaProvider<PushDeerApi>(callbackQueue: DispatchQueue.main)
|
||||
static let provider = MoyaProvider<PushDeerApi>(callbackQueue: DispatchQueue.main, plugins: [TokenAuthorizationPlugin()])
|
||||
|
||||
/// 统一处理接口请求, 并且封装成 Swift Concurrency 模式 (async / await)
|
||||
static func request<T: Codable>(_ targetType: PushDeerApi, resultType: T.Type) async throws -> T {
|
||||
do {
|
||||
return try await _request(targetType, resultType: resultType)
|
||||
} catch {
|
||||
if (error as NSError).code == 80403 {
|
||||
// token 失效处理
|
||||
let stoken = AppState.shared.getLocalSToken() // 取本地 stoken
|
||||
if isNotEmpty(stoken) {
|
||||
let token = try? await stokenLogin(stoken: stoken!).token // 用 stoken 登录换 token
|
||||
if isNotEmpty(token) {
|
||||
AppState.shared.token = token! // 更新 token
|
||||
do {
|
||||
return try await _request(targetType, resultType: resultType) // 用新 token 再次请求原接口
|
||||
} catch {
|
||||
throw error // 再次请求报错
|
||||
}
|
||||
}
|
||||
AppState.shared.deleteLocalSToken() // 到这来说明 stoken 已经失效, 删掉 stoken
|
||||
}
|
||||
// 退出登录
|
||||
AppState.shared.token = "" // 到这来 说明 stoken 不存在 或 已经失效, 清空 token 使其切换到登录界面
|
||||
}
|
||||
throw error // 抛出原请求的 error (stoken流程失败, 或者不是token失效的错误, 会到这儿来)
|
||||
}
|
||||
}
|
||||
|
||||
static func _request<T: Codable>(_ targetType: PushDeerApi, resultType: T.Type) async throws -> T {
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
provider.request(targetType) { result in
|
||||
switch result {
|
||||
|
@ -31,7 +81,6 @@ struct HttpRequest {
|
|||
if let content = result.content, result.code == 0 {
|
||||
continuation.resume(returning: content)
|
||||
} else if result.code == 80403 {
|
||||
AppState.shared.token = ""
|
||||
continuation.resume(throwing: NSError(domain: result.error ?? NSLocalizedString("登录过期", comment: "token失效时提示"), code: result.code, userInfo: [NSLocalizedDescriptionKey: result.error ?? NSLocalizedString("登录过期", comment: "token失效时提示")]))
|
||||
} else {
|
||||
continuation.resume(throwing: NSError(domain: result.error ?? NSLocalizedString("接口报错", comment: "接口报错时提示"), code: result.code, userInfo: [NSLocalizedDescriptionKey: result.error ?? NSLocalizedString("接口报错", comment: "接口报错时提示")]))
|
||||
|
@ -139,4 +188,17 @@ struct HttpRequest {
|
|||
static func rmAllMessage() async throws -> ActionContent {
|
||||
return try await request(.rmAllMessage(token: AppState.shared.token), resultType: ActionContent.self)
|
||||
}
|
||||
|
||||
|
||||
static func stokenLogin(stoken: String) async throws -> TokenContent {
|
||||
return try await request(.stokenLogin(stoken: stoken), resultType: TokenContent.self)
|
||||
}
|
||||
|
||||
static func stokenRegen() async throws -> STokenContent {
|
||||
return try await request(.stokenRegen(token: AppState.shared.token), resultType: STokenContent.self)
|
||||
}
|
||||
|
||||
static func stokenRemove() async throws -> STokenContent {
|
||||
return try await request(.stokenRemove(token: AppState.shared.token), resultType: STokenContent.self)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,6 +42,9 @@ enum PushDeerApi {
|
|||
case rmMessage(token: String, id: Int)
|
||||
case rmAllMessage(token: String)
|
||||
|
||||
case stokenLogin(stoken: String)
|
||||
case stokenRegen(token: String)
|
||||
case stokenRemove(token: String)
|
||||
}
|
||||
|
||||
extension PushDeerApi: TargetType {
|
||||
|
@ -94,6 +97,13 @@ extension PushDeerApi: TargetType {
|
|||
return "/message/remove"
|
||||
case .rmAllMessage:
|
||||
return "/message/clean"
|
||||
|
||||
case .stokenLogin:
|
||||
return "/login/simple_token"
|
||||
case .stokenRegen:
|
||||
return "/simple_token/regen"
|
||||
case .stokenRemove:
|
||||
return "/simple_token/remove"
|
||||
}
|
||||
}
|
||||
var method: Moya.Method {
|
||||
|
@ -145,6 +155,12 @@ extension PushDeerApi: TargetType {
|
|||
case let .rmAllMessage(token):
|
||||
return .requestParameters(parameters: ["token": token],encoding: URLEncoding.queryString)
|
||||
|
||||
case let .stokenLogin(stoken):
|
||||
return .requestParameters(parameters: ["stoken": stoken],encoding: URLEncoding.queryString)
|
||||
case let .stokenRegen(token):
|
||||
return .requestParameters(parameters: ["token": token],encoding: URLEncoding.queryString)
|
||||
case let .stokenRemove(token):
|
||||
return .requestParameters(parameters: ["token": token],encoding: URLEncoding.queryString)
|
||||
}
|
||||
}
|
||||
var headers: [String: String]? {
|
||||
|
|
|
@ -30,6 +30,9 @@ class WXDelegate: NSObject, WXApiDelegate {
|
|||
if state == "login" {
|
||||
AppState.shared.token = try await HttpRequest.wechatLogin(code: code).token
|
||||
// 给 AppState 的 token 赋值后, SwiftUI 写的 ContentView 页面会监听到并自动进入主页
|
||||
// 登录成功后的处理
|
||||
AppState.shared.loginAfter()
|
||||
|
||||
} else if state == "bind" {
|
||||
_ = try await HttpRequest.mergeUser(type: "wechat", tokenorcode: code)
|
||||
// 合并成功, 更新数据
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// CommonUtils.swift
|
||||
// PushDeer
|
||||
//
|
||||
// Created by HEXT on 2022/9/11.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 判断一个集合 **为空**
|
||||
/// - Parameter emptiable: 一个可能为空的集合, 需要实现了 Collection 协议, 如: String / Array / Dictionary / Set / Data 等
|
||||
/// - Returns: nil 或 空 为 true
|
||||
func isEmpty<T: Collection>(_ emptiable: T?) -> Bool {
|
||||
if emptiable == nil || emptiable!.isEmpty {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// 判断一个集合 **不为空**
|
||||
/// - Parameter emptiable: 一个可能为空的集合, 需要实现了 Collection 协议, 如: String / Array / Dictionary / Set / Data 等
|
||||
/// - Returns: nil 或 空 为 false
|
||||
func isNotEmpty<T: Collection>(_ emptiable: T?) -> Bool {
|
||||
return !isEmpty(emptiable)
|
||||
}
|
|
@ -41,6 +41,8 @@ struct LoginView: View {
|
|||
showLoading = true
|
||||
store.token = try await store.appleIdLogin(result).token
|
||||
// 获取成功去主页
|
||||
// 登录成功后的处理
|
||||
store.loginAfter()
|
||||
} catch {
|
||||
showLoading = false
|
||||
if (error as NSError).code == 1001 {
|
||||
|
|
|
@ -21,6 +21,7 @@ struct SettingsView: View {
|
|||
VStack {
|
||||
SettingsItemView(title: NSLocalizedString("登录为", comment: "") + " " + userName(), button: NSLocalizedString("退出", comment: "退出登录按钮上的文字")) {
|
||||
store.token = ""
|
||||
store.deleteLocalSToken()
|
||||
HToast.showText(NSLocalizedString("退出", comment: "退出登录按钮上的文字"))
|
||||
}
|
||||
.padding(EdgeInsets(top: 18, leading: 20, bottom: 0, trailing: 20))
|
||||
|
@ -37,6 +38,7 @@ struct SettingsView: View {
|
|||
SettingsItemView(title: NSLocalizedString("API endpoint", comment: ""), button: NSLocalizedString("重置", comment: "")) {
|
||||
store.api_endpoint = ""
|
||||
store.token = ""
|
||||
store.deleteLocalSToken()
|
||||
}
|
||||
.padding(EdgeInsets(top: 18, leading: 20, bottom: 0, trailing: 20))
|
||||
}
|
||||
|
|
BIN
push/c.p12
BIN
push/c.p12
Binary file not shown.
BIN
push/cc.p12
BIN
push/cc.p12
Binary file not shown.
Loading…
Reference in New Issue