Separated laravel app to its own folder (#540)
This commit is contained in:
204
api/app/Http/Controllers/Admin/AdminController.php
Normal file
204
api/app/Http/Controllers/Admin/AdminController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
103
api/app/Http/Controllers/Admin/BillingController.php
Normal file
103
api/app/Http/Controllers/Admin/BillingController.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
38
api/app/Http/Controllers/Admin/FormController.php
Normal file
38
api/app/Http/Controllers/Admin/FormController.php
Normal 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']);
|
||||
}
|
||||
}
|
||||
46
api/app/Http/Controllers/Admin/ImpersonationController.php
Normal file
46
api/app/Http/Controllers/Admin/ImpersonationController.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
120
api/app/Http/Controllers/Auth/AppSumoAuthController.php
Normal file
120
api/app/Http/Controllers/Auth/AppSumoAuthController.php
Normal 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;
|
||||
}
|
||||
}
|
||||
44
api/app/Http/Controllers/Auth/ForgotPasswordController.php
Normal file
44
api/app/Http/Controllers/Auth/ForgotPasswordController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
109
api/app/Http/Controllers/Auth/LoginController.php
Normal file
109
api/app/Http/Controllers/Auth/LoginController.php
Normal 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();
|
||||
}
|
||||
}
|
||||
146
api/app/Http/Controllers/Auth/OAuthController.php
Normal file
146
api/app/Http/Controllers/Auth/OAuthController.php
Normal 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;
|
||||
}
|
||||
}
|
||||
139
api/app/Http/Controllers/Auth/RegisterController.php
Normal file
139
api/app/Http/Controllers/Auth/RegisterController.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
44
api/app/Http/Controllers/Auth/ResetPasswordController.php
Normal file
44
api/app/Http/Controllers/Auth/ResetPasswordController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
34
api/app/Http/Controllers/Auth/UserController.php
Normal file
34
api/app/Http/Controllers/Auth/UserController.php
Normal 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.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
80
api/app/Http/Controllers/Auth/VerificationController.php
Normal file
80
api/app/Http/Controllers/Auth/VerificationController.php
Normal 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')]);
|
||||
}
|
||||
}
|
||||
51
api/app/Http/Controllers/CaddyController.php
Normal file
51
api/app/Http/Controllers/CaddyController.php
Normal 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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
22
api/app/Http/Controllers/Content/ChangelogController.php
Normal file
22
api/app/Http/Controllers/Content/ChangelogController.php
Normal 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');
|
||||
});
|
||||
}
|
||||
}
|
||||
28
api/app/Http/Controllers/Content/FileUploadController.php
Normal file
28
api/app/Http/Controllers/Content/FileUploadController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
29
api/app/Http/Controllers/Controller.php
Normal file
29
api/app/Http/Controllers/Controller.php
Normal 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);
|
||||
}
|
||||
}
|
||||
27
api/app/Http/Controllers/FontsController.php
Normal file
27
api/app/Http/Controllers/FontsController.php
Normal 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 [];
|
||||
});
|
||||
}
|
||||
}
|
||||
34
api/app/Http/Controllers/Forms/AiFormController.php
Normal file
34
api/app/Http/Controllers/Forms/AiFormController.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
274
api/app/Http/Controllers/Forms/FormController.php
Normal file
274
api/app/Http/Controllers/Forms/FormController.php
Normal 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.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
38
api/app/Http/Controllers/Forms/FormStatsController.php
Normal file
38
api/app/Http/Controllers/Forms/FormStatsController.php
Normal 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;
|
||||
}
|
||||
}
|
||||
96
api/app/Http/Controllers/Forms/FormSubmissionController.php
Normal file
96
api/app/Http/Controllers/Forms/FormSubmissionController.php
Normal 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())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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.'
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
133
api/app/Http/Controllers/Forms/PublicFormController.php
Normal file
133
api/app/Http/Controllers/Forms/PublicFormController.php
Normal 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));
|
||||
}
|
||||
}
|
||||
23
api/app/Http/Controllers/Forms/RecordController.php
Normal file
23
api/app/Http/Controllers/Forms/RecordController.php
Normal 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.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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)];
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
27
api/app/Http/Controllers/Settings/PasswordController.php
Normal file
27
api/app/Http/Controllers/Settings/PasswordController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
70
api/app/Http/Controllers/Settings/ProfileController.php
Normal file
70
api/app/Http/Controllers/Settings/ProfileController.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
43
api/app/Http/Controllers/Settings/TokenController.php
Normal file
43
api/app/Http/Controllers/Settings/TokenController.php
Normal 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();
|
||||
}
|
||||
}
|
||||
30
api/app/Http/Controllers/SitemapController.php
Normal file
30
api/app/Http/Controllers/SitemapController.php
Normal 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;
|
||||
}
|
||||
}
|
||||
101
api/app/Http/Controllers/SubscriptionController.php
Normal file
101
api/app/Http/Controllers/SubscriptionController.php
Normal 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')),
|
||||
]);
|
||||
}
|
||||
}
|
||||
89
api/app/Http/Controllers/TemplateController.php
Normal file
89
api/app/Http/Controllers/TemplateController.php
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
60
api/app/Http/Controllers/UserInviteController.php
Normal file
60
api/app/Http/Controllers/UserInviteController.php
Normal 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.']);
|
||||
}
|
||||
}
|
||||
112
api/app/Http/Controllers/Webhook/AppSumoController.php
Normal file
112
api/app/Http/Controllers/Webhook/AppSumoController.php
Normal 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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
136
api/app/Http/Controllers/Webhook/StripeController.php
Normal file
136
api/app/Http/Controllers/Webhook/StripeController.php
Normal 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';
|
||||
}
|
||||
}
|
||||
74
api/app/Http/Controllers/WorkspaceController.php
Normal file
74
api/app/Http/Controllers/WorkspaceController.php
Normal 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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
127
api/app/Http/Controllers/WorkspaceUserController.php
Normal file
127
api/app/Http/Controllers/WorkspaceUserController.php
Normal 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.'
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user