Compare commits
4 commits
930ff4417f
...
89362f473f
| Author | SHA1 | Date | |
|---|---|---|---|
| 89362f473f | |||
| 943d9b2a0c | |||
| d4090f983a | |||
| 94806782e1 |
12 changed files with 365 additions and 169 deletions
263
.drone.yml
263
.drone.yml
|
|
@ -22,55 +22,19 @@ steps:
|
|||
- yarn config set network-timeout 100000
|
||||
- yarn --cwd web install
|
||||
- tar --transform="s@.@./happydomain-${DRONE_COMMIT}@" --exclude-vcs --exclude=./web/node_modules/.cache -czf /dev/shm/happydomain-src.tar.gz .
|
||||
- mv /dev/shm/happydomain-src.tar.gz .
|
||||
- mkdir deploy
|
||||
- mv /dev/shm/happydomain-src.tar.gz deploy
|
||||
- yarn --cwd web --offline generate:api && sed -i "s/hey-api\.ts';/hey-api';/" web/src/lib/api-base/client.gen.ts
|
||||
- yarn --cwd web --offline build
|
||||
- yarn --cwd web-admin --offline generate:api && sed -i "s/hey-api\.ts';/hey-api';/" web/src/lib/api-admin/client.gen.ts
|
||||
- yarn --cwd web-admin --offline build
|
||||
|
||||
- name: deploy sources
|
||||
image: plugins/s3
|
||||
settings:
|
||||
endpoint: https://blob.nemunai.re
|
||||
path_style: true
|
||||
region: garage
|
||||
bucket: happydomain-dl
|
||||
access_key:
|
||||
from_secret: s3_access_key
|
||||
secret_key:
|
||||
from_secret: s3_secret_key
|
||||
source: happydomain-src.tar.gz
|
||||
target: /${DRONE_BRANCH//\//-}/
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
branch:
|
||||
exclude:
|
||||
- renovate/*
|
||||
|
||||
- name: deploy sources for release
|
||||
image: plugins/s3
|
||||
settings:
|
||||
endpoint: https://blob.nemunai.re
|
||||
path_style: true
|
||||
region: garage
|
||||
bucket: happydomain-dl
|
||||
access_key:
|
||||
from_secret: s3_access_key
|
||||
secret_key:
|
||||
from_secret: s3_secret_key
|
||||
source: happydomain-src.tar.gz
|
||||
target: /${DRONE_TAG}/
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: backend-commit
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- go build -tags netgo,swagger,web -ldflags '-w -X "main.Version=${DRONE_BRANCH}-${DRONE_COMMIT}" -X main.build=${DRONE_BUILD_NUMBER}' -o happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} ./cmd/happyDomain/
|
||||
- ln happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} happydomain
|
||||
- go build -tags netgo,swagger,web -ldflags '-w -X "main.Version=${DRONE_BRANCH}-${DRONE_COMMIT}" -X main.build=${DRONE_BUILD_NUMBER}' -o deploy/happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} ./cmd/happyDomain/
|
||||
- ln deploy/happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} happydomain
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
|
|
@ -82,14 +46,48 @@ steps:
|
|||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- go build -tags netgo,swagger,web -ldflags '-w -X main.Version=${DRONE_TAG##v} -X main.build=${DRONE_BUILD_NUMBER}' -o happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} ./cmd/happyDomain/
|
||||
- ln happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} happydomain
|
||||
- go build -tags netgo,swagger,web -ldflags '-w -X main.Version=${DRONE_TAG##v} -X main.build=${DRONE_BUILD_NUMBER}' -o deploy/happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} ./cmd/happyDomain/
|
||||
- ln deploy/happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} happydomain
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: generate SBOM
|
||||
image: nemunaire/drone-syft
|
||||
settings:
|
||||
select_catalogers: go,npm
|
||||
output: spdx-json=deploy/happydomain-sbom.spdx.json
|
||||
source_name: happyDomain
|
||||
|
||||
- name: build-commit macOS
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- go build -tags netgo,swagger,web -ldflags '-w -X "main.Version=${DRONE_BRANCH}-${DRONE_COMMIT}" -X main.build=${DRONE_BUILD_NUMBER}' -o deploy/happydomain-darwin-${DRONE_STAGE_ARCH} ./cmd/happyDomain/
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: darwin
|
||||
GOARCH: amd64
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- tag
|
||||
|
||||
- name: build-tag macOS
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- go build -tags netgo,swagger,web -ldflags '-w -X "main.Version=${DRONE_TAG##v}" -X main.build=${DRONE_BUILD_NUMBER}' -o deploy/happydomain-darwin-${DRONE_STAGE_ARCH} ./cmd/happyDomain/
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: darwin
|
||||
GOARCH: amd64
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: deploy
|
||||
image: plugins/s3
|
||||
settings:
|
||||
|
|
@ -101,8 +99,9 @@ steps:
|
|||
from_secret: s3_access_key
|
||||
secret_key:
|
||||
from_secret: s3_secret_key
|
||||
source: happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
source: deploy/*
|
||||
target: /${DRONE_BRANCH//\//-}/
|
||||
strip_prefix: deploy/
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
|
|
@ -121,72 +120,9 @@ steps:
|
|||
from_secret: s3_access_key
|
||||
secret_key:
|
||||
from_secret: s3_secret_key
|
||||
source: happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
target: /${DRONE_TAG}/
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: build-commit macOS
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- go build -tags netgo,swagger,web -ldflags '-w -X "main.Version=${DRONE_BRANCH}-${DRONE_COMMIT}" -X main.build=${DRONE_BUILD_NUMBER}' -o happydomain-darwin-${DRONE_STAGE_ARCH} ./cmd/happyDomain/
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: darwin
|
||||
GOARCH: amd64
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- tag
|
||||
|
||||
- name: build-tag macOS
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- go build -tags netgo,swagger,web -ldflags '-w -X "main.Version=${DRONE_TAG##v}" -X main.build=${DRONE_BUILD_NUMBER}' -o happydomain-darwin-${DRONE_STAGE_ARCH} ./cmd/happyDomain/
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: darwin
|
||||
GOARCH: amd64
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: deploy macOS
|
||||
image: plugins/s3
|
||||
settings:
|
||||
endpoint: https://blob.nemunai.re
|
||||
path_style: true
|
||||
region: garage
|
||||
bucket: happydomain-dl
|
||||
access_key:
|
||||
from_secret: s3_access_key
|
||||
secret_key:
|
||||
from_secret: s3_secret_key
|
||||
source: happydomain-darwin-${DRONE_STAGE_ARCH}
|
||||
target: /${DRONE_BRANCH//\//-}/
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
branch:
|
||||
exclude:
|
||||
- renovate/*
|
||||
|
||||
- name: deploy macOS release
|
||||
image: plugins/s3
|
||||
settings:
|
||||
endpoint: https://blob.nemunai.re
|
||||
path_style: true
|
||||
region: garage
|
||||
bucket: happydomain-dl
|
||||
access_key:
|
||||
from_secret: s3_access_key
|
||||
secret_key:
|
||||
from_secret: s3_secret_key
|
||||
source: happydomain-darwin-${DRONE_STAGE_ARCH}
|
||||
source: deploy/*
|
||||
target: /${DRONE_TAG}/
|
||||
strip_prefix: deploy/
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
|
@ -210,11 +146,11 @@ steps:
|
|||
from_secret: git_nemunaire_token
|
||||
base_url: https://git.nemunai.re
|
||||
draft: true
|
||||
prerelease: true
|
||||
files:
|
||||
- happydomain-src.tar.gz
|
||||
- happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
- happydomain-darwin-${DRONE_STAGE_ARCH}
|
||||
- happydomain-sbom.spdx.json
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
|
@ -226,11 +162,11 @@ steps:
|
|||
from_secret: codeberg_token
|
||||
base_url: https://codeberg.org
|
||||
draft: true
|
||||
prerelease: true
|
||||
files:
|
||||
- happydomain-src.tar.gz
|
||||
- happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
- happydomain-darwin-${DRONE_STAGE_ARCH}
|
||||
- happydomain-sbom.spdx.json
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
|
@ -241,12 +177,12 @@ steps:
|
|||
api_key:
|
||||
from_secret: github_release_token
|
||||
draft: true
|
||||
prerelease: true
|
||||
github_url: https://github.com
|
||||
files:
|
||||
- happydomain-src.tar.gz
|
||||
- happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
- happydomain-darwin-${DRONE_STAGE_ARCH}
|
||||
- happydomain-sbom.spdx.json
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
|
@ -294,8 +230,8 @@ steps:
|
|||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- go build -tags netgo,swagger,web -ldflags '-w -X "main.Version=${DRONE_BRANCH}-${DRONE_COMMIT}" -X main.build=${DRONE_BUILD_NUMBER}' -o happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} ./cmd/happyDomain/
|
||||
- ln happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} happydomain
|
||||
- go build -tags netgo,swagger,web -ldflags '-w -X "main.Version=${DRONE_BRANCH}-${DRONE_COMMIT}" -X main.build=${DRONE_BUILD_NUMBER}' -o deploy/happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} ./cmd/happyDomain/
|
||||
- ln deploy/happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} happydomain
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
|
|
@ -307,8 +243,8 @@ steps:
|
|||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- go build -tags netgo,swagger,web -ldflags '-w -X main.Version=${DRONE_TAG##v} -X main.build=${DRONE_BUILD_NUMBER}' -o happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} ./cmd/happyDomain/
|
||||
- ln happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} happydomain
|
||||
- go build -tags netgo,swagger,web -ldflags '-w -X main.Version=${DRONE_TAG##v} -X main.build=${DRONE_BUILD_NUMBER}' -o deploy/happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} ./cmd/happyDomain/
|
||||
- ln deploy/happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} happydomain
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
|
|
@ -324,6 +260,33 @@ steps:
|
|||
environment:
|
||||
CGO_ENABLED: 0
|
||||
|
||||
- name: build-commit macOS
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- go build -tags netgo,swagger,web -ldflags '-w -X "main.Version=${DRONE_BRANCH}-${DRONE_COMMIT}" -X main.build=${DRONE_BUILD_NUMBER}' -o deploy/happydomain-darwin-${DRONE_STAGE_ARCH} ./cmd/happyDomain/
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: darwin
|
||||
GOARCH: arm64
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- tag
|
||||
|
||||
- name: build-tag macOS
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- go build -tags netgo,swagger,web -ldflags '-w -X "main.Version=${DRONE_TAG##v}" -X main.build=${DRONE_BUILD_NUMBER}' -o deploy/happydomain-darwin-${DRONE_STAGE_ARCH} ./cmd/happyDomain/
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: darwin
|
||||
GOARCH: arm64
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: deploy
|
||||
image: plugins/s3
|
||||
settings:
|
||||
|
|
@ -335,8 +298,9 @@ steps:
|
|||
from_secret: s3_access_key
|
||||
secret_key:
|
||||
from_secret: s3_secret_key
|
||||
source: happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
source: deploy/*
|
||||
target: /${DRONE_BRANCH//\//-}/
|
||||
strip_prefix: deploy/
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
|
|
@ -355,72 +319,9 @@ steps:
|
|||
from_secret: s3_access_key
|
||||
secret_key:
|
||||
from_secret: s3_secret_key
|
||||
source: happydomain-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
target: /${DRONE_TAG}/
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: build-commit macOS
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- go build -tags netgo,swagger,web -ldflags '-w -X "main.Version=${DRONE_BRANCH}-${DRONE_COMMIT}" -X main.build=${DRONE_BUILD_NUMBER}' -o happydomain-darwin-${DRONE_STAGE_ARCH} ./cmd/happyDomain/
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: darwin
|
||||
GOARCH: arm64
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- tag
|
||||
|
||||
- name: build-tag macOS
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- go build -tags netgo,swagger,web -ldflags '-w -X "main.Version=${DRONE_TAG##v}" -X main.build=${DRONE_BUILD_NUMBER}' -o happydomain-darwin-${DRONE_STAGE_ARCH} ./cmd/happyDomain/
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: darwin
|
||||
GOARCH: arm64
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: deploy macOS
|
||||
image: plugins/s3
|
||||
settings:
|
||||
endpoint: https://blob.nemunai.re
|
||||
path_style: true
|
||||
region: garage
|
||||
bucket: happydomain-dl
|
||||
access_key:
|
||||
from_secret: s3_access_key
|
||||
secret_key:
|
||||
from_secret: s3_secret_key
|
||||
source: happydomain-darwin-${DRONE_STAGE_ARCH}
|
||||
target: /${DRONE_BRANCH//\//-}/
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
branch:
|
||||
exclude:
|
||||
- renovate/*
|
||||
|
||||
- name: deploy macOS release
|
||||
image: plugins/s3
|
||||
settings:
|
||||
endpoint: https://blob.nemunai.re
|
||||
path_style: true
|
||||
region: garage
|
||||
bucket: happydomain-dl
|
||||
access_key:
|
||||
from_secret: s3_access_key
|
||||
secret_key:
|
||||
from_secret: s3_secret_key
|
||||
source: happydomain-darwin-${DRONE_STAGE_ARCH}
|
||||
source: deploy/*
|
||||
target: /${DRONE_TAG}/
|
||||
strip_prefix: deploy/
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
|
|
|||
64
SECURITY.md
Normal file
64
SECURITY.md
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Only the latest version of happyDomain is supported with security fixes.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | --------- |
|
||||
| latest | ✓ |
|
||||
| < latest| ✗ |
|
||||
|
||||
|
||||
## Scope
|
||||
|
||||
### In scope
|
||||
|
||||
- happyDomain application code (API/backend and web frontend)
|
||||
- Other websites directly operated by the happyDomain team: documentation, main website, blog, git redirection, downloads website, demo instance, insights
|
||||
|
||||
### Out of scope
|
||||
|
||||
- Vulnerabilities in third-party dependencies that are not directly exploitable in happyDomain
|
||||
- Social engineering attacks
|
||||
- Denial-of-service attacks requiring significant resources
|
||||
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security vulnerability in happyDomain, please report it privately.
|
||||
|
||||
By email: security@happydomain.org
|
||||
On GitHub: https://github.com/happydomain/happydomain/security/advisories
|
||||
On Gitlab: https://gitlab.com/happyDomain/happyDomain/-/issues/new (check Confidential issue before submitting)
|
||||
On Framagit: https://framagit.org/happyDomain/happyDomain/-/issues/new (check Confidential issue before submitting)
|
||||
|
||||
Please include:
|
||||
- description of the vulnerability
|
||||
- steps to reproduce
|
||||
- potential impact
|
||||
|
||||
|
||||
## Disclosure policy
|
||||
|
||||
We follow a responsible disclosure process.
|
||||
|
||||
After receiving a report we will:
|
||||
1. acknowledge within 72 hours
|
||||
2. investigate the issue
|
||||
3. prepare a fix
|
||||
4. publish a security advisory when the fix is available
|
||||
|
||||
|
||||
## Safe Harbor
|
||||
|
||||
We consider security research conducted in good faith to be authorized. We will not pursue legal action against researchers who:
|
||||
- Report vulnerabilities through the channels listed above
|
||||
- Avoid accessing, modifying, or deleting data that doesn't belong to them
|
||||
- Avoid degrading the availability of our services
|
||||
- Do not publicly disclose the vulnerability before a fix is available
|
||||
|
||||
|
||||
## Credits
|
||||
|
||||
We are happy to credit security researchers who responsibly disclose vulnerabilities.
|
||||
|
|
@ -35,6 +35,9 @@
|
|||
"vite": "^7.0.0",
|
||||
"vitest": "^4.0.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"vite": "^7.0.0"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@hey-api/openapi-ts": "^0.90.6",
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import HListGroup from "$lib/components/ListGroup.svelte";
|
||||
import { groups } from "$lib/stores/domains";
|
||||
import { groups, newlyGroups } from "$lib/stores/domains";
|
||||
import { t } from "$lib/translations";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
|
@ -49,7 +49,7 @@
|
|||
|
||||
<HListGroup
|
||||
button
|
||||
items={$groups}
|
||||
items={[...$groups, ...$newlyGroups.filter((g) => !$groups.includes(g))]}
|
||||
{flush}
|
||||
isActive={(item) => selectedGroup != null && item === selectedGroup}
|
||||
on:click={selectGroup}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
import ZoneList from "$lib/components/zones/ZoneList.svelte";
|
||||
import { updateDomain } from "$lib/api/domains";
|
||||
import type { Domain } from "$lib/model/domain";
|
||||
import { groups, domains, refreshDomains } from "$lib/stores/domains";
|
||||
import { groups, domains, newlyGroups, refreshDomains } from "$lib/stores/domains";
|
||||
import { t } from "$lib/translations";
|
||||
|
||||
interface Props {
|
||||
|
|
@ -66,6 +66,7 @@
|
|||
if (newgroup.length && mygroups.indexOf(newgroup) < 0) {
|
||||
mygroups.push(newgroup);
|
||||
mygroups = mygroups;
|
||||
newlyGroups.update((gs) => gs.includes(newgroup) ? gs : [...gs, newgroup]);
|
||||
}
|
||||
newgroup = "";
|
||||
}
|
||||
|
|
|
|||
95
web/src/lib/components/modals/NewDomainGroup.svelte
Normal file
95
web/src/lib/components/modals/NewDomainGroup.svelte
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
<!--
|
||||
This file is part of the happyDomain (R) project.
|
||||
Copyright (c) 2022-2026 happyDomain
|
||||
Authors: Pierre-Olivier Mercier, et al.
|
||||
|
||||
This program is offered under a commercial and under the AGPL license.
|
||||
For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
|
||||
For AGPL licensing:
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<script module lang="ts">
|
||||
import type { ModalController } from "$lib/model/modal_controller";
|
||||
|
||||
export const controls: ModalController = {
|
||||
Open(): void {},
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
InputGroup,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalHeader,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
|
||||
import { newlyGroups } from "$lib/stores/domains";
|
||||
import { t } from "$lib/translations";
|
||||
|
||||
interface Props {
|
||||
isOpen?: boolean;
|
||||
}
|
||||
|
||||
let { isOpen = $bindable(false) }: Props = $props();
|
||||
const toggle = () => (isOpen = !isOpen);
|
||||
|
||||
let newgroup = $state("");
|
||||
let inputEl: HTMLInputElement | undefined = $state();
|
||||
|
||||
function focusInput() {
|
||||
inputEl?.focus();
|
||||
}
|
||||
|
||||
function addGroup(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
if (newgroup.length) {
|
||||
newlyGroups.update((gs) => (gs.includes(newgroup) ? gs : [...gs, newgroup]));
|
||||
}
|
||||
newgroup = "";
|
||||
isOpen = false;
|
||||
}
|
||||
|
||||
function Open(): void {
|
||||
newgroup = "";
|
||||
isOpen = true;
|
||||
}
|
||||
|
||||
controls.Open = Open;
|
||||
</script>
|
||||
|
||||
<Modal {isOpen} {toggle} on:open={focusInput}>
|
||||
<ModalHeader {toggle}>
|
||||
{$t("domaingroups.new")}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<form onsubmit={addGroup}>
|
||||
<InputGroup>
|
||||
<Input
|
||||
placeholder={$t("domaingroups.new")}
|
||||
required
|
||||
bind:value={newgroup}
|
||||
bind:inner={inputEl}
|
||||
/>
|
||||
<Button type="submit" color="primary" disabled={newgroup.length < 1}>
|
||||
{$t("common.add")}
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</form>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
|
|
@ -22,18 +22,14 @@
|
|||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Icon,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { Button, Card, Icon } from "@sveltestrap/sveltestrap";
|
||||
|
||||
import DomainGroupList from "$lib/components/forms/DomainGroupList.svelte";
|
||||
import DomainGroupModal, { controls as ctrlDomainGroup } from "$lib/components/modals/DomainGroup.svelte";
|
||||
import NewDomainGroupModal, { controls as ctrlNewDomainGroup } from "$lib/components/modals/NewDomainGroup.svelte";
|
||||
import { domains } from "$lib/stores/domains";
|
||||
import { t } from "$lib/translations";
|
||||
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
filteredGroup?: string | null;
|
||||
|
|
@ -46,18 +42,32 @@
|
|||
<Card class="mb-3 ${className}">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
{$t("domaingroups.title")}
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="light"
|
||||
title={$t("domaingroups.manage")}
|
||||
on:click={() => ctrlDomainGroup.Open()}
|
||||
>
|
||||
<Icon name="grid-fill" />
|
||||
</Button>
|
||||
<div>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="light"
|
||||
class="d-none d-lg-inline-block"
|
||||
title={$t("domaingroups.new")}
|
||||
on:click={() => ctrlNewDomainGroup.Open()}
|
||||
>
|
||||
<Icon name="plus-lg" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="light"
|
||||
class="d-lg-none"
|
||||
title={$t("domaingroups.manage")}
|
||||
on:click={() => ctrlDomainGroup.Open()}
|
||||
>
|
||||
<Icon name="grid-fill" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<DomainGroupList flush bind:selectedGroup={filteredGroup} />
|
||||
</Card>
|
||||
{/if}
|
||||
|
||||
<NewDomainGroupModal />
|
||||
<DomainGroupModal />
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@
|
|||
<FilterDomainInput class="mb-3" />
|
||||
|
||||
{#if filteredDomains.length}
|
||||
<ZoneList button display_by_groups domains={filteredDomains} links />
|
||||
<ZoneList button display_by_groups domains={filteredDomains} links show_empty_groups />
|
||||
{:else}
|
||||
<div class="my-4 text-center text-muted">
|
||||
{$t("domains.filtered-no-result")}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@
|
|||
import { Badge } from "@sveltestrap/sveltestrap";
|
||||
import { ListGroup } from "@sveltestrap/sveltestrap";
|
||||
import DomainWithProvider from "$lib/components/domains/DomainWithProvider.svelte";
|
||||
import { domains_idx } from "$lib/stores/domains";
|
||||
import { updateDomain } from "$lib/api/domains";
|
||||
import { domains_idx, newlyGroups, refreshDomains } from "$lib/stores/domains";
|
||||
import { t } from "$lib/translations";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
|
@ -44,38 +45,87 @@
|
|||
flush?: boolean;
|
||||
links?: boolean;
|
||||
display_by_groups?: boolean;
|
||||
show_empty_groups?: boolean;
|
||||
domains?: Array<ZoneListDomain>;
|
||||
no_domain?: import('svelte').Snippet;
|
||||
badges?: import('svelte').Snippet<[any]>;
|
||||
[key: string]: any
|
||||
no_domain?: import("svelte").Snippet;
|
||||
badges?: import("svelte").Snippet<[any]>;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
let {
|
||||
flush = false,
|
||||
links = false,
|
||||
display_by_groups = false,
|
||||
show_empty_groups = false,
|
||||
domains = [],
|
||||
no_domain,
|
||||
badges,
|
||||
...rest
|
||||
}: Props = $props();
|
||||
|
||||
function genGroups(domains: Array<ZoneListDomain>, display_by_groups: boolean) {
|
||||
function genGroups(domains: Array<ZoneListDomain>, display_by_groups: boolean, show_empty_groups: boolean, extraGroups: Array<string>) {
|
||||
if (!display_by_groups) {
|
||||
return { "": domains };
|
||||
}
|
||||
|
||||
const groups: Record<string, Array<ZoneListDomain>> = { };
|
||||
const groups: Record<string, Array<ZoneListDomain>> = {};
|
||||
|
||||
for (const domain of domains) {
|
||||
const group = domain.group ?? '';
|
||||
const group = domain.group ?? "";
|
||||
(groups[group] ??= []).push(domain);
|
||||
}
|
||||
|
||||
if (show_empty_groups) {
|
||||
for (const g of extraGroups) {
|
||||
if (!(g in groups)) {
|
||||
groups[g] = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
let groups: Record<string, Array<ZoneListDomain>> = $derived(genGroups(domains, display_by_groups));
|
||||
let localDomains: Array<ZoneListDomain> = $state([...domains]);
|
||||
$effect(() => {
|
||||
localDomains = [...domains];
|
||||
});
|
||||
|
||||
let groups: Record<string, Array<ZoneListDomain>> = $derived(
|
||||
genGroups(localDomains, display_by_groups, show_empty_groups, $newlyGroups),
|
||||
);
|
||||
|
||||
let draggedDomain: ZoneListDomain | null = $state(null);
|
||||
let dragOverGroup: string | null = $state(null);
|
||||
|
||||
async function handleDrop(targetGroup: string) {
|
||||
if (!draggedDomain || (draggedDomain.group ?? "") === targetGroup) return;
|
||||
|
||||
const fullDomain = $domains_idx[draggedDomain.domain] ?? $domains_idx[draggedDomain.id];
|
||||
if (!fullDomain) return;
|
||||
|
||||
const prevGroup = draggedDomain.group;
|
||||
const domainId = draggedDomain.domain || draggedDomain.id;
|
||||
|
||||
// Optimistic update
|
||||
localDomains = localDomains.map((d) =>
|
||||
d.domain === domainId || d.id === domainId ? { ...d, group: targetGroup } : d,
|
||||
);
|
||||
draggedDomain = null;
|
||||
dragOverGroup = null;
|
||||
|
||||
fullDomain.group = targetGroup;
|
||||
try {
|
||||
await updateDomain(fullDomain);
|
||||
refreshDomains();
|
||||
} catch {
|
||||
// Revert on error
|
||||
fullDomain.group = prevGroup;
|
||||
localDomains = localDomains.map((d) =>
|
||||
d.domain === domainId || d.id === domainId ? { ...d, group: prevGroup } : d,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getDomainHref(domain: ZoneListDomain): string | undefined {
|
||||
if (links && !domain.href) {
|
||||
|
|
@ -99,35 +149,76 @@
|
|||
{#if domains.length === 0}
|
||||
{@render no_domain?.()}
|
||||
{:else}
|
||||
{#each Object.keys(groups).sort((a,b) => !a || !b ? (!a ? 1 : -1) : a.toLowerCase().localeCompare(b.toLowerCase())) as gname}
|
||||
{#each Object.keys(groups).sort((a, b) => {
|
||||
const aEmpty = groups[a].length === 0;
|
||||
const bEmpty = groups[b].length === 0;
|
||||
if (aEmpty !== bEmpty) return aEmpty ? 1 : -1;
|
||||
if (!a || !b) return !a ? 1 : -1;
|
||||
return a.toLowerCase().localeCompare(b.toLowerCase());
|
||||
}) as gname}
|
||||
{@const gdomains = groups[gname]}
|
||||
<div
|
||||
class:mb-2={Object.keys(groups).length != 1}
|
||||
class:drag-over={display_by_groups && dragOverGroup === gname}
|
||||
ondragover={display_by_groups
|
||||
? (e) => {
|
||||
e.preventDefault();
|
||||
dragOverGroup = gname;
|
||||
}
|
||||
: undefined}
|
||||
ondragleave={display_by_groups
|
||||
? (e) => {
|
||||
if (!e.currentTarget.contains(e.relatedTarget as Node))
|
||||
dragOverGroup = null;
|
||||
}
|
||||
: undefined}
|
||||
ondrop={display_by_groups
|
||||
? (e) => {
|
||||
e.preventDefault();
|
||||
handleDrop(gname);
|
||||
}
|
||||
: undefined}
|
||||
>
|
||||
{#if Object.keys(groups).length != 1}
|
||||
<div class="d-flex align-items-center">
|
||||
<hr class="flex-fill">
|
||||
<h3
|
||||
class="px-2"
|
||||
>
|
||||
<hr class="flex-fill" />
|
||||
<h3 class="px-2">
|
||||
{#if gname === ""}
|
||||
{$t("domaingroups.no-group")}
|
||||
{:else}
|
||||
{gname}
|
||||
{/if}
|
||||
</h3>
|
||||
<hr class="flex-fill">
|
||||
<hr class="flex-fill" />
|
||||
</div>
|
||||
{/if}
|
||||
<ListGroup
|
||||
{flush}
|
||||
>
|
||||
<ListGroup {flush}>
|
||||
{#if display_by_groups && gdomains.length === 0}
|
||||
<div
|
||||
class="list-group-item text-center text-muted py-3 empty-group-placeholder"
|
||||
>
|
||||
{$t("domaingroups.drop-here")}
|
||||
</div>
|
||||
{/if}
|
||||
{#each gdomains as item}
|
||||
{@const href = getDomainHref(item)}
|
||||
{#if href}
|
||||
<a
|
||||
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center text-dark"
|
||||
class:draggable-item={display_by_groups}
|
||||
{href}
|
||||
draggable={display_by_groups || undefined}
|
||||
ondragstart={display_by_groups
|
||||
? () => {
|
||||
draggedDomain = item;
|
||||
}
|
||||
: undefined}
|
||||
ondragend={display_by_groups
|
||||
? () => {
|
||||
draggedDomain = null;
|
||||
dragOverGroup = null;
|
||||
}
|
||||
: undefined}
|
||||
onclick={() => dispatch("click", item)}
|
||||
>
|
||||
{@render domainRow(item)}
|
||||
|
|
@ -135,7 +226,20 @@
|
|||
{:else}
|
||||
<button
|
||||
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center text-dark"
|
||||
class:draggable-item={display_by_groups}
|
||||
type="button"
|
||||
draggable={display_by_groups || undefined}
|
||||
ondragstart={display_by_groups
|
||||
? () => {
|
||||
draggedDomain = item;
|
||||
}
|
||||
: undefined}
|
||||
ondragend={display_by_groups
|
||||
? () => {
|
||||
draggedDomain = null;
|
||||
dragOverGroup = null;
|
||||
}
|
||||
: undefined}
|
||||
onclick={() => dispatch("click", item)}
|
||||
>
|
||||
{@render domainRow(item)}
|
||||
|
|
@ -147,3 +251,18 @@
|
|||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.draggable-item {
|
||||
cursor: grab;
|
||||
}
|
||||
.drag-over {
|
||||
background-color: var(--bs-primary-bg-subtle, #cfe2ff);
|
||||
border-radius: 0.25rem;
|
||||
outline: 2px dashed var(--bs-primary, #0d6efd);
|
||||
}
|
||||
.empty-group-placeholder {
|
||||
min-height: 3rem;
|
||||
border-style: dashed;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@
|
|||
"all": "All groups",
|
||||
"manage": "Manage your domain groups",
|
||||
"new": "New group",
|
||||
"drop-here": "Drop a domain here",
|
||||
"no-group": "Miscellaneous",
|
||||
"title": "Your groups"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -485,6 +485,7 @@
|
|||
"all": "Tous les groupes",
|
||||
"manage": "Vos groupes de domaines",
|
||||
"new": "Nouveau groupe",
|
||||
"drop-here": "Déposez un domaine ici",
|
||||
"no-group": "Divers",
|
||||
"title": "Vos groupes"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import { listDomains } from "$lib/api/domains";
|
|||
import type { Domain } from "$lib/model/domain";
|
||||
|
||||
export const domains: Writable<Array<Domain> | undefined> = writable(undefined);
|
||||
export const newlyGroups: Writable<Array<string>> = writable([]);
|
||||
|
||||
export async function refreshDomains() {
|
||||
const data = await listDomains();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue