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:
parent
6b13f95322
commit
3280e38ee1
|
|
@ -1,3 +1,4 @@
|
|||
/.git
|
||||
/Dockerfile
|
||||
/data
|
||||
\.env
|
||||
|
|
|
|||
16
.env.docker
16
.env.docker
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -29,5 +29,5 @@ public/.DS_Store
|
|||
.env.production
|
||||
.env.staging
|
||||
_ide_helper.php
|
||||
|
||||
docker-compose.override.yml
|
||||
/.make.*
|
||||
|
|
|
|||
98
Dockerfile
98
Dockerfile
|
|
@ -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
130
README.md
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
/Dockerfile
|
||||
/.dockerignore
|
||||
|
|
@ -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
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
export default defineNuxtRouteMiddleware(() => {
|
||||
const authStore = useAuthStore()
|
||||
|
||||
if (authStore.check) {
|
||||
console.log("redirecting to home")
|
||||
return navigateTo({ name: "home" })
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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" })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -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" })
|
||||
}
|
||||
})
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ useOpnSeoMeta({
|
|||
title: "Create a new Form for free",
|
||||
})
|
||||
definePageMeta({
|
||||
middleware: "guest",
|
||||
middleware: ["guest", "self-hosted"],
|
||||
})
|
||||
|
||||
// Data
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
],
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@ import { computed } from "vue"
|
|||
useOpnSeoMeta({
|
||||
title: "Privacy Policy",
|
||||
})
|
||||
|
||||
definePageMeta({
|
||||
middleware: ["self-hosted"]
|
||||
})
|
||||
|
||||
defineRouteRules({
|
||||
swr: 3600,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@ import { computed } from "vue"
|
|||
useOpnSeoMeta({
|
||||
title: "Terms & Conditions",
|
||||
})
|
||||
|
||||
definePageMeta({
|
||||
middleware: ["self-hosted"]
|
||||
})
|
||||
|
||||
defineRouteRules({
|
||||
swr: 3600,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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),
|
||||
],
|
||||
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
@ -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
|
||||
|
|
@ -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" ]
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 "$@"
|
||||
|
|
@ -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
|
||||
|
|
@ -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 "$@"
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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']);
|
||||
|
|
|
|||
Loading…
Reference in New Issue