diff --git a/app/Console/Commands/ConfirmUser.php b/app/Console/Commands/ConfirmUser.php new file mode 100644 index 0000000..daeb24e --- /dev/null +++ b/app/Console/Commands/ConfirmUser.php @@ -0,0 +1,45 @@ +argument('user')); + $u->update('confirmed', true); + $this->info("User #$u->id with e-mail $u->email and handle @$u->name was confirmed."); + } +} diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index b2ea669..191b2b6 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -25,7 +25,7 @@ class LoginController extends Controller * * @var string */ - protected $redirectTo = '/home'; + protected $redirectTo = '/'; /** * Create a new controller instance. diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index 8973825..f21741e 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -7,6 +7,7 @@ use App\Http\Controllers\Controller; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator; use Illuminate\Foundation\Auth\RegistersUsers; +use Illuminate\Validation\Rule; class RegisterController extends Controller { @@ -28,7 +29,7 @@ class RegisterController extends Controller * * @var string */ - protected $redirectTo = '/home'; + protected $redirectTo = '/'; /** * Create a new controller instance. @@ -49,9 +50,15 @@ class RegisterController extends Controller protected function validator(array $data) { return Validator::make($data, [ - 'name' => 'required|string|max:255|regex:/^[a-zA-Z0-9_.-]+$/', + 'name' => [ + 'regex:/^[a-zA-Z0-9_.-]+$/', + 'required', + 'string', + 'max:255', + 'unique:users' + ], 'email' => 'required|string|email|max:255|unique:users', - 'password' => 'required|string|min:6|confirmed', + 'password' => 'required|string|min:6|max:1000|confirmed', // max len to foil DOS attempts ]); } @@ -65,7 +72,7 @@ class RegisterController extends Controller { return User::create([ 'name' => $data['name'], - 'title' => $data['name'], // display name - by default, init to name + 'title' => $data['name'], 'email' => $data['email'], 'password' => Hash::make($data['password']), ]); diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php deleted file mode 100644 index 94c3b2d..0000000 --- a/app/Http/Controllers/HomeController.php +++ /dev/null @@ -1,20 +0,0 @@ -tables()->paginate(10); - - return view('home')->with(compact('tables')); - } -} diff --git a/app/Http/Controllers/TableController.php b/app/Http/Controllers/TableController.php index d60af5d..2066139 100644 --- a/app/Http/Controllers/TableController.php +++ b/app/Http/Controllers/TableController.php @@ -8,6 +8,7 @@ use App\Models\Table; use App\Models\User; use App\Utils\Column; use Illuminate\Http\Request; +use Illuminate\Validation\Rule; use MightyPork\Exceptions\NotApplicableException; class TableController extends Controller @@ -16,7 +17,7 @@ class TableController extends Controller { /** @var Table $tableModel */ $tableModel = $user->tables()->where('name', $table)->first(); - $revision = $tableModel->activeRevision; + $revision = $tableModel->revision; return view('table.view', [ 'table' => $tableModel, @@ -54,12 +55,12 @@ class TableController extends Controller $u = \Auth::user(); $this->validate($request, [ - 'name' => 'required', - 'title' => 'string', - 'description' => 'string|nullable', - 'license' => 'string|nullable', - 'origin' => 'string|nullable', - 'columns' => 'required', + 'name' => 'required|string|max:255', + 'title' => 'string|string|max:255', + 'description' => 'string|nullable|max:4000', + 'license' => 'string|nullable|max:4000', + 'origin' => 'string|nullable|max:4000', + 'columns' => 'required|string', 'data' => 'string|nullable', ]); @@ -74,16 +75,24 @@ class TableController extends Controller // Parse and validate the columns specification /** @var Column[] $columns */ $columns = []; + $column_keys = []; // for checking duplicates $colTable = array_map('str_getcsv', explode("\n", $request->get('columns'))); + + // prevent griefing via long list of columns + if (count($colTable) > 100) return $this->backWithErrors(['columns' => "Too many columns"]); + foreach ($colTable as $col) { $col = array_map('trim', $col); - if (count($col) < 2) { - return $this->backWithErrors([ - 'columns' => "All columns must have at least name and type.", - ]); + if (count($col) < 2 || strlen($col[0])==0) { + return $this->backWithErrors(['columns' => "All columns must have at least name and type."]); } try { + if (in_array($col[0], $column_keys)) { + return $this->backWithErrors(['columns' => "Duplicate column: $col[0]"]); + } + $column_keys[] = $col[0]; + $columns[] = new Column([ 'name' => $col[0], 'type' => $col[1], @@ -93,6 +102,7 @@ class TableController extends Controller return $this->backWithErrors(['columns' => $e->getMessage()]); } } + if (count($columns) == 0) return $this->backWithErrors(['columns' => "Define at least one column"]); $rowTable = array_map('str_getcsv', explode("\n", $request->get('data'))); @@ -110,38 +120,42 @@ class TableController extends Controller } return [ 'data' => json_encode($parsed), - 'refs' => 1, ]; }, $rowTable); } catch (\Exception $e) { return $this->backWithErrors(['columns' => $e->getMessage()]); } - $revision = Revision::create([ - 'refs' => 1, // from the new table + $revisionFields = [ 'note' => "Initial revision of table $u->name/$tabName", 'columns' => json_encode($columns), - ]); + 'row_count' => count($rowsData), + ]; - $table = Table::create([ + $tableFields = [ 'owner_id' => $u->id, - 'revision_id' => $revision->id, + 'revision_id' => 0, 'name' => $tabName, 'title' => $request->get('title'), 'description' => $request->get('description'), 'license' => $request->get('license'), 'origin' => $request->get('origin'), - ]); + ]; + + \DB::transaction(function () use ($revisionFields, $tableFields, $rowsData) { + $revision = Revision::create($revisionFields); - // Attach the revision to the table, set as current - $table->revisions()->attach($revision); - $table->activeRevision()->associate($revision); + $tableFields['revision_id'] = $revision->id; // to meet the not-null constraint + $table = Table::create($tableFields); - // Spawn the rows, linked to the revision - $revision->rows()->createMany($rowsData); + // Attach the revision to the table, set as current + $table->revisions()->attach($revision); + $table->revision()->associate($revision); - // Now we create rows, a revision pointing to them, and the table using it. + // Spawn rows, linked to the revision + $revision->rows()->createMany($rowsData); + }); - return "Ok."; + return redirect(route('table.view', ['user' => $u, 'table' => $tabName])); } } diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php new file mode 100644 index 0000000..4c2a871 --- /dev/null +++ b/app/Http/Controllers/UserController.php @@ -0,0 +1,24 @@ +tables() + ->with('revision:id,row_count') + ->paginate(3); + + return view('user.view')->with(compact('tables', 'user')); + } +} diff --git a/app/Models/ContentReport.php b/app/Models/ContentReport.php index d79e369..d044f02 100644 --- a/app/Models/ContentReport.php +++ b/app/Models/ContentReport.php @@ -14,8 +14,8 @@ use Illuminate\Database\Eloquent\Model; * @property int $object_id * @property int $author_id * @property string $message - * @property User $author * @property mixed $object - morph + * @property-read User $author */ class ContentReport extends Model { diff --git a/app/Models/EmailConfirmation.php b/app/Models/EmailConfirmation.php new file mode 100644 index 0000000..31ba99e --- /dev/null +++ b/app/Models/EmailConfirmation.php @@ -0,0 +1,26 @@ +belongsTo(User::class, 'user_id'); + } +} diff --git a/app/Models/Proposal.php b/app/Models/Proposal.php index 877f881..ef9973c 100644 --- a/app/Models/Proposal.php +++ b/app/Models/Proposal.php @@ -15,10 +15,10 @@ use Illuminate\Database\Eloquent\Model; * @property int $revision_id * @property int $author_id * @property string $note - * @property User $author - * @property Table $table - * @property Revision $revision * @property object $changes - JSONB + * @property-read User $author + * @property-read Table $table + * @property-read Revision $revision */ class Proposal extends Model { diff --git a/app/Models/Revision.php b/app/Models/Revision.php index fbf3321..bfe2a35 100644 --- a/app/Models/Revision.php +++ b/app/Models/Revision.php @@ -15,10 +15,11 @@ use Riesjart\Relaquent\Model\Concerns\HasRelaquentRelationships; * @property int $ancestor_id * @property string $note * @property object $columns - * @property Revision|null $parentRevision - * @property Row[]|Collection $rows - * @property Proposal|null $sourceProposal - proposal that was used to create this revision - * @property Proposal[]|Collection $dependentProposals + * @property int $row_count - cached number of rows in the revision + * @property-read Revision|null $parentRevision + * @property-read Row[]|Collection $rows + * @property-read Proposal|null $sourceProposal - proposal that was used to create this revision + * @property-read Proposal[]|Collection $dependentProposals */ class Revision extends Model { diff --git a/app/Models/Table.php b/app/Models/Table.php index de6ebd3..f07047b 100644 --- a/app/Models/Table.php +++ b/app/Models/Table.php @@ -2,9 +2,11 @@ namespace App\Models; +use App\Models\Concerns\ManyManyThrough; use App\Models\Concerns\Reportable; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; +use Riesjart\Relaquent\Model\Concerns\HasRelaquentRelationships; /** * A data table object (referencing revisions) @@ -20,15 +22,15 @@ use Illuminate\Database\Eloquent\Model; * @property string $description * @property string $license * @property string $origin - * @property User $owner - * @property Table $parentTable - * @property Table[]|Collection $forks - * @property Revision[]|Collection $revisions - * @property Revision $activeRevision - * @property Proposal[]|Collection $proposal - * @property TableComment[]|Collection $comments - * @property User[]|Collection $favouritingUsers - * @property User[]|Collection $discussionFollowers + * @property-read User $owner + * @property-read Table $parentTable + * @property-read Table[]|Collection $forks + * @property-read Revision[]|Collection $revisions + * @property-read Revision $revision + * @property-read Proposal[]|Collection $proposal + * @property-read TableComment[]|Collection $comments + * @property-read User[]|Collection $favouritingUsers + * @property-read User[]|Collection $discussionFollowers */ class Table extends Model { @@ -60,7 +62,7 @@ class Table extends Model } /** Active revision */ - public function activeRevision() + public function revision() { return $this->belongsTo(Revision::class, 'revision_id'); } diff --git a/app/Models/TableComment.php b/app/Models/TableComment.php index 6b65e30..fb25afb 100644 --- a/app/Models/TableComment.php +++ b/app/Models/TableComment.php @@ -16,10 +16,10 @@ use Illuminate\Database\Eloquent\Model; * @property int $author_id * @property int $ancestor_id * @property string $message - * @property User $author - * @property Table $table - * @property TableComment|null $ancestor - * @property TableComment[]|Collection $replies + * @property-read User $author + * @property-read Table $table + * @property-read TableComment|null $ancestor + * @property-read TableComment[]|Collection $replies */ class TableComment extends Model { diff --git a/app/Models/User.php b/app/Models/User.php index 6c426b9..81403db 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -8,6 +8,8 @@ use Illuminate\Database\Eloquent\Collection; use Illuminate\Notifications\Notifiable; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notification; +use MightyPork\Exceptions\ArgumentException; +use MightyPork\Exceptions\NotExistException; /** * A user in the application @@ -19,16 +21,18 @@ use Illuminate\Notifications\Notification; * @property string $title - for display * @property string $email - unique, for login and social auth chaining * @property string $password - hashed pw - * @property Proposal[]|Collection $proposals - * @property Table[]|Collection $tables - * @property OAuthIdentity[]|Collection $socialIdentities - * @property TableComment[]|Collection $tableComments - * @property Table[]|Collection $favouriteTables - * @property Table[]|Collection $followedDiscussions - * @property ContentReport[]|Collection $authoredReports - * @property Notification[]|Collection $notifications - * @property Notification[]|Collection $readNotifications - * @property Notification[]|Collection $unreadNotifications + * @property bool $confirmed - user e-mail is confirmed + * @property-read Proposal[]|Collection $proposals + * @property-read Table[]|Collection $tables + * @property-read OAuthIdentity[]|Collection $socialIdentities + * @property-read TableComment[]|Collection $tableComments + * @property-read Table[]|Collection $favouriteTables + * @property-read Table[]|Collection $followedDiscussions + * @property-read ContentReport[]|Collection $authoredReports + * @property-read Notification[]|Collection $notifications + * @property-read Notification[]|Collection $readNotifications + * @property-read Notification[]|Collection $unreadNotifications + * @property-read string $handle - user handle, including @ */ class User extends Authenticatable { @@ -137,12 +141,36 @@ class User extends Authenticatable $this->followedDiscussions()->detach($table); } - public function fakeTables() + /** + * Resolve user by a given name, id, or e-mail + * + * @param string $ident + * @return User + */ + public static function resolve($ident) { - return [ - (object)['title' => 'Table 1'], - (object)['title' => 'Table 2'], - (object)['title' => 'Table 3'] - ]; + $ident = "$ident"; + if (strlen($ident) == 0) throw new ArgumentException("Can't find user by empty string."); + + if (is_numeric($ident)) { + $u = static::find((int)$ident); + } + else if ($ident[0] == '@') { + $u = static::where('name', substr($ident, 1))->first(); + } + else { + $u = static::where('email', $ident)->first(); + } + + if (!$u) throw new NotExistException("No user found matching \"$ident\""); + + return $u; + } + + public function __get($name) + { + if ($name == 'handle') return "@$this->name"; + + return parent::__get($name); } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 9111c52..55bc903 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -22,7 +22,8 @@ class AppServiceProvider extends ServiceProvider \Route::bind('user', function ($value) { $u = User::where('name', $value)->first(); // it may also be the _id directly - if (!$u) $u = User::find($value); + if (!$u && is_numeric($value)) $u = User::find((int)$value); + if (!$u) abort(404); return $u; }); } diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 69772c1..5ea48d3 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -53,11 +53,7 @@ class RouteServiceProvider extends ServiceProvider { Route::middleware('web') ->namespace($this->namespace) - ->group(base_path('routes/public.php')); - - Route::middleware(['web', 'auth']) - ->namespace($this->namespace) - ->group(base_path('routes/user.php')); + ->group(base_path('routes/web.php')); } /** diff --git a/app/View/Widget.php b/app/View/Widget.php index 4f5326c..4041400 100644 --- a/app/View/Widget.php +++ b/app/View/Widget.php @@ -19,9 +19,10 @@ class Widget { protected $attributesArray = [], $id, $name, $label, $value, - $viewName, - $help, - $labelCols, $fieldCols, + $viewName, // view to use inside the form namespace + $help, // help button with tooltip + $prepend, $append, // input-group (add-ons) + $labelCols, $fieldCols, // grid layout setting (label/field cols) $styleArray = []; public function __construct($viewName, $name, $label) diff --git a/app/helpers.php b/app/helpers.php index 08be97b..b93192b 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -1,3 +1,24 @@ timestamps(); $table->string('name')->unique(); $table->string('title')->index(); + $table->text('bio')->nullable(); + $table->text('website')->nullable(); $table->string('email')->unique(); // would have to be nullable if we supported login via providers that don't give us e-mail - $table->string('password')->nullable(); + $table->string('password')->nullable(); // null if registered via provider and not set a pw yet + $table->boolean('confirmed')->default(false); // set to 1 after e-mail is verified $table->rememberToken(); }); } diff --git a/database/migrations/2018_07_08_193600_create_revisions_table.php b/database/migrations/2018_07_08_193600_create_revisions_table.php index 3dc32f1..3a339b5 100644 --- a/database/migrations/2018_07_08_193600_create_revisions_table.php +++ b/database/migrations/2018_07_08_193600_create_revisions_table.php @@ -18,6 +18,8 @@ class CreateRevisionsTable extends Migration $table->timestamps(); $table->unsignedInteger('ancestor_id')->index()->nullable(); // parent revision + $table->unsignedInteger('row_count'); // cached nbr of rows + // columns specification // array with: {name, title, type} $table->jsonb('columns'); diff --git a/database/migrations/2018_07_22_083900_create_email_confirmations_table.php b/database/migrations/2018_07_22_083900_create_email_confirmations_table.php new file mode 100644 index 0000000..5f64e76 --- /dev/null +++ b/database/migrations/2018_07_22_083900_create_email_confirmations_table.php @@ -0,0 +1,32 @@ +unsignedInteger('user_id')->index(); + $table->timestamp('created_at')->nullable(); + $table->string('token'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('email_confirmations'); + } +} diff --git a/porklib/Utils/Str.php b/porklib/Utils/Str.php index 7c72f8a..0cdf125 100644 --- a/porklib/Utils/Str.php +++ b/porklib/Utils/Str.php @@ -654,11 +654,18 @@ class Str extends \Illuminate\Support\Str return preg_replace('/,\s*([}\]])/s','\1', $str); } - public static function ellipsis($str, $maxlen) + public static function ellipsis($str, $maxlen, $margin=10) { $len = mb_strlen($str); if ($len > $maxlen) { - return mb_substr($str, 0, $maxlen) . '…'; + $hard = mb_substr($str, 0, $maxlen); + + $lastspace = mb_strrpos($hard, ' '); + if ($lastspace > $maxlen - $margin) { + $hard = mb_substr($hard, 0, $lastspace); + } + + return $hard . '…'; } return $str; diff --git a/public/fonts/fa-dtbl-1-preview.html b/public/fonts/fa-dtbl-1-preview.html index 0fa4298..2540925 100644 --- a/public/fonts/fa-dtbl-1-preview.html +++ b/public/fonts/fa-dtbl-1-preview.html @@ -161,18 +161,47 @@ [data-icon]:before { content: attr(data-icon); } [data-icon]:before, - .fa-check:before, + .fa-bell:before, +.fa-bell-o:before, +.fa-calendar:before, +.fa-check:before, +.fa-clock-o:before, +.fa-cloud-upload:before, +.fa-code-fork:before, +.fa-download:before, +.fa-eye:before, +.fa-eye-slash:before, .fa-facebook-square:before, +.fa-filter:before, +.fa-flag:before, .fa-floppy-o:before, +.fa-gavel:before, .fa-github:before, .fa-google:before, +.fa-history:before, +.fa-link:before, +.fa-pencil-square-o:before, .fa-question-circle:before, +.fa-quote-left:before, +.fa-reply:before, +.fa-rss:before, +.fa-search:before, +.fa-share-alt:before, .fa-sign-in:before, .fa-sign-out:before, +.fa-sliders:before, +.fa-sort:before, +.fa-sort-asc:before, +.fa-sort-desc:before, +.fa-star:before, +.fa-star-o:before, +.fa-table:before, .fa-times:before, +.fa-trash:before, .fa-trash-o:before, .fa-user-circle-o:before, -.fa-user-plus:before { +.fa-user-plus:before, +.fa-wrench:before { display: inline-block; font-family: "fa-dtbl-1"; font-style: normal; @@ -187,18 +216,47 @@ font-smoothing: antialiased; } - .fa-check:before { content: "\f100"; } -.fa-facebook-square:before { content: "\f101"; } -.fa-floppy-o:before { content: "\f102"; } -.fa-github:before { content: "\f103"; } -.fa-google:before { content: "\f104"; } -.fa-question-circle:before { content: "\f105"; } -.fa-sign-in:before { content: "\f106"; } -.fa-sign-out:before { content: "\f107"; } -.fa-times:before { content: "\f108"; } -.fa-trash-o:before { content: "\f109"; } -.fa-user-circle-o:before { content: "\f10a"; } -.fa-user-plus:before { content: "\f10b"; } + .fa-bell:before { content: "\f100"; } +.fa-bell-o:before { content: "\f101"; } +.fa-calendar:before { content: "\f102"; } +.fa-check:before { content: "\f103"; } +.fa-clock-o:before { content: "\f104"; } +.fa-cloud-upload:before { content: "\f105"; } +.fa-code-fork:before { content: "\f106"; } +.fa-download:before { content: "\f107"; } +.fa-eye:before { content: "\f108"; } +.fa-eye-slash:before { content: "\f109"; } +.fa-facebook-square:before { content: "\f10a"; } +.fa-filter:before { content: "\f10b"; } +.fa-flag:before { content: "\f10c"; } +.fa-floppy-o:before { content: "\f10d"; } +.fa-gavel:before { content: "\f10e"; } +.fa-github:before { content: "\f10f"; } +.fa-google:before { content: "\f110"; } +.fa-history:before { content: "\f111"; } +.fa-link:before { content: "\f112"; } +.fa-pencil-square-o:before { content: "\f113"; } +.fa-question-circle:before { content: "\f114"; } +.fa-quote-left:before { content: "\f115"; } +.fa-reply:before { content: "\f116"; } +.fa-rss:before { content: "\f117"; } +.fa-search:before { content: "\f118"; } +.fa-share-alt:before { content: "\f119"; } +.fa-sign-in:before { content: "\f11a"; } +.fa-sign-out:before { content: "\f11b"; } +.fa-sliders:before { content: "\f11c"; } +.fa-sort:before { content: "\f11d"; } +.fa-sort-asc:before { content: "\f11e"; } +.fa-sort-desc:before { content: "\f11f"; } +.fa-star:before { content: "\f120"; } +.fa-star-o:before { content: "\f121"; } +.fa-table:before { content: "\f122"; } +.fa-times:before { content: "\f123"; } +.fa-trash:before { content: "\f124"; } +.fa-trash-o:before { content: "\f125"; } +.fa-user-circle-o:before { content: "\f126"; } +.fa-user-plus:before { content: "\f127"; } +.fa-wrench:before { content: "\f128"; } @@ -214,11 +272,50 @@
You are logged in!
- -blabla dashboard things ...
-