Adding Custom domains (#247)

* WIP

* wip

* Finished doing most of the work
This commit is contained in:
Julien Nahum
2023-11-29 14:53:08 +01:00
committed by GitHub
parent 57fdfb25a0
commit b50f579155
33 changed files with 1210 additions and 267 deletions

View 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',
]);
}
}

View File

@@ -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);

View File

@@ -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,
];
/**

View 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);
}
}

View 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);
}
}

View File

@@ -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}$/'
];
}

View 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]);
}
}

View File

@@ -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);

View File

@@ -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']];
}
}

View File

@@ -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'))){