Skip to content

Commit

Permalink
Merge pull request #815 from shikorism/develop
Browse files Browse the repository at this point in the history
Release 20220122.1500
  • Loading branch information
shibafu528 authored Jan 22, 2022
2 parents b59166b + 9e2c1f1 commit 3114c7e
Show file tree
Hide file tree
Showing 66 changed files with 20,190 additions and 14,434 deletions.
4 changes: 2 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ version: 2.1
executors:
build:
docker:
- image: circleci/php:7.3-node-browsers
- image: circleci/php:7.4-node-browsers
environment:
APP_DEBUG: true
APP_ENV: testing
Expand All @@ -12,7 +12,7 @@ executors:
DB_DATABASE: tissue
DB_USERNAME: tissue
DB_PASSWORD: tissue
- image: circleci/postgres:10-alpine
- image: circleci/postgres:13
environment:
POSTGRES_DB: tissue
POSTGRES_USER: tissue
Expand Down
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,8 @@ METADATA_RESOLVER_IGNORE_ACCESS_INTERVAL=false

# (開発用) メタデータをキャッシュしないようにする
METADATA_NO_CACHE=false

# php artisan passport:install の実行結果から、
# "Personal access client created successfully." の直後に出力されているClient IDとClient secretをここに設定する
PASSPORT_PERSONAL_ACCESS_CLIENT_ID=
PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET=
2 changes: 0 additions & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ module.exports = {
'plugin:prettier/recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
'prettier/@typescript-eslint',
'prettier/react',
],
parser: '@typescript-eslint/parser',
parserOptions: {
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
FROM node:16-buster as node

FROM php:7.3-apache
FROM php:7.4-apache

ENV APACHE_DOCUMENT_ROOT /var/www/html/public

Expand Down
41 changes: 36 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ a.k.a. shikorism.net

## 実行環境

- PHP 7.3
- PostgreSQL 9.6
- PHP 7.4
- PostgreSQL 14
- ⚠️ 2021年11月以前に環境を構築したことがある場合、移行作業が必要です!
[開発環境向けの移行手順](https://github.com/shikorism/tissue/issues/752#issuecomment-939257394) を参考にしてください。

## 開発環境の構築

Expand Down Expand Up @@ -48,20 +50,49 @@ docker-compose exec web php artisan migrate
docker-compose exec web php artisan db:seed
```

6. ファイルに書き込めるように権限を設定します。
6. OAuth2サーバ設定の初期化を行います。

```
docker-compose exec web php artisan passport:install
```

コマンドを実行すると、次のようなメッセージが出力されます。**この内容は控えておいてください。**

```
Personal access client created successfully.
Here is your new client secret. This is the only time it will be shown so don't lose it!
Client ID: 1
Client secret: xxxxxxxx
Password grant client created successfully.
Here is your new client secret. This is the only time it will be shown so don't lose it!
Client ID: 2
Client secret: yyyyyyyy
```

7. `.env` ファイルにPersonal access token発行用の設定を追加します。
直前の手順のメッセージから `Personal access client created successfully` の直後に出力されている ID と secret を `PASSPORT_PERSONAL_ACCESS_CLIENT_ID``PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET` に設定します。

```
PASSPORT_PERSONAL_ACCESS_CLIENT_ID=1
PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET=xxxxxxxx
```

8. ファイルに書き込めるように権限を設定します。

```
docker-compose exec web chown -R www-data /var/www/html/storage
```

7. アセットをビルドします。
9. アセットをビルドします。

```
docker-compose exec web yarn dev
```


8. 最後に `.env` を読み込み直すために起動し直します。
10. 最後に `.env` を読み込み直すために起動し直します。

```
docker-compose up -d
Expand Down
5 changes: 3 additions & 2 deletions app/Ejaculation.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ class Ejaculation extends Model
const SOURCE_WEB = 'web';
const SOURCE_CSV = 'csv';
const SOURCE_WEBHOOK = 'webhook';
const SOURCE_API = 'api';

protected $fillable = [
'user_id', 'ejaculated_date',
'note', 'geo_latitude', 'geo_longitude', 'link', 'source',
'is_private', 'is_too_sensitive', 'discard_elapsed_time',
'checkin_webhook_id'
'checkin_webhook_id', 'oauth_access_token_id',
];

protected $dates = [
Expand Down Expand Up @@ -89,7 +90,7 @@ public function likes()

public function scopeVisibleToTimeline(Builder $query)
{
return $query->whereIn('ejaculations.source', [Ejaculation::SOURCE_WEB, Ejaculation::SOURCE_WEBHOOK]);
return $query->whereIn('ejaculations.source', [Ejaculation::SOURCE_WEB, Ejaculation::SOURCE_WEBHOOK, Ejaculation::SOURCE_API]);
}

public function scopeWithLikes(Builder $query)
Expand Down
16 changes: 16 additions & 0 deletions app/Exceptions/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\JsonResponse;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class Handler extends ExceptionHandler
Expand Down Expand Up @@ -96,4 +97,19 @@ protected function prepareJsonResponse($request, Exception $e)
JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES
);
}

protected function invalidJson($request, ValidationException $exception)
{
return response()->json([
'status' => $exception->status,
'error' => [
'message' => $exception->getMessage(),
'violations' => collect($exception->validator->errors())->flatMap(function ($values, $field) {
return collect($values)->map(function ($message) use ($field) {
return compact('message', 'field');
});
}),
],
], $exception->status);
}
}
153 changes: 153 additions & 0 deletions app/Http/Controllers/Api/V1/CheckinController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
<?php

namespace App\Http\Controllers\Api\V1;

use App\Ejaculation;
use App\Events\LinkDiscovered;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V1\CheckinStoreRequest;
use App\Http\Resources\EjaculationResource;
use App\Tag;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;

class CheckinController extends Controller
{
public function store(CheckinStoreRequest $request)
{
$inputs = $request->validated();

$ejaculatedDate = empty($inputs['checked_in_at']) ? now() : new Carbon($inputs['checked_in_at']);
$ejaculatedDate = $ejaculatedDate->setTimezone(date_default_timezone_get())->startOfMinute();
if (Ejaculation::where(['user_id' => Auth::id(), 'ejaculated_date' => $ejaculatedDate])->count()) {
throw new UnprocessableEntityHttpException('Checkin already exists in this time');
}

$ejaculation = DB::transaction(function () use ($inputs, $ejaculatedDate) {
$ejaculation = Ejaculation::create([
'user_id' => Auth::id(),
'ejaculated_date' => $ejaculatedDate,
'note' => $inputs['note'] ?? '',
'link' => $inputs['link'] ?? '',
'source' => Ejaculation::SOURCE_API,
'is_private' => (bool)($inputs['is_private'] ?? false),
'is_too_sensitive' => (bool)($inputs['is_too_sensitive'] ?? false),
'discard_elapsed_time' => (bool)($inputs['discard_elapsed_time'] ?? false),
'oauth_access_token_id' => Auth::user()->token()->id,
]);

$tagIds = [];
if (!empty($inputs['tags'])) {
foreach ($inputs['tags'] as $tag) {
$tag = trim($tag);
if ($tag === '') {
continue;
}

$tag = Tag::firstOrCreate(['name' => $tag]);
$tagIds[] = $tag->id;
}
}
$ejaculation->tags()->sync($tagIds);

return $ejaculation;
});

if (!empty($ejaculation->link)) {
event(new LinkDiscovered($ejaculation->link));
}

return new EjaculationResource($ejaculation);
}

public function show(Ejaculation $checkin)
{
$owner = $checkin->user;
if (!$owner->isMe()) {
if ($owner->is_protected) {
throw new AccessDeniedHttpException('このユーザはチェックイン履歴を公開していません');
}
if ($checkin->is_private) {
throw new AccessDeniedHttpException('非公開チェックインのため、表示できません');
}
}

return new EjaculationResource($checkin);
}

public function update(CheckinStoreRequest $request, Ejaculation $checkin)
{
$inputs = $request->validated();

if (isset($inputs['checked_in_at'])) {
$ejaculatedDate = new Carbon($inputs['checked_in_at']);
$ejaculatedDate = $ejaculatedDate->setTimezone(date_default_timezone_get())->startOfMinute();
if (Ejaculation::where(['user_id' => Auth::id(), 'ejaculated_date' => $ejaculatedDate])->where('id', '<>', $checkin->id)->count()) {
throw new UnprocessableEntityHttpException('Checkin already exists in this time');
}

$checkin->ejaculated_date = $ejaculatedDate;
}
if (isset($inputs['note'])) {
$checkin->note = $inputs['note'];
}
if (isset($inputs['link'])) {
$checkin->link = $inputs['link'];
}
if (isset($inputs['is_private'])) {
$checkin->is_private = (bool)($inputs['is_private'] ?? false);
}
if (isset($inputs['is_too_sensitive'])) {
$checkin->is_too_sensitive = (bool)($inputs['is_too_sensitive'] ?? false);
}
if (isset($inputs['discard_elapsed_time'])) {
$checkin->discard_elapsed_time = (bool)($inputs['discard_elapsed_time'] ?? false);
}

DB::transaction(function () use ($inputs, $checkin) {
$checkin->save();

if (isset($inputs['tags'])) {
$tagIds = [];
if (!empty($inputs['tags'])) {
foreach ($inputs['tags'] as $tag) {
$tag = trim($tag);
if ($tag === '') {
continue;
}

$tag = Tag::firstOrCreate(['name' => $tag]);
$tagIds[] = $tag->id;
}
}
$checkin->tags()->sync($tagIds);
}
});

if (!empty($checkin->link)) {
event(new LinkDiscovered($checkin->link));
}

return new EjaculationResource($checkin);
}

public function destroy($checkin)
{
$ejaculation = Ejaculation::find($checkin);

if ($ejaculation !== null) {
$this->authorize('edit', $ejaculation);

DB::transaction(function () use ($ejaculation) {
$ejaculation->tags()->detach();
$ejaculation->delete();
});
}

return response()->noContent();
}
}
16 changes: 16 additions & 0 deletions app/Http/Controllers/Api/V1/MeController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace App\Http\Controllers\Api\V1;

use App\Http\Controllers\Controller;
use App\Http\Resources\UserResource;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class MeController extends Controller
{
public function show()
{
return new UserResource(Auth::user());
}
}
50 changes: 50 additions & 0 deletions app/Http/Controllers/Api/V1/UserCheckinController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace App\Http\Controllers\Api\V1;

use App\Ejaculation;
use App\Http\Controllers\Controller;
use App\Http\Resources\EjaculationResource;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;

class UserCheckinController extends Controller
{
public function index(Request $request, User $user)
{
if (!$user->isMe() && $user->is_protected) {
throw new AccessDeniedHttpException('このユーザはチェックイン履歴を公開していません');
}

$inputs = $request->validate([
'per_page' => 'nullable|integer|between:10,100',
]);

$query = Ejaculation::select(DB::raw(
<<<'SQL'
ejaculations.id,
ejaculated_date,
note,
is_private,
is_too_sensitive,
link,
source,
discard_elapsed_time
SQL
))
->where('user_id', $user->id);
if (!Auth::check() || $user->id !== Auth::id()) {
$query = $query->where('is_private', false);
}
$ejaculations = $query->orderBy('ejaculated_date', 'desc')
->with('tags')
->withLikes()
->withMutedStatus()
->paginate($inputs['per_page'] ?? 20);

return response()->fromPaginator($ejaculations, EjaculationResource::class);
}
}
Loading

0 comments on commit 3114c7e

Please sign in to comment.