ApiV1Dot1Controller.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. <?php
  2. namespace App\Http\Controllers\Api;
  3. use Cache;
  4. use DB;
  5. use App\Http\Controllers\Controller;
  6. use Illuminate\Http\Request;
  7. use League\Fractal;
  8. use League\Fractal\Serializer\ArraySerializer;
  9. use League\Fractal\Pagination\IlluminatePaginatorAdapter;
  10. use App\AccountLog;
  11. use App\EmailVerification;
  12. use App\Status;
  13. use App\Report;
  14. use App\Profile;
  15. use App\User;
  16. use App\Services\AccountService;
  17. use App\Services\StatusService;
  18. use App\Services\ProfileStatusService;
  19. use App\Util\Lexer\RestrictedNames;
  20. use App\Services\EmailService;
  21. use Illuminate\Support\Str;
  22. use Illuminate\Support\Facades\Hash;
  23. use Jenssegers\Agent\Agent;
  24. use Mail;
  25. use App\Mail\PasswordChange;
  26. use App\Mail\ConfirmAppEmail;
  27. class ApiV1Dot1Controller extends Controller
  28. {
  29. protected $fractal;
  30. public function __construct()
  31. {
  32. $this->fractal = new Fractal\Manager();
  33. $this->fractal->setSerializer(new ArraySerializer());
  34. }
  35. public function json($res, $code = 200, $headers = [])
  36. {
  37. return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES);
  38. }
  39. public function error($msg, $code = 400, $extra = [], $headers = [])
  40. {
  41. $res = [
  42. "msg" => $msg,
  43. "code" => $code
  44. ];
  45. return response()->json(array_merge($res, $extra), $code, $headers, JSON_UNESCAPED_SLASHES);
  46. }
  47. public function report(Request $request)
  48. {
  49. $user = $request->user();
  50. abort_if(!$user, 403);
  51. abort_if($user->status != null, 403);
  52. $report_type = $request->input('report_type');
  53. $object_id = $request->input('object_id');
  54. $object_type = $request->input('object_type');
  55. $types = [
  56. 'spam',
  57. 'sensitive',
  58. 'abusive',
  59. 'underage',
  60. 'violence',
  61. 'copyright',
  62. 'impersonation',
  63. 'scam',
  64. 'terrorism'
  65. ];
  66. if (!$report_type || !$object_id || !$object_type) {
  67. return $this->error("Invalid or missing parameters", 400, ["error_code" => "ERROR_INVALID_PARAMS"]);
  68. }
  69. if (!in_array($report_type, $types)) {
  70. return $this->error("Invalid report type", 400, ["error_code" => "ERROR_TYPE_INVALID"]);
  71. }
  72. if ($object_type === "user" && $object_id == $user->profile_id) {
  73. return $this->error("Cannot self report", 400, ["error_code" => "ERROR_NO_SELF_REPORTS"]);
  74. }
  75. $rpid = null;
  76. switch ($object_type) {
  77. case 'post':
  78. $object = Status::find($object_id);
  79. if (!$object) {
  80. return $this->error("Invalid object id", 400, ["error_code" => "ERROR_INVALID_OBJECT_ID"]);
  81. }
  82. $object_type = 'App\Status';
  83. $exists = Report::whereUserId($user->id)
  84. ->whereObjectId($object->id)
  85. ->whereObjectType('App\Status')
  86. ->count();
  87. $rpid = $object->profile_id;
  88. break;
  89. case 'user':
  90. $object = Profile::find($object_id);
  91. if (!$object) {
  92. return $this->error("Invalid object id", 400, ["error_code" => "ERROR_INVALID_OBJECT_ID"]);
  93. }
  94. $object_type = 'App\Profile';
  95. $exists = Report::whereUserId($user->id)
  96. ->whereObjectId($object->id)
  97. ->whereObjectType('App\Profile')
  98. ->count();
  99. $rpid = $object->id;
  100. break;
  101. default:
  102. return $this->error("Invalid report type", 400, ["error_code" => "ERROR_REPORT_OBJECT_TYPE_INVALID"]);
  103. break;
  104. }
  105. if ($exists !== 0) {
  106. return $this->error("Duplicate report", 400, ["error_code" => "ERROR_REPORT_DUPLICATE"]);
  107. }
  108. if ($object->profile_id == $user->profile_id) {
  109. return $this->error("Cannot self report", 400, ["error_code" => "ERROR_NO_SELF_REPORTS"]);
  110. }
  111. $report = new Report;
  112. $report->profile_id = $user->profile_id;
  113. $report->user_id = $user->id;
  114. $report->object_id = $object->id;
  115. $report->object_type = $object_type;
  116. $report->reported_profile_id = $rpid;
  117. $report->type = $report_type;
  118. $report->save();
  119. $res = [
  120. "msg" => "Successfully sent report",
  121. "code" => 200
  122. ];
  123. return $this->json($res);
  124. }
  125. /**
  126. * DELETE /api/v1.1/accounts/avatar
  127. *
  128. * @return \App\Transformer\Api\AccountTransformer
  129. */
  130. public function deleteAvatar(Request $request)
  131. {
  132. $user = $request->user();
  133. abort_if(!$user, 403);
  134. abort_if($user->status != null, 403);
  135. $avatar = $user->profile->avatar;
  136. if( $avatar->media_path == 'public/avatars/default.png' ||
  137. $avatar->media_path == 'public/avatars/default.jpg'
  138. ) {
  139. return AccountService::get($user->profile_id);
  140. }
  141. if(is_file(storage_path('app/' . $avatar->media_path))) {
  142. @unlink(storage_path('app/' . $avatar->media_path));
  143. }
  144. $avatar->media_path = 'public/avatars/default.jpg';
  145. $avatar->change_count = $avatar->change_count + 1;
  146. $avatar->save();
  147. Cache::forget('avatar:' . $user->profile_id);
  148. Cache::forget("avatar:{$user->profile_id}");
  149. Cache::forget('user:account:id:'.$user->id);
  150. AccountService::del($user->profile_id);
  151. return AccountService::get($user->profile_id);
  152. }
  153. /**
  154. * GET /api/v1.1/accounts/{id}/posts
  155. *
  156. * @return \App\Transformer\Api\StatusTransformer
  157. */
  158. public function accountPosts(Request $request, $id)
  159. {
  160. $user = $request->user();
  161. abort_if(!$user, 403);
  162. abort_if($user->status != null, 403);
  163. $account = AccountService::get($id);
  164. if(!$account || $account['username'] !== $request->input('username')) {
  165. return $this->json([]);
  166. }
  167. $posts = ProfileStatusService::get($id);
  168. if(!$posts) {
  169. return $this->json([]);
  170. }
  171. $res = collect($posts)
  172. ->map(function($id) {
  173. return StatusService::get($id);
  174. })
  175. ->filter(function($post) {
  176. return $post && isset($post['account']);
  177. })
  178. ->toArray();
  179. return $this->json($res);
  180. }
  181. /**
  182. * POST /api/v1.1/accounts/change-password
  183. *
  184. * @return \App\Transformer\Api\AccountTransformer
  185. */
  186. public function accountChangePassword(Request $request)
  187. {
  188. $user = $request->user();
  189. abort_if(!$user, 403);
  190. abort_if($user->status != null, 403);
  191. $this->validate($request, [
  192. 'current_password' => 'bail|required|current_password',
  193. 'new_password' => 'required|min:' . config('pixelfed.min_password_length', 8),
  194. 'confirm_password' => 'required|same:new_password'
  195. ],[
  196. 'current_password' => 'The password you entered is incorrect'
  197. ]);
  198. $user->password = bcrypt($request->input('new_password'));
  199. $user->save();
  200. $log = new AccountLog;
  201. $log->user_id = $user->id;
  202. $log->item_id = $user->id;
  203. $log->item_type = 'App\User';
  204. $log->action = 'account.edit.password';
  205. $log->message = 'Password changed';
  206. $log->link = null;
  207. $log->ip_address = $request->ip();
  208. $log->user_agent = $request->userAgent();
  209. $log->save();
  210. Mail::to($request->user())->send(new PasswordChange($user));
  211. return $this->json(AccountService::get($user->profile_id));
  212. }
  213. /**
  214. * GET /api/v1.1/accounts/login-activity
  215. *
  216. * @return array
  217. */
  218. public function accountLoginActivity(Request $request)
  219. {
  220. $user = $request->user();
  221. abort_if(!$user, 403);
  222. abort_if($user->status != null, 403);
  223. $agent = new Agent();
  224. $currentIp = $request->ip();
  225. $activity = AccountLog::whereUserId($user->id)
  226. ->whereAction('auth.login')
  227. ->orderBy('created_at', 'desc')
  228. ->groupBy('ip_address')
  229. ->limit(10)
  230. ->get()
  231. ->map(function($item) use($agent, $currentIp) {
  232. $agent->setUserAgent($item->user_agent);
  233. return [
  234. 'id' => $item->id,
  235. 'action' => $item->action,
  236. 'ip' => $item->ip_address,
  237. 'ip_current' => $item->ip_address === $currentIp,
  238. 'is_mobile' => $agent->isMobile(),
  239. 'device' => $agent->device(),
  240. 'browser' => $agent->browser(),
  241. 'platform' => $agent->platform(),
  242. 'created_at' => $item->created_at->format('c')
  243. ];
  244. });
  245. return $this->json($activity);
  246. }
  247. /**
  248. * GET /api/v1.1/accounts/two-factor
  249. *
  250. * @return array
  251. */
  252. public function accountTwoFactor(Request $request)
  253. {
  254. $user = $request->user();
  255. abort_if(!$user, 403);
  256. abort_if($user->status != null, 403);
  257. $res = [
  258. 'active' => (bool) $user->{'2fa_enabled'},
  259. 'setup_at' => $user->{'2fa_setup_at'}
  260. ];
  261. return $this->json($res);
  262. }
  263. /**
  264. * GET /api/v1.1/accounts/emails-from-pixelfed
  265. *
  266. * @return array
  267. */
  268. public function accountEmailsFromPixelfed(Request $request)
  269. {
  270. $user = $request->user();
  271. abort_if(!$user, 403);
  272. abort_if($user->status != null, 403);
  273. $from = config('mail.from.address');
  274. $emailVerifications = EmailVerification::whereUserId($user->id)
  275. ->orderByDesc('id')
  276. ->where('created_at', '>', now()->subDays(14))
  277. ->limit(10)
  278. ->get()
  279. ->map(function($mail) use($user, $from) {
  280. return [
  281. 'type' => 'Email Verification',
  282. 'subject' => 'Confirm Email',
  283. 'to_address' => $user->email,
  284. 'from_address' => $from,
  285. 'created_at' => str_replace('@', 'at', $mail->created_at->format('M j, Y @ g:i:s A'))
  286. ];
  287. })
  288. ->toArray();
  289. $passwordResets = DB::table('password_resets')
  290. ->whereEmail($user->email)
  291. ->where('created_at', '>', now()->subDays(14))
  292. ->orderByDesc('created_at')
  293. ->limit(10)
  294. ->get()
  295. ->map(function($mail) use($user, $from) {
  296. return [
  297. 'type' => 'Password Reset',
  298. 'subject' => 'Reset Password Notification',
  299. 'to_address' => $user->email,
  300. 'from_address' => $from,
  301. 'created_at' => str_replace('@', 'at', now()->parse($mail->created_at)->format('M j, Y @ g:i:s A'))
  302. ];
  303. })
  304. ->toArray();
  305. $passwordChanges = AccountLog::whereUserId($user->id)
  306. ->whereAction('account.edit.password')
  307. ->where('created_at', '>', now()->subDays(14))
  308. ->orderByDesc('created_at')
  309. ->limit(10)
  310. ->get()
  311. ->map(function($mail) use($user, $from) {
  312. return [
  313. 'type' => 'Password Change',
  314. 'subject' => 'Password Change',
  315. 'to_address' => $user->email,
  316. 'from_address' => $from,
  317. 'created_at' => str_replace('@', 'at', now()->parse($mail->created_at)->format('M j, Y @ g:i:s A'))
  318. ];
  319. })
  320. ->toArray();
  321. $res = collect([])
  322. ->merge($emailVerifications)
  323. ->merge($passwordResets)
  324. ->merge($passwordChanges)
  325. ->sortByDesc('created_at')
  326. ->values();
  327. return $this->json($res);
  328. }
  329. /**
  330. * GET /api/v1.1/accounts/apps-and-applications
  331. *
  332. * @return array
  333. */
  334. public function accountApps(Request $request)
  335. {
  336. $user = $request->user();
  337. abort_if(!$user, 403);
  338. abort_if($user->status != null, 403);
  339. $res = $user->tokens->sortByDesc('created_at')->take(10)->map(function($token, $key) {
  340. return [
  341. 'id' => $key + 1,
  342. 'did' => encrypt($token->id),
  343. 'name' => $token->client->name,
  344. 'scopes' => $token->scopes,
  345. 'revoked' => $token->revoked,
  346. 'created_at' => str_replace('@', 'at', now()->parse($token->created_at)->format('M j, Y @ g:i:s A')),
  347. 'expires_at' => str_replace('@', 'at', now()->parse($token->expires_at)->format('M j, Y @ g:i:s A'))
  348. ];
  349. });
  350. return $this->json($res);
  351. }
  352. public function inAppRegistrationPreFlightCheck(Request $request)
  353. {
  354. return [
  355. 'open' => config('pixelfed.open_registration'),
  356. 'iara' => config('pixelfed.allow_app_registration')
  357. ];
  358. }
  359. public function inAppRegistration(Request $request)
  360. {
  361. abort_if($request->user(), 404);
  362. abort_unless(config('pixelfed.open_registration'), 404);
  363. abort_unless(config('pixelfed.allow_app_registration'), 404);
  364. abort_unless($request->hasHeader('X-PIXELFED-APP'), 403);
  365. $this->validate($request, [
  366. 'email' => [
  367. 'required',
  368. 'string',
  369. 'email',
  370. 'max:255',
  371. 'unique:users',
  372. function ($attribute, $value, $fail) {
  373. $banned = EmailService::isBanned($value);
  374. if($banned) {
  375. return $fail('Email is invalid.');
  376. }
  377. },
  378. ],
  379. 'username' => [
  380. 'required',
  381. 'min:2',
  382. 'max:15',
  383. 'unique:users',
  384. function ($attribute, $value, $fail) {
  385. $dash = substr_count($value, '-');
  386. $underscore = substr_count($value, '_');
  387. $period = substr_count($value, '.');
  388. if(ends_with($value, ['.php', '.js', '.css'])) {
  389. return $fail('Username is invalid.');
  390. }
  391. if(($dash + $underscore + $period) > 1) {
  392. return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
  393. }
  394. if (!ctype_alnum($value[0])) {
  395. return $fail('Username is invalid. Must start with a letter or number.');
  396. }
  397. if (!ctype_alnum($value[strlen($value) - 1])) {
  398. return $fail('Username is invalid. Must end with a letter or number.');
  399. }
  400. $val = str_replace(['_', '.', '-'], '', $value);
  401. if(!ctype_alnum($val)) {
  402. return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
  403. }
  404. $restricted = RestrictedNames::get();
  405. if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
  406. return $fail('Username cannot be used.');
  407. }
  408. },
  409. ],
  410. 'password' => 'required|string|min:8',
  411. // 'avatar' => 'required|mimetypes:image/jpeg,image/png|max:15000',
  412. // 'bio' => 'required|max:140'
  413. ]);
  414. $email = $request->input('email');
  415. $username = $request->input('username');
  416. $password = $request->input('password');
  417. if(config('database.default') == 'pgsql') {
  418. $username = strtolower($username);
  419. $email = strtolower($email);
  420. }
  421. $user = new User;
  422. $user->name = $username;
  423. $user->username = $username;
  424. $user->email = $email;
  425. $user->password = Hash::make($password);
  426. $user->register_source = 'app';
  427. $user->app_register_ip = $request->ip();
  428. $user->app_register_token = Str::random(32);
  429. $user->save();
  430. $rtoken = Str::random(mt_rand(64, 70));
  431. $verify = new EmailVerification();
  432. $verify->user_id = $user->id;
  433. $verify->email = $user->email;
  434. $verify->user_token = $user->app_register_token;
  435. $verify->random_token = $rtoken;
  436. $verify->save();
  437. $appUrl = 'pixelfed://confirm-account/'. $user->app_register_token . '?rt=' . $rtoken;
  438. Mail::to($user->email)->send(new ConfirmAppEmail($verify, $appUrl));
  439. return response()->json([
  440. 'success' => true,
  441. ]);
  442. }
  443. public function inAppRegistrationConfirm(Request $request)
  444. {
  445. abort_if($request->user(), 404);
  446. abort_unless(config('pixelfed.open_registration'), 404);
  447. abort_unless(config('pixelfed.allow_app_registration'), 404);
  448. abort_unless($request->hasHeader('X-PIXELFED-APP'), 403);
  449. $this->validate($request, [
  450. 'user_token' => 'required',
  451. 'random_token' => 'required',
  452. 'email' => 'required'
  453. ]);
  454. $verify = EmailVerification::whereEmail($request->input('email'))
  455. ->whereUserToken($request->input('user_token'))
  456. ->whereRandomToken($request->input('random_token'))
  457. ->first();
  458. if(!$verify) {
  459. return response()->json(['error' => 'Invalid tokens'], 403);
  460. }
  461. $user = User::findOrFail($verify->user_id);
  462. $user->email_verified_at = now();
  463. $user->last_active_at = now();
  464. $user->save();
  465. $verify->delete();
  466. $token = $user->createToken('Pixelfed');
  467. return response()->json([
  468. 'access_token' => $token->accessToken
  469. ]);
  470. }
  471. }