Docker compose setup (#513)

* fix password reset bug

* self hosted mode middleware changes on  pages

* fix lint

* wip: self hosted changes

* wip: self hosted frontend changes

* wip self hosted mode changes

* typo correction

* remove commented logic

* fix env variable names

* fix lint issues

* fix minor updates

* #445 Switched from single monolithic docker image to a docker-compose
 orchestrated network of services

* Automatically configures shared secret

* Working through some issues

* Use local file storage

* Moved the dockerfiles

* Fixed some issues when building from clean

* Corrected workflow

* Hopefully schedules everything correctly now

* Prep storage for worker process as well

* .env files are required

* Pinned dependency versions

* Disable self hosted in the client as well

* Removed double defaulting logic

* Using regexs is more succinct

* Added FRONT_URL environment variable

* Merge 236e4-self-hosted-mode-changes

* Improve inital user setup

* Finalized the new docker-compose setup

* Fix back-end formatting issues

---------

Co-authored-by: Frank <csskfaves@gmail.com>
Co-authored-by: Don Benjamin <don@webhammer.co.uk>
This commit is contained in:
Julien Nahum 2024-08-05 12:06:20 +02:00 committed by GitHub
parent 6b13f95322
commit 3280e38ee1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
49 changed files with 6152 additions and 3431 deletions

View File

@ -1,3 +1,4 @@
/.git
/Dockerfile
/data
\.env

View File

@ -2,21 +2,12 @@ APP_NAME="OpnForm"
APP_ENV=local
APP_KEY=
APP_DEBUG=false
APP_LOG_LEVEL=debug
APP_URL=http://localhost
LOG_CHANNEL=errorlog
LOG_LEVEL=debug
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=postgres
DB_USERNAME=postgres
DB_PASSWORD=postgres
FILESYSTEM_DRIVER=s3
FILESYSTEM_DISK=s3
FILESYSTEM_DRIVER=local
BROADCAST_CONNECTION=log
CACHE_STORE=redis
@ -24,10 +15,6 @@ QUEUE_CONNECTION=redis
SESSION_DRIVER=file
SESSION_LIFETIME=120
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=log
MAIL_HOST=
MAIL_PORT=
@ -57,3 +44,4 @@ MUX_WORKSPACE_ID=
MUX_API_TOKEN=
OPEN_AI_API_KEY=
SELF_HOSTED=true

View File

@ -11,16 +11,27 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Get tag name
run: ( echo "TAG_NAME=${GITHUB_REF#refs/*/v}"; echo "DOCKER_REPO=${{secrets.DOCKER_REPO}}") >> $GITHUB_ENV
run: |
(
echo "TAG_NAME=${GITHUB_REF#refs/*/v}";
echo "DOCKER_UI_REPO=${{secrets.DOCKER_UI_REPO}}"
echo "DOCKER_API_REPO=${{secrets.DOCKER_API_REPO}}"
) >> $GITHUB_ENV
- name: Check out the repo
uses: actions/checkout@v3
- name: Log in to Docker Hub
run: docker login -u "${{ secrets.DOCKER_USERNAME }}" -p "${{ secrets.DOCKER_ACCESS_TOKEN }}"
- name: Build docker image
run: docker build . -t $DOCKER_REPO:latest -t $DOCKER_REPO:$TAG_NAME
- name: Push Docker image
run: docker push $DOCKER_REPO:latest && docker push $DOCKER_REPO:$TAG_NAME
- name: Build docker api image
run: docker build -f docker/Dockerfile.api . -t $DOCKER_API_REPO:latest -t $DOCKER_API_REPO:$TAG_NAME
- name: Build docker ui image
run: docker build -f docker/Dockerfile.client . -t $DOCKER_UI_REPO:latest -t $DOCKER_UI_REPO:$TAG_NAME
- name: Push Docker api image
run: docker push $DOCKER_API_REPO:latest && docker push $DOCKER_API_REPO:$TAG_NAME
- name: Push Docker ui image
run: docker push $DOCKER_UI_REPO:latest && docker push $DOCKER_UI_REPO:$TAG_NAME

2
.gitignore vendored
View File

@ -29,5 +29,5 @@ public/.DS_Store
.env.production
.env.staging
_ide_helper.php
docker-compose.override.yml
/.make.*

View File

@ -1,98 +0,0 @@
ARG PHP_PACKAGES="php8.1 composer php8.1-common php8.1-pgsql php8.1-redis php8.1-mbstring\
php8.1-simplexml php8.1-bcmath php8.1-gd php8.1-curl php8.1-zip\
php8.1-imagick php8.1-bz2 php8.1-gmp php8.1-int php8.1-pcov php8.1-soap php8.1-xsl"
FROM node:20-alpine AS javascript-builder
WORKDIR /app
# It's best to add as few files as possible before running the build commands
# as they will be re-run everytime one of those files changes.
#
# It's possible to run npm install with only the package.json and package-lock.json file.
ADD client/package.json client/package-lock.json ./
RUN npm install
ADD client /app/
RUN cp .env.docker .env
RUN npm run build
# syntax=docker/dockerfile:1.3-labs
FROM --platform=linux/amd64 ubuntu:23.04 AS php-dependency-installer
ARG PHP_PACKAGES
RUN apt-get update \
&& apt-get install -y $PHP_PACKAGES composer
WORKDIR /app
ADD composer.json composer.lock artisan ./
# NOTE: The project would build more reliably if all php files were added before running
# composer install. This would though introduce a dependency which would cause every
# dependency to be re-installed each time any php file is edited. It may be necessary in
# future to remove this 'optimisation' by moving the `RUN composer install` line after all
# the following ADD commands.
# Running artisan requires the full php app to be installed so we need to remove the
# post-autoload command from the composer file if we want to run composer without
# adding a dependency to all the php files.
RUN sed 's_@php artisan package:discover_/bin/true_;' -i composer.json
ADD app/helpers.php /app/app/helpers.php
RUN composer install --ignore-platform-req=php
ADD app /app/app
ADD bootstrap /app/bootstrap
ADD config /app/config
ADD database /app/database
ADD public public
ADD routes routes
ADD tests tests
# Manually run the command we deleted from composer.json earlier
RUN php artisan package:discover --ansi
FROM --platform=linux/amd64 ubuntu:23.04
# supervisord is a process manager which will be responsible for managing the
# various server processes. These are configured in docker/supervisord.conf
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
WORKDIR /app
ARG PHP_PACKAGES
RUN apt-get update \
&& apt-get install -y \
supervisor nginx sudo postgresql-15 redis\
$PHP_PACKAGES php8.1-fpm wget\
&& apt-get clean
RUN useradd nuxt && mkdir ~nuxt && chown nuxt ~nuxt
RUN wget -qO- https://raw.githubusercontent.com/creationix/nvm/v0.39.3/install.sh | sudo -u nuxt bash
RUN sudo -u nuxt bash -c ". ~nuxt/.nvm/nvm.sh && nvm install --no-progress 20"
ADD docker/postgres-wrapper.sh docker/php-fpm-wrapper.sh docker/redis-wrapper.sh docker/nuxt-wrapper.sh docker/generate-api-secret.sh /usr/local/bin/
ADD docker/php-fpm.conf /etc/php/8.1/fpm/pool.d/
ADD docker/nginx.conf /etc/nginx/sites-enabled/default
ADD docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
ADD . .
ADD .env.docker .env
ADD client/.env.docker client/.env
COPY --from=javascript-builder /app/.output/ ./nuxt/
RUN cp -r nuxt/public .
COPY --from=php-dependency-installer /app/vendor/ ./vendor/
RUN chmod a+x /usr/local/bin/*.sh /app/artisan \
&& ln -s /app/artisan /usr/local/bin/artisan \
&& useradd opnform \
&& echo "daemon off;" >> /etc/nginx/nginx.conf\
&& echo "daemonize no" >> /etc/redis/redis.conf\
&& echo "appendonly yes" >> /etc/redis/redis.conf\
&& echo "dir /persist/redis/data" >> /etc/redis/redis.conf
EXPOSE 80

130
README.md
View File

@ -9,8 +9,8 @@
<a href="https://github.com/JhumanJ/OpnForm/stargazers"><img src="https://img.shields.io/github/stars/JhumanJ/OpnForm" alt="Github Stars"></a>
</a>
<a href="https://github.com/JhumanJ/OpnForm/pulse"><img src="https://img.shields.io/github/commit-activity/m/JhumanJ/OpnForm" alt="Commits per month"></a>
<a href="https://hub.docker.com/r/jhumanj/opnform">
<img src="https://img.shields.io/docker/pulls/jhumanj/opnform">
<a href="https://hub.docker.com/r/jhumanj/opnform-api">
<img src="https://img.shields.io/docker/pulls/jhumanj/opnform-api">
</a>
<a href="https://github.com/JhumanJ/OpnForm/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPLv3-purple" alt="License">
<a href="https://github.com/JhumanJ/OpnForm/issues/new"><img src="https://img.shields.io/badge/Report a bug-Github-%231F80C0" alt="Report a bug"></a>
@ -66,94 +66,90 @@ It takes 1 minute to try out the builder for free. You'll have high availability
## Installation
### Docker Installation 🐳
### Docker installation 🐳
OpnForm can be easily set up using Docker. Pre-built images are available on Docker Hub, which is the recommended method for most users.
This can be built and run locally but is also hosted publicly on docker hub at `jhumanj/opnform` and is generally best run directly from there.
#### Prerequisites
- Docker
- Docker Compose
#### Running from docker hub
#### Quick Start
```
docker run --name opnform -v $PWD/my-opnform-data:/persist -p 80:80 jhumanj/opnform
```
1. Clone the repository:
```
git clone https://github.com/JhumanJ/OpnForm.git
cd OpnForm
```
You should now be able to access the application by visiting http://localhost in a web browser.
2. Set up environment files:
```
cp .env.docker .env
cp client/.env.docker client/.env
```
> 👀 **Server Deployment**: If you are deploying OpnForm on a server (not locally), then you will [need to use 2 .env files](https://github.com/JhumanJ/opnform?tab=readme-ov-file#using-custom-env-files) to configure the app URLs (`APP_URL` in `.env` and both `NUXT_PUBLIC_APP_URL` & `NUXT_PUBLIC_API_BASE` in `client/.env`).
3. Start the application:
```
docker-compose up -d
```
4. Access OpnForm at http://localhost
The `-v` argument creates a local directory called `my-opnform-data` which will store your database and files so that your work is not lost when you restart the container.
> 🌐 **Server Deployment Note**: When deploying to a server, configure the app URLs in both `.env` and `client/.env` files. Set `APP_URL` in `.env`, and both `NUXT_PUBLIC_APP_URL` & `NUXT_PUBLIC_API_BASE` in `client/.env`.
The `--name` argument names the running container so that you can refer back to it later, with e.g. `docker stop opnform`. You can use any name you'd like.
#### Customization
- **Environment Variables**: Modify `.env` and `client/.env` files to customize your setup. For example, to enable email features, configure a [supported mail driver](https://laravel.com/docs/11.x/mail) in the `.env` file.
#### Using custom .env files
#### Upgrading
If you have custom env file you can use them like so:
1. Check the upgrade instructions for your target version in the documentation.
2. Update your `docker-compose.yml` file if necessary.
3. Apply changes:
```
docker-compose up -d
```
Custom Laravel .env file:
```
docker run --name opnform -v $PWD/custom-laravel-env-file.env:/app/.env -v $PWD/my-opnform-data:/persist -p 80:80 jhumanj/opnform
```
### Initial Login
Custom Nuxt .env file:
```
docker run --name opnform -v $PWD/custom-nuxt-env-file.env:/app/client/.env -v $PWD/my-opnform-data:/persist -p 80:80 jhumanj/opnform
```
After installation, use these credentials to access the admin panel:
- Email: `admin@opnform.com`
- Password: `password`
This would load load in the env file located at `my-custom-env-file.env`, note that if you are creating a .env file for use like this it's best to start from the `.env.docker` example file as there are slightly different defaults for the dockerized setup.
⚠️ Change these credentials immediately after your first login.
#### Using a custom HTTP port
Note: Public registration is disabled in the self-hosted version. Use the admin account to invite additional users.
To run on port 8080
### Building from Source
```
docker run --name opnform -v $PWD/my-opnform-data:/persist -p 8080:80 jhumanj/opnform
```
For development or customization, you can build the Docker images locally:
#### Building a custom docker image
1. Build the images:
```
docker build -t opnform-ui:local -f docker/Dockerfile.client .
docker build -t opnform-api:local -f docker/Dockerfile.api .
```
To build a custom docker image from your local source code use this command from the root of the source repository:
2. Create a docker-compose override file:
```
cp docker-compose.override.yml.example docker-compose.override.yml
```
```
docker build . -t my-docker-image-name
```
Edit the `docker-compose.override.yml` file to use your locally built images:
```yaml
services:
api:
image: opnform-api:local
ui:
image: opnform-ui:local
```
This should create a new docker image tagged `my-docker-image-name` which can be run as follows:
```
docker run --name opnform -v $PWD/my-opnform-data:/persist -p 80:80 my-docker-image-name
```
#### Upgrading docker installations
**Please consult the upgrade instructions for the latest opnform version**, e.g. if upgrading from v1 to v2 please check the v2 instructions as the process may change in future releases.
Normal upgrade procedure would be to stop the running container, back up your data directory (you will need this backup if you want to rollback to the old version) and then start a container running the new image with the same arguments.
e.g. if you're running from a specific opnform version with
```docker run --name opnform -v $PWD/my-opnform-data:/persist -p 80:80 jhumanj/opnform:1.0.0```
You could run:
```
# stop the running container
docker stop opnform
# backup the data directory
cp -r my-opnform-data my-opnform-backup
# start the new container
docker run --name opnform-2 -v $PWD/my-opnform-data:/persist -p 80:80 jhumanj/opnform:2.0.0
```
Then if everything is running smoothly you can delete the old container with:
```
docker rm opnform
```
If you haven't specified a version e.g. if you are using the image `jhumanj/opnform` or `jhumanj/opnform:latest` you will need to run `docker pull jhumanj/opnform` or `docker pull jhumanj/opnform:latest` before starting the new container.
3. Start the application:
```
docker-compose up -d
```
This method allows you to make changes to the source code and rebuild the images as needed.
### Using Laravel Valet
This section explains how to get started locally with the project. It's most likely relevant if you're trying to work on the project.

View File

@ -0,0 +1,48 @@
<?php
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
class InitProjectCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:init-project';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Creates the default admin user';
/**
* Execute the console command.
*/
public function handle()
{
if (!config('app.self_hosted')) {
$this->error('This command can only be run in self-hosted mode.');
return;
}
// Check if there are any existing users or if the ID increment is not at 0
if (User::max('id') !== null) {
$this->error('Users already exist in the database or the User table is not empty. Aborting initialization.');
return;
}
User::create([
'name' => 'Admin',
'email' => 'admin@opnform.com',
'password' => bcrypt('password'),
]);
$this->info('Admin user created with default credentials: admin@opnform.com / password');
return 0;
}
}

View File

@ -98,7 +98,7 @@ class RegisterController extends Controller
private function checkRegistrationAllowed(array $data)
{
if (config('app.self_hosted') && !array_key_exists('invite_token', $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();
}
}

View File

@ -75,7 +75,13 @@ class PublicFormController extends Controller
]);
}
return redirect()->to(Storage::temporaryUrl($path, now()->addMinutes(5)));
$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)

View File

@ -3,7 +3,9 @@
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
{
@ -18,7 +20,7 @@ class ProfileController extends Controller
$this->validate($request, [
'name' => 'required',
'email' => 'required|email|unique:users,email,'.$user->id,
'email' => 'required|email|unique:users,email,' . $user->id,
]);
return tap($user)->update([
@ -26,4 +28,43 @@ class ProfileController extends Controller
'email' => strtolower($request->email),
]);
}
// For self-hosted mode, only admin can update their credentials
public function updateAdminCredentials(Request $request)
{
$request->validate([
'email' => 'required|email|not_in:admin@opnform.com',
'password' => 'required|min:6|confirmed|not_in:password',
], [
'email.not_in' => "Please provide email address other than 'admin@opnform.com'",
'password.not_in' => "Please another password other than 'password'."
]);
ray('in', $request->password);
$user = $request->user();
$user->update([
'email' => $request->email,
'password' => bcrypt($request->password),
]);
ray($user);
Cache::forget('initial_user_setup_complete');
Cache::forget('max_user_id');
$workspace = Workspace::create([
'name' => 'My Workspace',
'icon' => '🧪',
]);
$user->workspaces()->sync([
$workspace->id => [
'role' => 'admin',
],
], false);
return $this->success([
'message' => 'Congratulations, your account credentials have been updated successfully.',
'user' => $user,
]);
}
}

View File

@ -15,24 +15,26 @@ class TemplateController extends Controller
$limit = (int) $request->get('limit', 0);
$onlyMy = (bool) $request->get('onlymy', false);
$templates = Template::when(Auth::check(), function ($query) use ($onlyMy) {
$query = Template::query();
if (Auth::check()) {
if ($onlyMy) {
$query->where('creator_id', Auth::id());
} else {
$query->where(function ($query) {
$query->where('publicly_listed', true)
->orWhere('creator_id', Auth::id());
$query->where(function ($q) {
$q->where('publicly_listed', true)
->orWhere('creator_id', Auth::id());
});
}
})
->when(! Auth::check(), function ($query) {
$query->where('publicly_listed', true);
})
->when($limit > 0, function ($query) use ($limit) {
$query->limit($limit);
})
->orderByDesc('created_at')
->get();
} else {
$query->where('publicly_listed', true);
}
if ($limit > 0) {
$query->limit($limit);
}
$templates = $query->orderByDesc('created_at')->get();
return FormTemplateResource::collection($templates);
}

View File

@ -10,6 +10,7 @@ use App\Http\Middleware\IsAdmin;
use App\Http\Middleware\IsModerator;
use App\Http\Middleware\IsNotSubscribed;
use App\Http\Middleware\IsSubscribed;
use App\Http\Middleware\SelfHostedCredentialsMiddleware;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
@ -60,6 +61,7 @@ class Kernel extends HttpKernel
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Session\Middleware\StartSession::class,
SelfHostedCredentialsMiddleware::class,
ImpersonationMiddleware::class,
],
];

View File

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

2
client/.dockerignore Normal file
View File

@ -0,0 +1,2 @@
/Dockerfile
/.dockerignore

View File

@ -32,7 +32,7 @@
>
Templates
</NuxtLink>
<template v-if="featureBaseEnabled">
<template v-if="appStore.featureBaseEnabled">
<button
v-if="user"
:class="navLinkClasses"
@ -54,7 +54,7 @@
</a>
</template>
<NuxtLink
v-if="$route.name !== 'ai-form-builder' && user === null"
v-if="($route.name !== 'ai-form-builder' && user === null) && (!appStore.selfHosted || appStore.aiFeaturesEnabled)"
:to="{ name: 'ai-form-builder' }"
:class="navLinkClasses"
class="hidden lg:inline"
@ -63,9 +63,9 @@
</NuxtLink>
<NuxtLink
v-if="
paidPlansEnabled &&
(appStore.paidPlansEnabled &&
(user === null || (user && workspace && !workspace.is_pro)) &&
$route.name !== 'pricing'
$route.name !== 'pricing') && !appStore.selfHosted
"
:to="{ name: 'pricing' }"
:class="navLinkClasses"
@ -248,6 +248,7 @@
</NuxtLink>
<v-button
v-if="!appStore.selfHosted"
v-track.nav_create_form_click
size="small"
class="shrink-0"
@ -313,12 +314,6 @@ export default {
workspace() {
return this.workspacesStore.getCurrent
},
paidPlansEnabled() {
return this.config.public.paidPlansEnabled
},
featureBaseEnabled() {
return this.config.public.featureBaseOrganization !== null
},
showAuth() {
return this.$route.name && this.$route.name !== "forms-slug"
},
@ -340,14 +335,8 @@ export default {
userOnboarded() {
return this.user && this.user.has_forms === true
},
hasCrisp() {
return (
this.config.public.crispWebsiteId &&
this.config.public.crispWebsiteId !== ""
)
},
hasNewChanges() {
if (import.meta.server || !window.Featurebase) return false
if (import.meta.server || !window.Featurebase || !this.appStore.featureBaseEnabled) return false
return window.Featurebase("unviewed_changelog_count") > 0
},
},

View File

@ -22,6 +22,7 @@
<div class="flex justify-center mt-5 md:mt-0">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-x-4 gap-y-2">
<router-link
v-if="!appStore.selfHosted"
:to="{ name: 'privacy-policy' }"
class="text-gray-600 dark:text-gray-400 transition-colors duration-300 hover:text-nt-blue"
>
@ -29,6 +30,7 @@
</router-link>
<router-link
v-if="!appStore.selfHosted"
:to="{ name: 'terms-conditions' }"
class="text-gray-600 dark:text-gray-400 transition-colors duration-300 hover:text-nt-blue"
>
@ -70,6 +72,7 @@ export default {
const authStore = useAuthStore()
return {
user: computed(() => authStore.user),
appStore: useAppStore(),
opnformConfig,
}
},

View File

@ -58,7 +58,10 @@
Log in to continue
</v-button>
<p class="text-gray-500 mt-4">
<p
v-if="!appStore.selfHosted"
class="text-gray-500 mt-4"
>
Don't have an account?
<a
v-if="isQuick"
@ -99,6 +102,7 @@ export default {
emits: ['afterQuickLogin', 'openRegister'],
setup() {
return {
appStore: useAppStore(),
authStore: useAuthStore(),
formsStore: useFormsStore(),
workspaceStore: useWorkspacesStore(),
@ -139,7 +143,11 @@ export default {
this.redirect()
})
.catch((error) => {
console.error(error)
if (error.response?._data?.message == "You must change your credentials when in self host mode") {
// this.showForgotModal = true
this.redirect()
}
})
.finally(() => {
this.loading = false

View File

@ -1,8 +1,6 @@
export default defineNuxtRouteMiddleware(() => {
const authStore = useAuthStore()
if (authStore.check) {
console.log("redirecting to home")
return navigateTo({ name: "home" })
}
})

View File

@ -0,0 +1,9 @@
export default defineNuxtRouteMiddleware(() => {
const authStore = useAuthStore()
const runtimeConfig = useRuntimeConfig()
if (runtimeConfig.public?.selfHosted) {
if (authStore.check && authStore.user?.email === 'admin@opnform.com') {
return navigateTo({ name: "update-credentials" })
}
}
})

14
client/middleware/self-hosted.js vendored Normal file
View File

@ -0,0 +1,14 @@
export default defineNuxtRouteMiddleware((from, to, next) => {
const runtimeConfig = useRuntimeConfig()
const route = useRoute()
if (runtimeConfig.public?.selfHosted) {
console.log('in')
if (from.name === 'register' && route.query?.email && route.query?.invite_token) {
return
}
if (from.name === 'ai-form-builder' && runtimeConfig.public?.aiFeaturesEnabled) {
return
}
return navigateTo({ name: "index" })
}
})

View File

@ -5,8 +5,7 @@ export default {
githubAuth: null,
notion: { worker: "https://notion-forms-worker.notionforms.workers.dev/v1" },
links: {
help_url: "https://github.com/JhumanJ/OpnForm/discussions",
helpdesk_sitemap_url: "https://notionforms.crisp.help/sitemap.xml",
help_url: "https://help.opnform.com",
github_url: "https://github.com/JhumanJ/OpnForm",
github_forum_url: "https://github.com/JhumanJ/OpnForm/discussions",
discord: "https://discord.gg/YTSjU2a9TS",

7185
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,7 @@
"scripts": {
"build": "nuxt build",
"dev": "export NODE_TLS_REJECT_UNAUTHORIZED=0; nuxt dev",
"docker-dev": "export NODE_TLS_REJECT_UNAUTHORIZED=0; nuxt dev --host 0.0.0.0",
"generate": "export NODE_TLS_REJECT_UNAUTHORIZED=0; nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
@ -48,6 +49,7 @@
"crisp-sdk-web": "^1.0.21",
"date-fns": "^2.30.0",
"debounce": "^1.2.1",
"esbuild": "^0.23.0",
"fuse.js": "^6.4.6",
"js-sha256": "^0.10.0",
"libphonenumber-js": "^1.10.44",

View File

@ -627,11 +627,10 @@
</template>
<script setup>
import { computed } from "vue"
import { useAuthStore } from "../stores/auth"
const authStore = useAuthStore()
definePageMeta({
middleware: ["self-hosted",]
})
useOpnSeoMeta({
title: "Free AI form builder",
description:

View File

@ -72,7 +72,6 @@
</template>
<script setup>
import {computed} from 'vue'
import OpenCompleteForm from "~/components/open/forms/OpenCompleteForm.vue"
import sha256 from 'js-sha256'
import { onBeforeRouteLeave } from 'vue-router'

View File

@ -58,7 +58,7 @@ useOpnSeoMeta({
title: "Create a new Form for free",
})
definePageMeta({
middleware: "guest",
middleware: ["guest", "self-hosted"],
})
// Data

View File

@ -237,7 +237,7 @@ import ExtraMenu from "../components/pages/forms/show/ExtraMenu.vue"
import {refDebounced} from "@vueuse/core"
definePageMeta({
middleware: "auth",
middleware: ["auth", "self-hosted-credentials"],
})
useOpnSeoMeta({

View File

@ -416,13 +416,7 @@ export default {
definePageMeta({
middleware: [
function () {
// Custom inline middleware
if (!useRuntimeConfig().public.paidPlansEnabled) {
// If no paid plan so no need this page
return navigateTo("/", {redirectCode: 301})
}
},
"self-hosted"
],
})

View File

@ -22,6 +22,11 @@ import { computed } from "vue"
useOpnSeoMeta({
title: "Privacy Policy",
})
definePageMeta({
middleware: ["self-hosted"]
})
defineRouteRules({
swr: 3600,
})

View File

@ -11,7 +11,7 @@
Create an account
</h2>
<small>Sign up in less than 2 minutes.</small>
<template v-if="!isSelfHosted || isInvited">
<template v-if="!appStore.selfHosted || isInvited">
<register-form />
</template>
<div
@ -106,22 +106,21 @@ export default {
})
definePageMeta({
middleware: "guest",
middleware: ["self-hosted", "guest"]
})
defineRouteRules({
swr: 3600,
})
return {
appStore: useAppStore(),
}
},
data: () => ({}),
computed: {
isSelfHosted(){
return useRuntimeConfig().public.selfHosted
},
isInvited(){
isInvited() {
return this.$route.query?.email && this.$route.query?.invite_token
}
},

View File

@ -22,6 +22,11 @@ import { computed } from "vue"
useOpnSeoMeta({
title: "Terms & Conditions",
})
definePageMeta({
middleware: ["self-hosted"]
})
defineRouteRules({
swr: 3600,
})

View File

@ -0,0 +1,96 @@
<template>
<modal :show="showModal" @close="logout" max-width="lg">
<div class="">
<h2 class="font-medium text-3xl mb-3">Welcome to OpnForm!</h2>
<p class="text-sm text-gray-600">
You're using the self-hosted version of OpnForm and need to set up your account.
Please enter your email and create a password to continue.
</p>
</div>
<form
class="mt-4"
@submit.prevent="updateCredentials"
@keydown="form.onKeydown($event)"
>
<!-- Email -->
<text-input
name="email"
:form="form"
label="Email"
:required="true"
placeholder="Your email address"
/>
<!-- Password -->
<text-input
native-type="password"
placeholder="Your password"
name="password"
:form="form"
label="Password"
:required="true"
/>
<!-- Password Confirmation-->
<text-input
native-type="password"
:form="form"
:required="true"
placeholder="Enter confirm password"
name="password_confirmation"
label="Confirm Password"
/>
<!-- Submit Button -->
<v-button class="mx-auto" :loading="form.busy || loading">
Update Credentials
</v-button>
</form>
</modal>
</template>
<script setup>
import { onMounted } from "vue";
const authStore = useAuthStore();
const workspacesStore = useWorkspacesStore();
const formsStore = useFormsStore();
const user = computed(() => authStore.user);
const router = useRouter();
const showModal = ref(true);
const form = useForm({
name: "",
email: "",
password: "",
password_confirmation: "",
agree_terms: false,
appsumo_license: null,
});
onMounted(() => {
form.email = user?.value?.email;
});
const updateCredentials = () => {
form
.post("update-credentials")
.then(async (data) => {
authStore.setUser(data.user);
const workspaces = await fetchAllWorkspaces();
workspacesStore.set(workspaces.data.value);
formsStore.loadAll(workspacesStore.currentId);
router.push({ name: "home" });
})
.catch((error) => {
console.error(error);
useAlert().error(error.response._data.message);
});
};
const logout = () => {
authStore.logout();
showModal.value = false;
router.push({ name: "login" });
};
</script>

View File

@ -20,6 +20,13 @@ export const useAppStore = defineStore("app", {
_cut: null,
},
}),
getters: {
paidPlansEnabled: () => useRuntimeConfig().public.paidPlansEnabled,
featureBaseEnabled: () => useRuntimeConfig().public.featureBaseOrganization !== null,
selfHosted: () => useRuntimeConfig().public.selfHosted,
aiFeaturesEnabled: () => useRuntimeConfig().public.aiFeaturesEnabled,
crispEnabled: () => useRuntimeConfig().public.crispWebsiteId !== null && useRuntimeConfig().public.crispWebsiteId !== '',
},
actions: {
hideNavbar() {
this.navbarHidden = true

1172
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -46,6 +46,8 @@ return [
'local' => [
'driver' => 'local',
'root' => storage_path('app'),
'visibility' => env('LOCAL_FILESYSTEM_VISIBILITY', 'private'),
'directory_visibility' => env('LOCAL_FILESYSTEM_VISIBILITY', 'private'),
],
'public' => [
@ -64,6 +66,7 @@ return [
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', true),
'temporary_url_rewrites' => json_decode(env('AWS_TEMPORARY_URL_REWRITES', '{}'), true),
],
],

68
docker-compose.yml Normal file
View File

@ -0,0 +1,68 @@
---
services:
api: &api
image: jhumanj/opnform-api:latest
environment:
DB_HOST: db
REDIS_HOST: redis
DB_DATABASE: ${DB_DATABASE:-forge}
DB_USERNAME: ${DB_USERNAME:-forge}
DB_PASSWORD: ${DB_PASSWORD:-forge}
DB_CONNECTION: ${DB_CONNECTION:-pgsql}
LOG_LEVEL: ${LOG_LEVEL:-debug}
LOG_CHANNEL: ${LOG_CHANNEL:-errorlog}
AWS_ENDPOINT: http://minio:9000
AWS_ACCESS_KEY_ID: ${MINIO_ACCESS_KEY:-minio}
AWS_SECRET_ACCESS_KEY: ${MINIO_SECRET_KEY:-minio123}
FILESYSTEM_DISK: local
AWS_REGION: eu-west-1
AWS_BUCKET: laravel-bucket
LOCAL_FILESYSTEM_VISIBILITY: public
FRONT_URL: ${FRONT_URL:-http://localhost}
env_file: ./.env
volumes:
- laravel-persist:/persist
- secrets-config:/secrets
api-worker:
<<: *api
command: ./artisan queue:work
ui:
image: jhumanj/opnform-ui:latest
environment:
NUXT_PUBLIC_APP_URL: ${NUXT_PUBLIC_APP_URL:-/}
NUXT_PUBLIC_API_BASE: ${NUXT_PUBLIC_API_BASE:-/api}
NUXT_PRIVATE_API_BASE: http://ingress/api
env_file:
- ./client/.env
volumes:
- secrets-config:/secrets
- ./client/.env:/app/.env
redis:
image: redis:7
db:
image: postgres:16
environment:
POSTGRES_DB: ${DB_DATABASE:-forge}
POSTGRES_USER: ${DB_USERNAME:-forge}
POSTGRES_PASSWORD: ${DB_PASSWORD:-forge}
volumes:
- postgres-data:/var/lib/postgresql/data
ingress:
image: nginx:1
volumes:
- ./docker/nginx.conf:/etc/nginx/templates/default.conf.template
ports:
- 80:80
volumes:
laravel-persist:
postgres-data:
secrets-config:

49
docker/Dockerfile.api Normal file
View File

@ -0,0 +1,49 @@
FROM php:8.3-fpm
# syntax=docker/dockerfile:1.3-labs
RUN apt-get update && apt-get install -y libzip-dev libpng-dev postgresql-client libpq-dev && apt-get clean
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
ENV COMPOSER_ALLOW_SUPERUSER=1
RUN docker-php-ext-install pdo pgsql pdo_pgsql gd bcmath zip && pecl install redis && docker-php-ext-enable redis
WORKDIR /usr/share/nginx/html/
ADD composer.json composer.lock artisan ./
# NOTE: The project would build more reliably if all php files were added before running
# composer install. This would though introduce a dependency which would cause every
# dependency to be re-installed each time any php file is edited. It may be necessary in
# future to remove this 'optimisation' by moving the `RUN composer install` line after all
# the following ADD commands.
# Running artisan requires the full php app to be installed so we need to remove the
# post-autoload command from the composer file if we want to run composer without
# adding a dependency to all the php files.
RUN sed 's_@php artisan package:discover_/bin/true_;' -i composer.json
ADD app/helpers.php app/helpers.php
RUN composer install --ignore-platform-req=php
ADD app ./app
ADD bootstrap ./bootstrap
ADD config ./config
ADD database ./database
ADD public public
ADD routes routes
ADD tests tests
ADD resources resources
ADD storage ./storage
RUN chmod 777 -R storage
# Manually run the command we deleted from composer.json earlier
RUN php artisan package:discover --ansi
COPY docker/php-fpm-entrypoint /usr/local/bin/opnform-entrypoint
COPY docker/generate-api-secret.sh /usr/local/bin/
RUN ln -s /secrets/api.env .env
RUN chmod a+x /usr/local/bin/*
ENTRYPOINT [ "/usr/local/bin/opnform-entrypoint" ]
CMD php-fpm

34
docker/Dockerfile.client Normal file
View File

@ -0,0 +1,34 @@
FROM node:20-alpine AS javascript-builder
WORKDIR /app
# It's best to add as few files as possible before running the build commands
# as they will be re-run everytime one of those files changes.
#
# It's possible to run npm install with only the package.json and package-lock.json file.
ADD ./client/package.json ./client/package-lock.json ./
# Install git and other necessary build tools
RUN apk add --no-cache git
# Clear npm cache, remove existing node_modules, and install dependencies
RUN npm cache clean --force && \
rm -rf node_modules && \
npm install
# Explicitly install the correct version of esbuild
# RUN npm install esbuild@0.21.5
ADD ./client/ /app/
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=javascript-builder /app/.output/ /app/
RUN ls /app/
RUN ln -s /secrets/client.env .env
ADD ./docker/node-entrypoint /entrypoint.sh
RUN chmod a+x /entrypoint.sh
ENTRYPOINT [ "/entrypoint.sh" ]
CMD [ "node", "./server/index.mjs" ]

View File

@ -1,20 +1,21 @@
#!/bin/bash -e
main() {
( flock -n 100 || wait_for_other_instance; generate_api_secrets) 100> /var/lock/api_secret.lock
generate_api_secrets
}
generate_api_secrets() {
if ! is_configured; then
echo "Generating shared secret..."
SECRET="$(random_string)"
add_secret_to_env_file /app/client/.env NUXT_API_SECRET "$SECRET"
add_secret_to_env_file /app/.env FRONT_API_SECRET "$SECRET"
add_secret_to_env_file /secrets/client.env NUXT_API_SECRET "$SECRET"
add_secret_to_env_file /secrets/api.env FRONT_API_SECRET "$SECRET"
fi
}
random_string() {
array=()
for i in {a..z} {A..Z} {0..9};
for i in {a..z} {A..Z} {0..9};
do
array[$RANDOM]=$i
done
@ -27,21 +28,15 @@ add_secret_to_env_file() {
VAR=$2
VAL=$3
grep "^$VAR=" "$FILE" || ( echo "$VAR=" >> "$FILE" )
grep -q "^$VAR=" "$FILE" 2>/dev/null || ( echo "$VAR=" >> "$FILE" )
cp $FILE $TEMP_FILE
sed "s/^$VAR=.*$/$VAR=$VAL/" -i $TEMP_FILE
cat $TEMP_FILE > $FILE
}
wait_for_other_instance() {
while ! is_configured; do
sleep 1;
done
}
is_configured() {
grep -q "FRONT_API_SECRET=.\+" /app/.env
grep -q "FRONT_API_SECRET=.\+" .env 2>/dev/null
}
main

View File

@ -14,32 +14,27 @@ server {
index index.html index.htm index.php;
location / {
proxy_pass http://localhost:3000;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_http_version 1.1;
proxy_pass http://ui:3000;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
location /api/ {
set $original_uri $uri;
try_files $uri $uri/ /index.php$is_args$args;
}
location /local/temp/ {
set $original_uri $uri;
try_files $uri $uri/ /index.php$is_args$args;
}
location /forms/assets/ {
location ~/(api|open|local\/temp|forms\/assets)/ {
set $original_uri $uri;
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/var/run/php-fpm-opnform-site.sock;
fastcgi_pass api:9000;
fastcgi_index index.php;
include fastcgi.conf;
include fastcgi_params;
#fastcgi_param SCRIPT_FILENAME /usr/share/nginx/html/$fastcgi_script_name;
fastcgi_param SCRIPT_FILENAME /usr/share/nginx/html/public/index.php;
fastcgi_param REQUEST_URI $api_uri;
}
}

30
docker/node-entrypoint Normal file
View File

@ -0,0 +1,30 @@
#!/bin/sh
main() {
if [ "$1" == "bash" ]; then
"$@"
else
wait_for_api_secret
if [ ! -f .env ] && [ -f /secrets/client.env ]; then
ln -sf /secrets/client.env .env
fi
if [ -f .env ]; then
. .env
else
echo "Warning: .env file not found"
fi
run_server "$@"
fi
}
wait_for_api_secret() {
until [ -f /secrets/configured ]; do
echo "Waiting for api secret..."
sleep 1
done
}
run_server() {
echo "Running " node "$@"
"$@"
}
main "$@"

View File

@ -1,27 +0,0 @@
#!/bin/bash -e
echo + . ~nuxt/.nvm/nvm.sh
. ~nuxt/.nvm/nvm.sh
echo + nvm install --no-progress 20
nvm install --no-progress 20
echo + nvm use 20
nvm use 20
cd /app/nuxt/server/
export NUXT_PRIVATE_API_BASE=http://localhost/api
echo + . /app/client/.env
[ -f /app/client/.env ] && . /app/client/.env || echo "Environment file missing!"
[ "x$NUXT_API_SECRET" != "x" ] || (
echo + generate-api-secret.sh
generate-api-secret.sh
)
echo + eval \$\(sed 's/^/export /' \< /app/client/.env\)
eval $(sed 's/^/export /' < /app/client/.env)
echo + node index.mjs
node index.mjs

115
docker/php-fpm-entrypoint Normal file
View File

@ -0,0 +1,115 @@
#!/bin/bash
main() {
read_env
prep_file_permissions
prep_storage
if is_master "$@"; then
prep_laravel_secrets
wait_for_db
apply_db_migrations
run_init_project
mark_ready
else
wait_for_ready
wait_for_db
fi
read_env
run_server "$@"
}
is_master() {
echo "$@" | grep -q php-fpm
}
read_env() {
#set +x
[ -f .env ] || touch .env
. .env
#set -x
}
prep_file_permissions() {
chmod a+x ./artisan
}
prep_laravel_secrets() {
read_env
[ "x$APP_KEY" != "x" ] || {
echo "Generating Laravel key..."
grep -q "APP_KEY=" .env || {
echo "APP_KEY=" >> .env
}
./artisan key:generate
read_env
}
[ "x$JWT_SECRET" != "x" ] || {
echo "Generating Laravel Secret..."
./artisan jwt:secret -f
read_env
}
[ "x$FRONT_API_SECRET" != "x" ] || {
echo "Generating Shared Client Secret..."
/usr/local/bin/generate-api-secret.sh
read_env
}
echo "Done with secrets"
}
apply_db_migrations() {
echo "Running DB Migrations"
./artisan migrate
}
run_init_project() {
echo "Running app:init-project command"
./artisan app:init-project
}
wait_for_ready() {
echo "Checking keys have been generated"
until [ -f /secrets/configured ]; do
sleep 1;
echo "Waiting for keys to generate"
done
}
mark_ready() {
touch /secrets/configured
}
wait_for_db() {
until ./artisan migrate:status 2>&1 | grep -q -E "(Migration table not found|Migration name)"; do
echo "Waiting for DB to bootup"
sleep 1
done
}
run_server() {
echo "Booting $@"
read_env
/usr/local/bin/docker-php-entrypoint "$@"
}
prep_storage() {
[ -L storage ] || {
echo "Backing up initial storage directory"
rm -rf /etc/initial-storage
mv ./storage /etc/initial-storage
}
[ -d /persist/storage ] || {
echo "Initialising blank storage dir"
mkdir -p /persist
cp -a /etc/initial-storage /persist/storage
chmod 777 -R /persist/storage
}
touch /var/log/opnform.log
chown www-data /var/log/opnform.log
echo "Linking persistent storage into app"
ln -t . -sf /persist/storage
}
main "$@"

View File

@ -1,45 +0,0 @@
#!/bin/bash -ex
[ -L /app/storage ] || {
echo "Backing up initial storage directory"
rm -rf /etc/initial-storage
mv /app/storage /etc/initial-storage
}
[ -d /persist/storage ] || {
echo "Initialising blank storage dir"
mkdir -p /persist
cp -a /etc/initial-storage /persist/storage
chmod 777 -R /persist/storage
}
touch /var/log/opnform.log
chown opnform /var/log/opnform.log
echo "Linking persistent storage into app"
ln -t /app -sf /persist/storage
read_env() {
set +x
. /app/.env
set -x
}
read_env
[ "x$APP_KEY" != "x" ] || {
artisan key:generate
read_env
}
[ "x$JWT_SECRET" != "x" ] || {
artisan jwt:secret -f
read_env
}
[ "x$FRONT_API_SECRET" != "x" ] || {
generate-api-secret.sh
read_env
}
/usr/sbin/php-fpm8.1
tail -f /var/log/opnform.log

View File

@ -1,18 +0,0 @@
[opnform]
user = opnform
group = opnform
listen = /var/run/php-fpm-opnform-site.sock
listen.owner = www-data
listen.group = www-data
php_admin_value[disable_functions] = exec,passthru,shell_exec,system
php_admin_flag[allow_url_fopen] = off
php_admin_value[error_log] = /var/log/opnform.log
; Choose how the process manager will control the number of child processes.
pm = dynamic
pm.max_children = 75
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.process_idle_timeout = 10s
clear_env = no

View File

@ -1,48 +0,0 @@
#!/bin/bash -ex
DATA_DIR=/persist/pgsql/data
CONFIG_FILE=/etc/postgresql/postgresql.conf
PG_BASE=/usr/lib/postgresql/15/
touch $CONFIG_FILE
mkdir -p $DATA_DIR
chown postgres -R $DATA_DIR
chmod 0700 $DATA_DIR
set +x
. /app/.env
set -x
test -f $DATA_DIR/postgresql.conf || NEW_DB=true
if [ "x$NEW_DB" != "x" ]; then
echo "No database files found. Initialising blank database"
sudo -u postgres $PG_BASE/bin/initdb -D $DATA_DIR
fi
sudo -u postgres $PG_BASE/bin/postgres -D $DATA_DIR -c config_file=$CONFIG_FILE &
wait_for_database_to_be_ready() {
while ! (echo "select version()" | psql -U $DB_USERNAME); do
echo "Waiting 5 seconds for the database to come up"
sleep 5;
done
}
if [ "x$NEW_DB" != "x" ]; then
echo "Creating database users"
wait_for_database_to_be_ready
psql -U postgres <<EOF
CREATE ROLE $DB_USERNAME LOGIN PASSWORD '$DB_PASSWORD';
CREATE DATABASE $DB_DATABASE;
\c $DB_DATABASE;
GRANT ALL ON DATABASE $DB_DATABASE TO $DB_USERNAME;
GRANT ALL ON SCHEMA public TO $DB_USERNAME;
EOF
fi
wait_for_database_to_be_ready
sudo -u opnform artisan migrate --force
wait

View File

@ -1,7 +0,0 @@
#!/bin/bash -ex
sysctl vm.overcommit_memory=1
mkdir -p /persist/redis/data
chown redis -R /persist/redis/data
sudo -u redis /usr/bin/redis-server /etc/redis/redis.conf

View File

@ -1,44 +0,0 @@
[supervisord]
nodaemon=true
logfile=/dev/null
logfile_maxbytes=0
user=root
[program:nginx]
command=/usr/sbin/nginx
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
redirect_stderr=true
[program:php-fpm]
command=/usr/local/bin/php-fpm-wrapper.sh
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
redirect_stderr=true
[program:php-queue]
process_name=%(program_name)s_%(process_num)02d
command=/usr/local/bin/artisan queue:work
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
redirect_stderr=true
numprocs=5
[program:postgres]
command=/usr/local/bin/postgres-wrapper.sh
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
redirect_stderr=true
[program:redis]
command=/usr/local/bin/redis-wrapper.sh
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
redirect_stderr=true
[program:nuxt-backend]
command=/usr/local/bin/nuxt-wrapper.sh
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
redirect_stderr=true
user=nuxt

View File

@ -41,7 +41,8 @@ use Illuminate\Support\Facades\Storage;
*/
Route::group(['middleware' => 'auth:api'], function () {
Route::post('logout', [LoginController::class, 'logout']);
Route::post('logout', [LoginController::class, 'logout'])->name('logout');
Route::post('update-credentials', [ProfileController::class, 'updateAdminCredentials'])->name('credentials.update');
Route::get('user', [UserController::class, 'current'])->name('user.current');
Route::delete('user', [UserController::class, 'deleteAccount']);
@ -252,7 +253,7 @@ Route::group(['middleware' => 'auth:api'], function () {
});
Route::group(['middleware' => 'guest:api'], function () {
Route::post('login', [LoginController::class, 'login']);
Route::post('login', [LoginController::class, 'login'])->name('login');
Route::post('register', [RegisterController::class, 'register']);
Route::post('password/email', [ForgotPasswordController::class, 'sendResetLinkEmail']);