1
0

AccountController.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. <?php
  2. namespace App\Http\Controllers;
  3. use Auth;
  4. use Cache;
  5. use Mail;
  6. use Illuminate\Support\Facades\Redis;
  7. use Illuminate\Support\Str;
  8. use Carbon\Carbon;
  9. use App\Mail\ConfirmEmail;
  10. use Illuminate\Http\Request;
  11. use PragmaRX\Google2FA\Google2FA;
  12. use App\Jobs\FollowPipeline\FollowPipeline;
  13. use App\{
  14. DirectMessage,
  15. EmailVerification,
  16. Follower,
  17. FollowRequest,
  18. Media,
  19. Notification,
  20. Profile,
  21. User,
  22. UserDevice,
  23. UserFilter,
  24. UserSetting
  25. };
  26. use League\Fractal;
  27. use League\Fractal\Serializer\ArraySerializer;
  28. use League\Fractal\Pagination\IlluminatePaginatorAdapter;
  29. use App\Transformer\Api\Mastodon\v1\AccountTransformer;
  30. use App\Services\AccountService;
  31. use App\Services\NotificationService;
  32. use App\Services\UserFilterService;
  33. use App\Services\RelationshipService;
  34. use App\Jobs\FollowPipeline\FollowAcceptPipeline;
  35. use App\Jobs\FollowPipeline\FollowRejectPipeline;
  36. class AccountController extends Controller
  37. {
  38. protected $filters = [
  39. 'user.mute',
  40. 'user.block',
  41. ];
  42. const FILTER_LIMIT_MUTE_TEXT = 'You cannot mute more than ';
  43. const FILTER_LIMIT_BLOCK_TEXT = 'You cannot block more than ';
  44. public function __construct()
  45. {
  46. $this->middleware('auth');
  47. }
  48. public function notifications(Request $request)
  49. {
  50. return view('account.activity');
  51. }
  52. public function followingActivity(Request $request)
  53. {
  54. $this->validate($request, [
  55. 'page' => 'nullable|min:1|max:3',
  56. 'a' => 'nullable|alpha_dash',
  57. ]);
  58. $action = $request->input('a');
  59. $allowed = ['like', 'follow'];
  60. $timeago = Carbon::now()->subMonths(3);
  61. $profile = Auth::user()->profile;
  62. $following = $profile->following->pluck('id');
  63. $notifications = Notification::whereIn('actor_id', $following)
  64. ->whereIn('action', $allowed)
  65. ->where('actor_id', '<>', $profile->id)
  66. ->where('profile_id', '<>', $profile->id)
  67. ->whereDate('created_at', '>', $timeago)
  68. ->orderBy('notifications.created_at', 'desc')
  69. ->simplePaginate(30);
  70. return view('account.following', compact('profile', 'notifications'));
  71. }
  72. public function verifyEmail(Request $request)
  73. {
  74. $recentSent = EmailVerification::whereUserId(Auth::id())
  75. ->whereDate('created_at', '>', now()->subHours(12))->count();
  76. return view('account.verify_email', compact('recentSent'));
  77. }
  78. public function sendVerifyEmail(Request $request)
  79. {
  80. $recentAttempt = EmailVerification::whereUserId(Auth::id())
  81. ->whereDate('created_at', '>', now()->subHours(12))->count();
  82. if ($recentAttempt > 0) {
  83. return redirect()->back()->with('error', 'A verification email has already been sent recently. Please check your email, or try again later.');
  84. }
  85. EmailVerification::whereUserId(Auth::id())->delete();
  86. $user = User::whereNull('email_verified_at')->find(Auth::id());
  87. $utoken = Str::uuid() . Str::random(mt_rand(5,9));
  88. $rtoken = Str::random(mt_rand(64, 70));
  89. $verify = new EmailVerification();
  90. $verify->user_id = $user->id;
  91. $verify->email = $user->email;
  92. $verify->user_token = $utoken;
  93. $verify->random_token = $rtoken;
  94. $verify->save();
  95. Mail::to($user->email)->send(new ConfirmEmail($verify));
  96. return redirect()->back()->with('status', 'Verification email sent!');
  97. }
  98. public function confirmVerifyEmail(Request $request, $userToken, $randomToken)
  99. {
  100. $verify = EmailVerification::where('user_token', $userToken)
  101. ->where('created_at', '>', now()->subHours(24))
  102. ->where('random_token', $randomToken)
  103. ->firstOrFail();
  104. if (Auth::id() === $verify->user_id && $verify->user_token === $userToken && $verify->random_token === $randomToken) {
  105. $user = User::find(Auth::id());
  106. $user->email_verified_at = Carbon::now();
  107. $user->save();
  108. return redirect('/');
  109. } else {
  110. abort(403);
  111. }
  112. }
  113. public function direct()
  114. {
  115. return view('account.direct');
  116. }
  117. public function directMessage(Request $request, $id)
  118. {
  119. $profile = Profile::where('id', '!=', $request->user()->profile_id)
  120. // ->whereNull('domain')
  121. ->findOrFail($id);
  122. return view('account.directmessage', compact('id'));
  123. }
  124. public function mute(Request $request)
  125. {
  126. $this->validate($request, [
  127. 'type' => 'required|string|in:user',
  128. 'item' => 'required|integer|min:1',
  129. ]);
  130. $pid = $request->user()->profile_id;
  131. $count = UserFilterService::muteCount($pid);
  132. $maxLimit = intval(config('instance.user_filters.max_user_mutes'));
  133. abort_if($count >= $maxLimit, 422, self::FILTER_LIMIT_MUTE_TEXT . $maxLimit . ' accounts');
  134. if($count == 0) {
  135. $filterCount = UserFilter::whereUserId($pid)->count();
  136. abort_if($filterCount >= $maxLimit, 422, self::FILTER_LIMIT_MUTE_TEXT . $maxLimit . ' accounts');
  137. }
  138. $type = $request->input('type');
  139. $item = $request->input('item');
  140. $action = $type . '.mute';
  141. if (!in_array($action, $this->filters)) {
  142. return abort(406);
  143. }
  144. $filterable = [];
  145. switch ($type) {
  146. case 'user':
  147. $profile = Profile::findOrFail($item);
  148. if ($profile->id == $pid) {
  149. return abort(403);
  150. }
  151. $class = get_class($profile);
  152. $filterable['id'] = $profile->id;
  153. $filterable['type'] = $class;
  154. break;
  155. }
  156. $filter = UserFilter::firstOrCreate([
  157. 'user_id' => $pid,
  158. 'filterable_id' => $filterable['id'],
  159. 'filterable_type' => $filterable['type'],
  160. 'filter_type' => 'mute',
  161. ]);
  162. UserFilterService::mute($pid, $filterable['id']);
  163. $res = RelationshipService::refresh($pid, $profile->id);
  164. if($request->wantsJson()) {
  165. return response()->json($res);
  166. } else {
  167. return redirect()->back();
  168. }
  169. }
  170. public function unmute(Request $request)
  171. {
  172. $this->validate($request, [
  173. 'type' => 'required|string|in:user',
  174. 'item' => 'required|integer|min:1',
  175. ]);
  176. $pid = $request->user()->profile_id;
  177. $type = $request->input('type');
  178. $item = $request->input('item');
  179. $action = $type . '.mute';
  180. if (!in_array($action, $this->filters)) {
  181. return abort(406);
  182. }
  183. $filterable = [];
  184. switch ($type) {
  185. case 'user':
  186. $profile = Profile::findOrFail($item);
  187. if ($profile->id == $pid) {
  188. return abort(403);
  189. }
  190. $class = get_class($profile);
  191. $filterable['id'] = $profile->id;
  192. $filterable['type'] = $class;
  193. break;
  194. default:
  195. abort(400);
  196. break;
  197. }
  198. $filter = UserFilter::whereUserId($pid)
  199. ->whereFilterableId($filterable['id'])
  200. ->whereFilterableType($filterable['type'])
  201. ->whereFilterType('mute')
  202. ->first();
  203. if($filter) {
  204. UserFilterService::unmute($pid, $filterable['id']);
  205. $filter->delete();
  206. }
  207. $res = RelationshipService::refresh($pid, $profile->id);
  208. if($request->wantsJson()) {
  209. return response()->json($res);
  210. } else {
  211. return redirect()->back();
  212. }
  213. }
  214. public function block(Request $request)
  215. {
  216. $this->validate($request, [
  217. 'type' => 'required|string|in:user',
  218. 'item' => 'required|integer|min:1',
  219. ]);
  220. $pid = $request->user()->profile_id;
  221. $count = UserFilterService::blockCount($pid);
  222. $maxLimit = intval(config('instance.user_filters.max_user_blocks'));
  223. abort_if($count >= $maxLimit, 422, self::FILTER_LIMIT_BLOCK_TEXT . $maxLimit . ' accounts');
  224. if($count == 0) {
  225. $filterCount = UserFilter::whereUserId($pid)->whereFilterType('block')->count();
  226. abort_if($filterCount >= $maxLimit, 422, self::FILTER_LIMIT_BLOCK_TEXT . $maxLimit . ' accounts');
  227. }
  228. $type = $request->input('type');
  229. $item = $request->input('item');
  230. $action = $type.'.block';
  231. if (!in_array($action, $this->filters)) {
  232. return abort(406);
  233. }
  234. $filterable = [];
  235. switch ($type) {
  236. case 'user':
  237. $profile = Profile::findOrFail($item);
  238. if ($profile->id == $pid || ($profile->user && $profile->user->is_admin == true)) {
  239. return abort(403);
  240. }
  241. $class = get_class($profile);
  242. $filterable['id'] = $profile->id;
  243. $filterable['type'] = $class;
  244. Follower::whereProfileId($profile->id)->whereFollowingId($pid)->delete();
  245. Notification::whereProfileId($pid)
  246. ->whereActorId($profile->id)
  247. ->get()
  248. ->map(function($n) use($pid) {
  249. NotificationService::del($pid, $n['id']);
  250. $n->forceDelete();
  251. });
  252. break;
  253. }
  254. $filter = UserFilter::firstOrCreate([
  255. 'user_id' => $pid,
  256. 'filterable_id' => $filterable['id'],
  257. 'filterable_type' => $filterable['type'],
  258. 'filter_type' => 'block',
  259. ]);
  260. UserFilterService::block($pid, $filterable['id']);
  261. $res = RelationshipService::refresh($pid, $profile->id);
  262. if($request->wantsJson()) {
  263. return response()->json($res);
  264. } else {
  265. return redirect()->back();
  266. }
  267. }
  268. public function unblock(Request $request)
  269. {
  270. $this->validate($request, [
  271. 'type' => 'required|string|in:user',
  272. 'item' => 'required|integer|min:1',
  273. ]);
  274. $pid = $request->user()->profile_id;
  275. $type = $request->input('type');
  276. $item = $request->input('item');
  277. $action = $type . '.block';
  278. if (!in_array($action, $this->filters)) {
  279. return abort(406);
  280. }
  281. $filterable = [];
  282. switch ($type) {
  283. case 'user':
  284. $profile = Profile::findOrFail($item);
  285. if ($profile->id == $pid) {
  286. return abort(403);
  287. }
  288. $class = get_class($profile);
  289. $filterable['id'] = $profile->id;
  290. $filterable['type'] = $class;
  291. break;
  292. default:
  293. abort(400);
  294. break;
  295. }
  296. $filter = UserFilter::whereUserId($pid)
  297. ->whereFilterableId($filterable['id'])
  298. ->whereFilterableType($filterable['type'])
  299. ->whereFilterType('block')
  300. ->first();
  301. if($filter) {
  302. UserFilterService::unblock($pid, $filterable['id']);
  303. $filter->delete();
  304. }
  305. $res = RelationshipService::refresh($pid, $profile->id);
  306. if($request->wantsJson()) {
  307. return response()->json($res);
  308. } else {
  309. return redirect()->back();
  310. }
  311. }
  312. public function followRequests(Request $request)
  313. {
  314. $pid = Auth::user()->profile->id;
  315. $followers = FollowRequest::whereFollowingId($pid)->orderBy('id','desc')->whereIsRejected(0)->simplePaginate(10);
  316. return view('account.follow-requests', compact('followers'));
  317. }
  318. public function followRequestsJson(Request $request)
  319. {
  320. $pid = Auth::user()->profile_id;
  321. $followers = FollowRequest::whereFollowingId($pid)->orderBy('id','desc')->whereIsRejected(0)->get();
  322. $res = [
  323. 'count' => $followers->count(),
  324. 'accounts' => $followers->take(10)->map(function($a) {
  325. $actor = $a->actor;
  326. return [
  327. 'rid' => (string) $a->id,
  328. 'id' => (string) $actor->id,
  329. 'username' => $actor->username,
  330. 'avatar' => $actor->avatarUrl(),
  331. 'url' => $actor->url(),
  332. 'local' => $actor->domain == null,
  333. 'account' => AccountService::get($actor->id)
  334. ];
  335. })
  336. ];
  337. return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
  338. }
  339. public function followRequestHandle(Request $request)
  340. {
  341. $this->validate($request, [
  342. 'action' => 'required|string|max:10',
  343. 'id' => 'required|integer|min:1'
  344. ]);
  345. $pid = Auth::user()->profile->id;
  346. $action = $request->input('action') === 'accept' ? 'accept' : 'reject';
  347. $id = $request->input('id');
  348. $followRequest = FollowRequest::whereFollowingId($pid)->findOrFail($id);
  349. $follower = $followRequest->follower;
  350. switch ($action) {
  351. case 'accept':
  352. $follow = new Follower();
  353. $follow->profile_id = $follower->id;
  354. $follow->following_id = $pid;
  355. $follow->save();
  356. $profile = Profile::findOrFail($pid);
  357. $profile->followers_count++;
  358. $profile->save();
  359. AccountService::del($profile->id);
  360. $profile = Profile::findOrFail($follower->id);
  361. $profile->following_count++;
  362. $profile->save();
  363. AccountService::del($profile->id);
  364. if($follower->domain != null && $follower->private_key === null) {
  365. FollowAcceptPipeline::dispatch($followRequest)->onQueue('follow');
  366. } else {
  367. FollowPipeline::dispatch($follow);
  368. $followRequest->delete();
  369. }
  370. break;
  371. case 'reject':
  372. if($follower->domain != null && $follower->private_key === null) {
  373. FollowRejectPipeline::dispatch($followRequest)->onQueue('follow');
  374. } else {
  375. $followRequest->delete();
  376. }
  377. break;
  378. }
  379. Cache::forget('profile:follower_count:'.$pid);
  380. Cache::forget('profile:following_count:'.$pid);
  381. RelationshipService::refresh($pid, $follower->id);
  382. return response()->json(['msg' => 'success'], 200);
  383. }
  384. public function sudoMode(Request $request)
  385. {
  386. if($request->session()->has('sudoModeAttempts') && $request->session()->get('sudoModeAttempts') >= 3) {
  387. $request->session()->pull('2fa.session.active');
  388. $request->session()->pull('redirectNext');
  389. $request->session()->pull('sudoModeAttempts');
  390. Auth::logout();
  391. return redirect(route('login'));
  392. }
  393. return view('auth.sudo');
  394. }
  395. public function sudoModeVerify(Request $request)
  396. {
  397. $this->validate($request, [
  398. 'password' => 'required|string|max:500',
  399. 'trustDevice' => 'nullable'
  400. ]);
  401. $user = Auth::user();
  402. $password = $request->input('password');
  403. $trustDevice = $request->input('trustDevice') == 'on';
  404. $next = $request->session()->get('redirectNext', '/');
  405. if($request->session()->has('sudoModeAttempts')) {
  406. $count = (int) $request->session()->get('sudoModeAttempts');
  407. $request->session()->put('sudoModeAttempts', $count + 1);
  408. } else {
  409. $request->session()->put('sudoModeAttempts', 1);
  410. }
  411. if(password_verify($password, $user->password) === true) {
  412. $request->session()->put('sudoMode', time());
  413. if($trustDevice == true) {
  414. $request->session()->put('sudoTrustDevice', 1);
  415. }
  416. return redirect($next);
  417. } else {
  418. return redirect()
  419. ->back()
  420. ->withErrors(['password' => __('auth.failed')]);
  421. }
  422. }
  423. public function twoFactorCheckpoint(Request $request)
  424. {
  425. return view('auth.checkpoint');
  426. }
  427. public function twoFactorVerify(Request $request)
  428. {
  429. $this->validate($request, [
  430. 'code' => 'required|string|max:32'
  431. ]);
  432. $user = Auth::user();
  433. $code = $request->input('code');
  434. $google2fa = new Google2FA();
  435. $verify = $google2fa->verifyKey($user->{'2fa_secret'}, $code);
  436. if($verify) {
  437. $request->session()->push('2fa.session.active', true);
  438. return redirect('/');
  439. } else {
  440. if($this->twoFactorBackupCheck($request, $code, $user)) {
  441. return redirect('/');
  442. }
  443. if($request->session()->has('2fa.attempts')) {
  444. $count = (int) $request->session()->get('2fa.attempts');
  445. if($count == 3) {
  446. Auth::logout();
  447. return redirect('/');
  448. }
  449. $request->session()->put('2fa.attempts', $count + 1);
  450. } else {
  451. $request->session()->put('2fa.attempts', 1);
  452. }
  453. return redirect('/i/auth/checkpoint')->withErrors([
  454. 'code' => 'Invalid code'
  455. ]);
  456. }
  457. }
  458. protected function twoFactorBackupCheck($request, $code, User $user)
  459. {
  460. $backupCodes = $user->{'2fa_backup_codes'};
  461. if($backupCodes) {
  462. $codes = json_decode($backupCodes, true);
  463. foreach ($codes as $c) {
  464. if(hash_equals($c, $code)) {
  465. $codes = array_flatten(array_diff($codes, [$code]));
  466. $user->{'2fa_backup_codes'} = json_encode($codes);
  467. $user->save();
  468. $request->session()->push('2fa.session.active', true);
  469. return true;
  470. }
  471. }
  472. return false;
  473. } else {
  474. return false;
  475. }
  476. }
  477. public function accountRestored(Request $request)
  478. {
  479. }
  480. public function accountMutes(Request $request)
  481. {
  482. abort_if(!$request->user(), 403);
  483. $this->validate($request, [
  484. 'limit' => 'nullable|integer|min:1|max:40'
  485. ]);
  486. $user = $request->user();
  487. $limit = $request->input('limit') ?? 40;
  488. $mutes = UserFilter::whereUserId($user->profile_id)
  489. ->whereFilterableType('App\Profile')
  490. ->whereFilterType('mute')
  491. ->simplePaginate($limit)
  492. ->pluck('filterable_id');
  493. $accounts = Profile::find($mutes);
  494. $fractal = new Fractal\Manager();
  495. $fractal->setSerializer(new ArraySerializer());
  496. $resource = new Fractal\Resource\Collection($accounts, new AccountTransformer());
  497. $res = $fractal->createData($resource)->toArray();
  498. $url = $request->url();
  499. $page = $request->input('page', 1);
  500. $next = $page < 40 ? $page + 1 : 40;
  501. $prev = $page > 1 ? $page - 1 : 1;
  502. $links = '<'.$url.'?page='.$next.'&limit='.$limit.'>; rel="next", <'.$url.'?page='.$prev.'&limit='.$limit.'>; rel="prev"';
  503. return response()->json($res, 200, ['Link' => $links]);
  504. }
  505. public function accountBlocks(Request $request)
  506. {
  507. abort_if(!$request->user(), 403);
  508. $this->validate($request, [
  509. 'limit' => 'nullable|integer|min:1|max:40',
  510. 'page' => 'nullable|integer|min:1|max:10'
  511. ]);
  512. $user = $request->user();
  513. $limit = $request->input('limit') ?? 40;
  514. $blocked = UserFilter::select('filterable_id','filterable_type','filter_type','user_id')
  515. ->whereUserId($user->profile_id)
  516. ->whereFilterableType('App\Profile')
  517. ->whereFilterType('block')
  518. ->simplePaginate($limit)
  519. ->pluck('filterable_id');
  520. $profiles = Profile::findOrFail($blocked);
  521. $fractal = new Fractal\Manager();
  522. $fractal->setSerializer(new ArraySerializer());
  523. $resource = new Fractal\Resource\Collection($profiles, new AccountTransformer());
  524. $res = $fractal->createData($resource)->toArray();
  525. $url = $request->url();
  526. $page = $request->input('page', 1);
  527. $next = $page < 40 ? $page + 1 : 40;
  528. $prev = $page > 1 ? $page - 1 : 1;
  529. $links = '<'.$url.'?page='.$next.'&limit='.$limit.'>; rel="next", <'.$url.'?page='.$prev.'&limit='.$limit.'>; rel="prev"';
  530. return response()->json($res, 200, ['Link' => $links]);
  531. }
  532. public function accountBlocksV2(Request $request)
  533. {
  534. return response()->json(UserFilterService::blocks($request->user()->profile_id), 200, [], JSON_UNESCAPED_SLASHES);
  535. }
  536. public function accountMutesV2(Request $request)
  537. {
  538. return response()->json(UserFilterService::mutes($request->user()->profile_id), 200, [], JSON_UNESCAPED_SLASHES);
  539. }
  540. public function accountFiltersV2(Request $request)
  541. {
  542. return response()->json(UserFilterService::filters($request->user()->profile_id), 200, [], JSON_UNESCAPED_SLASHES);
  543. }
  544. }