Adding Custom domains (#247)
* WIP * wip * Finished doing most of the work
This commit is contained in:
36
app/Http/Controllers/CaddyController.php
Normal file
36
app/Http/Controllers/CaddyController.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CaddyController extends Controller
|
||||
{
|
||||
public function ask(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'domain' => 'required|string',
|
||||
]);
|
||||
// make sure domain is valid
|
||||
$domain = $request->input('domain');
|
||||
if (!preg_match('/^[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}$/', $domain)) {
|
||||
return $this->error([
|
||||
'success' => false,
|
||||
'message' => 'Invalid domain',
|
||||
]);
|
||||
}
|
||||
|
||||
if (Workspace::whereJsonContains('custom_domains',$domain)->exists()) {
|
||||
return $this->success([
|
||||
'success' => true,
|
||||
'message' => 'OK',
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->error([
|
||||
'success' => false,
|
||||
'message' => 'Unauthorized domain',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Workspace\CustomDomainRequest;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Service\WorkspaceHelper;
|
||||
@@ -29,6 +30,13 @@ class WorkspaceController extends Controller
|
||||
return (new WorkspaceHelper($workspace))->getAllUsers();
|
||||
}
|
||||
|
||||
public function saveCustomDomain(CustomDomainRequest $request)
|
||||
{
|
||||
$request->workspace->custom_domains = $request->customDomains;
|
||||
$request->workspace->save();
|
||||
return $request->workspace;
|
||||
}
|
||||
|
||||
public function delete($id)
|
||||
{
|
||||
$workspace = Workspace::findOrFail($id);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http;
|
||||
|
||||
use App\Http\Middleware\CustomDomainRestriction;
|
||||
use App\Http\Middleware\EmbeddableForms;
|
||||
use App\Http\Middleware\IsAdmin;
|
||||
use App\Http\Middleware\IsNotSubscribed;
|
||||
@@ -26,6 +27,7 @@ class Kernel extends HttpKernel
|
||||
\App\Http\Middleware\TrimStrings::class,
|
||||
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
|
||||
\App\Http\Middleware\SetLocale::class,
|
||||
CustomDomainRestriction::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
39
app/Http/Middleware/CaddyRequestMiddleware.php
Normal file
39
app/Http/Middleware/CaddyRequestMiddleware.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CaddyRequestMiddleware
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if (!config('custom-domains.enabled')) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Custom domains not enabled',
|
||||
], 401);
|
||||
}
|
||||
|
||||
if (config('custom-domains.enabled') && !in_array($request->ip(), config('custom-domains.authorized_ips'))) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Unauthorized',
|
||||
], 401);
|
||||
}
|
||||
|
||||
$secret = $request->route('secret');
|
||||
if (config('custom-domains.caddy_secret') && (!$secret || $secret !== config('custom-domains.caddy_secret'))) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Unauthorized',
|
||||
], 401);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
55
app/Http/Middleware/CustomDomainRestriction.php
Normal file
55
app/Http/Middleware/CustomDomainRestriction.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Models\Forms\Form;
|
||||
use App\Models\Workspace;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class CustomDomainRestriction
|
||||
{
|
||||
const CUSTOM_DOMAIN_HEADER = "User-Custom-Domain";
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if (!$request->hasHeader(self::CUSTOM_DOMAIN_HEADER) || !config('custom-domains.enabled')) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$customDomain = $request->header(self::CUSTOM_DOMAIN_HEADER);
|
||||
if (!preg_match('/^[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}$/', $customDomain)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Invalid domain',
|
||||
], 400);
|
||||
}
|
||||
|
||||
// Check if domain is different from current domain
|
||||
$notionFormsDomain = parse_url(config('app.url'))['host'];
|
||||
if ($customDomain == $notionFormsDomain) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// Check if domain is known
|
||||
if (!$workspace = Workspace::whereJsonContains('custom_domains',$customDomain)->first()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Unknown domain',
|
||||
], 400);
|
||||
}
|
||||
|
||||
Workspace::addGlobalScope('domain-restricted', function (Builder $builder) use ($workspace) {
|
||||
$builder->where('workspaces.id', $workspace->id);
|
||||
});
|
||||
Form::addGlobalScope('domain-restricted', function (Builder $builder) use ($workspace) {
|
||||
$builder->where('forms.workspace_id', $workspace->id);
|
||||
});
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -125,7 +125,8 @@ abstract class UserFormRequest extends \Illuminate\Foundation\Http\FormRequest
|
||||
'password' => 'sometimes|nullable',
|
||||
|
||||
// Custom SEO
|
||||
'seo_meta' => 'nullable|array'
|
||||
'seo_meta' => 'nullable|array',
|
||||
'custom_domain' => 'sometimes|nullable|regex:/^[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}$/'
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
58
app/Http/Requests/Workspace/CustomDomainRequest.php
Normal file
58
app/Http/Requests/Workspace/CustomDomainRequest.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Workspace;
|
||||
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CustomDomainRequest extends FormRequest
|
||||
{
|
||||
|
||||
public Workspace $workspace;
|
||||
public array $customDomains = [];
|
||||
public function __construct(Request $request, Workspace $workspace)
|
||||
{
|
||||
$this->workspace = Workspace::findOrFail($request->workspaceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'custom_domains' => [
|
||||
'present',
|
||||
'array',
|
||||
function($attribute, $value, $fail) {
|
||||
$errors = [];
|
||||
$domains = collect($value)->filter(function ($domain) {
|
||||
return !empty( trim($domain) );
|
||||
})->each(function($domain) use (&$errors) {
|
||||
if (!preg_match('/^[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}$/', $domain)) {
|
||||
$errors[] = 'Invalid domain: ' . $domain;
|
||||
}
|
||||
});
|
||||
|
||||
if (count($errors)) {
|
||||
$fail($errors);
|
||||
}
|
||||
|
||||
$limit = $this->workspace->custom_domain_count_limit;
|
||||
if ($limit && $domains->count() > $limit) {
|
||||
$fail('You can only add ' . $limit . ' domain(s).');
|
||||
}
|
||||
|
||||
$this->customDomains = $domains->toArray();
|
||||
}
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
protected function passedValidation(){
|
||||
$this->replace(['custom_domains' => $this->customDomains]);
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,7 @@ class Form extends Model
|
||||
'visibility',
|
||||
|
||||
// Customization
|
||||
'custom_domain',
|
||||
'theme',
|
||||
'width',
|
||||
'cover_picture',
|
||||
@@ -141,12 +142,15 @@ class Form extends Model
|
||||
|
||||
public function getShareUrlAttribute()
|
||||
{
|
||||
return url('/forms/'.$this->slug);
|
||||
if ($this->custom_domain) {
|
||||
return 'https://' . $this->custom_domain . '/forms/' . $this->slug;
|
||||
}
|
||||
return url('/forms/' . $this->slug);
|
||||
}
|
||||
|
||||
public function getEditUrlAttribute()
|
||||
{
|
||||
return url('/forms/'.$this->slug.'/show');
|
||||
return url('/forms/' . $this->slug . '/show');
|
||||
}
|
||||
|
||||
public function getSubmissionsCountAttribute()
|
||||
@@ -156,12 +160,12 @@ class Form extends Model
|
||||
|
||||
public function getViewsCountAttribute()
|
||||
{
|
||||
if(env('DB_CONNECTION') == 'pgsql') {
|
||||
if (env('DB_CONNECTION') == 'pgsql') {
|
||||
return $this->views()->count() +
|
||||
$this->statistics()->sum(DB::raw("cast(data->>'views' as integer)"));
|
||||
} elseif(env('DB_CONNECTION') == 'mysql') {
|
||||
$this->statistics()->sum(DB::raw("cast(data->>'views' as integer)"));
|
||||
} elseif (env('DB_CONNECTION') == 'mysql') {
|
||||
return (int)($this->views()->count() +
|
||||
$this->statistics()->sum(DB::raw("json_extract(data, '$.views')")));
|
||||
$this->statistics()->sum(DB::raw("json_extract(data, '$.views')")));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -219,7 +223,8 @@ class Form extends Model
|
||||
return !empty($this->password);
|
||||
}
|
||||
|
||||
protected function removedProperties(): Attribute {
|
||||
protected function removedProperties(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function ($value) {
|
||||
return $value ? json_decode($value, true) : [];
|
||||
@@ -283,7 +288,7 @@ class Form extends Model
|
||||
{
|
||||
return !empty($this->webhook_url);
|
||||
}
|
||||
|
||||
|
||||
public function getNotifiesDiscordAttribute()
|
||||
{
|
||||
return !empty($this->discord_webhook_url);
|
||||
|
||||
@@ -42,4 +42,13 @@ class License extends Model
|
||||
3 => 75000000, // 75 MB,
|
||||
][$this->meta['tier']];
|
||||
}
|
||||
|
||||
public function getCustomDomainLimitCountAttribute()
|
||||
{
|
||||
return [
|
||||
1 => 5,
|
||||
2 => 25,
|
||||
3 => null,
|
||||
][$this->meta['tier']];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,13 @@ class Workspace extends Model
|
||||
const MAX_FILE_SIZE_FREE = 5000000; // 5 MB
|
||||
const MAX_FILE_SIZE_PRO = 50000000; // 50 MB
|
||||
|
||||
const MAX_DOMAIN_PRO = 1;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'icon',
|
||||
'user_id',
|
||||
'custom_domain',
|
||||
];
|
||||
|
||||
protected $appends = [
|
||||
@@ -25,6 +28,10 @@ class Workspace extends Model
|
||||
'is_enterprise'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'custom_domains' => 'array',
|
||||
];
|
||||
|
||||
public function getIsProAttribute()
|
||||
{
|
||||
if(is_null(config('cashier.key'))){
|
||||
@@ -60,6 +67,26 @@ class Workspace extends Model
|
||||
return self::MAX_FILE_SIZE_FREE;
|
||||
}
|
||||
|
||||
public function getCustomDomainCountLimitAttribute()
|
||||
{
|
||||
if(is_null(config('cashier.key'))){
|
||||
return null;
|
||||
}
|
||||
|
||||
// Return max file size depending on subscription
|
||||
foreach ($this->owners as $owner) {
|
||||
if ($owner->is_subscribed) {
|
||||
if ($license = $owner->activeLicense()) {
|
||||
// In case of special License
|
||||
return $license->custom_domain_limit_count;
|
||||
}
|
||||
}
|
||||
return self::MAX_DOMAIN_PRO;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function getIsEnterpriseAttribute()
|
||||
{
|
||||
if(is_null(config('cashier.key'))){
|
||||
|
||||
Reference in New Issue
Block a user