More refactoring

This commit is contained in:
Saphire 2024-07-17 02:05:22 +06:00
parent f911e763ff
commit b0190b2ccf
Signed by: Saphire
GPG Key ID: B26EB7A1F07044C4
37 changed files with 7358 additions and 8591 deletions

View File

@ -1,5 +0,0 @@
{
"include": [
"./src/**/*"
],
}

BIN
package-lock.json generated

Binary file not shown.

View File

@ -18,7 +18,8 @@
"@vueuse/components": "^10.2.1",
"@vueuse/core": "^10.2.1",
"font-awesome": "^4.7.0",
"vue": "^3.4.29"
"vue": "^3.4.29",
"vue-router": "^4.3.3"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.8.0",
@ -75,7 +76,8 @@
"varsIgnorePattern": "^_",
"ignoreRestSiblings": true
}
]
],
"one-var": [ "error", "never" ]
}
},
"browserslist": [

View File

@ -1,5 +1,8 @@
<template>
<div id="app" v-if="user">
<div id="app" v-if="!user">
<login-view />
</div>
<div id="app" v-else>
<nav-bar
v-model:treeOpen="treeOpen"
:dn="activeDn"
@ -62,11 +65,12 @@ import { onMounted, provide, ref, watch } from "vue";
import AttributeCard from "./components/schema/AttributeCard.vue";
import EntryEditor from "./components/editor/EntryEditor.vue";
import { LdapSchema } from "./components/schema/schema";
import LdifImportDialog from "./components/LdifImportDialog.vue";
import LdifImportDialog from "@/views/dialogs/LdifImportDialog.vue";
import NavBar from "./components/NavBar.vue";
import ObjectClassCard from "./components/schema/ObjectClassCard.vue";
import type { Provided } from "./components/Provided";
import type { Provided } from "@/data/provided";
import TreeView from "./components/TreeView.vue";
import LoginView from "@/views/LoginView.vue";
interface Error {
counter: number;
@ -100,7 +104,7 @@ const provided: Provided = {
showInfo,
showException,
showWarning,
authHeaders
authHeaders,
};
provide("app", provided);
@ -204,3 +208,4 @@ select {
opacity: 0;
}
</style>
./data/Provided

View File

@ -69,20 +69,20 @@
<script setup lang="ts">
import { inject, nextTick, ref } from "vue";
import DropdownMenu from "./ui/DropdownMenu.vue";
import type { Provided } from "./Provided";
import type { Provided } from "@/data/provided";
import NodeLabel from "./NodeLabel.vue";
import SearchResults from "./SearchResults.vue";
const app = inject<Provided>("app"),
input = ref<HTMLInputElement | null>(null),
query = ref(""),
collapsed = ref(false),
emit = defineEmits([
"select-dn",
"show-modal",
"show-oc",
"update:treeOpen",
]);
const app = inject<Provided>("app");
const input = ref<HTMLInputElement | null>(null);
const query = ref("");
const collapsed = ref(false);
const emit = defineEmits([
"select-dn",
"show-modal",
"show-oc",
"update:treeOpen",
]);
defineProps({
baseDn: String,

View File

@ -12,35 +12,35 @@
<script setup lang="ts">
import { computed } from "vue";
const props = defineProps({
dn: String,
oc: String,
}),
icons: { [key: string]: string } = {
// OC -> icon mapping
account: "user",
groupOfNames: "users",
groupOfURLs: "users",
groupOfUniqueNames: "users",
inetOrgPerson: "address-book",
krbContainer: "lock",
krbPrincipal: "user-o",
krbRealmContainer: "globe",
organization: "globe",
organizationalRole: "android",
organizationalUnit: "sitemap",
person: "user",
posixGroup: "users",
},
icon = computed(() =>
// Get the icon for an OC
props.oc ? " fa-" + (icons[props.oc] || "question") : "fa-question"
),
// Shorten a DN for readability
label = computed(() =>
(props.dn || "")
.split(",")[0]
.replace(/^cn=/, "")
.replace(/^krbPrincipalName=/, "")
),
emit = defineEmits(["select-dn"]);
dn: String,
oc: String,
});
const icons: { [key: string]: string } = {
// OC -> icon mapping
account: "user",
groupOfNames: "users",
groupOfURLs: "users",
groupOfUniqueNames: "users",
inetOrgPerson: "address-book",
krbContainer: "lock",
krbPrincipal: "user-o",
krbRealmContainer: "globe",
organization: "globe",
organizationalRole: "android",
organizationalUnit: "sitemap",
person: "user",
posixGroup: "users",
};
const icon = computed(() =>
// Get the icon for an OC
props.oc ? " fa-" + (icons[props.oc] || "question") : "fa-question"
);
// Shorten a DN for readability
const label = computed(() =>
(props.dn || "")
.split(",")[0]
.replace(/^cn=/, "")
.replace(/^krbPrincipalName=/, "")
);
const emit = defineEmits(["select-dn"]);
</script>

View File

@ -15,7 +15,7 @@
<script setup lang="ts">
import { computed, inject, nextTick, ref, watch } from "vue";
import Popover from "./ui/Popover.vue";
import type { Provided } from "./Provided";
import type { Provided } from "@/data/provided";
interface Result {
dn: string;
@ -23,31 +23,28 @@ interface Result {
}
const props = defineProps({
query: {
type: String,
default: "",
},
for: String,
label: {
type: String,
default: "name",
validator: (value: string) => ["name", "dn"].includes(value),
},
shorten: String,
silent: {
type: Boolean,
default: false,
},
}),
app = inject<Provided>("app"),
results = ref<Result[]>([]),
show = computed(
() =>
props.query.trim() != "" &&
results.value &&
results.value.length > 1
),
emit = defineEmits(["select-dn"]);
query: {
type: String,
default: "",
},
for: String,
label: {
type: String,
default: "name",
validator: (value: string) => ["name", "dn"].includes(value),
},
shorten: String,
silent: {
type: Boolean,
default: false,
},
});
const app = inject<Provided>("app");
const results = ref<Result[]>([]);
const show = computed(
() => props.query.trim() != "" && results.value && results.value.length > 1
);
const emit = defineEmits(["select-dn"]);
watch(
() => props.query,

View File

@ -43,7 +43,7 @@
import { DN } from "./schema/schema";
import { onMounted, ref, watch } from "vue";
import NodeLabel from "./NodeLabel.vue";
import type { TreeNode } from "./TreeNode";
import type { TreeNode } from "@/data/treeNode";
class Node implements TreeNode {
dn: string;
@ -102,10 +102,10 @@ class Node implements TreeNode {
}
const props = defineProps({
activeDn: String,
}),
tree = ref<Node>(),
emit = defineEmits(["base-dn", "update:activeDn"]);
activeDn: String,
});
const tree = ref<Node>();
const emit = defineEmits(["base-dn", "update:activeDn"]);
onMounted(async () => {
await reload("base");
@ -134,8 +134,8 @@ watch(
// Reveal the selected entry by opening all parents
hierarchy.reverse();
for (let i = 0; i < hierarchy.length; ++i) {
const p = hierarchy[i].toString(),
node = tree.value?.find(p);
const p = hierarchy[i].toString();
const node = tree.value?.find(p);
if (!node) break;
if (!node.loaded) await reload(p);
node.open = true;

View File

@ -160,9 +160,9 @@
import { Attribute, generalizedTime } from "../schema/schema";
import { computed, inject, onMounted, onUpdated, ref, watch } from "vue";
import AttributeSearch from "./AttributeSearch.vue";
import type { Provided } from "../Provided";
import SearchResults from "../SearchResults.vue";
import ToggleButton from "../ui/ToggleButton.vue";
import type { Provided } from "@/data/provided";
import SearchResults from "@/components/SearchResults.vue";
import ToggleButton from "@/components/ui/ToggleButton.vue";
function unique(
element: unknown,
@ -301,8 +301,8 @@ function validate() {
function update(evt: Event) {
const target = evt.target as HTMLInputElement;
const value = target.value,
index = +target.id.split("-").slice(-1).pop()!;
const value = target.value;
const index = +target.id.split("-").slice(-1).pop()!;
updateValue(index, value);
}

View File

@ -15,8 +15,8 @@
<script setup lang="ts">
import type { Attribute } from "../schema/schema";
import { computed, inject, nextTick, ref, watch } from "vue";
import Popover from "../ui/Popover.vue";
import type { Provided } from "../Provided";
import Popover from "@/components/ui/Popover.vue";
import type { Provided } from "@/data/provided";
const props = defineProps({
query: { type: String, default: "" },

View File

@ -188,20 +188,20 @@
<script setup lang="ts">
import { computed, inject, nextTick, ref, watch } from "vue";
import AddAttributeDialog from "./AddAttributeDialog.vue";
import AddObjectClassDialog from "./AddObjectClassDialog.vue";
import AddPhotoDialog from "./AddPhotoDialog.vue";
import AddAttributeDialog from "@views/dialogs/AddAttributeDialog.vue";
import AddObjectClassDialog from "@views/dialogs/AddObjectClassDialog.vue";
import AddPhotoDialog from "@views/dialogs/AddPhotoDialog.vue";
import AttributeRow from "./AttributeRow.vue";
import CopyEntryDialog from "./CopyEntryDialog.vue";
import DeleteEntryDialog from "./DeleteEntryDialog.vue";
import DiscardEntryDialog from "./DiscardEntryDialog.vue";
import CopyEntryDialog from "@views/dialogs/CopyEntryDialog.vue";
import DeleteEntryDialog from "@views/dialogs/DeleteEntryDialog.vue";
import DiscardEntryDialog from "@views/dialogs/DiscardEntryDialog.vue";
import DropdownMenu from "../ui/DropdownMenu.vue";
import type { Entry } from "./Entry";
import NewEntryDialog from "./NewEntryDialog.vue";
import type { Entry } from "@/data/entry";
import NewEntryDialog from "@views/dialogs/NewEntryDialog.vue";
import NodeLabel from "../NodeLabel.vue";
import PasswordChangeDialog from "./PasswordChangeDialog.vue";
import type { Provided } from "../Provided";
import RenameEntryDialog from "./RenameEntryDialog.vue";
import PasswordChangeDialog from "@views/dialogs/PasswordChangeDialog.vue";
import type { Provided } from "@/data/provided";
import RenameEntryDialog from "@views/dialogs/RenameEntryDialog.vue";
function unique(
element: unknown,
@ -425,8 +425,8 @@ async function ldif() {
});
if (!response.ok) return;
const a = document.createElement("a"),
url = URL.createObjectURL(await response.blob());
const a = document.createElement("a");
const url = URL.createObjectURL(await response.blob());
a.href = url;
a.download = entry.value!.meta.dn.split(",")[0].split("=")[1] + ".ldif";
document.body.appendChild(a);

View File

@ -30,10 +30,10 @@
<script setup lang="ts">
import { computed, inject } from "vue";
import Card from "../ui/Card.vue";
import type { Provided } from "../Provided";
import type { Provided } from "@/data/provided";
const props = defineProps({ modelValue: String }),
app = inject<Provided>("app"),
attr = computed(() => app?.schema?.attr(props.modelValue)),
emit = defineEmits(["show-attr", "update:modelValue"]);
</script>
const props = defineProps({ modelValue: String });
const app = inject<Provided>("app");
const attr = computed(() => app?.schema?.attr(props.modelValue));
const emit = defineEmits(["show-attr", "update:modelValue"]);
</script>

View File

@ -51,10 +51,10 @@
<script setup lang="ts">
import { computed, inject } from "vue";
import Card from "../ui/Card.vue";
import type { Provided } from "../Provided";
import type { Provided } from "@/data/provided";
const props = defineProps({ modelValue: String }),
app = inject<Provided>("app"),
oc = computed(() => app?.schema?.oc(props.modelValue)),
emit = defineEmits(["show-attr", "show-oc", "update:modelValue"]);
</script>
const props = defineProps({ modelValue: String });
const app = inject<Provided>("app");
const oc = computed(() => app?.schema?.oc(props.modelValue));
const emit = defineEmits(["show-attr", "show-oc", "update:modelValue"]);
</script>

View File

@ -6,12 +6,12 @@ const sut = new LdapSchema(json);
describe("LDAP schema items", () => {
describe("DNs and RDNs", () => {
const dn1 = new DN("dc=foo,dc=bar"),
rdn1 = dn1.rdn;
const dn2 = new DN("domainComponent=FOO,domainComponent=BAR"),
rdn2 = dn2.rdn;
const dn3 = new DN("domainComponent=bar"),
rdn3 = dn3.rdn;
const dn1 = new DN("dc=foo,dc=bar");
const rdn1 = dn1.rdn;
const dn2 = new DN("domainComponent=FOO,domainComponent=BAR");
const rdn2 = dn2.rdn;
const dn3 = new DN("domainComponent=bar");
const rdn3 = dn3.rdn;
test("Test RDN attribute equality", () =>
expect(rdn1.attr).toEqual(sut.attr("domainComponent")));
@ -31,8 +31,8 @@ describe("LDAP schema items", () => {
});
describe("Attributes", () => {
const sn = sut.attr("sn"),
name = sut.attr("name");
const sn = sut.attr("sn");
const name = sut.attr("name");
test("SN is found in schema", () => expect(sn).toBeDefined());
@ -54,8 +54,8 @@ describe("LDAP schema items", () => {
});
describe("ObjectClass inheritance", () => {
const top = sut.oc("top"),
dnsDomain = sut.oc("dnsDomain");
const top = sut.oc("top");
const dnsDomain = sut.oc("dnsDomain");
function superClasses(cls: ObjectClass | undefined): ObjectClass[] {
const result = [];

View File

@ -1,231 +1,291 @@
function unique(element: unknown, index: number, array: Array<unknown>): boolean {
return array.indexOf(element) == index;
function unique(
element: unknown,
index: number,
array: Array<unknown>
): boolean {
return array.indexOf(element) == index;
}
export function generalizedTime(dt: string): Date {
let tz = dt.substring(14);
if (tz != 'Z') {
tz = tz.substring(0, 3) + ':'
+ (tz.length > 3 ? tz.substring(3, 5) : '00');
}
return new Date(dt.substring(0, 4) + '-'
+ dt.substring( 4, 6) + '-'
+ dt.substring( 6, 8) + 'T'
+ dt.substring( 8, 10) + ':'
+ dt.substring(10, 12) + ':'
+ dt.substring(12, 14) + tz);
let tz = dt.substring(14);
if (tz != "Z") {
tz =
tz.substring(0, 3) +
":" +
(tz.length > 3 ? tz.substring(3, 5) : "00");
}
return new Date(
dt.substring(0, 4) +
"-" +
dt.substring(4, 6) +
"-" +
dt.substring(6, 8) +
"T" +
dt.substring(8, 10) +
":" +
dt.substring(10, 12) +
":" +
dt.substring(12, 14) +
tz
);
}
let schema: LdapSchema;
export class RDN {
readonly text: string;
readonly attrName: string;
readonly value: string;
readonly text: string;
readonly attrName: string;
readonly value: string;
constructor(value: string) {
this.text = value;
const parts = value.split('=');
this.attrName = parts[0].trim();
this.value = parts[1].trim();
}
constructor(value: string) {
this.text = value;
const parts = value.split("=");
this.attrName = parts[0].trim();
this.value = parts[1].trim();
}
toString() { return this.text; }
toString() {
return this.text;
}
eq(other: RDN | undefined) {
return other !== undefined
&& this.attr !== undefined
&& this.attr.eq(other.attr)
&& this.attr.matcher(this.value, other.value);
}
eq(other: RDN | undefined) {
return (
other !== undefined &&
this.attr !== undefined &&
this.attr.eq(other.attr) &&
this.attr.matcher(this.value, other.value)
);
}
get attr() {
return schema.attr(this.attrName);
}
get attr() {
return schema.attr(this.attrName);
}
}
export class DN {
readonly text: string;
readonly rdn: RDN;
readonly parent: DN | undefined;
readonly text: string;
readonly rdn: RDN;
readonly parent: DN | undefined;
constructor(value: string) {
this.text = value;
const parts = value.split(',');
this.rdn = new RDN(parts[0]);
this.parent = parts.length == 1 ? undefined
: new DN(value.slice(parts[0].length + 1));
}
constructor(value: string) {
this.text = value;
const parts = value.split(",");
this.rdn = new RDN(parts[0]);
this.parent =
parts.length == 1
? undefined
: new DN(value.slice(parts[0].length + 1));
}
toString() { return this.text; }
toString() {
return this.text;
}
eq(other: DN | undefined) : boolean {
if (!other || !this.rdn.eq(other.rdn)) return false;
if (!this.parent && !other.parent) return true;
return !!this.parent && this.parent.eq(other.parent!);
}
eq(other: DN | undefined): boolean {
if (!other || !this.rdn.eq(other.rdn)) return false;
if (!this.parent && !other.parent) return true;
return !!this.parent && this.parent.eq(other.parent!);
}
}
class Element {
readonly oid?: string;
readonly name?: string;
readonly names?: string[];
readonly sup?: string[];
readonly oid?: string;
readonly name?: string;
readonly names?: string[];
readonly sup?: string[];
}
export class ObjectClass extends Element {
readonly desc?: string;
readonly obsolete?: boolean;
readonly may?: string[];
readonly must?: string[];
readonly kind?: string;
readonly desc?: string;
readonly obsolete?: boolean;
readonly may?: string[];
readonly must?: string[];
readonly kind?: string;
constructor(json: object) {
super();
Object.assign(this, json);
}
get structural() { return this.kind == 'structural'; }
// gather values from a field across all superclasses
$collect(name: "must" | "may"): string[] {
const attributes = [];
// eslint-disable-next-line @typescript-eslint/no-this-alias
for (let oc: ObjectClass | undefined = this; oc; oc = oc.$super) {
const attrs = oc[name];
if (attrs) attributes.push(attrs);
constructor(json: object) {
super();
Object.assign(this, json);
}
const result = attributes.flat()
.map(attr => schema.attr(attr))
.map(obj => obj?.name)
.filter(unique) as string[];
result.sort();
return result;
}
get structural() {
return this.kind == "structural";
}
toString() { return this.name!; }
// gather values from a field across all superclasses
$collect(name: "must" | "may"): string[] {
const attributes = [];
// eslint-disable-next-line @typescript-eslint/no-this-alias
for (let oc: ObjectClass | undefined = this; oc; oc = oc.$super) {
const attrs = oc[name];
if (attrs) attributes.push(attrs);
}
get $super(): ObjectClass | undefined {
const parent = Object.getPrototypeOf(this) as ObjectClass;
return parent.sup ? parent : undefined;
}
const result = attributes
.flat()
.map((attr) => schema.attr(attr))
.map((obj) => obj?.name)
.filter(unique) as string[];
result.sort();
return result;
}
toString() {
return this.name!;
}
get $super(): ObjectClass | undefined {
const parent = Object.getPrototypeOf(this) as ObjectClass;
return parent.sup ? parent : undefined;
}
}
const matchRules: {[key: string]: (a: string, b: string) => boolean} = {
// See: https://ldap.com/matching-rules/
distinguishedNameMatch: (a: string, b: string) => new DN(a).eq(new DN(b)),
caseIgnoreIA5Match: (a: string, b: string) => a.toLowerCase() == b.toLowerCase(),
caseIgnoreMatch: (a: string, b: string) => a.toLowerCase() == b.toLowerCase(),
// generalizedTimeMatch: ...
integerMatch: (a: string, b: string) => +a == +b,
numericStringMatch: (a: string, b: string) => +a == +b,
octetStringMatch: (a: string, b: string) => a == b,
const matchRules: { [key: string]: (a: string, b: string) => boolean } = {
// See: https://ldap.com/matching-rules/
distinguishedNameMatch: (a: string, b: string) => new DN(a).eq(new DN(b)),
caseIgnoreIA5Match: (a: string, b: string) =>
a.toLowerCase() == b.toLowerCase(),
caseIgnoreMatch: (a: string, b: string) =>
a.toLowerCase() == b.toLowerCase(),
// generalizedTimeMatch: ...
integerMatch: (a: string, b: string) => +a == +b,
numericStringMatch: (a: string, b: string) => +a == +b,
octetStringMatch: (a: string, b: string) => a == b,
};
export class Attribute extends Element {
desc?: string;
equality?: string; // possibly null in JSON
obsolete?: boolean;
ordering?: string; // possibly null in JSON
no_user_mod?: boolean;
single_value?: boolean;
substr?: string; // possibly null in JSON
syntax?: string; // possibly null in JSON
usage?: string;
desc?: string;
equality?: string; // possibly null in JSON
obsolete?: boolean;
ordering?: string; // possibly null in JSON
no_user_mod?: boolean;
single_value?: boolean;
substr?: string; // possibly null in JSON
syntax?: string; // possibly null in JSON
usage?: string;
constructor(json: object) {
super();
constructor(json: object) {
super();
// Hack alert: Wipe undefined attributes,
// they are looked up via the prototype chain
delete this.equality;
delete this.ordering;
delete this.substr;
delete this.syntax;
// End of hack
// Hack alert: Wipe undefined attributes,
// they are looked up via the prototype chain
delete this.equality;
delete this.ordering;
delete this.substr;
delete this.syntax;
// End of hack
Object.assign(this, Object.fromEntries(Object.entries(json)
.filter(([_prop, value]) => value != null)));
}
Object.assign(
this,
Object.fromEntries(
Object.entries(json).filter(([_prop, value]) => value != null)
)
);
}
toString() { return this.name!; }
toString() {
return this.name!;
}
get matcher() {
return (this.equality ? matchRules[this.equality] : undefined)
|| matchRules.octetStringMatch;
}
get matcher() {
return (
(this.equality ? matchRules[this.equality] : undefined) ||
matchRules.octetStringMatch
);
}
eq(other: Attribute | undefined) {
return other && this.oid == other.oid;
}
eq(other: Attribute | undefined) {
return other && this.oid == other.oid;
}
get binary() {
if (this.equality == 'octetStringMatch') return undefined;
return this.$syntax?.not_human_readable;
}
get binary() {
if (this.equality == "octetStringMatch") return undefined;
return this.$syntax?.not_human_readable;
}
get $syntax() { return schema.syntaxes.get(this.syntax!); }
get $syntax() {
return schema.syntaxes.get(this.syntax!);
}
get $super() {
const parent = Object.getPrototypeOf(this);
return parent.sup ? parent : undefined;
}
get $super() {
const parent = Object.getPrototypeOf(this);
return parent.sup ? parent : undefined;
}
}
class Syntax {
readonly oid?: string;
readonly desc?: string;
readonly not_human_readable?: boolean;
readonly oid?: string;
readonly desc?: string;
readonly not_human_readable?: boolean;
constructor(json: object) {
Object.assign(this, json);
}
constructor(json: object) {
Object.assign(this, json);
}
toString() { return this.desc!; }
toString() {
return this.desc!;
}
}
interface JsonSchema {
attributes: object;
objectClasses: object;
syntaxes: object;
attributes: object;
objectClasses: object;
syntaxes: object;
}
export class LdapSchema extends Object {
readonly attributes: Array<Attribute>;
readonly objectClasses: Map<string, ObjectClass>;
readonly syntaxes: Map<string, Syntax>;
readonly attributesByName: Map<string, Attribute>;
readonly attributes: Array<Attribute>;
readonly objectClasses: Map<string, ObjectClass>;
readonly syntaxes: Map<string, Syntax>;
readonly attributesByName: Map<string, Attribute>;
constructor(json: JsonSchema) {
super();
this.syntaxes = new Map(Object.entries(json.syntaxes)
.map(([oid, obj]) => [oid, new Syntax(obj)]));
this.attributes = Object.values(json.attributes)
.map(obj => new Attribute(obj));
this.objectClasses = new Map(Object.entries(json.objectClasses)
.map(([key, obj]) => [key.toLowerCase(), new ObjectClass(obj)]));
this.buildPrototypeChain(this.objectClasses);
this.attributesByName = new Map(this.attributes.flatMap(
attr => (attr.names || []).map(name => [name.toLowerCase(), attr])));
this.buildPrototypeChain(this.attributesByName);
schema = this as LdapSchema;
}
constructor(json: JsonSchema) {
super();
this.syntaxes = new Map(
Object.entries(json.syntaxes).map(([oid, obj]) => [
oid,
new Syntax(obj),
])
);
this.attributes = Object.values(json.attributes).map(
(obj) => new Attribute(obj)
);
this.objectClasses = new Map(
Object.entries(json.objectClasses).map(([key, obj]) => [
key.toLowerCase(),
new ObjectClass(obj),
])
);
this.buildPrototypeChain(this.objectClasses);
private buildPrototypeChain(elements: Map<string, Element>): void {
for (const element of elements.values()) {
const key = element.sup ? element.sup[0] : undefined,
parent = key ? elements.get(key.toLowerCase()) : undefined;
if (parent) Object.setPrototypeOf(element, parent);
this.attributesByName = new Map(
this.attributes.flatMap((attr) =>
(attr.names || []).map((name) => [name.toLowerCase(), attr])
)
);
this.buildPrototypeChain(this.attributesByName);
schema = this as LdapSchema;
}
}
attr(name: string | undefined) { return this.attributesByName.get(name?.toLowerCase() || ''); }
oc(name: string | undefined) { return this.objectClasses.get(name?.toLowerCase() || ''); }
private buildPrototypeChain(elements: Map<string, Element>): void {
for (const element of elements.values()) {
const key = element.sup ? element.sup[0] : undefined;
const parent = key ? elements.get(key.toLowerCase()) : undefined;
if (parent) Object.setPrototypeOf(element, parent);
}
}
search(q: string) {
return this.attributes.filter(
attr => attr.names?.some(name => name.toLowerCase().startsWith(q.toLowerCase())));
}
attr(name: string | undefined) {
return this.attributesByName.get(name?.toLowerCase() || "");
}
oc(name: string | undefined) {
return this.objectClasses.get(name?.toLowerCase() || "");
}
search(q: string) {
return this.attributes.filter((attr) =>
attr.names?.some((name) =>
name.toLowerCase().startsWith(q.toLowerCase())
)
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -90,16 +90,16 @@
<script setup lang="ts">
const props = defineProps({
title: { type: String, required: true },
open: { type: Boolean, required: true },
okTitle: { type: String, default: "OK" },
okClasses: { type: String, default: "bg-primary/80" },
cancelTitle: { type: String, default: "Cancel" },
cancelClasses: { type: String, default: "bg-secondary" },
hideFooter: { type: Boolean, default: false },
returnTo: String,
}),
emit = defineEmits(["ok", "cancel", "show", "shown", "hide", "hidden"]);
title: { type: String, required: true },
open: { type: Boolean, required: true },
okTitle: { type: String, default: "OK" },
okClasses: { type: String, default: "bg-primary/80" },
cancelTitle: { type: String, default: "Cancel" },
cancelClasses: { type: String, default: "bg-secondary" },
hideFooter: { type: Boolean, default: false },
returnTo: String,
});
const emit = defineEmits(["ok", "cancel", "show", "shown", "hide", "hidden"]);
function onOk() {
if (props.open) emit("ok");

View File

@ -23,11 +23,11 @@
import { onMounted, ref, watch } from "vue";
import { useEventListener, useMouseInElement } from "@vueuse/core";
const props = defineProps({ open: Boolean }),
emit = defineEmits(["opened", "closed", "update:open"]),
items = ref<HTMLElement | null>(null),
selected = ref<number>(),
{ isOutside } = useMouseInElement(items);
const props = defineProps({ open: Boolean });
const emit = defineEmits(["opened", "closed", "update:open"]);
const items = ref<HTMLElement | null>(null);
const selected = ref<number>();
const { isOutside } = useMouseInElement(items);
function close() {
selected.value = undefined;

View File

@ -1,9 +1,9 @@
<script setup lang="ts">
import { computed } from "vue";
const props = defineProps(["value"]),
emit = defineEmits(["update:value"]),
on = computed(() => props.value == "TRUE");
const props = defineProps(["value"]);
const emit = defineEmits(["update:value"]);
const on = computed(() => props.value == "TRUE");
</script>
<template>

View File

@ -8,6 +8,6 @@ export interface Entry {
// hints: object;
isNew?: boolean;
required: string[];
}
};
changed?: string[];
}

View File

@ -1,5 +1,5 @@
import type { Ref } from "vue";
import type { LdapSchema } from "./schema/schema";
import type { LdapSchema } from "../components/schema/schema";
export interface Provided {
readonly schema?: LdapSchema;

View File

@ -3,4 +3,4 @@ export interface TreeNode {
level?: number;
hasSubordinates: boolean;
structuralObjectClass: string;
}
}

View File

@ -1,8 +1,8 @@
"use strict";
import { createApp } from 'vue';
import App from './App.vue';
import './tailwind.css';
import 'font-awesome/css/font-awesome.min.css';
import { createApp } from "vue";
import App from "./App.vue";
import "./tailwind.css";
import "font-awesome/css/font-awesome.min.css";
createApp(App).mount('#app');
createApp(App).mount("#app");

52
src/utils.ts Normal file
View File

@ -0,0 +1,52 @@
export async function get<T>(
url: string,
headers?: Record<string, string>
): Promise<T> {
return send(url, undefined, "GET", headers);
}
export async function send<Treceived, Tsent = any | undefined>(
url: string,
data?: Tsent,
method: "POST" | "PUT" | "PATCH" | "DELETE" | "GET" = "POST",
headers?: Record<string, string>
): Promise<Treceived> {
if (fetch) {
return fetch(url, {
method: method,
headers: data
? {
"Content-Type": "application/json",
...headers,
}
: headers,
body: data ? JSON.stringify(data) : undefined,
}).then((response) => {
if (!response.ok) return Promise.reject(response);
return response.json();
});
}
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
for (const header in headers ?? {}) {
if (
!headers ||
!Object.prototype.hasOwnProperty.call(headers, header)
)
continue;
const value = headers[header];
xhr.setRequestHeader(header, value);
}
xhr.open(method, url, true);
xhr.addEventListener("load", function (e) {
resolve(JSON.parse(this.response));
});
xhr.addEventListener("error", function (e) {
reject(e);
});
if (data) xhr.send(JSON.stringify(data));
else xhr.send();
});
}

7
src/views/LoginView.vue Normal file
View File

@ -0,0 +1,7 @@
<script setup lang="ts">
console.log("Rawr");
</script>
<template>
<div>Rawr!</div>
</template>

View File

@ -16,7 +16,7 @@
<script setup lang="ts">
import { computed, ref } from "vue";
import Modal from "../ui/Modal.vue";
import Modal from "@/components/ui/Modal.vue";
const props = defineProps({
entry: { type: Object, required: true },

View File

@ -15,7 +15,7 @@
<script setup lang="ts">
import { computed, ref } from "vue";
import Modal from "../ui/Modal.vue";
import Modal from "@/components/ui/Modal.vue";
const props = defineProps({
entry: { type: Object, required: true },

View File

@ -19,7 +19,7 @@
<script setup lang="ts">
import { ref } from "vue";
import Modal from "../ui/Modal.vue";
import Modal from "@/components/ui/Modal.vue";
const props = defineProps({
dn: { type: String, required: true },

View File

@ -22,7 +22,7 @@
<script setup lang="ts">
import { ref } from "vue";
import Modal from "../ui/Modal.vue";
import Modal from "@/components/ui/Modal.vue";
const props = defineProps({
entry: { type: Object, required: true },
@ -46,9 +46,9 @@ function onOk() {
return;
}
const parts = dn.value.split(","),
rdnpart = parts[0].split("="),
rdn = rdnpart[0];
const parts = dn.value.split(",");
const rdnpart = parts[0].split("=");
const rdn = rdnpart[0];
if (rdnpart.length != 2) {
error.value = "Invalid RDN: " + parts[0];

View File

@ -32,9 +32,9 @@
<script setup lang="ts">
import { ref } from "vue";
import Modal from "../ui/Modal.vue";
import NodeLabel from "../NodeLabel.vue";
import type { TreeNode } from "../TreeNode";
import Modal from "@/components/ui/Modal.vue";
import NodeLabel from "@/components/NodeLabel.vue";
import type { TreeNode } from "@/data/treeNode";
const props = defineProps({
dn: { type: String, required: true },

View File

@ -20,7 +20,7 @@
<script setup lang="ts">
import { ref } from "vue";
import Modal from "../ui/Modal.vue";
import Modal from "@/components/ui/Modal.vue";
defineProps({
dn: String,

View File

@ -18,8 +18,8 @@
<script setup lang="ts">
import { inject, ref } from "vue";
import Modal from "./ui/Modal.vue";
import type { Provided } from "./Provided";
import Modal from "@/components/ui/Modal.vue";
import type { Provided } from "@/data/provided";
const app = inject<Provided>("app");
const ldifData = ref("");
@ -33,10 +33,10 @@ function init() {
// Load LDIF from file
function upload(evt: Event) {
const target = evt.target as HTMLInputElement,
files = target.files as FileList,
file = files[0],
reader = new FileReader();
const target = evt.target as HTMLInputElement;
const files = target.files as FileList;
const file = files[0];
const reader = new FileReader();
reader.onload = function () {
ldifData.value = reader.result as string;

View File

@ -40,9 +40,9 @@
<script setup lang="ts">
import { computed, inject, ref } from "vue";
import Modal from "../ui/Modal.vue";
import type { Provided } from "../Provided";
import type { Entry } from "./Entry";
import Modal from "@/components/ui/Modal.vue";
import type { Provided } from "@/data/provided";
import type { Entry } from "@/data/entry";
const props = defineProps({
dn: { type: String, required: true },

View File

@ -48,9 +48,10 @@
</template>
<script setup lang="ts">
import { computed, ref } from "vue";
import Modal from "../ui/Modal.vue";
import { computed, inject, ref } from "vue";
import Modal from "@/components/ui/Modal.vue";
import "@/utils";
import type { Provided } from "@/data/provided";
const props = defineProps({
entry: { type: Object, required: true },
@ -58,12 +59,18 @@ const props = defineProps({
returnTo: String,
user: String,
});
const app = inject<Provided>("app");
const oldPassword = ref("");
const newPassword = ref("");
const repeated = ref("");
const passwordOk = ref<boolean>();
const old = ref<HTMLInputElement | null>(null);
const changed = ref<HTMLInputElement | null>(null);
const currentUser = computed(() => props.user == props.entry.meta.dn);
const passwordsMatch = computed(
() => newPassword.value && newPassword.value == repeated.value
@ -97,6 +104,7 @@ async function check() {
method: "POST",
body: JSON.stringify({ check: oldPassword.value }),
headers: {
...(app?.authHeaders.value ?? {}),
"Content-Type": "application/json",
},
});

View File

@ -19,7 +19,7 @@
<script setup lang="ts">
import { computed, ref } from "vue";
import Modal from "../ui/Modal.vue";
import Modal from "@/components/ui/Modal.vue";
const props = defineProps({
entry: { type: Object, required: true },

View File

@ -1,13 +1,15 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.js", "src/**/*.vue"],
"include": ["env.d.ts", "src/**/*", "src/**/*.js", "src/**/*.vue", "src/**/*.ts"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
"@/*": ["./src/*"],
"@views/*": ["./src/views/*"]
}
}
}

View File

@ -7,8 +7,6 @@ import vue from "@vitejs/plugin-vue";
export default defineConfig({
plugins: [vue(), viteCompression()],
base: "./",
build: {
manifest: true,
chunkSizeWarningLimit: 600,
@ -17,6 +15,7 @@ export default defineConfig({
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
"@views": fileURLToPath(new URL("./src/views", import.meta.url)),
},
},