Separated laravel app to its own folder (#540)

This commit is contained in:
Julien Nahum
2024-08-26 18:24:56 +02:00
committed by GitHub
parent 39b8df5eed
commit 5bd1dda504
546 changed files with 124 additions and 143 deletions

View File

@@ -0,0 +1,204 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Forms\Form;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Laravel\Cashier\Cashier;
class AdminController extends Controller
{
public const ADMIN_LOG_PREFIX = '[admin_action] ';
public function __construct()
{
$this->middleware('moderator');
}
public function fetchUser($identifier)
{
$user = null;
if (is_numeric($identifier)) {
$user = User::find($identifier);
} elseif (filter_var($identifier, FILTER_VALIDATE_EMAIL)) {
$user = User::whereEmail($identifier)->first();
} else {
// Find by form slug
$form = Form::whereSlug($identifier)->first();
if ($form) {
$user = $form->creator;
}
}
if (!$user) {
return $this->error([
'message' => 'User not found.'
]);
} elseif ($user->admin) {
return $this->error([
'message' => 'You cannot fetch an admin.'
]);
}
$workspaces = $user->workspaces()
->withCount('forms')
->get()
->map(function ($workspace) {
$plan = 'free';
if ($workspace->is_trialing) {
$plan = 'trialing';
}
if ($workspace->is_pro) {
$plan = 'pro';
}
if ($workspace->is_enterprise) {
$plan = 'enterprise';
}
return [
'id' => $workspace->id,
'name' => $workspace->name,
'plan' => $plan,
'forms_count' => $workspace->forms_count
];
});
return $this->success([
'user' => $user,
'workspaces' => $workspaces
]);
}
public function applyDiscount(Request $request)
{
$request->validate([
'user_id' => 'required'
]);
$user = User::find($request->get("user_id"));
$activeSubscriptions = $user->subscriptions()->where(function ($q) {
$q->where('stripe_status', 'trialing')
->orWhere('stripe_status', 'active');
})->get();
if ($activeSubscriptions->count() != 1) {
return $this->error([
"message" => "The user has more than one active subscriptions or doesn't have one."
]);
}
$couponId = config('pricing.discount_coupon_id');
if (is_null($couponId)) {
return $this->error([
"message" => "Coupon id not defined."
]);
}
$subscription = $activeSubscriptions->first();
Cashier::stripe()->subscriptions->update($subscription->stripe_id, [
'coupon' => $couponId
]);
self::log('Applying NGO/Student discount to sub', [
'user_id' => $user->id,
'subcription_id' => $subscription->id,
'coupon_id' => $couponId,
'subscription_stripe_id' => $subscription->stripe_id,
'moderator_id' => auth()->id(),
]);
return $this->success([
"message" => "40% Discount applied for the next 12 months."
]);
}
public function extendTrial(Request $request)
{
$request->validate([
'user_id' => 'required',
'number_of_day' => 'required|numeric|max:14'
]);
$user = User::find($request->get("user_id"));
$subscription = $user->subscriptions()
->where('stripe_status', 'trialing')
->firstOrFail();
$trialEndDate = now()->addDays($request->get('number_of_day'));
$subscription->extendTrial($trialEndDate);
self::log('Trial extended', [
'user_id' => $user->id,
'subcription_id' => $subscription->id,
'nb_days' => $request->get('number_of_day'),
'subscription_stripe_id' => $subscription->stripe_id,
'moderator_id' => auth()->id(),
]);
return $this->success([
"message" => "Subscription trial extend until the " . $trialEndDate->format('d/m/Y')
]);
}
public function cancelSubscription(Request $request)
{
$request->validate([
'user_id' => 'required',
'cancellation_reason' => 'required'
]);
$user = User::find($request->get("user_id"));
$activeSubscriptions = $user->subscriptions()->where(function ($q) {
$q->where('stripe_status', 'trialing')
->orWhere('stripe_status', 'active');
})->get();
if ($activeSubscriptions->count() != 1) {
return $this->error([
"message" => "The user has more than one active subscriptions or doesn't have one."
]);
}
$subscription = $activeSubscriptions->first();
$subscription->cancel();
self::log('Cancel Subscription', [
'user_id' => $user->id,
'cancel_reason' => $request->get('cancellation_reason'),
'moderator_id' => auth()->id(),
'subcription_id' => $subscription->id,
'subscription_stripe_id' => $subscription->stripe_id
]);
return $this->success([
"message" => "The subscription cancellation has been successfully completed."
]);
}
public function sendPasswordResetEmail(Request $request)
{
$user = User::findOrFail($request->user_id);
$status = Password::sendResetLink(['email' => $user->email]);
if ($status !== Password::RESET_LINK_SENT) {
return $this->error([
'message' => "Password reset email failed to send"
]);
}
self::log('Sent password reset email', [
'user_id' => $user->id,
'moderator_id' => auth()->id(),
]);
return $this->success([
'message' => "Password reset email has been sent to the user's email address"
]);
}
public static function log($message, $data = [])
{
\Log::warning(self::ADMIN_LOG_PREFIX . $message, $data);
}
}

View File

@@ -0,0 +1,103 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Http\Request;
class BillingController extends Controller
{
public function __construct()
{
$this->middleware('moderator');
}
public function getEmail($userId)
{
$user = User::find($userId);
if (!$user->hasStripeId()) {
return $this->error([
"message" => "Stripe user not created",
]);
}
$user = $user->asStripeCustomer();
return $this->success([
'billing_email' => $user->email
]);
}
public function updateEmail(Request $request)
{
$request->validate([
'user_id' => 'required',
'billing_email' => 'required|email'
]);
$user = User::findOrFail($request->get("user_id"));
if (!$user->hasStripeId()) {
return $this->error([
"message" => "Stripe user not created",
]);
}
AdminController::log('Update billing email', [
'user_id' => $user->id,
'stripe_id' => $user->stripe_id,
'moderator_id' => auth()->id()
]);
$user->updateStripeCustomer(['email' => $request->billing_email]);
return $this->success(['message' => 'Billing email updated successfully']);
}
public function getSubscriptions($userId)
{
$user = User::find($userId);
if (!$user->hasStripeId()) {
return $this->error([
"message" => "Stripe user not created",
]);
}
$subscriptions = $user->subscriptions()->latest()->take(100)->get()->map(function ($subscription) use ($user) {
return [
"id" => $subscription->id,
"stripe_id" => $subscription->stripe_id,
"name" => ucfirst($user->name),
"plan" => $subscription->type,
"status" => $subscription->stripe_status,
"creation_date" => $subscription->created_at->format('Y-m-d')
];
});
return $this->success([
'subscriptions' => $subscriptions,
]);
}
public function getPayments($userId)
{
$user = User::find($userId);
if (!$user->hasStripeId()) {
return $this->error([
"message" => "Stripe user not created",
]);
}
$payments = $user->invoices();
$payments = $payments->map(function ($payment) use ($user) {
return [
"id" => $payment->id,
"amount_paid" => ($payment->amount_paid),
"name" => ucfirst($payment->account_name),
"creation_date" => Carbon::parse($payment->created)->format("Y-m-d H:i:s"),
"status" => $payment->status,
];
});
return $this->success([
'payments' => $payments,
]);
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Forms\Form;
use App\Models\User;
class FormController extends Controller
{
public function getDeletedForms($userId)
{
$user = User::find($userId);
$deletedForms = $user->forms()->with('creator')->onlyTrashed()->get()->map(function ($form) {
return [
"id" => $form->id,
"slug" => $form->slug,
"title" => $form->title,
"created_by" => $form->creator->email,
"deleted_at" => $form->deleted_at->format('Y-m-d'),
];
});
return $this->success(['forms' => $deletedForms]);
}
public function restoreDeletedForm(string $slug)
{
$form = Form::onlyTrashed()->whereSlug($slug)->firstOrFail();
$form->restore();
AdminController::log('Restore deleted form', [
'form_id' => $form->id,
'moderator_id' => auth()->id()
]);
return $this->success(['message' => 'Form restored successfully']);
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\User;
class ImpersonationController extends Controller
{
public function __construct()
{
$this->middleware('moderator');
}
public function impersonate($userId)
{
$user = User::find($userId);
if (!$user) {
return $this->error([
'message' => 'User not found.',
]);
} elseif ($user->admin) {
return $this->error([
'message' => 'You cannot impersonate an admin.',
]);
}
AdminController::log('Impersonation started', [
'from_id' => auth()->id(),
'from_email' => auth()->user()->email,
'target_id' => $user->id,
'target_email' => $user->id,
]);
$token = auth()->claims(
auth()->user()->admin ? [] : [
'impersonating' => true,
'impersonator_id' => auth()->id(),
]
)->login($user);
return $this->success([
'token' => $token,
]);
}
}

View File

@@ -0,0 +1,120 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\License;
use App\Models\User;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Http;
class AppSumoAuthController extends Controller
{
use AuthenticatesUsers;
public function handleCallback(Request $request)
{
if (! $code = $request->code) {
return response()->json(['message' => 'Healthy'], 200);
}
$accessToken = $this->retrieveAccessToken($code);
$license = $this->fetchOrCreateLicense($accessToken);
// If user connected, attach license
if (Auth::check()) {
return $this->attachLicense($license);
}
// otherwise start login flow by passing the encrypted license key id
if (is_null($license->user_id)) {
return redirect(front_url('/register?appsumo_license='.encrypt($license->id)));
}
return redirect(front_url('/register?appsumo_error=1'));
}
private function retrieveAccessToken(string $requestCode): string
{
return Http::withHeaders([
'Content-type' => 'application/json',
])->post('https://appsumo.com/openid/token/', [
'grant_type' => 'authorization_code',
'code' => $requestCode,
'redirect_uri' => route('appsumo.callback'),
'client_id' => config('services.appsumo.client_id'),
'client_secret' => config('services.appsumo.client_secret'),
])->throw()->json('access_token');
}
private function fetchOrCreateLicense(string $accessToken): License
{
// Fetch license from API
$licenseKey = Http::get('https://appsumo.com/openid/license_key/?access_token='.$accessToken)
->throw()
->json('license_key');
// Fetch or create license model
$license = License::where('license_provider', 'appsumo')->where('license_key', $licenseKey)->first();
if (! $license) {
$licenseData = Http::withHeaders([
'X-AppSumo-Licensing-Key' => config('services.appsumo.api_key'),
])->get('https://api.licensing.appsumo.com/v2/licenses/'.$licenseKey)->json();
// Create new license
$license = License::create([
'license_key' => $licenseKey,
'license_provider' => 'appsumo',
'status' => $licenseData['status'] === 'active' ? License::STATUS_ACTIVE : License::STATUS_INACTIVE,
'meta' => $licenseData,
]);
}
return $license;
}
private function attachLicense(License $license)
{
if (! Auth::check()) {
throw new AuthenticationException('User not authenticated');
}
// Attach license if not already attached
if (is_null($license->user_id)) {
$license->user_id = Auth::id();
$license->save();
return redirect(front_url('/home?appsumo_connect=1'));
}
// Licensed already attached
return redirect(front_url('/home?appsumo_error=1'));
}
/**
* @return string|null
*
* Returns null if no license found
* Returns true if license was found and attached
* Returns false if there was an error (license not found or already attached)
*/
public static function registerWithLicense(User $user, ?string $licenseHash): ?bool
{
if (! $licenseHash) {
return null;
}
$licenseId = decrypt($licenseHash);
$license = License::find($licenseId);
if ($license && is_null($license->user_id)) {
$license->user_id = $user->id;
$license->save();
return true;
}
return false;
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
use Illuminate\Http\Request;
class ForgotPasswordController extends Controller
{
use SendsPasswordResetEmails;
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest');
}
/**
* Get the response for a successful password reset link.
*
* @param string $response
* @return \Illuminate\Http\RedirectResponse
*/
protected function sendResetLinkResponse(Request $request, $response)
{
return ['status' => trans($response)];
}
/**
* Get the response for a failed password reset link.
*
* @param string $response
* @return \Illuminate\Http\RedirectResponse
*/
protected function sendResetLinkFailedResponse(Request $request, $response)
{
return response()->json(['email' => trans($response)], 400);
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Exceptions\VerifyEmailException;
use App\Http\Controllers\Controller;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class LoginController extends Controller
{
use AuthenticatesUsers;
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest')->except('logout');
}
/**
* Attempt to log the user into the application.
*
* @return bool
*/
protected function attemptLogin(Request $request)
{
$token = $this->guard()->attempt($this->credentials($request));
if (! $token) {
return false;
}
$user = $this->guard()->user();
if ($user instanceof MustVerifyEmail && ! $user->hasVerifiedEmail()) {
return false;
}
$this->guard()->setToken($token);
return true;
}
/**
* Get the needed authorization credentials from the request.
*
* @return array
*/
protected function credentials(Request $request)
{
return [
$this->username() => strtolower($request->get($this->username())),
'password' => $request->password,
];
}
/**
* Send the response after the user was authenticated.
*
* @return \Illuminate\Http\JsonResponse
*/
protected function sendLoginResponse(Request $request)
{
$this->clearLoginAttempts($request);
$token = (string) $this->guard()->getToken();
$expiration = $this->guard()->getPayload()->get('exp');
return response()->json([
'token' => $token,
'token_type' => 'bearer',
'expires_in' => $expiration - time(),
]);
}
/**
* Get the failed login response instance.
*
* @return \Illuminate\Http\JsonResponse
*
* @throws \Illuminate\Validation\ValidationException
*/
protected function sendFailedLoginResponse(Request $request)
{
$user = $this->guard()->user();
if ($user instanceof MustVerifyEmail && ! $user->hasVerifiedEmail()) {
throw VerifyEmailException::forUser($user);
}
throw ValidationException::withMessages([
$this->username() => [trans('auth.failed')],
]);
}
/**
* Log the user out of the application.
*
* @return \Illuminate\Http\Response
*/
public function logout(Request $request)
{
$this->guard()->logout();
}
}

View File

@@ -0,0 +1,146 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Integrations\OAuth\OAuthProviderService;
use App\Models\OAuthProvider;
use App\Models\User;
use App\Models\Workspace;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
class OAuthController extends Controller
{
use AuthenticatesUsers;
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
config([
'services.github.redirect' => route('oauth.callback', 'github'),
]);
}
/**
* Redirect the user to the provider authentication page.
*
* @param string $provider
* @return \Illuminate\Http\RedirectResponse
*/
public function redirect(OAuthProviderService $provider)
{
return response()->json([
'url' => $provider->getDriver()->setRedirectUrl(config('services.google.auth_redirect'))->getRedirectUrl()
]);
}
/**
* Obtain the user information from the provider.
*
* @param string $driver
* @return \Illuminate\Http\Response
*/
public function handleCallback(OAuthProviderService $provider)
{
try {
$driverUser = $provider->getDriver()->setRedirectUrl(config('services.google.auth_redirect'))->getUser();
} catch (\Exception $e) {
return $this->error([
"message" => "OAuth service failed to authenticate: " . $e->getMessage()
]);
}
$user = $this->findOrCreateUser($provider, $driverUser);
if (!$user) {
return $this->error([
"message" => "User not found."
]);
}
if ($user->has_registered) {
return $this->error([
"message" => "This email is already registered. Please sign in with your password."
]);
}
$this->guard()->setToken(
$token = $this->guard()->login($user)
);
return response()->json([
'token' => $token,
'token_type' => 'bearer',
'expires_in' => $this->guard()->getPayload()->get('exp') - time(),
'new_user' => $user->new_user
]);
}
/**
* @p aram \Laravel\Socialite\Contracts\User $socialiteUser
* @return \App\Models\User | null
*/
protected function findOrCreateUser($provider, $socialiteUser)
{
$oauthProvider = OAuthProvider::where('provider', $provider)
->where('provider_user_id', $socialiteUser->getId())
->first();
if ($oauthProvider) {
$oauthProvider->update([
'access_token' => $socialiteUser->token,
'refresh_token' => $socialiteUser->refreshToken,
]);
return $oauthProvider->user;
}
if (!$provider->getDriver()->canCreateUser()) {
return null;
}
$email = strtolower($socialiteUser->getEmail());
$user = User::whereEmail($email)->first();
if ($user) {
$user->has_registered = true;
return $user;
}
$user = User::create([
'name' => $socialiteUser->getName(),
'email' => $email,
'email_verified_at' => now(),
]);
// Create and sync workspace
$workspace = Workspace::create([
'name' => 'My Workspace',
'icon' => '🧪',
]);
$user->workspaces()->sync([
$workspace->id => [
'role' => User::ROLE_ADMIN,
],
], false);
$user->new_user = true;
OAuthProvider::create(
[
'user_id' => $user->id,
'provider' => $provider,
'provider_user_id' => $socialiteUser->getId(),
'access_token' => $socialiteUser->token,
'refresh_token' => $socialiteUser->refreshToken,
'name' => $socialiteUser->getName(),
'email' => $socialiteUser->getEmail(),
]
);
return $user;
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Resources\UserResource;
use App\Models\User;
use App\Models\UserInvite;
use App\Models\Workspace;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
class RegisterController extends Controller
{
use RegistersUsers;
private ?bool $appsumoLicense = null;
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest');
}
/**
* The user has been registered.
*
* @param \App\User $user
* @return \Illuminate\Http\JsonResponse
*/
protected function registered(Request $request, User $user)
{
if ($user instanceof MustVerifyEmail) {
return response()->json(['status' => trans('verification.sent')]);
}
return response()->json(array_merge(
(new UserResource($user))->toArray($request),
[
'appsumo_license' => $this->appsumoLicense,
]
));
}
/**
* Get a validator for an incoming registration request.
*
* @return \Illuminate\Contracts\Validation\Validator
*/
protected function validator(array $data)
{
return Validator::make($data, [
'name' => 'required|max:255',
'email' => 'required|email:filter|max:255|unique:users|indisposable',
'password' => 'required|min:6|confirmed',
'hear_about_us' => 'required|string',
'agree_terms' => ['required', Rule::in([true])],
'appsumo_license' => ['nullable'],
'invite_token' => ['nullable', 'string'],
], [
'agree_terms' => 'Please agree with the terms and conditions.',
]);
}
/**
* Create a new user instance after a valid registration.
*/
protected function create(array $data)
{
$this->checkRegistrationAllowed($data);
[$workspace, $role] = $this->getWorkspaceAndRole($data);
$user = User::create([
'name' => $data['name'],
'email' => strtolower($data['email']),
'password' => bcrypt($data['password']),
'hear_about_us' => $data['hear_about_us'],
]);
// Add relation with user
$user->workspaces()->sync([
$workspace->id => [
'role' => $role,
],
], false);
$this->appsumoLicense = AppSumoAuthController::registerWithLicense($user, $data['appsumo_license'] ?? null);
return $user;
}
private function checkRegistrationAllowed(array $data)
{
if (config('app.self_hosted') && !array_key_exists('invite_token', $data) && (app()->environment() !== 'testing')) {
response()->json(['message' => 'Registration is not allowed in self host mode'], 400)->throwResponse();
}
}
private function getWorkspaceAndRole(array $data)
{
if (!array_key_exists('invite_token', $data)) {
return [
Workspace::create([
'name' => 'My Workspace',
'icon' => '🧪',
]),
User::ROLE_ADMIN
];
}
$userInvite = UserInvite::where('email', $data['email'])
->where('token', $data['invite_token'])
->first();
if (!$userInvite) {
response()->json(['message' => 'Invite token is invalid.'], 400)->throwResponse();
}
if ($userInvite->hasExpired()) {
response()->json(['message' => 'Invite token has expired.'], 400)->throwResponse();
}
if ($userInvite->status == UserInvite::ACCEPTED_STATUS) {
response()->json(['message' => 'Invite is already accepted.'], 400)->throwResponse();
}
$userInvite->markAsAccepted();
return [
$userInvite->workspace,
$userInvite->role,
];
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Http\Request;
class ResetPasswordController extends Controller
{
use ResetsPasswords;
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest');
}
/**
* Get the response for a successful password reset.
*
* @param string $response
* @return \Illuminate\Http\RedirectResponse
*/
protected function sendResetResponse(Request $request, $response)
{
return ['status' => trans($response)];
}
/**
* Get the response for a failed password reset.
*
* @param string $response
* @return \Illuminate\Http\RedirectResponse
*/
protected function sendResetFailedResponse(Request $request, $response)
{
return response()->json(['email' => trans($response)], 400);
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Resources\UserResource;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class UserController extends Controller
{
/**
* Get authenticated user.
*/
public function current(Request $request)
{
return new UserResource($request->user());
}
public function deleteAccount()
{
$this->middleware('auth');
if (Auth::user()->admin) {
return $this->error([
'message' => 'Cannot delete an admin. Stay with us 🙏',
]);
}
Auth::user()->delete();
return $this->success([
'message' => 'User deleted.',
]);
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Auth\Events\Verified;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\URL;
use Illuminate\Validation\ValidationException;
class VerificationController extends Controller
{
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('throttle:6,1')->only('verify', 'resend');
}
/**
* Mark the user's email address as verified.
*
* @param \App\User $user
* @return \Illuminate\Http\JsonResponse
*/
public function verify(Request $request, User $user)
{
if (! URL::hasValidSignature($request)) {
return response()->json([
'status' => trans('verification.invalid'),
], 400);
}
if ($user->hasVerifiedEmail()) {
return response()->json([
'status' => trans('verification.already_verified'),
], 400);
}
$user->markEmailAsVerified();
event(new Verified($user));
return response()->json([
'status' => trans('verification.verified'),
]);
}
/**
* Resend the email verification notification.
*
* @return \Illuminate\Http\JsonResponse
*/
public function resend(Request $request)
{
$this->validate($request, ['email' => 'required|email']);
$user = User::where('email', $request->email)->first();
if (is_null($user)) {
throw ValidationException::withMessages([
'email' => [trans('verification.user')],
]);
}
if ($user->hasVerifiedEmail()) {
throw ValidationException::withMessages([
'email' => [trans('verification.already_verified')],
]);
}
$user->sendEmailVerificationNotification();
return response()->json(['status' => trans('verification.sent')]);
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\Workspace\CustomDomainRequest;
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(CustomDomainRequest::CUSTOM_DOMAINS_REGEX, $domain)) {
return $this->error([
'success' => false,
'message' => 'Invalid domain',
]);
}
\Log::info('Caddy request received', [
'domain' => $domain,
]);
if ($workspace = Workspace::whereJsonContains('custom_domains', $domain)->first()) {
\Log::info('Caddy request successful', [
'domain' => $domain,
'workspace' => $workspace->id,
]);
return $this->success([
'success' => true,
'message' => 'OK',
]);
}
\Log::info('Caddy request failed', [
'domain' => $domain,
'workspace' => $workspace?->id,
]);
return $this->error([
'success' => false,
'message' => 'Unauthorized domain',
]);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Controllers\Content;
use App\Http\Controllers\Controller;
class ChangelogController extends Controller
{
public const CANNY_ENDPOINT = 'https://canny.io/api/v1/';
public function index()
{
return \Cache::remember('changelog_entries', now()->addHour(), function () {
$response = \Http::post(self::CANNY_ENDPOINT.'entries/list', [
'apiKey' => config('services.canny.api_key'),
'limit' => 3,
]);
return $response->json('entries');
});
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Http\Controllers\Content;
use App\Http\Controllers\Controller;
use App\Http\Controllers\Forms\PublicFormController;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class FileUploadController extends Controller
{
/**
* Upload file to local temp
*
* @return \Illuminate\Http\JsonResponse
*/
public function upload(Request $request)
{
$request->validate(['file' => 'required|file']);
$uuid = (string) Str::uuid();
$path = $request->file('file')->storeAs(PublicFormController::TMP_FILE_UPLOAD_PATH, $uuid);
return response()->json([
'uuid' => $uuid,
'key' => $path,
], 201);
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Http\Controllers\Content;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Laravel\Vapor\Http\Controllers\SignedStorageUrlController as Controller;
class SignedStorageUrlController extends Controller
{
/**
* Create a new signed URL.
*
* @return \Illuminate\Http\JsonResponse
*/
public function store(Request $request)
{
$this->ensureEnvironmentVariablesAreAvailable($request);
$bucket = $request->input('bucket') ?: $_ENV['AWS_BUCKET'];
$client = $this->storageClient();
$uuid = (string) Str::uuid();
$expiresAfter = config('vapor.signed_storage_url_expires_after', 5);
$signedRequest = $client->createPresignedRequest(
$this->createCommand($request, $client, $bucket, $key = ('tmp/'.$uuid)),
sprintf('+%s minutes', $expiresAfter)
);
$uri = $signedRequest->getUri();
return response()->json([
'uuid' => $uuid,
'bucket' => $bucket,
'key' => $key,
'url' => $uri->getScheme().'://'.$uri->getAuthority().$uri->getPath().'?'.$uri->getQuery(),
'headers' => $this->headers($request, $signedRequest),
], 201);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{
use AuthorizesRequests;
use DispatchesJobs;
use ValidatesRequests;
public function success($data = [])
{
return response()->json(array_merge([
'type' => 'success',
], $data));
}
public function error($data = [], $statusCode = 400)
{
return response()->json(array_merge([
'type' => 'error',
], $data), $statusCode);
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
class FontsController extends Controller
{
public function index(Request $request)
{
return \Cache::remember('google_fonts', 60 * 60, function () {
$url = "https://www.googleapis.com/webfonts/v1/webfonts?sort=popularity&key=" . config('services.google.fonts_api_key');
$response = Http::get($url);
if ($response->successful()) {
$fonts = collect($response->json()['items'])->filter(function ($font) {
return !in_array($font['category'], ['monospace']);
})->map(function ($font) {
return $font['family'];
})->toArray();
return response()->json($fonts);
}
return [];
});
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Http\Controllers\Forms;
use App\Http\Controllers\Controller;
use App\Http\Requests\AiGenerateFormRequest;
use App\Models\Forms\AI\AiFormCompletion;
class AiFormController extends Controller
{
public function generateForm(AiGenerateFormRequest $request)
{
$this->middleware('throttle:4,1');
return $this->success([
'message' => 'We\'re working on your form, please wait ~1 min.',
'ai_form_completion_id' => AiFormCompletion::create([
'form_prompt' => $request->input('form_prompt'),
'ip' => $request->ip(),
])->id,
]);
}
public function show(AiFormCompletion $aiFormCompletion)
{
if ($aiFormCompletion->ip != request()->ip()) {
return $this->error('You are not authorized to view this AI completion.', 403);
}
return $this->success([
'ai_form_completion' => $aiFormCompletion,
]);
}
}

View File

@@ -0,0 +1,274 @@
<?php
namespace App\Http\Controllers\Forms;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreFormRequest;
use App\Http\Requests\UpdateFormRequest;
use App\Http\Requests\UploadAssetRequest;
use App\Http\Resources\FormResource;
use App\Models\Forms\Form;
use App\Models\Workspace;
use App\Service\Forms\FormCleaner;
use App\Service\Storage\StorageFileNameParser;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class FormController extends Controller
{
public const ASSETS_UPLOAD_PATH = 'assets/forms';
private FormCleaner $formCleaner;
public function __construct()
{
$this->middleware('auth', ['except' => ['uploadAsset']]);
$this->formCleaner = new FormCleaner();
}
public function index($workspaceId)
{
$workspace = Workspace::findOrFail($workspaceId);
$this->authorize('view', $workspace);
$this->authorize('viewAny', Form::class);
$workspaceIsPro = $workspace->is_pro;
$forms = $workspace->forms()
->orderByDesc('updated_at')
->paginate(10)->through(function (Form $form) use ($workspace, $workspaceIsPro) {
// Add attributes for faster loading
$form->extra = (object) [
'loadedWorkspace' => $workspace,
'workspaceIsPro' => $workspaceIsPro,
'userIsOwner' => true,
'cleanings' => $this->formCleaner
->processForm(request(), $form)
->simulateCleaning($workspace)
->getPerformedCleanings(),
];
return $form;
});
return FormResource::collection($forms);
}
public function show($slug)
{
$form = Form::whereSlug($slug)->firstOrFail();
$this->authorize('view', $form);
// Add attributes for faster loading
$workspace = $form->workspace;
$form->extra = (object)[
'loadedWorkspace' => $workspace,
'workspaceIsPro' => $workspace->is_pro,
'userIsOwner' => true,
'cleanings' => $this->formCleaner
->processForm(request(), $form)
->simulateCleaning($workspace)
->getPerformedCleanings(),
];
return new FormResource($form);
}
/**
* Return all user forms, used for zapier
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function indexAll()
{
$forms = collect();
foreach (Auth::user()->workspaces as $workspace) {
$this->authorize('view', $workspace);
$this->authorize('viewAny', Form::class);
$workspaceIsPro = $workspace->is_pro;
$newForms = $workspace->forms()->get()->map(function (Form $form) use ($workspace, $workspaceIsPro) {
// Add attributes for faster loading
$form->extra = (object) [
'loadedWorkspace' => $workspace,
'workspaceIsPro' => $workspaceIsPro,
'userIsOwner' => true,
];
return $form;
});
$forms = $forms->merge($newForms);
}
return FormResource::collection($forms);
}
public function store(StoreFormRequest $request)
{
$this->authorize('create', Form::class);
$workspace = Workspace::findOrFail($request->get('workspace_id'));
$this->authorize('view', $workspace);
$formData = $this->formCleaner
->processRequest($request)
->simulateCleaning($workspace)
->getData();
$form = Form::create(array_merge($formData, [
'creator_id' => $request->user()->id,
]));
if ($this->formCleaner->hasCleaned()) {
$formStatus = $form->workspace->is_trialing ? 'Non-trial' : 'Pro';
$message = 'Form successfully created, but the ' . $formStatus . ' features you used will be disabled when sharing your form:';
} else {
$message = 'Form created.';
}
return $this->success([
'message' => $message . ($form->visibility == 'draft' ? ' But other people won\'t be able to see the form since it\'s currently in draft mode' : ''),
'form' => (new FormResource($form))->setCleanings($this->formCleaner->getPerformedCleanings()),
'users_first_form' => $request->user()->forms()->count() == 1,
]);
}
public function update(UpdateFormRequest $request, string $id)
{
$form = Form::findOrFail($id);
$this->authorize('update', $form);
$formData = $this->formCleaner
->processRequest($request)
->simulateCleaning($form->workspace)
->getData();
// Set Removed Properties
$formData['removed_properties'] = array_merge($form->removed_properties, collect($form->properties)->filter(function ($field) use ($formData) {
return !Str::of($field['type'])->startsWith('nf-') && !in_array($field['id'], collect($formData['properties'])->pluck('id')->toArray());
})->toArray());
$form->update($formData);
if ($this->formCleaner->hasCleaned()) {
$formSubscription = $form->is_pro ? 'Enterprise' : 'Pro';
$formStatus = $form->workspace->is_trialing ? 'Non-trial' : $formSubscription;
$message = 'Form successfully updated, but the ' . $formStatus . ' features you used will be disabled when sharing your form.';
} else {
$message = 'Form updated.';
}
return $this->success([
'message' => $message . ($form->visibility == 'draft' ? ' But other people won\'t be able to see the form since it\'s currently in draft mode' : ''),
'form' => (new FormResource($form))->setCleanings($this->formCleaner->getPerformedCleanings()),
]);
}
public function destroy($id)
{
$form = Form::findOrFail($id);
$this->authorize('delete', $form);
$form->delete();
return $this->success([
'message' => 'Form was deleted.',
]);
}
public function duplicate($id)
{
$form = Form::findOrFail($id);
$this->authorize('update', $form);
// Create copy
$formCopy = $form->replicate();
$formCopy->title = 'Copy of ' . $formCopy->title;
$formCopy->save();
return $this->success([
'message' => 'Form successfully duplicated. You are now editing the duplicated version of the form.',
'new_form' => new FormResource($formCopy),
]);
}
public function regenerateLink($id, $option)
{
$form = Form::findOrFail($id);
$this->authorize('update', $form);
if ($option == 'slug') {
$form->generateSlug();
} elseif ($option == 'uuid') {
$form->slug = Str::uuid();
}
$form->save();
return $this->success([
'message' => 'Form url successfully updated. Your new form url now is: ' . $form->share_url . '.',
'form' => new FormResource($form),
]);
}
/**
* Upload a form asset
*/
public function uploadAsset(UploadAssetRequest $request)
{
$fileNameParser = StorageFileNameParser::parse($request->url);
// Make sure we retrieve the file in tmp storage, move it to persistent
$fileName = PublicFormController::TMP_FILE_UPLOAD_PATH . '/' . $fileNameParser->uuid;
if (!Storage::exists($fileName)) {
// File not found, we skip
return null;
}
$newPath = self::ASSETS_UPLOAD_PATH . '/' . $fileNameParser->getMovedFileName();
Storage::move($fileName, $newPath);
return $this->success([
'message' => 'File uploaded.',
'url' => route('forms.assets.show', [$fileNameParser->getMovedFileName()]),
]);
}
/**
* File uploads retrieval
*/
public function viewFile($id, $fileName)
{
$form = Form::findOrFail($id);
$this->authorize('view', $form);
$path = Str::of(PublicFormController::FILE_UPLOAD_PATH)->replace('?', $form->id) . '/' . $fileName;
if (!Storage::exists($path)) {
return $this->error([
'message' => 'File not found.',
]);
}
return redirect()->to(Storage::temporaryUrl($path, now()->addMinutes(5)));
}
/**
* Updates a form's workspace
*/
public function updateWorkspace($id, $workspace_id)
{
$form = Form::findOrFail($id);
$workspace = Workspace::findOrFail($workspace_id);
$this->authorize('update', $form);
$this->authorize('view', $workspace);
$form->workspace_id = $workspace_id;
$form->creator_id = auth()->user()->id;
$form->save();
return $this->success([
'message' => 'Form workspace updated successfully.',
]);
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Http\Controllers\Forms;
use App\Http\Controllers\Controller;
use App\Models\Forms\Form;
use Carbon\CarbonPeriod;
class FormStatsController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
public function getFormStats(string $workspaceId, string $formId)
{
$form = Form::findOrFail($formId);
$this->authorize('view', $form);
$formStats = $form->statistics()->where('date', '>', now()->subDays(29)->startOfDay())->get();
$periodStats = ['views' => [], 'submissions' => []];
foreach (CarbonPeriod::create(now()->subDays(29), now()) as $dateObj) {
$date = $dateObj->format('d-m-Y');
$statisticData = $formStats->where('date', $dateObj->format('Y-m-d'))->first();
$periodStats['views'][$date] = $statisticData->data['views'] ?? 0;
$periodStats['submissions'][$date] = $form->submissions()->whereDate('created_at', $dateObj)->count();
if ($dateObj->toDateString() === now()->toDateString()) {
$periodStats['views'][$date] += $form->views()->count();
}
}
return $periodStats;
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace App\Http\Controllers\Forms;
use App\Exports\FormSubmissionExport;
use App\Http\Controllers\Controller;
use App\Http\Requests\AnswerFormRequest;
use App\Http\Resources\FormSubmissionResource;
use App\Jobs\Form\StoreFormSubmissionJob;
use App\Models\Forms\Form;
use App\Models\Forms\FormSubmission;
use App\Service\Forms\FormSubmissionFormatter;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Maatwebsite\Excel\Facades\Excel;
use Vinkla\Hashids\Facades\Hashids;
class FormSubmissionController extends Controller
{
public function __construct()
{
$this->middleware('auth', ['except' => ['submissionFile']]);
$this->middleware('signed', ['only' => ['submissionFile']]);
}
public function submissions(string $id)
{
$form = Form::findOrFail((int) $id);
$this->authorize('view', $form);
return FormSubmissionResource::collection($form->submissions()->paginate(100));
}
public function update(AnswerFormRequest $request, $id, $submissionId)
{
$form = $request->form;
$this->authorize('update', $form);
$job = new StoreFormSubmissionJob($request->form, $request->validated());
$job->setSubmissionId($submissionId)->handle();
$data = new FormSubmissionResource(FormSubmission::findOrFail($submissionId));
return $this->success([
'message' => 'Record successfully updated.',
'data' => $data,
]);
}
public function export(string $id)
{
$form = Form::findOrFail((int) $id);
$this->authorize('view', $form);
$allRows = [];
foreach ($form->submissions->toArray() as $row) {
$formatter = (new FormSubmissionFormatter($form, $row['data']))
->outputStringsOnly()
->setEmptyForNoValue()
->showRemovedFields()
->showHiddenFields()
->useSignedUrlForFiles();
$allRows[] = [
'id' => Hashids::encode($row['id']),
'created_at' => date('Y-m-d H:i', strtotime($row['created_at'])),
...$formatter->getCleanKeyValue(),
];
}
$csvExport = (new FormSubmissionExport($allRows));
return Excel::download(
$csvExport,
$form->slug.'-submission-data.csv',
\Maatwebsite\Excel\Excel::CSV
);
}
public function submissionFile($id, $fileName)
{
$fileName = Str::of(PublicFormController::FILE_UPLOAD_PATH)->replace('?', $id).'/'
.urldecode($fileName);
if (! Storage::exists($fileName)) {
return $this->error([
'message' => 'File not found.',
], 404);
}
if (config('filesystems.default') !== 's3') {
return response()->file(Storage::path($fileName));
}
return redirect(
Storage::temporaryUrl($fileName, now()->addMinute())
);
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace App\Http\Controllers\Forms\Integration;
use App\Http\Controllers\Controller;
use App\Http\Requests\Integration\FormIntegrationsRequest;
use App\Http\Resources\FormIntegrationResource;
use App\Models\Forms\Form;
use App\Models\Integration\FormIntegration;
class FormIntegrationsController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
public function index(string $id)
{
$form = Form::findOrFail((int)$id);
$this->authorize('view', $form);
$integrations = FormIntegration::query()
->where('form_id', $form->id)
->with('provider.user')
->get();
return FormIntegrationResource::collection($integrations);
}
public function create(FormIntegrationsRequest $request, string $id)
{
$form = Form::findOrFail((int)$id);
$this->authorize('update', $form);
/** @var FormIntegration $formIntegration */
$formIntegration = FormIntegration::create(
array_merge([
'form_id' => $form->id,
], $request->toIntegrationData())
);
$formIntegration->refresh();
$formIntegration->load('provider.user');
return $this->success([
'message' => 'Form Integration was created.',
'form_integration' => FormIntegrationResource::make($formIntegration)
]);
}
public function update(FormIntegrationsRequest $request, string $id, string $integrationid)
{
$form = Form::findOrFail((int)$id);
$this->authorize('update', $form);
$formIntegration = FormIntegration::findOrFail((int)$integrationid);
$formIntegration->update($request->toIntegrationData());
$formIntegration->load('provider.user');
return $this->success([
'message' => 'Form Integration was updated.',
'form_integration' => FormIntegrationResource::make($formIntegration)
]);
}
public function destroy(string $id, string $integrationid)
{
$form = Form::findOrFail((int)$id);
$this->authorize('update', $form);
$formIntegration = FormIntegration::findOrFail((int)$integrationid);
$formIntegration->delete();
return $this->success([
'message' => 'Form Integration was deleted.'
]);
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Http\Controllers\Forms\Integration;
use App\Http\Controllers\Controller;
use App\Http\Resources\FormIntegrationsEventResource;
use App\Models\Forms\Form;
use App\Models\Integration\FormIntegrationsEvent;
class FormIntegrationsEventController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
public function index(string $id, string $integrationid)
{
$form = Form::findOrFail((int)$id);
$this->authorize('view', $form);
return FormIntegrationsEventResource::collection(
FormIntegrationsEvent::where('integration_id', (int)$integrationid)->orderByDesc('created_at')->get()
);
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Http\Controllers\Forms\Integration;
use App\Http\Controllers\Controller;
use App\Http\Requests\Integration\StoreFormZapierWebhookRequest;
use App\Models\Integration\FormZapierWebhook;
class FormZapierWebhookController extends Controller
{
/**
* Controller for Zappier webhook subscriptions.
*/
public function __construct()
{
$this->middleware('auth');
}
public function store(StoreFormZapierWebhookRequest $request)
{
$hook = $request->instanciateHook();
$this->authorize('store', $hook);
$hook->save();
return $this->success([
'message' => 'Webhook created.',
'hook' => $hook,
]);
}
public function delete($id)
{
$hook = FormZapierWebhook::findOrFail($id);
$this->authorize('store', $hook);
$hook->delete();
return $this->success([
'message' => 'Webhook deleted.',
]);
}
}

View File

@@ -0,0 +1,133 @@
<?php
namespace App\Http\Controllers\Forms;
use App\Http\Controllers\Controller;
use App\Http\Requests\AnswerFormRequest;
use App\Http\Resources\FormResource;
use App\Http\Resources\FormSubmissionResource;
use App\Jobs\Form\StoreFormSubmissionJob;
use App\Models\Forms\Form;
use App\Models\Forms\FormSubmission;
use App\Service\Forms\FormCleaner;
use App\Service\WorkspaceHelper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Vinkla\Hashids\Facades\Hashids;
class PublicFormController extends Controller
{
public const FILE_UPLOAD_PATH = 'forms/?/submissions';
public const TMP_FILE_UPLOAD_PATH = 'tmp/';
public function show(Request $request, string $slug)
{
$form = Form::whereSlug($slug)->whereIn('visibility', ['public', 'closed'])->firstOrFail();
if ($form->workspace == null) {
// Workspace deleted
return $this->error([
'message' => 'Form not found.',
], 404);
}
$formCleaner = new FormCleaner();
// Disable pro features if needed
$form->fill(
$formCleaner
->processForm($request, $form)
->performCleaning($form->workspace)
->getData()
);
// Increase form view counter if not login
if (!Auth::check()) {
$form->views()->create();
}
return (new FormResource($form))
->setCleanings($formCleaner->getPerformedCleanings());
}
public function listUsers(Request $request)
{
// Check that form has user field
$form = $request->form;
if (!$form->has_user_field) {
return [];
}
// Use serializer
$workspace = $form->workspace;
return (new WorkspaceHelper($workspace))->getAllUsers();
}
public function showAsset($assetFileName)
{
$path = FormController::ASSETS_UPLOAD_PATH . '/' . $assetFileName;
if (!Storage::exists($path)) {
return $this->error([
'message' => 'File not found.',
'file_name' => $assetFileName,
]);
}
$internal_url = Storage::temporaryUrl($path, now()->addMinutes(5));
foreach(config('filesystems.disks.s3.temporary_url_rewrites') as $from => $to) {
$internal_url = str_replace($from, $to, $internal_url);
}
return redirect()->to($internal_url);
}
public function answer(AnswerFormRequest $request)
{
$form = $request->form;
$submissionId = false;
if ($form->editable_submissions) {
$job = new StoreFormSubmissionJob($form, $request->validated());
$job->handle();
$submissionId = Hashids::encode($job->getSubmissionId());
} else {
StoreFormSubmissionJob::dispatch($form, $request->validated());
}
return $this->success(array_merge([
'message' => 'Form submission saved.',
'submission_id' => $submissionId,
], $request->form->is_pro && $request->form->redirect_url ? [
'redirect' => true,
'redirect_url' => $request->form->redirect_url,
] : [
'redirect' => false,
]));
}
public function fetchSubmission(Request $request, string $slug, string $submissionId)
{
$submissionId = ($submissionId) ? Hashids::decode($submissionId) : false;
$submissionId = isset($submissionId[0]) ? $submissionId[0] : false;
$form = Form::whereSlug($slug)->whereVisibility('public')->firstOrFail();
if ($form->workspace == null || !$form->editable_submissions || !$submissionId) {
return $this->error([
'message' => 'Not allowed.',
]);
}
$submission = new FormSubmissionResource(FormSubmission::findOrFail($submissionId));
$submission->publiclyAccessed();
if ($submission->form_id != $form->id) {
return $this->error([
'message' => 'Not allowed.',
], 403);
}
return $this->success($submission->toArray($request));
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Http\Controllers\Forms;
use App\Http\Controllers\Controller;
use App\Models\Forms\Form;
use Illuminate\Http\Request;
class RecordController extends Controller
{
public function delete(Request $request, $id, $recordId)
{
$form = Form::findOrFail((int) $id);
$this->authorize('delete', $form);
$record = $form->submissions()->where('id', $recordId)->firstOrFail();
$record->delete();
return $this->success([
'message' => 'Record successfully removed.',
]);
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Http\Controllers\Integrations\Zapier;
use App\Http\Requests\Integration\Zapier\PollSubmissionRequest;
use App\Http\Requests\Zapier\CreateIntegrationRequest;
use App\Http\Requests\Zapier\DeleteIntegrationRequest;
use App\Integrations\Handlers\ZapierIntegration;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Tests\Helpers\FormSubmissionDataFactory;
class IntegrationController
{
use AuthorizesRequests;
public function store(CreateIntegrationRequest $request)
{
$form = $request->form();
$this->authorize('view', $form);
$form->integrations()
->create([
'integration_id' => 'zapier',
'status' => 'active',
'data' => [
'hook_url' => $request->input('hookUrl'),
],
]);
return response()->json();
}
public function destroy(DeleteIntegrationRequest $request)
{
$form = $request->form();
$this->authorize('view', $form);
$form
->integrations()
->where('data->hook_url', $request->input('hookUrl'))
->delete();
return response()->json();
}
public function poll(PollSubmissionRequest $request)
{
$form = $request->form();
$this->authorize('view', $form);
$lastSubmission = $form->submissions()->latest()->first();
if (!$lastSubmission) {
// Generate fake data when no previous submissions
$submissionData = (new FormSubmissionDataFactory($form))->asFormSubmissionData()->createSubmissionData();
}
return [ZapierIntegration::formatWebhookData($form, $submissionData ?? $lastSubmission->data)];
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Http\Controllers\Integrations\Zapier;
use App\Http\Requests\Zapier\ListFormsRequest;
use App\Http\Resources\Zapier\FormResource;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
class ListFormsController
{
use AuthorizesRequests;
public function __invoke(ListFormsRequest $request)
{
$workspace = $request->workspace();
$this->authorize('view', $workspace);
return FormResource::collection(
$workspace->forms()->get()
);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Controllers\Integrations\Zapier;
use App\Http\Resources\Zapier\WorkspaceResource;
use App\Models\Workspace;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
class ListWorkspacesController
{
use AuthorizesRequests;
public function __invoke()
{
$this->authorize('viewAny', Workspace::class);
return WorkspaceResource::collection(
Auth::user()->workspaces()->get()
);
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Http\Controllers\Integrations\Zapier;
use Illuminate\Support\Facades\Auth;
class ValidateAuthController
{
public function __invoke()
{
$user = Auth::user();
return [
'name' => $user->name,
'email' => $user->email,
];
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Http\Controllers\Settings;
use App\Http\Controllers\Controller;
use App\Http\Resources\OAuthProviderResource;
use App\Integrations\OAuth\OAuthProviderService;
use App\Models\OAuthProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class OAuthProviderController extends Controller
{
public function index()
{
/** @var \App\Models\User $user */
$user = Auth::user();
$providers = $user->oauthProviders()->get();
return OAuthProviderResource::collection($providers);
}
public function connect(Request $request, OAuthProviderService $service)
{
$userId = Auth::id();
cache()->put("oauth-intention:{$userId}", $request->input('intention'), 60 * 5);
return response()->json([
'url' => $service->getDriver()->getRedirectUrl(),
]);
}
public function handleRedirect(OAuthProviderService $service)
{
$driverUser = $service->getDriver()->getUser();
$provider = OAuthProvider::query()
->updateOrCreate(
[
'user_id' => Auth::id(),
'provider' => $service,
'provider_user_id' => $driverUser->getId(),
],
[
'access_token' => $driverUser->token,
'refresh_token' => $driverUser->refreshToken,
'name' => $driverUser->getName(),
'email' => $driverUser->getEmail(),
]
);
return OAuthProviderResource::make($provider);
}
public function destroy(OAuthProvider $provider)
{
$this->authorize('delete', $provider);
$provider->delete();
return response()->json();
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers\Settings;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class PasswordController extends Controller
{
/**
* Update the user's password.
*
* @return \Illuminate\Http\Response
*/
public function update(Request $request)
{
$this->validate($request, [
'password' => 'required|confirmed|min:6',
]);
$request->user()->update([
'password' => bcrypt($request->password),
]);
return response()->json(null, 204);
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace App\Http\Controllers\Settings;
use App\Http\Controllers\Controller;
use App\Models\Workspace;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class ProfileController extends Controller
{
/**
* Update the user's profile information.
*
* @return \Illuminate\Http\Response
*/
public function update(Request $request)
{
$user = $request->user();
$this->validate($request, [
'name' => 'required',
'email' => 'required|email|unique:users,email,' . $user->id,
]);
return tap($user)->update([
'name' => $request->name,
'email' => strtolower($request->email),
]);
}
// For self-hosted mode, only admin can update their credentials
public function updateAdminCredentials(Request $request)
{
$request->validate([
'email' => 'required|email|not_in:admin@opnform.com',
'password' => 'required|min:6|confirmed|not_in:password',
], [
'email.not_in' => "Please provide email address other than 'admin@opnform.com'",
'password.not_in' => "Please another password other than 'password'."
]);
ray('in', $request->password);
$user = $request->user();
$user->update([
'email' => $request->email,
'password' => bcrypt($request->password),
]);
ray($user);
Cache::forget('initial_user_setup_complete');
Cache::forget('max_user_id');
$workspace = Workspace::create([
'name' => 'My Workspace',
'icon' => '🧪',
]);
$user->workspaces()->sync([
$workspace->id => [
'role' => 'admin',
],
], false);
return $this->success([
'message' => 'Congratulations, your account credentials have been updated successfully.',
'user' => $user,
]);
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Http\Controllers\Settings;
use App\Enums\AccessTokenAbility;
use App\Http\Requests\CreateTokenRequest;
use App\Http\Resources\TokenResource;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Laravel\Sanctum\PersonalAccessToken;
class TokenController
{
use AuthorizesRequests;
public function index()
{
return TokenResource::collection(
Auth::user()->tokens()->get()
);
}
public function store(CreateTokenRequest $request)
{
$token = Auth::user()->createToken(
$request->input('name'),
AccessTokenAbility::allowed($request->input('abilities'))
);
return response()->json([
'token' => $token->plainTextToken,
]);
}
public function destroy(PersonalAccessToken $token)
{
$this->authorize('delete', $token);
$token->delete();
return response()->json();
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Http\Controllers;
use App\Models\Template;
use Illuminate\Http\Request;
class SitemapController extends Controller
{
public function index(Request $request)
{
return [
...$this->getTemplatesUrls(),
];
}
private function getTemplatesUrls()
{
$urls = [];
Template::where('publicly_listed', true)->chunk(100, function ($templates) use (&$urls) {
foreach ($templates as $template) {
$urls[] = [
'loc' => '/templates/'.$template->slug,
];
}
});
return $urls;
}
}

View File

@@ -0,0 +1,101 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\Subscriptions\UpdateStripeDetailsRequest;
use App\Service\BillingHelper;
use App\Service\UserHelper;
use Illuminate\Support\Facades\Auth;
use Laravel\Cashier\Subscription;
class SubscriptionController extends Controller
{
public const SUBSCRIPTION_PLANS = ['monthly', 'yearly'];
public const PRO_SUBSCRIPTION_NAME = 'default';
public const SUBSCRIPTION_NAMES = [
self::PRO_SUBSCRIPTION_NAME,
];
/**
* Returns stripe checkout URL
*
* $plan is constrained with regex in the api.php
*/
public function checkout($pricing, $plan, $trial = null)
{
$this->middleware('not-subscribed');
// Check User does not have a pending subscription
$user = Auth::user();
if ($user->subscriptions()->where('stripe_status', 'past_due')->first()) {
return $this->error([
'message' => 'You already have a past due subscription. Please verify your details in the billing page,
and contact us if the issue persists.',
]);
}
$checkoutBuilder = $user
->newSubscription($pricing, BillingHelper::getPricing($pricing)[$plan])
->allowPromotionCodes();
if ($trial != null) {
$checkoutBuilder->trialUntil(now()->addDays(3)->addHour());
}
$checkout = $checkoutBuilder
->collectTaxIds()
->checkout([
'success_url' => front_url('/subscriptions/success'),
'cancel_url' => front_url('/subscriptions/error'),
'billing_address_collection' => 'required',
'customer_update' => [
'address' => 'auto',
'name' => 'never',
],
]);
return $this->success([
'checkout_url' => $checkout->url,
]);
}
public function getUsersCount()
{
$this->middleware('auth');
return [
'count' => (new UserHelper(Auth::user()))->getActiveMembersCount() - 1,
];
}
public function updateStripeDetails(UpdateStripeDetailsRequest $request)
{
$user = Auth::user();
if (!$user->hasStripeId()) {
$user->createAsStripeCustomer();
}
$user->updateStripeCustomer([
'email' => $request->email,
'name' => $request->name,
]);
return $this->success([
'message' => 'Details saved.',
]);
}
public function billingPortal()
{
$this->middleware('auth');
if (!Auth::user()->has_customer_id) {
return $this->error([
'message' => 'Please subscribe before accessing your billing portal.',
]);
}
return $this->success([
'portal_url' => Auth::user()->billingPortalUrl(front_url('/home')),
]);
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\Templates\FormTemplateRequest;
use App\Http\Resources\FormTemplateResource;
use App\Models\Template;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class TemplateController extends Controller
{
public function index(Request $request)
{
$limit = (int) $request->get('limit', 0);
$onlyMy = (bool) $request->get('onlymy', false);
$query = Template::query();
if (Auth::check()) {
if ($onlyMy) {
$query->where('creator_id', Auth::id());
} else {
$query->where(function ($q) {
$q->where('publicly_listed', true)
->orWhere('creator_id', Auth::id());
});
}
} else {
$query->where('publicly_listed', true);
}
if ($limit > 0) {
$query->limit($limit);
}
$templates = $query->orderByDesc('created_at')->get();
return FormTemplateResource::collection($templates);
}
public function create(FormTemplateRequest $request)
{
$this->authorize('create', Template::class);
// Create template
$template = $request->getTemplate();
$template->save();
return $this->success([
'message' => 'Template was created.',
'template_id' => $template->id,
'data' => new FormTemplateResource($template),
]);
}
public function update(FormTemplateRequest $request, string $id)
{
$template = Template::findOrFail($id);
$this->authorize('update', $template);
$template->update($request->all());
return $this->success([
'message' => 'Template was updated.',
'template_id' => $template->id,
'data' => new FormTemplateResource($template),
]);
}
public function destroy($id)
{
$template = Template::findOrFail($id);
$this->authorize('delete', $template);
$template->delete();
return $this->success([
'message' => 'Template was deleted.',
]);
}
public function show(string $slug)
{
return new FormTemplateResource(
Template::whereSlug($slug)->firstOrFail()
);
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Http\Controllers;
use App\Models\UserInvite;
use App\Models\Workspace;
use App\Service\WorkspaceHelper;
use Illuminate\Http\Request;
class UserInviteController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
public function listInvites(Request $request, $workspaceId)
{
$workspace = Workspace::findOrFail($workspaceId);
$this->authorize('view', $workspace);
return (new WorkspaceHelper($workspace))->getAllInvites();
}
public function resendInvite($workspaceId, $inviteId)
{
$workspace = Workspace::findOrFail($workspaceId);
$this->authorize('adminAction', $workspace);
$userInvite = $workspace->invites()->find($inviteId);
if (!$userInvite) {
return $this->error(['success' => false, 'message' => 'Invite not found for this workspace.']);
}
if($userInvite->status == UserInvite::ACCEPTED_STATUS) {
return $this->error(['success' => false, 'message' => 'Invite already accepted.']);
}
$userInvite->sendEmail();
return $this->success(['message' => 'Invite email resent successfully.']);
}
public function cancelInvite($workspaceId, $inviteId)
{
$workspace = Workspace::findOrFail($workspaceId);
$this->authorize('adminAction', $workspace);
$userInvite = $workspace->invites()->find($inviteId);
if (!$userInvite) {
return $this->error(['success' => false, 'message' => 'Invite not found for this workspace.']);
}
if($userInvite->status == UserInvite::ACCEPTED_STATUS) {
return $this->error(['success' => false, 'message' => 'Invite already accepted.']);
}
$userInvite->delete();
return $this->success(['message' => 'Invite deleted successfully.']);
}
}

View File

@@ -0,0 +1,112 @@
<?php
namespace App\Http\Controllers\Webhook;
use App\Http\Controllers\Controller;
use App\Jobs\Billing\RemoveWorkspaceGuests;
use App\Models\License;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\UnauthorizedException;
class AppSumoController extends Controller
{
public function handle(Request $request)
{
$this->validateSignature($request);
if ($request->test) {
Log::info('[APPSUMO] test request received', $request->toArray());
return $this->success([
'message' => 'Webhook received.',
'event' => $request->event,
'success' => true,
]);
}
Log::info('[APPSUMO] request received', $request->toArray());
// Call the right function depending on the event using match()
match ($request->event) {
'activate' => $this->handleActivateEvent($request),
'upgrade', 'downgrade' => $this->handleChangeEvent($request),
'deactivate' => $this->handleDeactivateEvent($request),
default => null,
};
return $this->success([
'message' => 'Webhook received.',
'event' => $request->event,
'success' => true,
]);
}
private function handleActivateEvent($request)
{
$this->createLicense($request->json()->all());
}
private function handleChangeEvent($request)
{
$license = $this->deactivateLicense($request->prev_license_key);
$this->createLicense(array_merge($request->json()->all(), [
'user_id' => $license->user_id,
]));
}
private function handleDeactivateEvent($request)
{
$license = $this->deactivateLicense($request->license_key);
RemoveWorkspaceGuests::dispatch($license->user);
}
private function createLicense(array $licenseData): License
{
$license = License::firstOrNew([
'license_key' => $licenseData['license_key'],
'license_provider' => 'appsumo',
'status' => License::STATUS_ACTIVE,
]);
$license->meta = $licenseData;
$license->user_id = $licenseData['user_id'] ?? null;
$license->save();
Log::info(
'[APPSUMO] creating new license',
[
'license_key' => $license->license_key,
'license_id' => $license->id,
]
);
return $license;
}
private function deactivateLicense(string $licenseKey): License
{
$license = License::where([
'license_key' => $licenseKey,
'license_provider' => 'appsumo',
])->firstOrFail();
$license->update([
'status' => License::STATUS_INACTIVE,
]);
Log::info('[APPSUMO] De-activating license', [
'license_key' => $licenseKey,
'license_id' => $license->id,
]);
return $license;
}
private function validateSignature(Request $request)
{
$signature = $request->header('x-appsumo-signature');
$payload = $request->getContent();
if ($signature === hash_hmac('sha256', $payload, config('services.appsumo.api_key'))) {
throw new UnauthorizedException('Invalid signature.');
}
}
}

View File

@@ -0,0 +1,136 @@
<?php
namespace App\Http\Controllers\Webhook;
use App\Notifications\Subscription\FailedPaymentNotification;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\App;
use Laravel\Cashier\Http\Controllers\WebhookController;
use Stripe\Subscription as StripeSubscription;
class StripeController extends WebhookController
{
public function handleCustomerSubscriptionCreated(array $payload)
{
return parent::handleCustomerSubscriptionCreated($payload);
}
/**
* Override to add a sleep, and to detect plan upgrades
*
* @return \Symfony\Component\HttpFoundation\Response|void
*/
protected function handleCustomerSubscriptionUpdated(array $payload)
{
sleep(1);
if ($user = $this->getUserByStripeId($payload['data']['object']['customer'])) {
$data = $payload['data']['object'];
$subscription = $user->subscriptions()->firstOrNew(['stripe_id' => $data['id']]);
if (
isset($data['status']) &&
$data['status'] === StripeSubscription::STATUS_INCOMPLETE_EXPIRED
) {
$subscription->items()->delete();
$subscription->delete();
return;
}
$subscription->type = $subscription->type ?? $data['metadata']['name'] ?? $this->newSubscriptionName($payload);
$mainItem = $this->getMainSubscriptionLineItem($data['items']['data']);
$isSinglePrice = count($data['items']['data']) === 1;
// Price...
$subscription->stripe_price = $isSinglePrice ? $mainItem['price']['id'] : null;
// Type - previously (Name)
$subscription->type = $this->getSubscriptionName($mainItem['price']['product']);
// Quantity...
$subscription->quantity = $isSinglePrice && isset($mainItem['quantity']) ? $mainItem['quantity'] : null;
// Trial ending date...
if (isset($data['trial_end'])) {
$trialEnd = Carbon::createFromTimestamp($data['trial_end']);
if (! $subscription->trial_ends_at || $subscription->trial_ends_at->ne($trialEnd)) {
$subscription->trial_ends_at = $trialEnd;
}
}
// Cancellation date...
if (isset($data['cancel_at_period_end'])) {
if ($data['cancel_at_period_end']) {
$subscription->ends_at = $subscription->onTrial()
? $subscription->trial_ends_at
: Carbon::createFromTimestamp($data['current_period_end']);
} elseif (isset($data['cancel_at'])) {
$subscription->ends_at = Carbon::createFromTimestamp($data['cancel_at']);
} else {
$subscription->ends_at = null;
}
}
// Status...
if (isset($data['status'])) {
$subscription->stripe_status = $data['status'];
}
$subscription->save();
// Update subscription items...
if (isset($data['items'])) {
$prices = [];
foreach ($data['items']['data'] as $item) {
$prices[] = $item['price']['id'];
$subscription->items()->updateOrCreate([
'stripe_id' => $item['id'],
], [
'stripe_product' => $item['price']['product'],
'stripe_price' => $item['price']['id'],
'quantity' => $item['quantity'] ?? null,
]);
}
// Delete items that aren't attached to the subscription anymore...
$subscription->items()->whereNotIn('stripe_price', $prices)->delete();
}
}
return $this->successMethod();
}
protected function handleChargeFailed(array $payload)
{
if ($user = $this->getUserByStripeId($payload['data']['object']['customer'])) {
$user->notify(new FailedPaymentNotification());
}
return $this->successMethod();
}
private function getMainSubscriptionLineItem(array $items)
{
return collect($items)->first(function ($item) {
return in_array($this->getSubscriptionName($item['price']['product']), ['default']);
});
}
private function getSubscriptionName(string $stripeProductId)
{
$config = App::environment() == 'production' ? config('pricing.production') : config('pricing.test');
foreach ($config as $plan => $data) {
if ($stripeProductId == $config[$plan]['product_id']) {
return $plan;
}
}
return 'default';
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\Workspace\CustomDomainRequest;
use App\Http\Resources\WorkspaceResource;
use App\Models\Workspace;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class WorkspaceController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
public function index()
{
$this->authorize('viewAny', Workspace::class);
return WorkspaceResource::collection(Auth::user()->workspaces);
}
public function saveCustomDomain(CustomDomainRequest $request)
{
$request->workspace->custom_domains = $request->customDomains;
$request->workspace->save();
return new WorkspaceResource($request->workspace);
}
public function delete($id)
{
$workspace = Workspace::findOrFail($id);
$this->authorize('delete', $workspace);
$id = $workspace->id;
$workspace->delete();
return $this->success([
'message' => 'Workspace deleted.',
'workspace_id' => $id,
]);
}
public function create(Request $request)
{
$user = $request->user();
$this->validate($request, [
'name' => 'required',
]);
// Create workspace
$workspace = Workspace::create([
'name' => $request->name,
'icon' => ($request->emoji) ? $request->emoji : '',
]);
// Add relation with user
$user->workspaces()->sync([
$workspace->id => [
'role' => 'admin',
],
], false);
return $this->success([
'message' => 'Workspace created.',
'workspace_id' => $workspace->id,
'workspace' => new WorkspaceResource($workspace),
]);
}
}

View File

@@ -0,0 +1,127 @@
<?php
namespace App\Http\Controllers;
use App\Jobs\Billing\WorkspaceUsersUpdated;
use App\Models\UserInvite;
use Illuminate\Http\Request;
use App\Models\Workspace;
use App\Models\User;
use App\Service\WorkspaceHelper;
class WorkspaceUserController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
public function listUsers(Request $request, $workspaceId)
{
$workspace = Workspace::findOrFail($workspaceId);
$this->authorize('view', $workspace);
return (new WorkspaceHelper($workspace))->getAllUsers();
}
public function addUser(Request $request, $workspaceId)
{
$workspace = Workspace::findOrFail($workspaceId);
$this->authorize('inviteUser', $workspace);
$this->validate($request, [
'email' => 'required|email',
'role' => 'required|in:admin,user',
]);
$user = User::where('email', $request->email)->first();
if (!$user) {
return $this->inviteUser($workspace, $request->email, $request->role);
}
if ($workspace->users->contains($user->id)) {
return $this->success([
'message' => 'User is already in workspace.'
]);
}
// User found - add user to workspace
$workspace->users()->sync([
$user->id => [
'role' => $request->role,
],
], false);
WorkspaceUsersUpdated::dispatch($workspace);
return $this->success([
'message' => 'User has been successfully added to workspace.'
]);
}
private function inviteUser(Workspace $workspace, string $email, string $role)
{
if (
UserInvite::where('email', $email)
->where('workspace_id', $workspace->id)
->notExpired()
->pending()
->exists()) {
return $this->success([
'message' => 'User has already been invited.'
]);
}
// Send new invite
UserInvite::inviteUser($email, $role, $workspace, now()->addDays(7));
return $this->success([
'message' => 'Registration invitation email sent to user.'
]);
}
public function updateUserRole(Request $request, $workspaceId, $userId)
{
$workspace = Workspace::findOrFail($workspaceId);
$user = User::findOrFail($userId);
$this->authorize('adminAction', $workspace);
$this->validate($request, [
'role' => 'required|in:admin,user',
]);
$workspace->users()->sync([
$user->id => [
'role' => $request->role,
],
], false);
return $this->success([
'message' => 'User role changed successfully.'
]);
}
public function removeUser(Request $request, $workspaceId, $userId)
{
$workspace = Workspace::findOrFail($workspaceId);
$this->authorize('adminAction', $workspace);
$workspace->users()->detach($userId);
WorkspaceUsersUpdated::dispatch($workspace);
return $this->success([
'message' => 'User removed from workspace successfully.'
]);
}
public function leaveWorkspace(Request $request, $workspaceId)
{
$workspace = Workspace::findOrFail($workspaceId);
$this->authorize('view', $workspace);
$workspace->users()->detach($request->user()->id);
return $this->success([
'message' => 'You have left the workspace successfully.'
]);
}
}

102
api/app/Http/Kernel.php Normal file
View File

@@ -0,0 +1,102 @@
<?php
namespace App\Http;
use App\Http\Middleware\AcceptsJsonMiddleware;
use App\Http\Middleware\AuthenticateJWT;
use App\Http\Middleware\CustomDomainRestriction;
use App\Http\Middleware\ImpersonationMiddleware;
use App\Http\Middleware\IsAdmin;
use App\Http\Middleware\IsModerator;
use App\Http\Middleware\IsNotSubscribed;
use App\Http\Middleware\IsSubscribed;
use App\Http\Middleware\SelfHostedCredentialsMiddleware;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array
*/
protected $middleware = [
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\SetLocale::class,
AuthenticateJWT::class,
CustomDomainRestriction::class,
AcceptsJsonMiddleware::class,
];
/**
* The application's route middleware groups.
*
* @var array
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'spa' => [
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
'throttle:100,1',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Session\Middleware\StartSession::class,
SelfHostedCredentialsMiddleware::class,
ImpersonationMiddleware::class,
],
'api-external' => [
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array
*/
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'admin' => IsAdmin::class,
'moderator' => IsModerator::class,
'subscribed' => IsSubscribed::class,
'not-subscribed' => IsNotSubscribed::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'pro-form' => \App\Http\Middleware\Form\ProForm::class,
'protected-form' => \App\Http\Middleware\Form\ProtectedForm::class,
'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class,
'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class,
];
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class AcceptsJsonMiddleware
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle(Request $request, Closure $next)
{
$request->headers->set('Accept', 'application/json');
return $next($request);
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* @param \Illuminate\Http\Request $request
* @return string|null
*/
protected function redirectTo($request)
{
if (! $request->expectsJson()) {
return redirect(front_url('login'));
}
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Tymon\JWTAuth\Exceptions\JWTException;
class AuthenticateJWT
{
public const API_SERVER_SECRET_HEADER_NAME = 'x-api-secret';
/**
* Verifies the JWT token and validates the IP and User Agent
* Invalidates token otherwise
*/
public function handle(Request $request, Closure $next)
{
// Parse JWT Payload
try {
$payload = \JWTAuth::parseToken()->getPayload();
} catch (JWTException $e) {
return $next($request);
}
// Validate IP and User Agent
if ($payload) {
if ($frontApiSecret = $request->header(self::API_SERVER_SECRET_HEADER_NAME)) {
// If it's a trusted SSR request, skip the rest
if ($frontApiSecret === config('app.front_api_secret')) {
return $next($request);
}
}
// If it's impersonating, skip the rest
if ($payload->get('impersonating')) {
return $next($request);
}
$error = null;
if (! \Hash::check($request->ip(), $payload->get('ip'))) {
$error = 'Origin IP is invalid';
}
if (! \Hash::check($request->userAgent(), $payload->get('ua'))) {
$error = 'Origin User Agent is invalid';
}
if ($error) {
auth()->invalidate();
return response()->json([
'message' => $error,
], 403);
}
}
return $next($request);
}
}

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 IP',
], 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,59 @@
<?php
namespace App\Http\Middleware;
use App\Http\Requests\Workspace\CustomDomainRequest;
use App\Models\Forms\Form;
use App\Models\Workspace;
use Closure;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
class CustomDomainRestriction
{
public const CUSTOM_DOMAIN_HEADER = 'x-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(CustomDomainRequest::CUSTOM_DOMAINS_REGEX, $customDomain)) {
return response()->json([
'success' => false,
'message' => 'Invalid domain',
'error' => 'invalid_domain',
], 420);
}
// 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 (! $workspaces = Workspace::whereJsonContains('custom_domains', $customDomain)->get()) {
return response()->json([
'success' => false,
'message' => 'Unknown domain',
'error' => 'invalid_domain',
], 420);
}
$workspacesIds = $workspaces->pluck('id')->toArray();
Workspace::addGlobalScope('domain-restricted', function (Builder $builder) use ($workspacesIds) {
$builder->whereIn('id', $workspacesIds);
});
Form::addGlobalScope('domain-restricted', function (Builder $builder) use ($workspacesIds) {
$builder->whereIn('workspace_id', $workspacesIds);
});
return $next($request);
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array
*/
protected $except = [
//
];
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Http\Middleware\Form;
use App\Models\Forms\Form;
use Closure;
use Illuminate\Http\Request;
class ProForm
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle(Request $request, Closure $next)
{
if ($request->route('formId') && $form = Form::findOrFail($request->route('formId'))) {
if ($form->is_pro) {
$request->merge([
'form' => $form,
]);
return $next($request);
}
}
return response([
'status' => 'Unauthorized',
'message' => 'You need a subscription to access this content.',
], 403);
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Http\Middleware\Form;
use App\Models\Forms\Form;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class ProtectedForm
{
public const PASSWORD_HEADER_NAME = 'form-password';
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle(Request $request, Closure $next)
{
if (! $request->route('slug')) {
return $next($request);
}
$form = Form::where('slug', $request->route('slug'))->firstOrFail();
$request->merge([
'form' => $form,
]);
$userIsFormOwner = Auth::check() && Auth::user()->ownsForm($form);
if (! $userIsFormOwner && $this->isProtected($request, $form)) {
return response([
'status' => 'Unauthorized',
'message' => 'Form is protected.',
], 403);
}
return $next($request);
}
public static function isProtected(Request $request, Form $form)
{
if (! $form->has_password) {
return false;
}
return ! self::hasCorrectPassword($request, $form);
}
public static function hasCorrectPassword(Request $request, Form $form)
{
return $request->headers->has(self::PASSWORD_HEADER_NAME) && $request->headers->get(self::PASSWORD_HEADER_NAME) == hash('sha256', $form->password);
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Http\Middleware\Form;
use App\Models\Forms\Form;
use Closure;
use Illuminate\Http\Request;
class ResolveFormMiddleware
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle(Request $request, Closure $next, string $routeParamName = 'id')
{
$form = Form::where($routeParamName, $request->route($routeParamName))->firstOrFail();
$request->merge([
'form' => $form,
]);
return $next($request);
}
}

View File

@@ -0,0 +1,104 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Tymon\JWTAuth\Exceptions\JWTException;
class ImpersonationMiddleware
{
public const ADMIN_LOG_PREFIX = '[admin_action] ';
public const LOG_ROUTES = [
'open.forms.store',
'open.forms.update',
'open.forms.duplicate',
'open.forms.regenerate-link',
];
public const ALLOWED_ROUTES = [
'logout',
// Forms
'forms.ai.generate',
'forms.ai.show',
'forms.assets.show',
'forms.show',
'forms.answer',
'forms.fetchSubmission',
'forms.users.index',
'open.forms.index-all',
'open.forms.store',
'open.forms.assets.upload',
'open.forms.update',
'open.forms.duplicate',
'open.forms.regenerate-link',
'open.forms.submissions',
'open.forms.submissions.file',
'open.providers',
'open.forms.integrations',
'open.forms.integrations.events',
// Workspaces
'open.workspaces.index',
'open.workspaces.create',
'open.workspaces.delete',
'open.workspaces.save-custom-domains',
'open.workspaces.databases.search',
'open.workspaces.databases.show',
'open.workspaces.form.stats',
'open.workspaces.forms.index',
'open.workspaces.users.index',
'templates.index',
'templates.create',
'templates.update',
'templates.show',
'user.current',
'local.temp',
'vapor.signed-storage-url',
'upload-file'
];
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle(Request $request, Closure $next)
{
try {
if (!auth()->check() || !auth()->payload()->get('impersonating')) {
return $next($request);
}
} catch (JWTException $e) {
return $next($request);
}
// Check that route is allowed
$routeName = $request->route()->getName();
if (!in_array($routeName, self::ALLOWED_ROUTES)) {
return response([
'message' => 'Unauthorized when impersonating',
'route' => $routeName,
'impersonator' => auth()->payload()->get('impersonator_id'),
'impersonated_account' => auth()->id(),
'url' => $request->fullUrl(),
'payload' => $request->all(),
], 403);
} elseif (in_array($routeName, self::LOG_ROUTES)) {
\Log::warning(self::ADMIN_LOG_PREFIX . 'Impersonator action', [
'route' => $routeName,
'url' => $request->fullUrl(),
'impersonated_account' => auth()->id(),
'impersonator' => auth()->payload()->get('impersonator_id'),
'payload' => $request->all(),
]);
}
return $next($request);
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class IsAdmin
{
/**
* Handle an incoming request.
*
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
if ($request->user() && ! $request->user()->admin) {
// This user is not a paying customer...
if ($request->expectsJson()) {
return response([
'message' => 'You are not allowed.',
'type' => 'error',
], 403);
}
return redirect('home');
}
return $next($request);
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class IsModerator
{
/**
* Handle an incoming request.
*
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
if ($request->user() && ! $request->user()->moderator) {
// This user is not a paying customer...
if ($request->expectsJson()) {
return response([
'message' => 'You are not allowed.',
'type' => 'error',
], 403);
}
return redirect('home');
}
return $next($request);
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class IsNotSubscribed
{
/**
* Handle an incoming request.
*
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
if ($request->user() && $request->user()->subscribed()) {
// This user is a paying customer...
if ($request->expectsJson()) {
return response([
'message' => 'You are already subscribed to NotionForms Pro.',
'type' => 'error',
], 401);
}
return redirect('billing');
}
return $next($request);
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class IsSubscribed
{
/**
* Handle an incoming request.
*
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
if ($request->user() && ! $request->user()->subscribed()) {
// This user is not a paying customer...
if ($request->expectsJson()) {
return response([
'message' => 'You are not subscribed to NotionForms Pro.',
'type' => 'error',
], 401);
}
return redirect('billing');
}
return $next($request);
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Middleware;
class PreventRequestsDuringMaintenance extends Middleware
{
/**
* The URIs that should be reachable while maintenance mode is enabled.
*
* @var array
*/
protected $except = [
//
];
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Http\Middleware;
use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* @param string|null ...$guards
* @return mixed
*/
public function handle(Request $request, Closure $next, ...$guards)
{
$guards = empty($guards) ? [null] : $guards;
foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) {
if ($request->expectsJson()) {
return response()->json(['error' => 'Already authenticated.'], 400);
} else {
return redirect(RouteServiceProvider::HOME);
}
}
}
return $next($request);
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Support\Facades\Cache;
use App\Models\User;
class SelfHostedCredentialsMiddleware
{
public const ALLOWED_ROUTES = [
'login',
'credentials.update',
'user.current',
'logout',
];
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if (app()->environment('testing')) {
return $next($request);
}
if (in_array($request->route()->getName(), self::ALLOWED_ROUTES)) {
return $next($request);
}
if (
config('app.self_hosted') &&
$request->user() &&
!$this->isInitialSetupComplete()
) {
return response()->json([
'message' => 'You must change your credentials when in self-hosted mode',
'type' => 'error',
], Response::HTTP_FORBIDDEN);
}
return $next($request);
}
private function isInitialSetupComplete(): bool
{
return (bool) Cache::remember('initial_user_setup_complete', 60 * 60, function () {
$maxUserId = $this->getMaxUserId();
if ($maxUserId === 0) {
return false;
}
return !User::where('email', 'admin@opnform.com')->exists();
});
}
private function getMaxUserId(): int
{
return (int) Cache::remember('max_user_id', 60 * 60, function () {
return User::max('id') ?? 0;
});
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Http\Middleware;
use Closure;
class SetLocale
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @return mixed
*/
public function handle($request, Closure $next)
{
if ($locale = $this->parseLocale($request)) {
app()->setLocale($locale);
}
return $next($request);
}
/**
* @param \Illuminate\Http\Request $request
* @return string|null
*/
protected function parseLocale($request)
{
$locales = config('app.locales');
$locale = $request->server('HTTP_ACCEPT_LANGUAGE');
$locale = substr($locale, 0, strpos($locale, ',') ?: strlen($locale));
if (array_key_exists($locale, $locales)) {
return $locale;
}
$locale = substr($locale, 0, 2);
if (array_key_exists($locale, $locales)) {
return $locale;
}
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
class TrimStrings extends Middleware
{
/**
* The names of the attributes that should not be trimmed.
*
* @var array
*/
protected $except = [
'password',
'password_confirmation',
];
/**
* The route name where this shouldn't be applied
*
* @var string[]
*/
protected $exceptUrls = [
'/\/api\/forms\/(.*)\/answer/',
];
public function handle($request, \Closure $next)
{
// Check if URL matches
foreach ($this->exceptUrls as $urlRegex) {
$matches = null;
preg_match($urlRegex, $request->url(), $matches);
if (count($matches)) {
return $next($request);
}
}
return parent::handle($request, $next);
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustHosts as Middleware;
class TrustHosts extends Middleware
{
/**
* Get the host patterns that should be trusted.
*
* @return array
*/
public function hosts()
{
return [
$this->allSubdomainsOfApplicationUrl(),
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*
* @var array|string|null
*/
protected $proxies;
/**
* The headers that should be used to detect proxies.
*
* @var int
*/
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB;
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array
*/
protected $except = [
'stripe/webhook',
'vapor/signed-storage-url',
'upload-file',
];
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class AiGenerateFormRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'form_prompt' => 'required|string|max:1000',
];
}
}

View File

@@ -0,0 +1,281 @@
<?php
namespace App\Http\Requests;
use App\Models\Forms\Form;
use App\Rules\CustomFieldValidationRule;
use App\Rules\MatrixValidationRule;
use App\Rules\StorageFile;
use App\Rules\ValidHCaptcha;
use App\Rules\ValidPhoneInputRule;
use App\Rules\ValidUrl;
use App\Service\Forms\FormLogicPropertyResolver;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
class AnswerFormRequest extends FormRequest
{
public Form $form;
protected array $requestRules = [];
protected int $maxFileSize;
public function __construct(Request $request)
{
$this->form = $request->form;
$this->maxFileSize = $this->form->workspace->max_file_size;
}
private function getFieldMaxFileSize($fieldProps)
{
return array_key_exists('max_file_size', $fieldProps) ?
min($fieldProps['max_file_size'] * 1000000, $this->maxFileSize) : $this->maxFileSize;
}
/**
* Validate form before use it
*
* @return bool
*/
public function authorize()
{
return !$this->form->is_closed && !$this->form->max_number_of_submissions_reached && $this->form->visibility === 'public';
}
/**
* Get the validation rules that apply to the form.
*
* @return array
*/
public function rules()
{
$selectionFields = collect($this->form->properties)->filter(function ($pro) {
return in_array($pro['type'], ['select', 'multi_select']);
});
foreach ($this->form->properties as $property) {
$rules = [];
/*if (!$this->form->is_pro) { // If not pro then not check logic
$property['logic'] = false;
}*/
// For get values instead of Id for select/multi select options
$data = $this->toArray();
foreach ($selectionFields as $field) {
if (isset($data[$field['id']]) && is_array($data[$field['id']])) {
$data[$field['id']] = array_map(function ($val) use ($field) {
$tmpop = collect($field[$field['type']]['options'])->first(function ($op) use ($val) {
return $op['id'] ?? $op['value'] === $val;
});
return isset($tmpop['name']) ? $tmpop['name'] : '';
}, $data[$field['id']]);
}
}
if (FormLogicPropertyResolver::isRequired($property, $data)) {
$rules[] = 'required';
if ($property['type'] == 'checkbox') {
// Required for checkboxes means true
$rules[] = 'accepted';
} elseif ($property['type'] == 'rating') {
// For star rating, needs a minimum of 1 star
$rules[] = 'min:1';
} elseif ($property['type'] == 'matrix') {
$rules[] = new MatrixValidationRule($property, true);
}
} else {
$rules[] = 'nullable';
if ($property['type'] == 'matrix') {
$rules[] = new MatrixValidationRule($property, false);
}
}
// Clean id to escape "."
$propertyId = $property['id'];
if (in_array($property['type'], ['multi_select'])) {
$rules[] = 'array';
$this->requestRules[$propertyId . '.*'] = $this->getPropertyRules($property);
} else {
$rules = array_merge($rules, $this->getPropertyRules($property));
}
// User custom validation
if (!(Str::of($property['type'])->startsWith('nf-')) && isset($property['validation'])) {
$rules[] = (new CustomFieldValidationRule($property['validation'], $data));
}
$this->requestRules[$propertyId] = $rules;
}
// Validate hCaptcha
if ($this->form->use_captcha) {
$this->requestRules['h-captcha-response'] = [new ValidHCaptcha()];
}
// Validate submission_id for edit mode
if ($this->form->is_pro && $this->form->editable_submissions) {
$this->requestRules['submission_id'] = 'string';
}
return $this->requestRules;
}
/**
* Renames validated fields (because field names are ids)
*
* @return array
*/
public function attributes()
{
$fields = [];
foreach ($this->form->properties as $property) {
$fields[$property['id']] = $property['name'];
}
return $fields;
}
/**
* Get the validation messages that apply to the request.
*
* @return array
*/
public function messages()
{
$messages = [];
foreach ($this->form->properties as $property) {
if ($property['type'] == 'date' && isset($property['date_range']) && $property['date_range']) {
$messages[$property['id'] . '.0.required_with'] = 'From date is required';
$messages[$property['id'] . '.1.required_with'] = 'To date is required';
$messages[$property['id'] . '.0.before_or_equal'] = 'From date must be before or equal To date';
}
if ($property['type'] == 'rating') {
$messages[$property['id'] . '.min'] = 'A rating must be selected';
}
}
return $messages;
}
/**
* Return validation rules for a given form property
*/
private function getPropertyRules($property): array
{
switch ($property['type']) {
case 'text':
case 'signature':
return ['string'];
case 'number':
case 'rating':
case 'scale':
case 'slider':
return ['numeric'];
case 'select':
case 'multi_select':
if (($property['allow_creation'] ?? false)) {
return ['string'];
}
return [Rule::in($this->getSelectPropertyOptions($property))];
case 'checkbox':
return ['boolean'];
case 'url':
if (isset($property['file_upload']) && $property['file_upload']) {
$this->requestRules[$property['id'] . '.*'] = [new StorageFile($this->maxFileSize, [], $this->form)];
return ['array'];
}
return [new ValidUrl()];
case 'files':
$allowedFileTypes = [];
if (!empty($property['allowed_file_types'])) {
$allowedFileTypes = explode(',', $property['allowed_file_types']);
}
$this->requestRules[$property['id'] . '.*'] = [new StorageFile($this->getFieldMaxFileSize($property), $allowedFileTypes, $this->form)];
return ['array'];
case 'email':
return ['email:filter'];
case 'date':
if (isset($property['date_range']) && $property['date_range']) {
$this->requestRules[$property['id'] . '.*'] = $this->getRulesForDate($property);
$this->requestRules[$property['id'] . '.0'] = ['required_with:' . $property['id'] . '.1', 'before_or_equal:' . $property['id'] . '.1'];
$this->requestRules[$property['id'] . '.1'] = ['required_with:' . $property['id'] . '.0'];
return ['array', 'min:2'];
}
return $this->getRulesForDate($property);
case 'phone_number':
if (isset($property['use_simple_text_input']) && $property['use_simple_text_input']) {
return ['string'];
}
return ['string', 'min:6', new ValidPhoneInputRule()];
default:
return [];
}
}
private function getRulesForDate($property)
{
if (isset($property['disable_past_dates']) && $property['disable_past_dates']) {
return ['date', 'after:yesterday'];
} elseif (isset($property['disable_future_dates']) && $property['disable_future_dates']) {
return ['date', 'before:tomorrow'];
}
return ['date'];
}
private function getSelectPropertyOptions($property): array
{
$type = $property['type'];
if (!isset($property[$type])) {
return [];
}
return array_column($property[$type]['options'], 'name');
}
protected function prepareForValidation()
{
$receivedData = $this->toArray();
$mergeData = [];
$countryCodeMapper = json_decode(file_get_contents(resource_path('data/country_code_mapper.json')), true);
collect($this->form->properties)->each(function ($property) use ($countryCodeMapper, $receivedData, &$mergeData) {
$receivedValue = $receivedData[$property['id']] ?? null;
// Escape all '\' in select options
if (in_array($property['type'], ['select', 'multi_select']) && !is_null($receivedValue)) {
if (is_array($receivedValue)) {
$mergeData[$property['id']] = collect($receivedValue)->map(function ($value) {
$value = Str::of($value);
return $value->replace(
["\e", "\f", "\n", "\r", "\t", "\v", '\\'],
['\\e', '\\f', '\\n', '\\r', '\\t', '\\v', '\\\\']
)->toString();
})->toArray();
} else {
$receivedValue = Str::of($receivedValue);
$mergeData[$property['id']] = $receivedValue->replace(
["\e", "\f", "\n", "\r", "\t", "\v", '\\'],
['\\e', '\\f', '\\n', '\\r', '\\t', '\\v', '\\\\']
)->toString();
}
}
if ($property['type'] === 'phone_number' && (!isset($property['use_simple_text_input']) || !$property['use_simple_text_input']) && $receivedValue && in_array($receivedValue, $countryCodeMapper)) {
$mergeData[$property['id']] = null;
}
});
$this->merge($mergeData);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class CreateTokenRequest extends FormRequest
{
public function rules()
{
return [
'name' => [
'required',
'string',
],
'abilities' => [
'nullable',
'array'
]
];
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace App\Http\Requests\Integration;
use App\Models\Integration\FormIntegration;
use App\Rules\IntegrationLogicRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
class FormIntegrationsRequest extends FormRequest
{
public array $integrationRules = [];
private ?string $integrationClassName = null;
public function __construct(Request $request)
{
if ($request->integration_id) {
// Load integration class, and get rules
$integration = FormIntegration::getIntegration($request->integration_id);
if ($integration && isset($integration['file_name']) && class_exists(
'App\Integrations\Handlers\\' . $integration['file_name']
)) {
$this->integrationClassName = 'App\Integrations\Handlers\\' . $integration['file_name'];
$this->loadIntegrationRules();
return;
}
throw new \Exception('Unknown Integration!');
}
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return array_merge([
'integration_id' => ['required', Rule::in(array_keys(FormIntegration::getAllIntegrations()))],
'oauth_id' => [
$this->isOAuthRequired() ? 'required' : 'nullable',
Rule::exists('oauth_providers', 'id')
],
'settings' => 'present|array',
'status' => 'required|boolean',
'logic' => [new IntegrationLogicRule()],
], $this->integrationRules);
}
/**
* Give the validated fields a better "human-readable" name
*
* @return array
*/
public function attributes()
{
$attributes = $this->integrationClassName::getValidationAttributes();
$fields = [];
foreach ($this->rules() as $key => $value) {
$fields[$key] = $attributes[$key] ?? Str::of($key)
->replace('settings.', '')
->headline()
->toString();
}
return $fields;
}
protected function isOAuthRequired(): bool
{
return $this->integrationClassName::isOAuthRequired();
}
private function loadIntegrationRules()
{
foreach ($this->integrationClassName::getValidationRules() as $key => $value) {
$this->integrationRules['settings.' . $key] = $value;
}
}
public function toIntegrationData(): array
{
return $this->integrationClassName::formatData([
'status' => ($this->validated(
'status'
)) ? FormIntegration::STATUS_ACTIVE : FormIntegration::STATUS_INACTIVE,
'integration_id' => $this->validated('integration_id'),
'data' => $this->validated('settings') ?? [],
'logic' => $this->validated('logic') ?? [],
'oauth_id' => $this->validated('oauth_id'),
]);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Http\Requests\Integration;
use App\Models\Forms\Form;
use App\Models\Integration\FormZapierWebhook;
use Illuminate\Foundation\Http\FormRequest;
class StoreFormZapierWebhookRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'form_slug' => 'required|exists:forms,slug',
'hook_url' => 'required|string|url',
];
}
public function instanciateHook()
{
$form = Form::whereSlug($this->form_slug)->firstOrFail();
return new FormZapierWebhook([
'form_id' => $form->id,
'hook_url' => $this->hook_url,
]);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests\Integration\Zapier;
use App\Models\Forms\Form;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class PollSubmissionRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'form_id' => [
'required',
Rule::exists(Form::getModel()->getTable(), 'id'),
],
];
}
public function form(): Form
{
return Form::findOrFail($this->input('form_id'));
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Http\Requests;
class StoreFormRequest extends UserFormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return array_merge(parent::rules(), [// Info about database
'workspace_id' => 'required|exists:workspaces,id',
]);
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Http\Requests\Subscriptions;
use Illuminate\Foundation\Http\FormRequest;
class UpdateStripeDetailsRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'name' => 'required|string',
'email' => 'required|email',
];
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace App\Http\Requests\Templates;
use App\Models\Template;
use Illuminate\Foundation\Http\FormRequest;
class FormTemplateRequest extends FormRequest
{
public const IGNORED_KEYS = [
'id',
'creator',
'cleanings',
'closes_at',
'deleted_at',
'updated_at',
'form_pending_submission_key',
'is_closed',
'is_pro',
'is_password_protected',
'last_edited_human',
'max_number_of_submissions_reached',
'removed_properties',
'creator_id',
'extra',
'workspace',
'workspace_id',
'submissions',
'submissions_count',
'views',
'views_count',
'visibility',
'webhook_url',
];
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
$slugRule = '';
if ($this->id) {
$slugRule = ',' . $this->id;
}
return [
'form' => 'required|array',
'publicly_listed' => 'boolean',
'name' => 'required|string|max:60',
'slug' => 'required|string|alpha_dash|unique:templates,slug' . $slugRule,
'short_description' => 'required|string|max:1000',
'description' => 'required|string',
'image_url' => 'required|string',
'types' => 'nullable|array',
'industries' => 'nullable|array',
'related_templates' => 'nullable|array',
'questions' => 'array',
];
}
public function getTemplate(): Template
{
$structure = $this->form;
foreach ($structure as $key => $val) {
if (in_array($key, self::IGNORED_KEYS)) {
unset($structure[$key]);
}
}
return new Template([
'creator_id' => $this->user()?->id ?? null,
'publicly_listed' => $this->publicly_listed,
'name' => $this->name,
'slug' => $this->slug,
'short_description' => $this->short_description,
'description' => $this->description,
'image_url' => $this->image_url,
'structure' => $structure,
'types' => $this->types ?? [],
'industries' => $this->industries ?? [],
'related_templates' => $this->related_templates ?? [],
'questions' => $this->questions ?? [],
]);
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace App\Http\Requests;
class UpdateFormRequest extends UserFormRequest
{
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Http\Requests;
use App\Rules\StorageFile;
use Illuminate\Foundation\Http\FormRequest;
class UploadAssetRequest extends FormRequest
{
public const FORM_ASSET_MAX_SIZE = 5000000;
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
$fileTypes = [
'png',
'jpeg',
'jpg',
'bmp',
'gif',
'svg',
];
if ($this->offsetExists('type') && $this->get('type') === 'files') {
$fileTypes = [];
}
return [
'url' => ['required', new StorageFile(self::FORM_ASSET_MAX_SIZE, $fileTypes)],
];
}
}

View File

@@ -0,0 +1,140 @@
<?php
namespace App\Http\Requests;
use App\Http\Requests\Workspace\CustomDomainRequest;
use App\Models\Forms\Form;
use App\Rules\FormPropertyLogicRule;
use Illuminate\Validation\Rule;
/**
* Abstract class to validate create/update forms
*
* Class UserFormRequest
*/
abstract class UserFormRequest extends \Illuminate\Foundation\Http\FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
// Form Info
'title' => 'required|string|max:60',
'description' => 'nullable|string|max:2000',
'tags' => 'nullable|array',
'visibility' => ['required', Rule::in(Form::VISIBILITY)],
// Customization
'font_family' => 'string|nullable',
'theme' => ['required', Rule::in(Form::THEMES)],
'width' => ['required', Rule::in(Form::WIDTHS)],
'size' => ['required', Rule::in(Form::SIZES)],
'border_radius' => ['required', Rule::in(Form::BORDER_RADIUS)],
'cover_picture' => 'url|nullable',
'logo_picture' => 'url|nullable',
'dark_mode' => ['required', Rule::in(Form::DARK_MODE_VALUES)],
'color' => 'required|string',
'hide_title' => 'required|boolean',
'uppercase_labels' => 'required|boolean',
'no_branding' => 'required|boolean',
'transparent_background' => 'required|boolean',
'closes_at' => 'date|nullable',
'closed_text' => 'string|nullable',
// Custom Code
'custom_code' => 'string|nullable',
// Submission
'submit_button_text' => 'string|min:1|max:50',
're_fillable' => 'boolean',
're_fill_button_text' => 'string|min:1|max:50',
'submitted_text' => 'string|max:2000',
'redirect_url' => 'nullable|active_url|max:255',
'database_fields_update' => 'nullable|array',
'max_submissions_count' => 'integer|nullable|min:1',
'max_submissions_reached_text' => 'string|nullable',
'editable_submissions' => 'boolean|nullable',
'editable_submissions_button_text' => 'string|min:1|max:50',
'confetti_on_submission' => 'boolean',
'show_progress_bar' => 'boolean',
'auto_save' => 'boolean',
'auto_focus' => 'boolean',
// Properties
'properties' => 'required|array',
'properties.*.id' => 'required',
'properties.*.name' => 'required',
'properties.*.type' => 'required',
'properties.*.placeholder' => 'sometimes|nullable',
'properties.*.prefill' => 'sometimes|nullable',
'properties.*.help' => 'sometimes|nullable',
'properties.*.help_position' => ['sometimes', Rule::in(['below_input', 'above_input'])],
'properties.*.hidden' => 'boolean|nullable',
'properties.*.required' => 'boolean|nullable',
'properties.*.multiple' => 'boolean|nullable',
'properties.*.timezone' => 'sometimes|nullable',
'properties.*.width' => ['sometimes', Rule::in(['full', '1/2', '1/3', '2/3', '1/3', '3/4', '1/4'])],
'properties.*.align' => ['sometimes', Rule::in(['left', 'center', 'right', 'justify'])],
'properties.*.allowed_file_types' => 'sometimes|nullable',
'properties.*.use_toggle_switch' => 'boolean|nullable',
// Logic
'properties.*.logic' => ['array', 'nullable', new FormPropertyLogicRule()],
// Form blocks
'properties.*.content' => 'sometimes|nullable',
// Text field
'properties.*.multi_lines' => 'boolean|nullable',
'properties.*.max_char_limit' => 'integer|nullable|min:1|max:2000',
'properties.*.show_char_limit ' => 'boolean|nullable',
'properties.*.secret_input' => 'boolean|nullable',
// Date field
'properties.*.with_time' => 'boolean|nullable',
'properties.*.date_range' => 'boolean|nullable',
'properties.*.prefill_today' => 'boolean|nullable',
'properties.*.disable_past_dates' => 'boolean|nullable',
'properties.*.disable_future_dates' => 'boolean|nullable',
// Select / Multi Select field
'properties.*.allow_creation' => 'boolean|nullable',
'properties.*.without_dropdown' => 'boolean|nullable',
// Advanced Options
'properties.*.generates_uuid' => 'boolean|nullable',
'properties.*.generates_auto_increment_id' => 'boolean|nullable',
// For file (min and max)
'properties.*.max_file_size' => 'min:1|numeric',
// Security & Privacy
'can_be_indexed' => 'boolean',
'password' => 'sometimes|nullable',
'use_captcha' => 'boolean',
// Custom SEO
'seo_meta' => 'nullable|array',
'custom_domain' => 'sometimes|nullable|regex:' . CustomDomainRequest::CUSTOM_DOMAINS_REGEX,
];
}
/**
* Get the validation messages that apply to the request.
*
* @return array
*/
public function messages()
{
return [
'properties.*.name.required' => 'The form block number :position is missing a name.',
'properties.*.type.required' => 'The form block number :position is missing a type.',
'properties.*.max_char_limit.min' => 'The form block number :position max character limit must be at least 1 OR Empty',
'properties.*.max_char_limit.max' => 'The form block number :position max character limit may not be greater than 2000.',
];
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Http\Requests\Workspace;
use App\Models\Workspace;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Request;
class CustomDomainRequest extends FormRequest
{
public const CUSTOM_DOMAINS_REGEX = '/^[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,20}$/';
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(self::CUSTOM_DOMAINS_REGEX, $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

@@ -0,0 +1,29 @@
<?php
namespace App\Http\Requests\Zapier;
use App\Models\Forms\Form;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class CreateIntegrationRequest extends FormRequest
{
public function rules()
{
return [
'form_id' => [
'required',
Rule::exists(Form::getModel()->getTable(), 'id'),
],
'hookUrl' => [
'required',
'url',
],
];
}
public function form(): Form
{
return Form::findOrFail($this->input('form_id'));
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Http\Requests\Zapier;
use App\Models\Forms\Form;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class DeleteIntegrationRequest extends FormRequest
{
public function rules()
{
return [
'form_id' => [
'required',
Rule::exists(Form::getModel()->getTable(), 'id'),
],
'hookUrl' => [
'required',
'url',
],
];
}
public function form(): Form
{
return Form::findOrFail($this->input('form_id'));
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Http\Requests\Zapier;
use App\Models\Workspace;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class ListFormsRequest extends FormRequest
{
public function rules()
{
return [
'workspace_id' => [
'required',
Rule::exists(Workspace::getModel()->getTable(), 'id'),
],
];
}
public function workspace(): Workspace
{
return Workspace::findOrFail($this->input('workspace_id'));
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
/**
* @property \App\Models\Integration\FormIntegration $resource
*/
class FormIntegrationResource extends JsonResource
{
public function toArray($request)
{
return [
...parent::toArray($request),
'provider' => OAuthProviderResource::make($this->resource->provider),
];
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class FormIntegrationsEventResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
return [
'date' => date('Y-m-d H:i', strtotime($this->created_at)),
'status' => ucfirst($this->status),
'data' => $this->data
];
}
}

View File

@@ -0,0 +1,112 @@
<?php
namespace App\Http\Resources;
use App\Http\Middleware\Form\ProtectedForm;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Auth;
class FormResource extends JsonResource
{
private array $cleanings = [];
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
if (!$this->userIsFormOwner() && ProtectedForm::isProtected($request, $this->resource)) {
return $this->getProtectedForm();
}
$ownerData = $this->userIsFormOwner() ? [
'views_count' => $this->views_count,
'submissions_count' => $this->submissions_count,
'redirect_url' => $this->redirect_url,
'database_fields_update' => $this->database_fields_update,
'cleanings' => $this->getCleanigns(),
'can_be_indexed' => $this->can_be_indexed,
'password' => $this->password,
'tags' => $this->tags,
'visibility' => $this->visibility,
'removed_properties' => $this->removed_properties,
'last_edited_human' => $this->updated_at?->diffForHumans(),
'seo_meta' => $this->seo_meta,
] : [];
return array_merge(parent::toArray($request), $ownerData, [
'is_pro' => $this->workspaceIsPro(),
'is_trialing' => $this->workspaceIsTrialing(),
'workspace_id' => $this->workspace_id,
'workspace' => new WorkspaceResource($this->getWorkspace()),
'is_closed' => $this->is_closed,
'is_password_protected' => false,
'has_password' => $this->has_password,
'max_number_of_submissions_reached' => $this->max_number_of_submissions_reached,
'form_pending_submission_key' => $this->form_pending_submission_key,
'max_file_size' => $this->max_file_size / 1000000,
]);
}
public function setCleanings(array $cleanings)
{
$this->cleanings = $cleanings;
return $this;
}
private function getProtectedForm()
{
return [
'id' => $this->id,
'title' => $this->title,
'description' => $this->description,
'slug' => $this->slug,
'custom_code' => $this->custom_code,
'dark_mode' => $this->dark_mode,
'transparent_background' => $this->transparent_background,
'color' => $this->color,
'theme' => $this->theme,
'is_password_protected' => true,
'has_password' => $this->has_password,
'width' => 'centered',
'no_branding' => $this->no_branding,
'properties' => [],
'logo_picture' => $this->logo_picture,
'seo_meta' => $this->seo_meta,
'cover_picture' => $this->cover_picture,
];
}
private function getWorkspace()
{
return $this->extra?->loadedWorkspace ?? $this->workspace;
}
private function workspaceIsPro()
{
return $this->extra?->workspaceIsPro ?? $this->getWorkspace()->is_pro ?? $this->is_pro;
}
private function workspaceIsTrialing()
{
return $this->getWorkspace()->is_trialing;
}
private function userIsFormOwner()
{
return $this->extra?->userIsOwner ??
(
Auth::check() && Auth::user()->ownsForm($this->resource)
);
}
private function getCleanigns()
{
return $this->extra?->cleanings ?? $this->cleanings;
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class FormSubmissionResource extends JsonResource
{
public bool $publiclyAccessed = false;
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
$this->generateFileLinks();
if (!$this->publiclyAccessed) {
$this->addExtraData();
}
return array_merge([
'data' => $this->data
], ($this->publiclyAccessed) ? [] : [
'form_id' => $this->form_id,
'id' => $this->id
]);
}
public function publiclyAccessed($publiclyAccessed = true)
{
$this->publiclyAccessed = $publiclyAccessed;
return $this;
}
private function addExtraData()
{
$this->data = array_merge($this->data, [
'created_at' => $this->created_at->toDateTimeString(),
'id' => $this->id,
]);
}
/**
* Link to the file (generating signed s3 URL)
*
* @return void
*/
private function generateFileLinks()
{
$data = $this->data;
$formFields = collect($this->form->properties)->concat(collect($this->form->removed_properties));
$fileFields = $formFields->filter(function ($field) {
return in_array($field['type'], ['files', 'signature']);
});
foreach ($fileFields as $field) {
if (isset($data[$field['id']]) && !empty($data[$field['id']])) {
$data[$field['id']] = collect($data[$field['id']])->filter(function ($file) {
return $file !== null && $file;
})->map(function ($file) {
return [
'file_url' => \URL::signedRoute(
'open.forms.submissions.file',
[$this->form_id, $file],
now()->addMinutes(10)
),
'file_name' => $file,
];
});
}
}
$this->data = $data;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class FormTemplateResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
return array_merge(parent::toArray($request), [
'is_new' => $this->created_at->isAfter(now()->subDays(7)),
]);
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Auth;
/**
* @property \App\Models\OAuthProvider $resource
*/
class OAuthProviderResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
$userId = Auth::id();
$intention = cache()->get("oauth-intention:{$userId}");
return [
'id' => $this->resource->id,
'provider' => $this->resource->provider,
'name' => $this->resource->name,
'email' => $this->resource->email,
'intention' => $intention,
'user' => $this->whenLoaded(
'user',
fn () => OAuthProviderUserResource::make($this->resource->user),
null,
),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
/**
* @property \App\Models\User $resource
*/
class OAuthProviderUserResource extends JsonResource
{
public function toArray($request)
{
return [
'name' => $this->resource->name,
'email' => $this->resource->email,
];
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
/**
* @property \Laravel\Sanctum\PersonalAccessToken $resource
*/
class TokenResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->resource->id,
'name' => $this->resource->name,
'abilities' => $this->resource->abilities,
];
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
$personalData = \Auth::id() === $this->id ? [
'is_subscribed' => $this->is_subscribed,
'has_enterprise_subscription' => $this->has_enterprise_subscription,
'admin' => $this->admin,
'moderator' => $this->moderator,
'template_editor' => $this->template_editor,
'has_customer_id' => $this->has_customer_id,
'has_forms' => $this->has_forms,
'active_license' => $this->licenses()->active()->first(),
] : [];
return array_merge(parent::toArray($request), $personalData);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class WorkspaceResource extends JsonResource
{
public static $wrap = null;
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return array_merge(parent::toArray($request), [
'max_file_size' => $this->max_file_size / 1000000,
]);
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Http\Resources\Zapier;
use Illuminate\Http\Resources\Json\JsonResource;
/**
* @property \App\Models\Forms\Form $resource
*/
class FormResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->resource->id,
'name' => $this->resource->title,
'label' => $this->resource->title . ' (' . $this->resource->slug . ')'
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Http\Resources\Zapier;
use Illuminate\Http\Resources\Json\JsonResource;
/**
* @property \App\Models\Workspace $resource
*/
class WorkspaceResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->resource->id,
'name' => $this->resource->name,
];
}
}