AccountController.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644
  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. $followed = Follower::whereProfileId($profile->id)->whereFollowingId($pid)->first();
  245. if($followed) {
  246. $followed->delete();
  247. $selfProfile = $request->user()->profile;
  248. $selfProfile->followers_count = Follower::whereFollowingId($pid)->count();
  249. $selfProfile->save();
  250. AccountService::del($selfProfile->id);
  251. }
  252. Notification::whereProfileId($pid)
  253. ->whereActorId($profile->id)
  254. ->get()
  255. ->map(function($n) use($pid) {
  256. NotificationService::del($pid, $n['id']);
  257. $n->forceDelete();
  258. });
  259. break;
  260. }
  261. $filter = UserFilter::firstOrCreate([
  262. 'user_id' => $pid,
  263. 'filterable_id' => $filterable['id'],
  264. 'filterable_type' => $filterable['type'],
  265. 'filter_type' => 'block',
  266. ]);
  267. UserFilterService::block($pid, $filterable['id']);
  268. $res = RelationshipService::refresh($pid, $profile->id);
  269. if($request->wantsJson()) {
  270. return response()->json($res);
  271. } else {
  272. return redirect()->back();
  273. }
  274. }
  275. public function unblock(Request $request)
  276. {
  277. $this->validate($request, [
  278. 'type' => 'required|string|in:user',
  279. 'item' => 'required|integer|min:1',
  280. ]);
  281. $pid = $request->user()->profile_id;
  282. $type = $request->input('type');
  283. $item = $request->input('item');
  284. $action = $type . '.block';
  285. if (!in_array($action, $this->filters)) {
  286. return abort(406);
  287. }
  288. $filterable = [];
  289. switch ($type) {
  290. case 'user':
  291. $profile = Profile::findOrFail($item);
  292. if ($profile->id == $pid) {
  293. return abort(403);
  294. }
  295. $class = get_class($profile);
  296. $filterable['id'] = $profile->id;
  297. $filterable['type'] = $class;
  298. break;
  299. default:
  300. abort(400);
  301. break;
  302. }
  303. $filter = UserFilter::whereUserId($pid)
  304. ->whereFilterableId($filterable['id'])
  305. ->whereFilterableType($filterable['type'])
  306. ->whereFilterType('block')
  307. ->first();
  308. if($filter) {
  309. UserFilterService::unblock($pid, $filterable['id']);
  310. $filter->delete();
  311. }
  312. $res = RelationshipService::refresh($pid, $profile->id);
  313. if($request->wantsJson()) {
  314. return response()->json($res);
  315. } else {
  316. return redirect()->back();
  317. }
  318. }
  319. public function followRequests(Request $request)
  320. {
  321. $pid = Auth::user()->profile->id;
  322. $followers = FollowRequest::whereFollowingId($pid)->orderBy('id','desc')->whereIsRejected(0)->simplePaginate(10);
  323. return view('account.follow-requests', compact('followers'));
  324. }
  325. public function followRequestsJson(Request $request)
  326. {
  327. $pid = Auth::user()->profile_id;
  328. $followers = FollowRequest::whereFollowingId($pid)->orderBy('id','desc')->whereIsRejected(0)->get();
  329. $res = [
  330. 'count' => $followers->count(),
  331. 'accounts' => $followers->take(10)->map(function($a) {
  332. $actor = $a->actor;
  333. return [
  334. 'rid' => (string) $a->id,
  335. 'id' => (string) $actor->id,
  336. 'username' => $actor->username,
  337. 'avatar' => $actor->avatarUrl(),
  338. 'url' => $actor->url(),
  339. 'local' => $actor->domain == null,
  340. 'account' => AccountService::get($actor->id)
  341. ];
  342. })
  343. ];
  344. return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
  345. }
  346. public function followRequestHandle(Request $request)
  347. {
  348. $this->validate($request, [
  349. 'action' => 'required|string|max:10',
  350. 'id' => 'required|integer|min:1'
  351. ]);
  352. $pid = Auth::user()->profile->id;
  353. $action = $request->input('action') === 'accept' ? 'accept' : 'reject';
  354. $id = $request->input('id');
  355. $followRequest = FollowRequest::whereFollowingId($pid)->findOrFail($id);
  356. $follower = $followRequest->follower;
  357. switch ($action) {
  358. case 'accept':
  359. $follow = new Follower();
  360. $follow->profile_id = $follower->id;
  361. $follow->following_id = $pid;
  362. $follow->save();
  363. $profile = Profile::findOrFail($pid);
  364. $profile->followers_count++;
  365. $profile->save();
  366. AccountService::del($profile->id);
  367. $profile = Profile::findOrFail($follower->id);
  368. $profile->following_count++;
  369. $profile->save();
  370. AccountService::del($profile->id);
  371. if($follower->domain != null && $follower->private_key === null) {
  372. FollowAcceptPipeline::dispatch($followRequest)->onQueue('follow');
  373. } else {
  374. FollowPipeline::dispatch($follow);
  375. $followRequest->delete();
  376. }
  377. break;
  378. case 'reject':
  379. if($follower->domain != null && $follower->private_key === null) {
  380. FollowRejectPipeline::dispatch($followRequest)->onQueue('follow');
  381. } else {
  382. $followRequest->delete();
  383. }
  384. break;
  385. }
  386. Cache::forget('profile:follower_count:'.$pid);
  387. Cache::forget('profile:following_count:'.$pid);
  388. RelationshipService::refresh($pid, $follower->id);
  389. return response()->json(['msg' => 'success'], 200);
  390. }
  391. public function sudoMode(Request $request)
  392. {
  393. if($request->session()->has('sudoModeAttempts') && $request->session()->get('sudoModeAttempts') >= 3) {
  394. $request->session()->pull('2fa.session.active');
  395. $request->session()->pull('redirectNext');
  396. $request->session()->pull('sudoModeAttempts');
  397. Auth::logout();
  398. return redirect(route('login'));
  399. }
  400. return view('auth.sudo');
  401. }
  402. public function sudoModeVerify(Request $request)
  403. {
  404. $this->validate($request, [
  405. 'password' => 'required|string|max:500',
  406. 'trustDevice' => 'nullable'
  407. ]);
  408. $user = Auth::user();
  409. $password = $request->input('password');
  410. $trustDevice = $request->input('trustDevice') == 'on';
  411. $next = $request->session()->get('redirectNext', '/');
  412. if($request->session()->has('sudoModeAttempts')) {
  413. $count = (int) $request->session()->get('sudoModeAttempts');
  414. $request->session()->put('sudoModeAttempts', $count + 1);
  415. } else {
  416. $request->session()->put('sudoModeAttempts', 1);
  417. }
  418. if(password_verify($password, $user->password) === true) {
  419. $request->session()->put('sudoMode', time());
  420. if($trustDevice == true) {
  421. $request->session()->put('sudoTrustDevice', 1);
  422. }
  423. //Fix wrong scheme when using reverse proxy
  424. if(!str_contains($next, 'https') && config('instance.force_https_urls', true)) {
  425. $next = Str::of($next)->replace('http', 'https')->toString();
  426. }
  427. return redirect($next);
  428. } else {
  429. return redirect()
  430. ->back()
  431. ->withErrors(['password' => __('auth.failed')]);
  432. }
  433. }
  434. public function twoFactorCheckpoint(Request $request)
  435. {
  436. return view('auth.checkpoint');
  437. }
  438. public function twoFactorVerify(Request $request)
  439. {
  440. $this->validate($request, [
  441. 'code' => 'required|string|max:32'
  442. ]);
  443. $user = Auth::user();
  444. $code = $request->input('code');
  445. $google2fa = new Google2FA();
  446. $verify = $google2fa->verifyKey($user->{'2fa_secret'}, $code);
  447. if($verify) {
  448. $request->session()->push('2fa.session.active', true);
  449. return redirect('/');
  450. } else {
  451. if($this->twoFactorBackupCheck($request, $code, $user)) {
  452. return redirect('/');
  453. }
  454. if($request->session()->has('2fa.attempts')) {
  455. $count = (int) $request->session()->get('2fa.attempts');
  456. if($count == 3) {
  457. Auth::logout();
  458. return redirect('/');
  459. }
  460. $request->session()->put('2fa.attempts', $count + 1);
  461. } else {
  462. $request->session()->put('2fa.attempts', 1);
  463. }
  464. return redirect('/i/auth/checkpoint')->withErrors([
  465. 'code' => 'Invalid code'
  466. ]);
  467. }
  468. }
  469. protected function twoFactorBackupCheck($request, $code, User $user)
  470. {
  471. $backupCodes = $user->{'2fa_backup_codes'};
  472. if($backupCodes) {
  473. $codes = json_decode($backupCodes, true);
  474. foreach ($codes as $c) {
  475. if(hash_equals($c, $code)) {
  476. $codes = array_flatten(array_diff($codes, [$code]));
  477. $user->{'2fa_backup_codes'} = json_encode($codes);
  478. $user->save();
  479. $request->session()->push('2fa.session.active', true);
  480. return true;
  481. }
  482. }
  483. return false;
  484. } else {
  485. return false;
  486. }
  487. }
  488. public function accountRestored(Request $request)
  489. {
  490. }
  491. public function accountMutes(Request $request)
  492. {
  493. abort_if(!$request->user(), 403);
  494. $this->validate($request, [
  495. 'limit' => 'nullable|integer|min:1|max:40'
  496. ]);
  497. $user = $request->user();
  498. $limit = $request->input('limit') ?? 40;
  499. $mutes = UserFilter::whereUserId($user->profile_id)
  500. ->whereFilterableType('App\Profile')
  501. ->whereFilterType('mute')
  502. ->simplePaginate($limit)
  503. ->pluck('filterable_id');
  504. $accounts = Profile::find($mutes);
  505. $fractal = new Fractal\Manager();
  506. $fractal->setSerializer(new ArraySerializer());
  507. $resource = new Fractal\Resource\Collection($accounts, new AccountTransformer());
  508. $res = $fractal->createData($resource)->toArray();
  509. $url = $request->url();
  510. $page = $request->input('page', 1);
  511. $next = $page < 40 ? $page + 1 : 40;
  512. $prev = $page > 1 ? $page - 1 : 1;
  513. $links = '<'.$url.'?page='.$next.'&limit='.$limit.'>; rel="next", <'.$url.'?page='.$prev.'&limit='.$limit.'>; rel="prev"';
  514. return response()->json($res, 200, ['Link' => $links]);
  515. }
  516. public function accountBlocks(Request $request)
  517. {
  518. abort_if(!$request->user(), 403);
  519. $this->validate($request, [
  520. 'limit' => 'nullable|integer|min:1|max:40',
  521. 'page' => 'nullable|integer|min:1|max:10'
  522. ]);
  523. $user = $request->user();
  524. $limit = $request->input('limit') ?? 40;
  525. $blocked = UserFilter::select('filterable_id','filterable_type','filter_type','user_id')
  526. ->whereUserId($user->profile_id)
  527. ->whereFilterableType('App\Profile')
  528. ->whereFilterType('block')
  529. ->simplePaginate($limit)
  530. ->pluck('filterable_id');
  531. $profiles = Profile::findOrFail($blocked);
  532. $fractal = new Fractal\Manager();
  533. $fractal->setSerializer(new ArraySerializer());
  534. $resource = new Fractal\Resource\Collection($profiles, new AccountTransformer());
  535. $res = $fractal->createData($resource)->toArray();
  536. $url = $request->url();
  537. $page = $request->input('page', 1);
  538. $next = $page < 40 ? $page + 1 : 40;
  539. $prev = $page > 1 ? $page - 1 : 1;
  540. $links = '<'.$url.'?page='.$next.'&limit='.$limit.'>; rel="next", <'.$url.'?page='.$prev.'&limit='.$limit.'>; rel="prev"';
  541. return response()->json($res, 200, ['Link' => $links]);
  542. }
  543. public function accountBlocksV2(Request $request)
  544. {
  545. return response()->json(UserFilterService::blocks($request->user()->profile_id), 200, [], JSON_UNESCAPED_SLASHES);
  546. }
  547. public function accountMutesV2(Request $request)
  548. {
  549. return response()->json(UserFilterService::mutes($request->user()->profile_id), 200, [], JSON_UNESCAPED_SLASHES);
  550. }
  551. public function accountFiltersV2(Request $request)
  552. {
  553. return response()->json(UserFilterService::filters($request->user()->profile_id), 200, [], JSON_UNESCAPED_SLASHES);
  554. }
  555. }