Compare commits

...

20 Commits

Author SHA1 Message Date
Ruben Fiszel
96f54f5f44 chore: release 1.6.0 (#6)
* Apply automatic changes

* Update version.txt

* Apply automatic changes

* Update CHANGELOG.md

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2022-05-10 12:48:04 +02:00
Ruben Fiszel
0863e12e6a ci: add codeowners 2022-05-10 09:41:44 +02:00
Ruben Fiszel
d03266b0a4 ci: add CLA 2022-05-10 09:12:24 +02:00
Ruben Fiszel
4a4eaa90e2 ci: add CLA 2022-05-10 09:02:28 +02:00
Ruben Fiszel
5e7c14b722 ci: add CLA 2022-05-10 08:52:11 +02:00
Ruben Fiszel
55b5695673 fix: display more than default 30 workspaces as superadmin 2022-05-09 15:18:28 +02:00
Ruben Fiszel
8596ac50b9 delete starter script without lock files 2022-05-08 17:56:16 +02:00
Ruben Fiszel
13fb52117b feat: self host minimal 2 2022-05-08 17:51:33 +02:00
Ruben Fiszel
2c70a15594 feat: self host minimal 2022-05-08 17:26:51 +02:00
Ruben Fiszel
7a51f842f0 feat: superadmin settings 2022-05-08 17:03:13 +02:00
Ruben Fiszel
a130806e19 feat: user settings is now at workspace level 2022-05-08 12:58:58 +02:00
Ruben Fiszel
fd1f05dd16 ci: refactor + dockerhub 2022-05-08 11:57:37 +02:00
Ruben Fiszel
48e51733e0 docs: add main ci badge 2022-05-06 14:59:42 +02:00
Ruben Fiszel
e7817e6c9f alpha.windmill -> app.windmill 2022-05-06 13:55:14 +02:00
Ruben Fiszel
51ad6edfcb docs: typos 2022-05-05 15:59:59 +02:00
Ruben Fiszel
315f7edd64 docs: windmill imgs 2022-05-05 15:53:40 +02:00
Ruben Fiszel
a2c3deab74 docs: README general idea 2022-05-05 15:24:35 +02:00
Ruben Fiszel
891b7eb93a docs: architecture diagram 2022-05-05 13:22:13 +02:00
Ruben Fiszel
7efd87be79 docs: architecture diagram 2022-05-05 13:20:42 +02:00
Ruben Fiszel
5acbc8b48c Create FUNDING.yml 2022-05-05 10:50:54 +02:00
62 changed files with 879 additions and 653 deletions

1
.env
View File

@@ -1,3 +1,2 @@
SITE_URL=localhost
DB_PASSWORD=changeme
POSTGRES_VERSION=13.3.0

1
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1 @@
* @rubenfiszel

3
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,3 @@
# These are supported funding model platforms
github: [rubenfiszel]

View File

@@ -1,38 +1,24 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
title: 'bug:'
labels: 'bug'
assignees: 'rubenfiszel'
---
**Describe the bug**
A clear and concise description of what the bug is.
**Describe the bug** A clear and concise description of what the bug is.
**To Reproduce** Steps to reproduce the behavior:
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Expected behavior** A clear and concise description of what you expected to
happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Screenshots** If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
**Additional context** Add any other context about the problem here.

View File

@@ -0,0 +1,8 @@
---
name: Feature Request
about: Create a feature request
title: 'feature: '
labels: 'feature'
assignees: 'rubenfiszel'
---

View File

@@ -3,6 +3,8 @@ name: Deploy to windmill.dev
on:
push:
branches: [main]
paths:
- "community/*"
jobs:
deploy:

View File

@@ -1,7 +1,13 @@
name: Docker Image CI
env:
LOCAL_REGISTRY: registry.wimill.xyz
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
name: Build and push docker image
on:
push:
branches: [main]
tags: ["*"]
pull_request:
types: [opened, synchronize, reopened]
@@ -12,28 +18,51 @@ concurrency:
jobs:
build:
runs-on: [self-hosted, new]
env:
DOCKER_BUILDKIT: 1
steps:
- name: Wait for release to succeed
if: github.ref == 'refs/heads/main'
uses: lewagon/wait-on-check-action@v1.0.0
with:
ref: ${{ github.ref }}
check-name: "Release please"
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 10
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: deploy staging stack
run: |
docker build . --cache-from "registry.wimill.xyz/windmill:staging" -t "registry.wimill.xyz/windmill:staging" --build-arg BUILDKIT_INLINE_CACHE=1
docker push "registry.wimill.xyz/windmill:staging"
- name: deploy demo stack
if: github.ref == 'refs/heads/main'
run: |
docker tag registry.wimill.xyz/windmill:staging registry.wimill.xyz/windmill:main
docker push registry.wimill.xyz/windmill:main
# - name: pruning unused images
# run: sudo docker image prune -a
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Docker meta local
id: metalocal
uses: docker/metadata-action@v4
with:
images: ${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Login to registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: |
${{ steps.metalocal.outputs.tags }}
${{ steps.meta.outputs.tags }}
labels: ${{ steps.metalocal.outputs.labels }}
cache-from: type=registry,ref=registry.wimill.xyz/windmilllabs/windmill:buildcache
cache-to: type=registry,ref=registry.wimill.xyz/windmilllabs/windmill:buildcache,mode=max

37
.github/workflows/lsp_on_release.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Publish LSP Server
on:
push:
branches: [main]
paths:
- "python-client/*W"
- "lsp/*"
tags:
- "*"
jobs:
build:
runs-on: [self-hosted, new]
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Docker meta local
id: metalocal
uses: docker/metadata-action@v4
with:
images: registry.wimill.xyz/windmilllabs/lsp
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Build and push
uses: docker/build-push-action@v3
with:
context: "{{defaultContext}}:lsp"
push: true
tags: ${{ steps.metalocal.outputs.tags }}
labels: ${{ steps.metalocal.outputs.labels }}
cache-from: type=registry,ref=registry.wimill.xyz/windmilllabs/lsp:buildcache
cache-to: type=registry,ref=registry.wimill.xyz/windmilllabs/lsp:buildcache,mode=max

View File

@@ -1,38 +0,0 @@
name: Build LSP Docker
on:
push:
branches: [main]
paths:
- "python-client/**"
- "Pipfile"
- ".github/workflows/on-release.yml"
jobs:
build_lsp:
runs-on: [self-hosted, new]
steps:
- name: Wait for release to succeed
if: github.ref == 'refs/heads/main'
uses: lewagon/wait-on-check-action@v1.0.0
with:
ref: ${{ github.ref }}
check-name: "Release please"
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 10
- uses: actions/checkout@v2
- name: Upload python client
env:
PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: |
cd python-client
export PATH=$PATH:/usr/local/bin
export PATH=$PATH:/root/.local/bin
./publish.sh
- name: Build the Docker image
run: |
cd lsp
sudo docker pull "registry.wimill.xyz/lsp:main" || true
sudo docker build . --cache-from "registry.wimill.xyz/lsp:main" -t "registry.wimill.xyz/lsp:main" --build-arg BUILDKIT_INLINE_CACHE=1
- name: push to registry
run: |
sudo docker push "registry.wimill.xyz/lsp:main"

19
.github/workflows/pypi_on_release.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
name: Publish python-client
on:
push:
tags:
- "v*"
jobs:
build_lsp:
runs-on: [self-hosted, new]
steps:
- uses: actions/checkout@v2
- name: Upload python client
env:
PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: |
cd python-client
export PATH=$PATH:/usr/local/bin
export PATH=$PATH:/root/.local/bin
./publish.sh

View File

@@ -1,7 +1,7 @@
on:
push:
branches:
- main
branches: [main]
name: release-please
jobs:
release-please:

34
.github/workflows/sign-cla.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
name: "CLA Assistant"
on:
issue_comment:
types: [created]
pull_request_target:
types: [opened, closed, synchronize]
jobs:
CLAssistant:
runs-on: ubuntu-latest
steps:
- name: "CLA Assistant"
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
# Beta Release
uses: cla-assistant/github-action@v2.1.3-beta
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PERSONAL_ACCESS_TOKEN: ${{ secrets.CLA_PAT }}
with:
path-to-signatures: "signatures/cla.json"
path-to-document: "https://github.com/windmill-labs/windmill/blob/master/CLA.md"
branch: "signatures"
allowlist: rubenfiszel,bot*
#below are the optional inputs - If the optional inputs are not given, then default values will be taken
#remote-organization-name: enter the remote organization name where the signatures should be stored (Default is storing the signatures in the same repository)
#remote-repository-name: enter the remote repository name where the signatures should be stored (Default is storing the signatures in the same repository)
#create-file-commit-message: 'For example: Creating file for storing CLA Signatures'
#signed-commit-message: 'For example: $contributorName has signed the CLA in #$pullRequestNo'
#custom-notsigned-prcomment: 'pull request comment with Introductory message to ask new contributors to sign'
#custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA'
#custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.'
#lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true)
#use-dco-flag: true - If you are using DCO instead of CLA

View File

@@ -1,3 +1,15 @@
# Changelog
## 1.6.0 (2022-05-10)
### Features
* superadmin settings ([7a51f84](https://www.github.com/windmill-labs/windmill/commit/7a51f842f01e17c4d230c060fa0de558553ad3ed))
* user settings is now at workspace level ([a130806](https://www.github.com/windmill-labs/windmill/commit/a130806e1929267ee40ca443e3dac6e1a5d80da3))
### Bug Fixes
* display more than default 30 workspaces as superadmin ([55b5695](https://www.github.com/windmill-labs/windmill/commit/55b5695673912ffe040d3011c020b1002b4e3268))
## [1.5.0](https://www.github.com/windmill-labs/windmill/v1.5.0) (2022-05-02)

145
CLA.md Normal file
View File

@@ -0,0 +1,145 @@
## Contributor Agreement
## Individual Contributor Non-Exclusive License Agreement
Thank you for your interest in contributing to Ruben Fiszel's Windmill ("We" or
"Us").
The purpose of this contributor agreement ("Agreement") is to clarify and
document the rights granted by contributors to Us.
### 1\. Definitions
**"You"** means the individual Copyright owner who Submits a Contribution to Us.
**"Legal Entity"** means an entity that is not a natural person.
**"Affiliate"** means any other Legal Entity that controls, is controlled by, or
under common control with that Legal Entity. For the purposes of this
definition, "control" means (i) the power, direct or indirect, to cause the
direction or management of such Legal Entity, whether by contract or otherwise,
(ii) ownership of fifty percent (50%) or more of the outstanding shares or
securities that vote to elect the management or other persons who direct such
Legal Entity or (iii) beneficial ownership of such entity.
**"Contribution"** means any original work of authorship, including any original
modifications or additions to an existing work of authorship, Submitted by You
to Us, in which You own the Copyright.
**"Copyright"** means all rights protecting works of authorship, including
copyright, moral and neighboring rights, as appropriate, for the full term of
their existence.
**"Material"** means the software or documentation made available by Us to third
parties.
**"Submit"** means any act by which a Contribution is transferred to Us by You
by means of tangible or intangible media, including but not limited to
electronic mailing lists, source code control systems, and issue tracking
systems that are managed by, or on behalf of, Us, but excluding any transfer
that is conspicuously marked or otherwise designated in writing by You as "Not a
Contribution."
**"Documentation"** means any non-software portion of a Contribution.
### 2\. License grant
#### 2.1 Copyright license to Us
Subject to the terms and conditions of this Agreement, You hereby grant to Us a
worldwide, royalty-free, NON-exclusive, perpetual and irrevocable (except as
stated in Section 8.2) license, with the right to transfer an unlimited number
of non-exclusive licenses or to grant sublicenses to third parties, under the
Copyright covering the Contribution to use the Contribution by all means,
including, but not limited to:
- publish the Contribution,
- modify the Contribution,
- prepare derivative works based upon or containing the Contribution and/or to
combine the Contribution with other Materials,
- reproduce the Contribution in original or modified form,
- distribute, to make the Contribution available to the public, display and
publicly perform the Contribution in original or modified form.
#### 2.2 Moral rights
Moral Rights remain unaffected to the extent they are recognized and not
waivable by applicable law. Notwithstanding, You may add your name to the
attribution mechanism customary used in the Materials you Contribute to, such as
the header of the source code files of Your Contribution, and We will respect
this attribution when using Your Contribution.
### 3\. Patents
#### 3.1 Patent license
Subject to the terms and conditions of this Agreement You hereby grant to Us and
to recipients of Materials distributed by Us a worldwide, royalty-free,
non-exclusive, perpetual and irrevocable (except as stated in Section 3.2)
patent license, with the right to transfer an unlimited number of non-exclusive
licenses or to grant sublicenses to third parties, to make, have made, use,
sell, offer for sale, import and otherwise transfer the Contribution and the
Contribution in combination with any Material (and portions of such
combination). This license applies to all patents owned or controlled by You,
whether already acquired or hereafter acquired, that would be infringed by
making, having made, using, selling, offering for sale, importing or otherwise
transferring of Your Contribution(s) alone or by combination of Your
Contribution(s) with any Material.
### 4. Disclaimer
THE CONTRIBUTION IS PROVIDED "AS IS". MORE PARTICULARLY, ALL EXPRESS OR IMPLIED
WARRANTIES INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTY OF SATISFACTORY
QUALITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT ARE EXPRESSLY
DISCLAIMED BY YOU TO US AND BY US TO YOU. TO THE EXTENT THAT ANY SUCH WARRANTIES
CANNOT BE DISCLAIMED, SUCH WARRANTY IS LIMITED IN DURATION AND EXTENT TO THE
MINIMUM PERIOD AND EXTENT PERMITTED BY LAW.
### 5. Consequential damage waiver
TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT WILL YOU OR WE BE
LIABLE FOR ANY LOSS OF PROFITS, LOSS OF ANTICIPATED SAVINGS, LOSS OF DATA,
INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL AND EXEMPLARY DAMAGES ARISING OUT
OF THIS AGREEMENT REGARDLESS OF THE LEGAL OR EQUITABLE THEORY (CONTRACT, TORT OR
OTHERWISE) UPON WHICH THE CLAIM IS BASED.
### 6. Approximation of disclaimer and damage waiver
IF THE DISCLAIMER AND DAMAGE WAIVER MENTIONED IN SECTION 4. AND SECTION 5.
CANNOT BE GIVEN LEGAL EFFECT UNDER APPLICABLE LOCAL LAW, REVIEWING COURTS SHALL
APPLY LOCAL LAW THAT MOST CLOSELY APPROXIMATES AN ABSOLUTE WAIVER OF ALL CIVIL
OR CONTRACTUAL LIABILITY IN CONNECTION WITH THE CONTRIBUTION.
### 7. Term
7.1 This Agreement shall come into effect upon Your acceptance of the terms and
conditions.
7.3 In the event of a termination of this Agreement Sections 4, 5, 6, 7 and 8
shall survive such termination and shall remain in full force thereafter. For
the avoidance of doubt, Free and Open Source Software (sub)licenses that have
already been granted for Contributions at the date of the termination shall
remain in full force after the termination of this Agreement.
### 8 Miscellaneous
8.1 This Agreement and all disputes, claims, actions, suits or other proceedings
arising out of this agreement or relating in any way to it shall be governed by
the laws of France excluding its private international law provisions.
8.2 This Agreement sets out the entire agreement between You and Us for Your
Contributions to Us and overrides all other agreements or understandings.
8.3 In case of Your death, this agreement shall continue with Your heirs. In
case of more than one heir, all heirs must exercise their rights through a
commonly authorized person.
8.4 If any provision of this Agreement is found void and unenforceable, such
provision will be replaced to the extent possible with a provision that comes
closest to the meaning of the original provision and that is enforceable. The
terms and conditions set forth in this Agreement shall apply notwithstanding any
failure of essential purpose of this Agreement or any limited remedy to the
maximum extent possible under law.
8.5 You agree to notify Us of any facts or circumstances of which you become
aware that would make this Agreement inaccurate in any respect.

View File

@@ -1,4 +1,4 @@
{$SITE_URL} {
bind {$ADDRESS}
reverse_proxy /* server:8000
reverse_proxy /* windmill:8000
}

View File

@@ -1,10 +1,13 @@
<p align="center">
<a href="https://alpha.windmill.dev"><img src="./windmill.svg" alt="windmill.dev"></a>
<a href="https://app.windmill.dev"><img src="./imgs/windmill.svg" alt="windmill.dev"></a>
</p>
<p align="center">
<em>Windmill.dev is an OSS developer platform to quickly build production-grade multi-steps automations and internal apps from minimal Python and Typescript scripts.</em>
</p>
<p align="center">
<a href="https://github.com/windmill-labs/windmill/actions/workflows/docker-image.yml" target="_blank">
<img src="https://github.com/windmill-labs/windmill/actions/workflows/docker-image.yml/badge.svg" alt="Docker Image CI">
</a>
<a href="https://pypi.org/project/wmill" target="_blank">
<img src="https://img.shields.io/pypi/v/wmill?color=%2334D058&label=pypi%20package" alt="Package version">
</a>
@@ -16,7 +19,7 @@
---
**Join the alpha (personal workspaces are free forever)**:
<https://alpha.windmill.dev>
<https://app.windmill.dev>
**Documentation**: <https://docs.windmill.dev>
@@ -36,17 +39,36 @@ You can show your support for the project by starring this repo.
especially concerning flows.
</p>
![Windmill](./windmill.webp)
![Windmill Screenshot](./imgs/windmill.webp)
Windmill is <b>fully open-sourced</b>:
- `community/` and `python-client/` are Apache 2.0
- backend, frontend and everything else under AGPLv3.
## What is the general idea behind Windmill
1. Define a minimal and generic script in Python or Typescript that solve a
specific task. Here sending an email with SMTP. The code can be defined in
the provided Web IDE or synchronized with your own github repo:
![Step 1](./imgs/step1.png)
2. Your scripts parameters are automatically parsed and generate a frontend. You
can narrow down the types during task definition to specify regex for string,
an enum or a specific format for objects. Each script correspond to an app by
itself: ![Step 2](./imgs/step2.png)
3. Make it flow! You can chain your scripts or scripts made by the community
inside flow by piping output to input using "Dynamic" fields that are just
plain Javascript. You can also refer to external variables, output from any
steps or inputs of the flow itself. The flow parameters then generate
automatically an intuitive forms that can be triggered by anyone, like for
scripts. ![Step 3](./imgs/step3.png)
## Layout
- `backend/`: The whole Rust backend
- `frontend`: The whole Svelte fronten
- `frontend`: The whole Svelte frontend
- `community/`: Scripts and resource types created and curated by the community,
included in every workspace
- `lsp/`: The lsp asssistant for the monaco editor
@@ -69,18 +91,28 @@ Windmill is <b>fully open-sourced</b>:
- typescript runtime is deno
- python runtime is python3
## Architecture
A detailed section about Windmill architecture is coming soon
### Development stack
- caddy is the reverse proxy used for local development, see frontend's
Caddyfile and CaddyfileRemote
## Architecture
![Architecture](./imgs/architecture.svg)
## How to self-host
Complete instructions coming soon
`docker volume create caddy_data && docker-compose up` with the following
docker-compose is sufficient:
<https://github.com/windmill-labs/windmill-server/blob/main/docker-compose.yml>
The default super-admin user is: admin@windmill.dev / changeme
From there, you can create other users (do not forget to change the password!)
Detailed instructions for more complex deployments will come soon. For simpler
docker based ones, the docker-compose.yml file contains all the necessary
informations.
## Copyright

2
backend/Cargo.lock generated
View File

@@ -4011,7 +4011,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windmill"
version = "1.5.0"
version = "1.6.0"
dependencies = [
"anyhow",
"argon2",

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.5.0"
version = "1.6.0"
authors = ["Ruben Fiszel <ruben@rubenfiszel.com>"]
edition = "2021"

View File

@@ -1 +1,2 @@
-- Add down migration script here

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,4 @@
-- Add up migration script here
UPDATE password
SET password_hash = '$argon2id$v=19$m=4096,t=3,p=1$oLJo/lPn/gezXCuFOEyaNw$i0T2tCkw3xUFsrBIKZwr8jVNHlIfoxQe+HfDnLtd12I'
WHERE password_hash = '$argon2id$v=19$m=4096,t=3,p=1$z0Kg3qyaS14e+YHeihkJLQ$N69flI6yQ/U98pjAHtbNxbdz2f4PrJEi9Tx1VoYk1as';

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,24 @@
-- Add up migration script here
DO
$do$
BEGIN
IF NOT EXISTS (
SELECT
FROM pg_catalog.pg_roles
WHERE rolname = 'app') THEN
CREATE ROLE app LOGIN PASSWORD 'changeme';
END IF;
END
$do$;
DO
$do$
BEGIN
IF NOT EXISTS (
SELECT
FROM pg_catalog.pg_roles
WHERE rolname = 'admin') THEN
CREATE ROLE admin LOGIN PASSWORD 'changeme';
END IF;
END
$do$;

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,3 @@
-- Add up migration script here
DELETE FROM script WHERE lock IS NULL;

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.5.0
version: 1.6.0
title: Windmill server API
contact:
name: Windmill contact
@@ -207,6 +207,72 @@ paths:
schema:
type: string
/users/create:
post:
summary: create user
operationId: createUserGlobally
tags:
- user
requestBody:
description: user info
required: true
content:
application/json:
schema:
type: object
properties:
email:
type: string
password:
type: string
super_admin:
type: boolean
name:
type: string
company:
type: string
required:
- email
- password
- super_admin
responses:
"201":
description: user created
content:
text/plain:
schema:
type: string
/users/update/{email}:
post:
summary: global update user (require super admin)
operationId: globalUserUpdate
tags:
- user
parameters:
- name: email
in: path
required: true
schema:
type: string
requestBody:
description: new user info
required: true
content:
application/json:
schema:
type: object
properties:
is_super_admin:
type: boolean
responses:
"200":
description: user updated
content:
text/plain:
schema:
type: string
/w/{workspace}/users/delete/{username}:
delete:
summary: delete user (require admin privilege)
@@ -393,7 +459,7 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/GlobalWhoami"
$ref: "#/components/schemas/GlobalUserInfo"
/users/list_invites:
get:
@@ -614,7 +680,7 @@ paths:
schema:
type: array
items:
$ref: "#/components/schemas/User"
$ref: "#/components/schemas/GlobalUserInfo"
/w/{workspace}/workspaces/list_pending_invites:
get:
@@ -3243,7 +3309,7 @@ components:
- email
- is_admin
GlobalWhoami:
GlobalUserInfo:
type: object
properties:
email:

View File

@@ -170,6 +170,23 @@
]
}
},
"11b1586acdfc180c5a077861ee1f7201fcbcec9d0ebada464f9d952c9c3e400d": {
"query": "INSERT INTO password(email, verified, password_hash, login_type, super_admin, name, company)\n VALUES ($1, $2, $3, 'password', $4, $5, $6)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Bool",
"Varchar",
"Bool",
"Varchar",
"Varchar"
]
},
"nullable": []
}
},
"11eb4dd4a2c9b0b759294dde5e8b505c5a4391aa0d8cb629c665711ee0fc04a0": {
"query": "SELECT substr(logs, $1) as logs FROM queue WHERE workspace_id = $2 AND id = $3",
"describe": {
@@ -1249,6 +1266,57 @@
"nullable": []
}
},
"77ba7207c8f5fd7156542cfd9943aa9a9fa87a652131c261f5020bab9ba6b5a3": {
"query": "SELECT email, login_type::text, verified, super_admin, name, company from password LIMIT $1 OFFSET $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "login_type",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "verified",
"type_info": "Bool"
},
{
"ordinal": 3,
"name": "super_admin",
"type_info": "Bool"
},
{
"ordinal": 4,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "company",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Int8",
"Int8"
]
},
"nullable": [
false,
null,
false,
false,
true,
true
]
}
},
"7b1239ad6460e8f5fb41bfe12f662a779528784ec8cf3f6dcce5545ab90bf234": {
"query": "SELECT * FROM resource_type WHERE workspace_id = $1",
"describe": {
@@ -2713,69 +2781,6 @@
"nullable": []
}
},
"ef8b14d0feb4bda6a1f9834712ab47bd21e07f0a743f74a8d1f84cb3d54bfdae": {
"query": "SELECT * from usr LIMIT $1 OFFSET $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "username",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "is_admin",
"type_info": "Bool"
},
{
"ordinal": 4,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 5,
"name": "operator",
"type_info": "Bool"
},
{
"ordinal": 6,
"name": "disabled",
"type_info": "Bool"
},
{
"ordinal": 7,
"name": "role",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Int8",
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
true
]
}
},
"f056b5f3e66a764748925f1bfd3180923fde8c7fdf69088d0e4a5555cc049545": {
"query": "SELECT result FROM completed_job WHERE id = $1 AND workspace_id = $2",
"describe": {

View File

@@ -59,6 +59,7 @@ pub fn global_service() -> Router {
.route("/accept_invite", post(accept_invite))
.route("/list_as_super_admin", get(list_users_as_super_admin))
.route("/setpassword", post(set_password))
.route("/create", post(create_user))
.route("/update/:user", post(update_user))
.route("/logout", post(logout))
.route("/tokens/create", post(create_token))
@@ -316,6 +317,18 @@ pub struct User {
pub role: Option<String>
}
#[derive(FromRow, Serialize)]
pub struct GlobalUserInfo {
email: String,
login_type: Option<String>,
super_admin: bool,
verified: bool,
name: Option<String>,
company: Option<String>,
}
#[derive(Serialize)]
pub struct UserInfo {
pub workspace_id: String,
@@ -368,10 +381,10 @@ pub struct NewUser {
pub email: String,
pub password: String,
pub super_admin: bool,
pub name: Option<String>,
pub company: Option<String>
}
#[derive(Deserialize)]
pub struct AcceptInvite {
pub workspace_id: String,
@@ -385,7 +398,6 @@ pub struct DeclineInvite {
#[derive(Deserialize)]
pub struct EditUser {
pub email: String,
pub is_super_admin: Option<bool>,
}
@@ -482,12 +494,12 @@ async fn list_users_as_super_admin(
authed: Authed,
Extension(db): Extension<DB>,
Query(pagination): Query<Pagination>
) -> JsonResult<Vec<User>> {
) -> JsonResult<Vec<GlobalUserInfo>> {
let mut tx = db.begin().await?;
require_super_admin(&mut tx, authed.email).await?;
let (per_page, offset) = crate::utils::paginate(pagination);
let rows = sqlx::query_as!(User, "SELECT * from usr LIMIT $1 OFFSET $2", per_page as i32, offset as i32)
let rows = sqlx::query_as!(GlobalUserInfo, "SELECT email, login_type::text, verified, super_admin, name, company from password LIMIT $1 OFFSET $2", per_page as i32, offset as i32)
.fetch_all(&mut tx)
.await?;
tx.commit().await?;
@@ -588,16 +600,6 @@ async fn whoami(
}
}
#[derive(FromRow, Serialize)]
pub struct GlobalUserInfo {
email: String,
login_type: Option<String>,
super_admin: bool,
verified: bool,
name: Option<String>,
company: Option<String>,
}
async fn global_whoami(
Extension(db): Extension<DB>,
Authed { email, .. }: Authed,
@@ -837,6 +839,7 @@ async fn update_workspace_user(
async fn update_user(
Authed { email, .. }: Authed,
Path(email_to_update): Path<String>,
Extension(db): Extension<DB>,
Json(eu): Json<EditUser>,
) -> Result<String> {
@@ -848,7 +851,7 @@ async fn update_user(
sqlx::query_scalar!(
"UPDATE password SET super_admin = $1 WHERE email = $2",
sa,
&eu.email
&email_to_update
)
.execute(&mut tx)
.await?;
@@ -860,14 +863,53 @@ async fn update_user(
"users.update",
ActionKind::Update,
"global",
Some(&eu.email),
Some(&email_to_update),
None,
)
.await?;
tx.commit().await?;
Ok(format!("email {} updated", eu.email))
Ok(format!("email {} updated", &email_to_update))
}
async fn create_user(
Authed { email, .. }: Authed,
Extension(db): Extension<DB>,
Extension(argon2): Extension<Arc<Argon2<'_>>>,
Json(nu): Json<NewUser>,
) -> Result<(StatusCode, String)> {
let mut tx = db.begin().await?;
require_super_admin(&mut tx, email.clone()).await?;
sqlx::query!(
"INSERT INTO password(email, verified, password_hash, login_type, super_admin, name, company)
VALUES ($1, $2, $3, 'password', $4, $5, $6)",
&nu.email,
true,
&hash_password(argon2, nu.password)?,
&nu.super_admin,
nu.name,
nu.company
)
.execute(&mut tx)
.await?;
audit_log(
&mut tx,
&email.unwrap(),
"users.update",
ActionKind::Update,
"global",
Some(&nu.email),
None,
)
.await?;
tx.commit().await?;
Ok((StatusCode::CREATED, format!("email {} created", nu.email)))
}
pub fn owner_to_token_owner(user: &str, is_group: bool) -> String {
let prefix = if is_group { 'g' } else { 'u' };
format!("{}/{}", prefix, user)
@@ -909,7 +951,6 @@ async fn delete_user(
)
.await?;
tx.commit().await?;
Ok(format!("username {} deleted", username_to_delete))
}

View File

@@ -7,6 +7,7 @@ services:
restart: always
volumes:
- db_data:/var/lib/postgresql/data
- ./init-db.sql:/docker-entrypoint-initdb.d/create_tables.sql
ports:
- 5432:5432
environment:
@@ -18,13 +19,20 @@ services:
timeout: 5s
retries: 5
windmill:
image: windmill:main
image: ghcr.io/windmill-labs/windmill:main
privileged: true
restart: unless-stopped
ports:
- 8000:8000
environment:
- DATABASE_URL=postgres://postgres:${DB_PASSWORD}@db/windmill?sslmode=disable
- VARIABLES_KEY=changeme
- APP_USER_PASSWORD=changeme
- BASE_URL=https://localhost"
- BASE_INTERNAL_URL=http://localhost:8000"
- RUST_LOG=info
- NUM_WORKERS=3
- RUST_BACKTRACE=1
depends_on:
db:
condition: service_healthy

View File

@@ -1,16 +0,0 @@
http://localhost {
bind {$ADDRESS}
reverse_proxy /api/* https://demo.windmill.dev {
header_up Host {http.reverse_proxy.upstream.hostport}
}
reverse_proxy /* http://localhost:3000
}
https://localhost {
bind {$ADDRESS}
reverse_proxy /ws/* https://demo.windmill.dev {
header_up Host {http.reverse_proxy.upstream.hostport}
}
}

View File

@@ -1,6 +1,6 @@
http://localhost {
bind {$ADDRESS}
reverse_proxy /api/* https://alpha.windmill.dev {
reverse_proxy /api/* https://app.windmill.dev {
header_up Host {http.reverse_proxy.upstream.hostport}
}
reverse_proxy /* http://localhost:3000
@@ -9,7 +9,7 @@ http://localhost {
https://localhost {
bind {$ADDRESS}
reverse_proxy /ws/* https://alpha.windmill.dev {
reverse_proxy /ws/* https://app.windmill.dev {
header_up Host {http.reverse_proxy.upstream.hostport}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "windmill",
"version": "1.5.0",
"version": "1.6.0",
"scripts": {
"dev": "svelte-kit dev",
"build": "svelte-kit build",

View File

@@ -1,24 +0,0 @@
export function pathToMeta(path) {
const splitted = path.split('/');
let ownerKind;
if (splitted[0] == 'g') {
ownerKind = 'group';
}
else if (splitted[0] == 'u') {
ownerKind = 'user';
}
else {
console.error('Not recognized owner:' + splitted[0]);
return {
ownerKind: 'user',
owner: '',
name: ''
};
}
return {
ownerKind,
owner: splitted[1],
name: splitted.slice(2).join('/')
};
}
//# sourceMappingURL=common.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"common.js","sourceRoot":"","sources":["common.ts"],"names":[],"mappings":"AA0BA,MAAM,UAAU,UAAU,CAAC,IAAY;IACtC,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAChC,IAAI,SAAoB,CAAA;IACxB,IAAI,QAAQ,CAAC,CAAC,CAAC,IAAI,GAAG,EAAE;QACvB,SAAS,GAAG,OAAO,CAAA;KACnB;SAAM,IAAI,QAAQ,CAAC,CAAC,CAAC,IAAI,GAAG,EAAE;QAC9B,SAAS,GAAG,MAAM,CAAA;KAClB;SAAM;QACN,OAAO,CAAC,KAAK,CAAC,uBAAuB,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAA;QACpD,OAAO;YACN,SAAS,EAAE,MAAM;YACjB,KAAK,EAAE,EAAE;YACT,IAAI,EAAE,EAAE;SACR,CAAA;KACD;IACD,OAAO;QACN,SAAS;QACT,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC;QAClB,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;KACjC,CAAA;AACF,CAAC"}

View File

@@ -1,8 +0,0 @@
/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
const response = await resolve(event, {
ssr: false
});
return response;
}
//# sourceMappingURL=hooks.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"hooks.js","sourceRoot":"","sources":["hooks.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE;IAC3C,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,KAAK,EAAE;QAClC,GAAG,EAAE,KAAK;KACb,CAAC,CAAA;IAEF,OAAO,QAAQ,CAAA;AACnB,CAAC"}

View File

@@ -1,66 +0,0 @@
import { ScriptService } from "./gen";
import { sendUserToast } from "./utils";
export async function inferArgs(code, schema) {
try {
const inferedSchema = await ScriptService.toJsonschema({
requestBody: code
});
schema.required = [];
const oldProperties = Object.assign({}, schema.properties);
schema.properties = {};
for (const arg of inferedSchema.args) {
if (!(arg.name in oldProperties)) {
schema.properties[arg.name] = { description: '', type: '' };
}
else {
schema.properties[arg.name] = oldProperties[arg.name];
}
pythonToJsonSchemaType(arg.typ, schema.properties[arg.name]);
schema.properties[arg.name].default = arg.default;
if (!arg.has_default) {
schema.required.push(arg.name);
}
}
}
catch (err) {
console.error(err);
sendUserToast(`Could not infer schema: ${err.body ?? err}`, true);
}
}
function array_move(arr, fromIndex, toIndex) {
var element = arr[fromIndex];
arr.splice(fromIndex, 1);
arr.splice(toIndex, 0, element);
}
function pythonToJsonSchemaType(t, s) {
if (t === 'int') {
s.type = 'integer';
}
else if (t === 'float') {
s.type = 'number';
}
else if (t === 'bool') {
s.type = 'boolean';
}
else if (t === 'str') {
s.type = 'string';
}
else if (t === 'dict') {
s.type = 'object';
}
else if (t === 'list') {
s.type = 'array';
}
else if (t === 'bytes') {
s.type = 'string';
s.contentEncoding = 'base64';
}
else if (t === 'datetime') {
s.type = 'string';
s.format = 'date-time';
}
else {
s.type = undefined;
}
}
//# sourceMappingURL=infer.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"infer.js","sourceRoot":"","sources":["infer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAEvC,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,IAAY,EAAE,MAAc;IACxD,IAAI;QACA,MAAM,aAAa,GAAG,MAAM,aAAa,CAAC,YAAY,CAAC;YACnD,WAAW,EAAE,IAAI;SACpB,CAAC,CAAA;QACF,MAAM,CAAC,QAAQ,GAAG,EAAE,CAAA;QACpB,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,UAAU,CAAC,CAAA;QAC1D,MAAM,CAAC,UAAU,GAAG,EAAE,CAAA;QAEtB,KAAK,MAAM,GAAG,IAAI,aAAa,CAAC,IAAI,EAAE;YAClC,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,IAAI,aAAa,CAAC,EAAE;gBAC9B,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,CAAA;aAC9D;iBAAM;gBACH,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;aACxD;YACD,sBAAsB,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAA;YAC5D,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,OAAO,GAAG,GAAG,CAAC,OAAO,CAAA;YAEjD,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE;gBAClB,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;aACjC;SACJ;KACJ;IAAC,OAAO,GAAG,EAAE;QACV,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QAClB,aAAa,CAAC,2BAA2B,GAAG,CAAC,IAAI,IAAI,GAAG,EAAE,EAAE,IAAI,CAAC,CAAA;KACpE;AACL,CAAC;AAED,SAAS,UAAU,CAAI,GAAQ,EAAE,SAAiB,EAAE,OAAe;IAC/D,IAAI,OAAO,GAAG,GAAG,CAAC,SAAS,CAAC,CAAA;IAC5B,GAAG,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,CAAA;IACxB,GAAG,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,EAAE,OAAO,CAAC,CAAA;AACnC,CAAC;AAED,SAAS,sBAAsB,CAAC,CAAS,EAAE,CAAiB;IACxD,IAAI,CAAC,KAAK,KAAK,EAAE;QACb,CAAC,CAAC,IAAI,GAAG,SAAS,CAAA;KACrB;SAAM,IAAI,CAAC,KAAK,OAAO,EAAE;QACtB,CAAC,CAAC,IAAI,GAAG,QAAQ,CAAA;KACpB;SAAM,IAAI,CAAC,KAAK,MAAM,EAAE;QACrB,CAAC,CAAC,IAAI,GAAG,SAAS,CAAA;KACrB;SAAM,IAAI,CAAC,KAAK,KAAK,EAAE;QACpB,CAAC,CAAC,IAAI,GAAG,QAAQ,CAAA;KACpB;SAAM,IAAI,CAAC,KAAK,MAAM,EAAE;QACrB,CAAC,CAAC,IAAI,GAAG,QAAQ,CAAA;KACpB;SAAM,IAAI,CAAC,KAAK,MAAM,EAAE;QACrB,CAAC,CAAC,IAAI,GAAG,OAAO,CAAA;KACnB;SAAM,IAAI,CAAC,KAAK,OAAO,EAAE;QACtB,CAAC,CAAC,IAAI,GAAG,QAAQ,CAAA;QACjB,CAAC,CAAC,eAAe,GAAG,QAAQ,CAAA;KAC/B;SAAM,IAAI,CAAC,KAAK,UAAU,EAAE;QACzB,CAAC,CAAC,IAAI,GAAG,QAAQ,CAAA;QACjB,CAAC,CAAC,MAAM,GAAG,WAAW,CAAA;KACzB;SAAM;QACH,CAAC,CAAC,IAAI,GAAG,SAAS,CAAA;KACrB;AACL,CAAC"}

View File

@@ -2,7 +2,14 @@
import '../app.css';
import { OpenAPI, UserService, WorkspaceService } from '../gen';
import { logout, clickOutside, sendUserToast, logoutWithRedirect, getUser } from '../utils';
import {
logout,
clickOutside,
sendUserToast,
logoutWithRedirect,
getUser,
refreshSuperadmin
} from '../utils';
import { onDestroy, onMount } from 'svelte';
import Icon from 'svelte-awesome';
import {
@@ -73,15 +80,7 @@
}
async function loadUserInfo() {
if ($superadmin == undefined) {
UserService.globalWhoami().then((x) => {
if (x.super_admin) {
superadmin.set(x.email);
} else {
superadmin.set(false);
}
});
}
refreshSuperadmin();
if (!$usersWorkspaceStore) {
try {
@@ -278,7 +277,7 @@
>
<span class="block px-4 py-2 text-sm text-gray-500">{$usersWorkspaceStore?.email}</span>
<a
href="/settings"
href="/user/settings"
class="block px-4 py-2 text-sm text-gray-700"
role="menuitem"
tabindex="-1"

View File

@@ -0,0 +1,66 @@
<script lang="ts">
import { sendUserToast } from '../../utils';
import Switch from '../components/Switch.svelte';
import type Modal from './Modal.svelte';
import { createEventDispatcher } from 'svelte';
import { UserService } from '../../gen';
const dispatch = createEventDispatcher();
let valid = true;
let modal: Modal;
export function openModal(): void {
modal.openModal();
}
let email: string;
let is_super_admin = false;
let password: string;
let name: string | undefined;
let company: string | undefined;
function handleKeyUp(event: KeyboardEvent) {
const key = event.key || event.keyCode;
if (key === 13 || key === 'Enter') {
event.preventDefault();
addUser();
}
}
async function addUser() {
await UserService.createUserGlobally({
requestBody: {
email,
password,
super_admin: is_super_admin,
name,
company
}
});
sendUserToast(`Successfully added ${email}. Welcome to them!`);
dispatch('new');
}
</script>
<div class="flex flex-row">
<input on:keyup={handleKeyUp} placeholder="email" bind:value={email} />
<Switch class="ml-2" bind:checked={is_super_admin} horizontal={true} label={'admin: '} />
<input on:keyup={handleKeyUp} type="password" placeholder="" bind:value={password} />
<input on:keyup={handleKeyUp} placeholder="name" bind:value={name} />
<input on:keyup={handleKeyUp} placeholder="company" bind:value={company} />
<button
class="ml-4 w-40 {valid ? 'default-button' : 'default-button-disabled'}"
type="button"
on:click={() => {
addUser();
}}
disabled={email == undefined || password == undefined}
>
Add
</button>
</div>

View File

@@ -2,7 +2,7 @@
import { sendUserToast } from '../../utils';
import Switch from '../components/Switch.svelte';
import Modal from './Modal.svelte';
import type Modal from './Modal.svelte';
import { createEventDispatcher } from 'svelte';
import { workspaceStore } from '../../stores';
import { WorkspaceService } from '../../gen';

View File

@@ -2,7 +2,7 @@
import { SettingsService } from '../../gen';
export let subtitle: string | undefined = undefined;
export let title = 'Welcome to windmill.dev (alpha)';
export let title = 'Welcome to Windmill (beta)';
let version = '';
SettingsService.backendVersion().then((x) => {
@@ -10,8 +10,8 @@
});
</script>
<div class="flex justify-center h-screen bg-gray-50">
<div class="w-10/12 md:w-7/12 lg:w-4/12 xl:3/12 m-auto">
<div class="flex justify-center min-h-screen bg-gray-50 pt-10">
<div class="w-10/12 md:w-7/12 lg:w-6/12 xl:4/12 m-auto">
<h1 class="justify-center text-center font-medium pb-2 text-gray-700">
{title}
</h1>
@@ -27,7 +27,7 @@
<!-- empty row to make the form a little bit above vertical centering-->
<div class="py-12" />
</div>
<div class="absolute bottom-0 right-0 text-2xs text-gray-500 italic px-3 py-1">
<div class="absolute top-0 right-0 text-2xs text-gray-500 italic px-3 py-1">
windmill.dev backend v. <span class="font-mono">{version}</span>
</div>
</div>

View File

@@ -1,10 +1,9 @@
<script lang="ts">
import { SvelteToast } from '@zerodevx/svelte-toast';
import { logout, sendUserToast } from '../../utils';
import { logout, refreshSuperadmin, sendUserToast } from '../../utils';
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { superadmin, userStore, workspaceStore } from '../../stores';
import { UserService } from '../../gen';
// Default options
const toastOptions = {
@@ -51,15 +50,7 @@
};
workspaceStore.set(undefined);
userStore.set(undefined);
if ($superadmin == undefined) {
UserService.globalWhoami().then((x) => {
if (x.super_admin) {
superadmin.set(x.email);
} else {
superadmin.set(false);
}
});
}
refreshSuperadmin();
});
</script>

View File

@@ -2,7 +2,7 @@
import { goto } from '$app/navigation';
import { UserService } from '../../gen';
import { sendUserToast } from '../../utils';
import { refreshSuperadmin, sendUserToast } from '../../utils';
import { page } from '$app/stores';
import { userStore, usersWorkspaceStore, workspaceStore } from '../../stores';
import CenteredModal from './CenteredModal.svelte';
@@ -26,6 +26,7 @@
password
}
});
refreshSuperadmin();
if (rd) {
goto(decodeURI(rd));
} else {
@@ -62,10 +63,10 @@
<!-- Enable submit form on enter -->
<CenteredModal subtitle={error}>
<div class="justify-center text-center">
<div class="justify-center text-center flex flex-col">
<span class="text-xs text-gray-600">Currently only signup through Github is supported</span>
<a rel="external" href="/api/oauth/login/github"
><button class="w-full default-button bg-black mt-2 py-2"
><button class="m-auto default-button bg-black mt-2 py-2 w-96"
>Signup or login with Github &nbsp;
<Icon class="text-white pb-1" data={faGithub} scale={1.4} />
</button></a
@@ -73,7 +74,7 @@
</div>
<div class="flex flex-row-reverse w-full">
<button
class="my-6 text-xs text-blue-400"
class="my-6 text-xs text-blue-400 m-auto"
id="showPassword"
on:click={() => {
showPassword = !showPassword;

View File

@@ -1,15 +1,14 @@
<script lang="ts">
import { userStore, workspaceStore } from '../stores';
import { usersWorkspaceStore } from '../../stores';
import { onMount } from 'svelte';
import type { TruncatedToken, NewToken } from '../gen';
import { UserService, SettingsService } from '../gen';
import { displayDate, sendUserToast, getToday } from '../utils';
import PageHeader from './components/PageHeader.svelte';
import CenteredPage from './components/CenteredPage.svelte';
import type { TruncatedToken, NewToken } from '../../gen';
import { UserService, SettingsService } from '../../gen';
import { displayDate, sendUserToast, getToday } from '../../utils';
import PageHeader from './../components/PageHeader.svelte';
import Icon from 'svelte-awesome';
import { faPlus } from '@fortawesome/free-solid-svg-icons';
import TableCustom from './components/TableCustom.svelte';
import TableCustom from '../components/TableCustom.svelte';
import CenteredModal from './CenteredModal.svelte';
let newPassword: string | undefined;
let passwordError: string | undefined;
@@ -21,14 +20,6 @@
let displayCreateToken = false;
let login_type = 'none';
function handleKeyUp(event: KeyboardEvent) {
const key = event.key || event.keyCode;
if (key === 13 || key === 'Enter') {
event.preventDefault();
setPassword();
}
}
async function setPassword(): Promise<void> {
try {
if (newPassword) {
@@ -90,19 +81,17 @@
loadVersion();
loadLoginType();
$: {
if ($workspaceStore) {
listTokens();
}
}
listTokens();
</script>
<CenteredPage>
<PageHeader title="User settings" />
<CenteredModal title="User settings">
<div class="flex flex-row justify-between">
<a href="/user/workspaces">&leftarrow; Back to workspaces</a>
</div>
<div class="text-2xs text-gray-500 italic pb-6">
Running windmill version (backend) {version}
</div>
<h2 class="border-b">Change password</h2>
<h2 class="border-b">User info</h2>
<div class="">
{#if passwordError}
<div class="text-purple-500 text-2xs grow">{passwordError}</div>
@@ -110,10 +99,11 @@
<div class="flex flex-col gap-2 w-full ">
<div class="mt-4">
<label class="block w-60 mb-2 text-gray-500">
<input disabled value={$userStore?.email ?? ''} class="input mt-1" />
<div class="text-gray-700">email</div>
<input disabled value={$usersWorkspaceStore?.email} class="input mt-1" />
</label>
{#if login_type == 'password'}
<label class="block w-12/12">
<label class="block w-120">
<div class="text-gray-700">password</div>
<input
type="password"
@@ -131,6 +121,7 @@
text-sm
"
/>
<button on:click={() => setPassword()} class="mt-4 default-button">Set password</button>
</label>
{:else if login_type == 'github'}
<span>Authentified through Github OAuth2. Cannot set a password.</span>
@@ -243,7 +234,8 @@
</tbody>
</TableCustom>
</div>
</CenteredPage>
<style>
</style>
<div class="flex flex-row justify-between pt-4">
<a href="/user/workspaces">&leftarrow; Back to workspaces</a>
</div>
</CenteredModal>

View File

@@ -0,0 +1,96 @@
<script lang="ts">
import Fuse from 'fuse.js';
import { superadmin, usersWorkspaceStore } from '../../stores';
import { UserService, SettingsService, GlobalUserInfo } from '../../gen';
import { displayDate, sendUserToast, getToday } from '../../utils';
import Icon from 'svelte-awesome';
import { faPlus } from '@fortawesome/free-solid-svg-icons';
import TableCustom from '../components/TableCustom.svelte';
import CenteredModal from './CenteredModal.svelte';
import PageHeader from '../components/PageHeader.svelte';
import InviteGlobalUser from '../components/InviteGlobalUser.svelte';
let version: string | undefined;
let users: GlobalUserInfo[] = [];
let filteredUsers: GlobalUserInfo[] | undefined;
let userFilter = '';
const fuseOptions = {
includeScore: false,
keys: ['email', 'name', 'company']
};
const fuse: Fuse<GlobalUserInfo> = new Fuse(users, fuseOptions);
$: filteredUsers = fuse?.search(userFilter).map((value) => value.item);
async function loadVersion(): Promise<void> {
version = await SettingsService.backendVersion();
}
async function listUsers(): Promise<void> {
users = await UserService.listUsersAsSuperAdmin({});
fuse?.setCollection(users);
}
loadVersion();
listUsers();
</script>
<CenteredModal title="Global Users Settings">
<div class="flex flex-row justify-between">
<a href="/user/workspaces">&leftarrow; Back to workspaces</a>
</div>
<div class="text-2xs text-gray-500 italic pb-6">
Running windmill version (backend) {version}
</div>
<PageHeader title="All users" primary={false} />
<div class="pb-1" />
<InviteGlobalUser on:new={listUsers} />
<div class="pb-1" />
<input placeholder="Search users" bind:value={userFilter} class="input mt-1" />
<TableCustom>
<tr slot="header-row">
<th>email</th>
<th>superadmin</th>
<th>login_type</th>
<th>name</th>
<th>company</th>
<th />
</tr>
<tbody slot="body">
{#if filteredUsers && users}
{#each userFilter === '' ? users : filteredUsers as { email, super_admin, login_type, name, company }}
<tr class="border">
<td>{email}</td>
<td>{super_admin}</td>
<td>{login_type}</td>
<td>{name}</td>
<td>{company}</td>
<td>
<button
class="text-blue-500"
on:click={async () => {
await UserService.globalUserUpdate({
email,
requestBody: {
is_super_admin: !super_admin
}
});
listUsers();
}}>{super_admin ? 'demote' : 'promote'}</button
></td
>
</tr>
{/each}
{/if}
</tbody>
</TableCustom>
<div class="flex flex-row justify-between pt-4">
<a href="/user/workspaces">&leftarrow; Back to workspaces</a>
</div>
</CenteredModal>

View File

@@ -6,6 +6,8 @@
import { superadmin, usersWorkspaceStore, workspaceStore } from '../../stores';
import CenteredModal from './CenteredModal.svelte';
import Switch from '../components/Switch.svelte';
import { faCrown, faUser, faUserAlt, faUserCog } from '@fortawesome/free-solid-svg-icons';
import Icon from 'svelte-awesome';
let invites: WorkspaceInvite[] = [];
let list_all_as_super_admin: boolean = false;
@@ -31,7 +33,7 @@
}
async function loadWorkspacesAsAdmin() {
workspaces = (await WorkspaceService.listWorkspacesAsSuperAdmin({})).map((x) => {
workspaces = (await WorkspaceService.listWorkspacesAsSuperAdmin({ perPage: 1000 })).map((x) => {
return { ...x, username: 'superadmin' };
});
}
@@ -68,7 +70,8 @@
<button
class="
block
w-full
w-96
mx-auto
py-1
px-2
rounded-md
@@ -102,7 +105,8 @@
<div
class="
block
w-full
w-96
mx-auto
py-1
px-2
rounded-md
@@ -142,7 +146,15 @@
</span>
</div>
{/each}
<div class="flex flex-row-reverse mt-10">
<div class="flex justify-between mt-10">
{#if $superadmin}
<a class="mr-10" href="/user/superadmin_settings">
<Icon data={faCrown} class="mr-1" scale={1} />Superadmin settings</a
>
{/if}
<a class="mr-10" href="/user/settings">
<Icon data={faUserCog} class="mr-1" scale={1} />User settings</a
>
<button
class="default-button-secondary"
on:click={async () => {

View File

@@ -1,9 +0,0 @@
import { writable, derived, readable } from 'svelte/store';
export const userStore = writable(undefined);
export const workspaceStore = writable(undefined);
export const usersWorkspaceStore = writable(undefined);
export const usernameStore = derived([usersWorkspaceStore, workspaceStore], ($values, set) => {
set($values[0]?.workspaces.find(x => x.id == $values[1])?.username);
});
export const superadmin = writable(undefined);
//# sourceMappingURL=stores.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"stores.js","sourceRoot":"","sources":["stores.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAa1D,MAAM,CAAC,MAAM,SAAS,GAAG,QAAQ,CAAsB,SAAS,CAAC,CAAA;AACjE,MAAM,CAAC,MAAM,cAAc,GAAG,QAAQ,CAAqB,SAAS,CAAC,CAAA;AACrE,MAAM,CAAC,MAAM,mBAAmB,GAAG,QAAQ,CAAgC,SAAS,CAAC,CAAA;AACrF,MAAM,CAAC,MAAM,aAAa,GAAiC,OAAO,CAAC,CAAC,mBAAmB,EAAE,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE;IACvH,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAA;AACvE,CAAC,CAAC,CAAA;AACF,MAAM,CAAC,MAAM,UAAU,GAAG,QAAQ,CAA6B,SAAS,CAAC,CAAA"}

View File

@@ -1,266 +0,0 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { toast } from '@zerodevx/svelte-toast';
import { CancelablePromise, UserService } from './gen';
import { superadmin, userStore, workspaceStore } from './stores';
import { get } from 'svelte/store';
import { goto } from '$app/navigation';
export function isToday(someDate) {
const today = new Date();
return (someDate.getDate() == today.getDate() &&
someDate.getMonth() == today.getMonth() &&
someDate.getFullYear() == today.getFullYear());
}
export function daysAgo(someDate) {
const today = new Date();
return Math.floor((today.getTime() - someDate.getTime()) / 86400000);
}
export function secondsAgo(date) {
return Math.floor((new Date().getTime() - date.getTime()) / 1000);
}
export function displayDaysAgo(dateString) {
const date = new Date(dateString);
const nbSecondsAgo = secondsAgo(date);
if (nbSecondsAgo < 600) {
return `${nbSecondsAgo}s ago`;
}
else if (isToday(date)) {
return `today at ${date.toLocaleTimeString()}`;
}
else if (daysAgo(date) === 0) {
return `${daysAgo(date) + 1} day ago`;
}
else {
return `${daysAgo(date) + 1} day ago`;
}
}
export function displayDate(dateString) {
const date = new Date(dateString ?? '');
if (date.toString() === 'Invalid Date') {
return '';
}
else {
return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()} at ${date.toLocaleTimeString()}`;
}
}
export function getToday() {
var today = new Date();
return today;
}
export function sendUserToast(message, error = false) {
if (error) {
toast.push(message, {
theme: {
'--toastBackground': '#FEE2E2',
'--toastBarBackground': '#FEE2E2'
}
});
}
else
toast.push(message);
}
export function truncateHash(hash) {
if (hash.length >= 6) {
return hash.substr(hash.length - 6);
}
else {
return hash;
}
}
async function loadStore(workspace) {
try {
const user = await UserService.whoami({ workspace });
const nuser = {
username: user.username,
email: user.email,
created_at: user.created_at,
is_admin: user.is_admin,
groups: user.groups,
pgroups: user.groups.map((x) => `g/${x}`)
};
userStore.set(nuser);
return nuser;
}
catch (error) {
userStore.set(undefined);
return undefined;
}
}
export async function getUser(workspace) {
const user = get(userStore);
if (user === undefined) {
return loadStore(workspace);
}
else {
return user;
}
}
export function logoutWithRedirect(rd) {
const error = encodeURIComponent('You have been logged out because your session has expired.');
goto(`/user/login?error=${error}${rd ? '&rd=' + encodeURIComponent(rd) : ''}`);
}
export async function handle401(promise, rd) {
// Redirects to login if the `promise` returns a 401 due to lack of authentication
// Optionnally provide `rd`, to which the user will be redirected after logging back in
return promise.catch(async (error) => {
if (error.status === 401) {
if (getUser(get(workspaceStore)) === undefined) {
logoutWithRedirect(rd);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return null;
}
else {
throw Error('You do not have enough privilege to access this');
}
}
else {
throw error;
}
});
}
export function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export function validatePassword(password) {
const re = /^(?=.*[\d])(?=.*[!@#$%^&*])[\w!@#$%^&*]{8,30}$/;
return re.test(password);
}
export async function logout(logoutMessage) {
try {
superadmin.set(undefined);
goto(`/user/login${logoutMessage ? '?error=' + encodeURIComponent(logoutMessage) : ''}`);
await UserService.logout();
sendUserToast('you have been logged out');
}
catch (error) {
goto(`/user/login?error=${encodeURIComponent('There was a problem logging you out, check the logs')}`);
console.error(error);
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function clickOutside(node) {
const handleClick = (event) => {
if (node && !node.contains(event.target) && !event.defaultPrevented) {
node.dispatchEvent(new CustomEvent('click_outside', node));
}
};
document.addEventListener('click', handleClick, true);
return {
destroy() {
document.removeEventListener('click', handleClick, true);
}
};
}
export function emptySchema() {
return {
$schema: 'https://json-schema.org/draft/2020-12/schema',
properties: {},
required: [],
type: 'object'
};
}
export function simpleSchema() {
return {
$schema: 'https://json-schema.org/draft/2020-12/schema',
type: 'object',
properties: {
name: {
description: 'The name to hello world to',
type: 'string'
}
},
required: []
};
}
export function removeItemAll(arr, value) {
var i = 0;
while (i < arr.length) {
if (arr[i] === value) {
arr.splice(i, 1);
}
else {
++i;
}
}
return arr;
}
export function canWrite(path, extra_perms, user) {
let keys = Object.keys(extra_perms);
if (!user) {
return false;
}
if (user.is_admin) {
return true;
}
let userOwner = `u/${user.username}`;
if (path.startsWith(userOwner)) {
return true;
}
if (keys.includes(userOwner) && extra_perms[userOwner]) {
return true;
}
if (user.pgroups.findIndex((x) => path.startsWith(x) || (keys.includes(x) && extra_perms[x])) != -1) {
return true;
}
return false;
}
export function removeKeysWithEmptyValues(obj) {
Object.keys(obj).forEach((key) => (obj[key] === undefined ? delete obj[key] : {}));
}
export function allTrue(dict) {
for (let v of Object.values(dict)) {
if (!v)
return false;
}
return true;
}
export function forLater(scheduledString) {
return new Date() < new Date(scheduledString);
}
export function elapsedSinceSecs(date) {
return Math.round((new Date().getTime() - new Date(date).getTime()) / 1000);
}
export function groupBy(scripts, toGroup, dflts = []) {
let r = {};
for (const dflt of dflts) {
r[dflt] = [];
}
scripts.forEach((sc) => {
let section = toGroup(sc);
if (section in r) {
r[section].push(sc);
}
else {
r[section] = [sc];
}
});
return Object.entries(r).sort((s1, s2) => {
let n1 = s1[0];
let n2 = s2[0];
if (n1 > n2) {
return 1;
}
else if (n1 < n2) {
return -1;
}
else {
return 0;
}
});
}
export function truncate(s, n, suffix = '...') {
if (s.length <= n) {
return s;
}
else {
return s.substring(0, n) + suffix;
}
}
export function truncateRev(s, n, prefix = '...') {
if (s.length <= n) {
return s;
}
else {
return prefix + s.substring(s.length - n, s.length);
}
}
//# sourceMappingURL=utils.js.map

File diff suppressed because one or more lines are too long

View File

@@ -136,6 +136,18 @@ export function validatePassword(password: string): boolean {
return re.test(password)
}
export async function refreshSuperadmin(): Promise<void> {
if (get(superadmin) == undefined) {
UserService.globalWhoami().then((x) => {
if (x.super_admin) {
superadmin.set(x.email)
} else {
superadmin.set(false)
}
})
}
}
export async function logout(logoutMessage?: string): Promise<void> {
try {
superadmin.set(undefined)

4
imgs/architecture.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 31 KiB

BIN
imgs/step1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

BIN
imgs/step2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

BIN
imgs/step3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

23
init-db.sql Normal file
View File

@@ -0,0 +1,23 @@
DO
$do$
BEGIN
IF NOT EXISTS (
SELECT
FROM pg_catalog.pg_roles
WHERE rolname = 'app') THEN
CREATE ROLE app LOGIN PASSWORD 'changeme';
END IF;
END
$do$;
DO
$do$
BEGIN
IF NOT EXISTS (
SELECT
FROM pg_catalog.pg_roles
WHERE rolname = 'admin') THEN
CREATE ROLE admin LOGIN PASSWORD 'changeme';
END IF;
END
$do$;

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "wmill"
version = "1.5.0"
version = "1.6.0"
description = "A client library for accessing Windmill server wrapping the Windmill client API"
license = "Apache-2.0"
homepage = "https://windmill.dev"
@@ -16,7 +16,7 @@ include = ["wmill/py.typed"]
[tool.poetry.dependencies]
python = "^3.7"
windmill-api = "^1.5.0"
windmill-api = "^1.6.0"
[build-system]
requires = ["poetry>=1.0.2", "poetry-dynamic-versioning"]

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "wmill-pg"
version = "1.5.0"
version = "1.6.0"
description = "An extension client for the wmill client library focused on pg"
license = "Apache-2.0"
homepage = "https://windmill.dev"
@@ -16,7 +16,7 @@ include = ["wmill_pg/py.typed"]
[tool.poetry.dependencies]
python = "^3.7"
wmill = "^1.5.0"
wmill = "^1.6.0"
psycopg2-binary = "*"
[build-system]

View File

@@ -1 +1 @@
1.5.0
1.6.0