✨ Working create and delete
This commit is contained in:
1 parent
39942447ed
commit
865ae17785
16 files changed
+421
-281
No files matched your search
+5
-5
@@ -30,7 +30,7 @@ class DjangoPostfixDovecotAPI(object):
|
||||
self.username = username
|
||||
self.password = password
|
||||
|
||||
def _api(self, method, path, params=None, json=None):
|
||||
def _request(self, method, path, params=None, json=None):
|
||||
headers = {'content-type': 'application/json'}
|
||||
url = "{}{}".format(self.base_url, path)
|
||||
|
||||
@@ -64,10 +64,10 @@ class DjangoPostfixDovecotAPI(object):
|
||||
return ret
|
||||
|
||||
def create_aliases(self, aliases):
|
||||
return self._api('POST', '/aliases/create_bulk/', json=aliases)
|
||||
return self._request('POST', '/aliases/create_bulk/', json=aliases)
|
||||
|
||||
def delete_aliases(self, aliases):
|
||||
return self._api('DELETE', '/aliases/delete_bulk/', json=aliases)
|
||||
return self._request('DELETE', '/aliases/delete_bulk/', json=aliases)
|
||||
|
||||
def list_aliases_regex(self, regex):
|
||||
""" Limit aliases by regular expression and domain name
|
||||
@@ -77,7 +77,7 @@ class DjangoPostfixDovecotAPI(object):
|
||||
'domain__name': settings.NEUF_EMAIL_DOMAIN_NAME,
|
||||
'source__iregex': regex
|
||||
}
|
||||
return self._api(
|
||||
return self._request(
|
||||
'GET',
|
||||
'/aliases/',
|
||||
params=params
|
||||
@@ -89,7 +89,7 @@ class DjangoPostfixDovecotAPI(object):
|
||||
if name is not None:
|
||||
params['name'] = name
|
||||
|
||||
return self._api(
|
||||
return self._request(
|
||||
'GET',
|
||||
'/domains/',
|
||||
params=params
|
||||
|
||||
@@ -8,7 +8,7 @@ class SourcePrefixOwner(permissions.BasePermission):
|
||||
""" Ref: http://www.django-rest-framework.org/api-guide/permissions """
|
||||
|
||||
def has_permission(self, request, view):
|
||||
assert hasattr(view, 'get_user_prefixes')
|
||||
assert hasattr(view, 'user_prefixes')
|
||||
|
||||
# Read permissions (GET, HEAD or OPTIONS) are allowed for all authenticated request
|
||||
if request.method in permissions.SAFE_METHODS or request.method not in ['DELETE', 'POST']:
|
||||
@@ -22,13 +22,13 @@ class SourcePrefixOwner(permissions.BasePermission):
|
||||
if not serializer.is_valid():
|
||||
return True # Skip auth if not valid data
|
||||
|
||||
my_prefixes = view.get_user_prefixes(request)
|
||||
if len(my_prefixes) == 0:
|
||||
my_prefixes = view.user_prefixes(request)
|
||||
if not my_prefixes:
|
||||
return False
|
||||
|
||||
# Prepare regular expression
|
||||
regexp = my_prefixes.as_regex()
|
||||
# All source-adresses should match the prefix regexp
|
||||
# All source adresses should match the prefix regexp
|
||||
for alias in serializer.initial_data:
|
||||
if re.search(regexp, alias.get('source', '')) is None:
|
||||
return False
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"presets": ["@babel/preset-env", "@babel/preset-react"]
|
||||
"presets": ["@babel/preset-env", "@babel/preset-react"],
|
||||
"plugins": ["@babel/plugin-transform-runtime"]
|
||||
}
|
||||
@@ -15,7 +15,9 @@
|
||||
"no-underscore-dangle": "off",
|
||||
"no-param-reassign": "off",
|
||||
"no-restricted-syntax": "off",
|
||||
"react/prop-types": "off"
|
||||
"react/prop-types": "off",
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"react-hooks/exhaustive-deps": "warn"
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
|
||||
+25
@@ -767,6 +767,18 @@
|
||||
"@babel/helper-plugin-utils": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"@babel/plugin-transform-runtime": {
|
||||
"version": "7.7.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.7.6.tgz",
|
||||
"integrity": "sha512-tajQY+YmXR7JjTwRvwL4HePqoL3DYxpYXIHKVvrOIvJmeHe2y1w4tz5qz9ObUDC9m76rCzIMPyn4eERuwA4a4A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-module-imports": "^7.7.4",
|
||||
"@babel/helper-plugin-utils": "^7.0.0",
|
||||
"resolve": "^1.8.1",
|
||||
"semver": "^5.5.1"
|
||||
}
|
||||
},
|
||||
"@babel/plugin-transform-shorthand-properties": {
|
||||
"version": "7.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.7.4.tgz",
|
||||
@@ -896,6 +908,14 @@
|
||||
"@babel/plugin-transform-react-jsx-source": "^7.7.4"
|
||||
}
|
||||
},
|
||||
"@babel/runtime": {
|
||||
"version": "7.7.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.7.7.tgz",
|
||||
"integrity": "sha512-uCnC2JEVAu8AKB5do1WRIsvrdJ0flYx/A/9f/6chdacnEZ7LmavjdsDXr5ksYBegxtuTPR5Va9/+13QF/kFkCA==",
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.13.2"
|
||||
}
|
||||
},
|
||||
"@babel/template": {
|
||||
"version": "7.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.7.4.tgz",
|
||||
@@ -9746,6 +9766,11 @@
|
||||
"regenerate": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"version": "0.13.3",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz",
|
||||
"integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw=="
|
||||
},
|
||||
"regenerator-transform": {
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.1.tgz",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@apollo/react-hooks": "^3.1.3",
|
||||
"@babel/runtime": "^7.7.7",
|
||||
"@fortawesome/fontawesome-free": "^5.12.0",
|
||||
"apollo-cache-inmemory": "^1.6.5",
|
||||
"apollo-client": "^2.6.8",
|
||||
@@ -34,6 +35,7 @@
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.7.7",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.7.4",
|
||||
"@babel/plugin-transform-runtime": "^7.7.6",
|
||||
"@babel/preset-env": "^7.7.7",
|
||||
"@babel/preset-react": "^7.7.4",
|
||||
"autoprefixer": "^9.7.3",
|
||||
|
||||
@@ -6,15 +6,24 @@ import { InMemoryCache } from 'apollo-cache-inmemory';
|
||||
|
||||
import { OrgUnitList, MainArea } from './components';
|
||||
|
||||
const restLink = new RestLink({ uri: '/api/' });
|
||||
import { getCookie } from './utils';
|
||||
|
||||
const restLink = new RestLink({
|
||||
uri: '/api/',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'x-csrftoken': getCookie('csrftoken')
|
||||
}
|
||||
});
|
||||
const client = new ApolloClient({
|
||||
link: restLink,
|
||||
cache: new InMemoryCache()
|
||||
cache: new InMemoryCache(),
|
||||
typeDefs: {}
|
||||
});
|
||||
|
||||
const App = () => (
|
||||
<ApolloProvider client={client}>
|
||||
<div className="container">
|
||||
<div className="container" style={{ marginTop: '2rem' }}>
|
||||
<div className="row">
|
||||
<OrgUnitList />
|
||||
<MainArea />
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import ctx from 'classnames';
|
||||
import { useMutation } from '@apollo/react-hooks';
|
||||
|
||||
import EmailsTextarea from './EmailsTextarea';
|
||||
import { DELETE_ALIASES_MUTATION } from '../mutations';
|
||||
import { notify } from '../utils';
|
||||
import { GET_ALIASES } from '../queries';
|
||||
|
||||
const AliasRow = ({ id, destination, updateDeletionList }) => {
|
||||
const [checked, setChecked] = useState(false);
|
||||
return (
|
||||
<tr className={ctx({ 'table-danger': checked })}>
|
||||
<td>{destination}</td>
|
||||
<td className="del-cell">
|
||||
<div className="custom-control custom-checkbox custom-control-inline">
|
||||
<input
|
||||
id={`alias-delete-${id}`}
|
||||
type="checkbox"
|
||||
className="custom-control-input"
|
||||
value={checked}
|
||||
onChange={(e) => {
|
||||
setChecked(e.target.checked);
|
||||
updateDeletionList(id, e.target.checked);
|
||||
}}
|
||||
/>
|
||||
<label className="custom-control-label" htmlFor={`alias-delete-${id}`} />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
const ActionRow = ({ onAddEmailClick, deleteVisible, onDelete }) => {
|
||||
return (
|
||||
<tr className="action-row">
|
||||
<td>
|
||||
<a href="#" className="link-add" onClick={onAddEmailClick}>
|
||||
<span className="fas fa-plus" aria-hidden="true" /> Legg til
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" className={ctx('link-del', { visible: deleteVisible })} type="button" onClick={onDelete}>
|
||||
<span className="fas fa-trash" aria-hidden="true" />
|
||||
Slett
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
const AliasList = ({ list, aliases, domain }) => {
|
||||
const [textareaVisible, setTextareaVisible] = useState(false);
|
||||
const [deletionList, setDeletionList] = useState([]);
|
||||
const [deleteAliases] = useMutation(DELETE_ALIASES_MUTATION);
|
||||
|
||||
const onDelete = useCallback(
|
||||
async (e) => {
|
||||
e.preventDefault();
|
||||
const input = deletionList.map((alias) => ({ ...alias, domain: domain.id }));
|
||||
const errorMessage = `Kunne ikke slette e-poster fra ${list}`;
|
||||
try {
|
||||
const { errors } = await deleteAliases({
|
||||
variables: { input },
|
||||
update: (cache) => {
|
||||
setDeletionList([]);
|
||||
const deletedIds = deletionList.map(({ id }) => id);
|
||||
const { aliases: aliasesFromCache } = cache.readQuery({ query: GET_ALIASES });
|
||||
cache.writeQuery({
|
||||
query: GET_ALIASES,
|
||||
data: { aliases: aliasesFromCache.filter(({ id }) => !deletedIds.includes(id)) }
|
||||
});
|
||||
notify('Slettet e-poster', `${deletionList.map((alias) => alias.destination).join('\n')} slettet.`);
|
||||
}
|
||||
});
|
||||
if (errors) {
|
||||
notify('Feil', errorMessage, 'error');
|
||||
console.error(errors);
|
||||
}
|
||||
} catch (err) {
|
||||
notify('Feil', errorMessage, 'error');
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
[deleteAliases, deletionList, domain, list]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="card mb-3">
|
||||
<div className="card-header">
|
||||
<span className="fas fa-list-alt icon-faded" aria-hidden="true" />
|
||||
{list}
|
||||
<span className="badge badge-secondary badge-list-total">{aliases.length}</span>
|
||||
</div>
|
||||
<table className="table table-striped table-hover table-condensed">
|
||||
<tbody>
|
||||
{aliases.map((alias) => (
|
||||
<AliasRow
|
||||
key={alias.id}
|
||||
{...alias}
|
||||
updateDeletionList={(aliasId, checked) => {
|
||||
if (checked) {
|
||||
const { __typename, ...aliasForDeletion } = alias;
|
||||
setDeletionList([...deletionList, aliasForDeletion]); // add
|
||||
} else {
|
||||
setDeletionList(deletionList.filter(({ id }) => id !== aliasId)); // remove
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<ActionRow
|
||||
onAddEmailClick={(e) => {
|
||||
e.preventDefault();
|
||||
setTextareaVisible(!textareaVisible);
|
||||
}}
|
||||
deleteVisible={Boolean(deletionList.length)}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
|
||||
<tr className={ctx('textarea-row', { visible: textareaVisible })}>
|
||||
<td colSpan="2">
|
||||
<EmailsTextarea source={list} domainId={domain.id} />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default AliasList;
|
||||
@@ -0,0 +1,82 @@
|
||||
import React, { useState } from 'react';
|
||||
import ctx from 'classnames';
|
||||
|
||||
import EmailsTextarea from './EmailsTextarea';
|
||||
|
||||
const AliasListCreate = ({ domain, lists, orgUnits }) => {
|
||||
const firstOrgUnit = orgUnits[0].prefixes[0];
|
||||
|
||||
const [textareaVisible, setTextareaVisible] = useState(false);
|
||||
const [name, setName] = useState('');
|
||||
const [selectedPrefix, setSelectedPrefix] = useState(firstOrgUnit);
|
||||
|
||||
const listName = `${selectedPrefix}-${name}@${domain.name}`;
|
||||
const shouldCreate = Boolean(Object.entries(lists).filter(([list]) => list === listName).length);
|
||||
|
||||
return (
|
||||
<div className="new-list-container">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-primary new-list-btn"
|
||||
onClick={() => {
|
||||
setTextareaVisible(!textareaVisible);
|
||||
}}
|
||||
>
|
||||
<span className="fas fa-plus" aria-hidden="true" /> Opprett ny liste
|
||||
</button>
|
||||
<div className={ctx('new-list fwdlist card', { visible: textareaVisible })}>
|
||||
<div className="card-header">
|
||||
<span className="fas fa-list-alt icon-faded" aria-hidden="true" />
|
||||
{name ? listName : `Ny liste`}
|
||||
{name && !shouldCreate && <span className="badge badge-success">Ny</span>}
|
||||
{name && shouldCreate && <span className="badge badge-info">Eksisterende</span>}
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="new-list-name input-group">
|
||||
<div className="input-group-prepend">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn btn-outline-secondary dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
>
|
||||
{selectedPrefix}- <span className="caret" />
|
||||
</button>
|
||||
<div className="dropdown-menu prefix-select">
|
||||
{orgUnits.map((ou) =>
|
||||
ou.prefixes.map((prefix) => (
|
||||
<a
|
||||
key={`${ou.id}-${prefix}`}
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setSelectedPrefix(prefix);
|
||||
}}
|
||||
>
|
||||
{prefix}-
|
||||
</a>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control js-new-list-name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
<div className="input-group-append">
|
||||
<span className="input-group-text">{`@${domain.name}`}</span>
|
||||
</div>
|
||||
</div>
|
||||
<EmailsTextarea source={listName} domainId={domain.id} create={shouldCreate} />
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="new-list-result"></div> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AliasListCreate;
|
||||
@@ -0,0 +1,62 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { useMutation } from '@apollo/react-hooks';
|
||||
|
||||
import { parseEmails, notify } from '../utils';
|
||||
import { CREATE_ALIASES_MUTATION } from '../mutations';
|
||||
import { GET_ALIASES } from '../queries';
|
||||
|
||||
const EmailsTextarea = ({ source, domainId, create = false }) => {
|
||||
const [rawAliases, setRawAliases] = useState('');
|
||||
const [createAliases] = useMutation(CREATE_ALIASES_MUTATION);
|
||||
|
||||
const aliases = parseEmails(rawAliases);
|
||||
|
||||
const onAddClick = useCallback(
|
||||
async (e) => {
|
||||
e.preventDefault();
|
||||
const input = aliases.map((alias) => ({ source, destination: alias, domain: domainId }));
|
||||
const { errors } = await createAliases({
|
||||
variables: { input },
|
||||
update: (cache, { data }) => {
|
||||
setRawAliases('');
|
||||
const { createAliasesResponse: newAliases } = data;
|
||||
const { aliases: aliasesFromCache } = cache.readQuery({ query: GET_ALIASES });
|
||||
cache.writeQuery({
|
||||
query: GET_ALIASES,
|
||||
data: { aliases: aliasesFromCache.concat(newAliases) }
|
||||
});
|
||||
const title = `${newAliases[0].source} ${create ? 'opprettet' : 'oppdatert'}`;
|
||||
notify(title, `${newAliases.map((alias) => alias.destination).join('\n')} lagt til.`);
|
||||
}
|
||||
});
|
||||
|
||||
if (errors) {
|
||||
notify('Feil', `Kunne ikke legge til e-poster til liste ${source}`, 'error');
|
||||
console.error(errors);
|
||||
}
|
||||
},
|
||||
[aliases, create, createAliases, domainId, source]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<h5>
|
||||
Eposter <span className="badge badge-secondary email-counter">{!aliases.length ? '' : aliases.length}</span>
|
||||
</h5>
|
||||
<textarea
|
||||
className="form-control"
|
||||
placeholder="asdf@studentersamfundet.no, qwerty@studentersamfundet.no f.eks. Klipp og lim så mye du orker"
|
||||
rows="4"
|
||||
value={rawAliases}
|
||||
onChange={(e) => {
|
||||
setRawAliases(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<div className="text-right">
|
||||
<button type="button" className="btn btn-primary btn-add-list" onClick={onAddClick}>
|
||||
<span className="fas fa-plus" /> {create ? 'Opprett liste' : 'Legg til'}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default EmailsTextarea;
|
||||
@@ -1,248 +1,43 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import { groupBy, sortBy } from 'lodash';
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import ctx from 'classnames';
|
||||
|
||||
import { parseEmails } from '../utils';
|
||||
import { GET_ALIASES, GET_DOMAINS, GET_ORG_UNITS } from '../queries';
|
||||
import AliasList from './AliasList';
|
||||
import AliasListCreate from './AliasListCreate';
|
||||
|
||||
const ListCreate = () => {
|
||||
const { data, loading, error } = useQuery(GET_ORG_UNITS);
|
||||
const { data: emailData, loading: emailLoading, error: emailError } = useQuery(GET_DOMAINS);
|
||||
const [textareaVisible, setTextareaVisible] = useState(false);
|
||||
const [listName, setListName] = useState('');
|
||||
const [selectedPrefix, setSelectedPrefix] = useState(null);
|
||||
|
||||
if (loading || emailLoading || error || emailError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { orgUnits } = data;
|
||||
const { domains } = emailData;
|
||||
const emailDomain = domains[0];
|
||||
|
||||
return (
|
||||
<div className="new-list-container">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-primary new-list-btn"
|
||||
onClick={() => {
|
||||
setTextareaVisible(!textareaVisible);
|
||||
}}
|
||||
>
|
||||
<span className="fas fa-plus" aria-hidden="true" /> Opprett ny liste
|
||||
</button>
|
||||
<div className={ctx('new-list fwdlist card', { visible: textareaVisible })}>
|
||||
<div className="card-header">
|
||||
<span className="fas fa-list-alt icon-faded" aria-hidden="true" /> <span className="">Ny liste</span>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="new-list-name input-group">
|
||||
<div className="input-group-prepend">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn btn-outline-secondary dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
>
|
||||
{selectedPrefix || orgUnits[0].prefixes[0]}- <span className="caret" />
|
||||
</button>
|
||||
<div className="dropdown-menu prefix-select">
|
||||
{orgUnits.map((ou) =>
|
||||
ou.prefixes.map((prefix) => (
|
||||
<a
|
||||
key={`${ou.id}-${prefix}`}
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setSelectedPrefix(prefix);
|
||||
}}
|
||||
>
|
||||
{prefix}-
|
||||
</a>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control js-new-list-name"
|
||||
value={listName}
|
||||
onChange={(e) => setListName(e.target.value)}
|
||||
/>
|
||||
<div className="input-group-append">
|
||||
<span className="input-group-text">{`@${emailDomain.name}`}</span>
|
||||
</div>
|
||||
</div>
|
||||
<EmailsTextarea listName={listName} selectedPrefix={selectedPrefix} newList={true} />
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="new-list-result"></div> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AliasRow = ({ id, destination, updateDeletionList }) => {
|
||||
const [checked, setChecked] = useState(false);
|
||||
return (
|
||||
<tr className="">
|
||||
<td>{destination}</td>
|
||||
<td className="del-cell">
|
||||
<div className="custom-control custom-checkbox custom-control-inline">
|
||||
<input
|
||||
id={`alias-delete-${id}`}
|
||||
type="checkbox"
|
||||
className="custom-control-input"
|
||||
value={checked}
|
||||
onChange={(e) => {
|
||||
setChecked(e.target.checked);
|
||||
updateDeletionList(id, e.target.checked);
|
||||
}}
|
||||
/>
|
||||
<label className="custom-control-label" htmlFor={`alias-delete-${id}`} />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
const ActionRow = ({ onAddEmailClick, deleteVisible, onDelete }) => {
|
||||
return (
|
||||
<tr className="action-row">
|
||||
<td>
|
||||
<a href="#" className="link-add" onClick={onAddEmailClick}>
|
||||
<span className="fas fa-plus" aria-hidden="true" /> Legg til
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" className={ctx('link-del', { visible: deleteVisible })} type="button" onClick={onDelete}>
|
||||
<span className="fas fa-trash" aria-hidden="true" />
|
||||
Slett
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
const EmailsTextarea = ({ list, selectedPrefix, newList = false }) => {
|
||||
const [rawAliases, setRawAliases] = useState('');
|
||||
const [aliases, setAliases] = useState([]);
|
||||
|
||||
const createAliases = () => {
|
||||
console.log(list, aliases, selectedPrefix, newList);
|
||||
// TODO: POST /aliases
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<h5>
|
||||
Eposter{' '}
|
||||
<span className="badge badge-secondary email-counter">{aliases.length === 0 ? '' : aliases.length}</span>
|
||||
</h5>
|
||||
<textarea
|
||||
className="form-control"
|
||||
placeholder="asdf@studentersamfundet.no, qwerty@studentersamfundet.no f.eks. Klipp og lim så mye du orker"
|
||||
rows="4"
|
||||
value={rawAliases}
|
||||
onChange={(e) => {
|
||||
setRawAliases(e.target.value);
|
||||
setAliases(parseEmails(e.target.value));
|
||||
}}
|
||||
/>
|
||||
<div className="text-right">
|
||||
<button type="button" className="btn btn-primary btn-add-list" onClick={createAliases}>
|
||||
<span className="fas fa-plus" /> {newList ? 'Opprett liste' : 'Legg til'}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ForwardContainer = ({ filterQuery }) => {
|
||||
const MainArea = ({ filterQuery }) => {
|
||||
// TODO: Filter aliases by filterQuery
|
||||
const { data, loading, error } = useQuery(GET_ALIASES);
|
||||
const { data: ouData, loading: ouLoading, error: ouError } = useQuery(GET_ORG_UNITS);
|
||||
const { data: domainData, loading: domainLoading, error: domainError } = useQuery(GET_DOMAINS);
|
||||
|
||||
const [textareaVisible, setTextareaVisible] = useState(false);
|
||||
const [deletionList, setdeletionList] = useState([]);
|
||||
|
||||
const deleteAliases = (aliases) => {
|
||||
// TODO: delete aliases
|
||||
console.log(aliases);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
if (loading || ouLoading || domainLoading) {
|
||||
return (
|
||||
<div className="forwards-container">
|
||||
<span className="fas fa-sync spin" aria-hidden="true" /> Laster...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (error) {
|
||||
if (error || ouError || domainError) {
|
||||
return null;
|
||||
}
|
||||
const { orgUnits } = ouData;
|
||||
const domain = domainData.domains[0];
|
||||
|
||||
const lists = groupBy(
|
||||
sortBy(data.aliases, (alias) => {
|
||||
return alias.source;
|
||||
}),
|
||||
sortBy(data.aliases, (alias) => alias.source),
|
||||
(alias) => alias.source
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="forwards-container">
|
||||
{Object.entries(lists).map(([list, aliases]) => {
|
||||
return (
|
||||
<div key={list} className="card">
|
||||
<div className="card-header">
|
||||
<span className="fas fa-list-alt icon-faded" aria-hidden="true" /> {list}{' '}
|
||||
<span className="badge badge-secondary badge-list-total">{aliases.length}</span>
|
||||
</div>
|
||||
<table className="table table-striped table-hover table-condensed">
|
||||
<tbody>
|
||||
{aliases.map((alias) => (
|
||||
<AliasRow
|
||||
key={alias.id}
|
||||
{...alias}
|
||||
updateDeletionList={(aliasId, checked) => {
|
||||
if (checked) {
|
||||
setdeletionList([...deletionList, aliasId]); // add
|
||||
} else {
|
||||
setdeletionList(deletionList.filter((id) => id !== aliasId)); // remove
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<ActionRow
|
||||
onAddEmailClick={() => {
|
||||
setTextareaVisible(!textareaVisible);
|
||||
}}
|
||||
deleteVisible={Boolean(deletionList.length)}
|
||||
onDelete={(e) => {
|
||||
e.preventDefault();
|
||||
deleteAliases(deletionList);
|
||||
}}
|
||||
/>
|
||||
|
||||
<tr className={ctx('textarea-row', { visible: textareaVisible })}>
|
||||
<td colSpan="2">
|
||||
<EmailsTextarea list={list} />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MainArea = () => {
|
||||
return (
|
||||
<div className="col-sm-8">
|
||||
{/* <div className="orgunit-select-container"></div> */}
|
||||
<ListCreate />
|
||||
<ForwardContainer />
|
||||
<AliasListCreate lists={lists} orgUnits={orgUnits} domain={domain} />
|
||||
<div className="forwards-container">
|
||||
{Object.entries(lists).map(([list, aliases]) => (
|
||||
<AliasList key={list} list={list} aliases={aliases} domain={domain} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const CREATE_ALIASES_MUTATION = gql`
|
||||
mutation createAliases($input: [Alias]!) {
|
||||
createAliasesResponse(input: $input) @rest(type: "Alias", path: "aliases/", method: "POST") {
|
||||
id
|
||||
source
|
||||
destination
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const DELETE_ALIASES_MUTATION = gql`
|
||||
mutation deleteAliases($input: [Alias]!) {
|
||||
deleteAliasesResponse(input: $input) @rest(type: "Alias", path: "aliases/delete/", method: "POST") {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -10,7 +10,6 @@ export const GET_ORG_UNITS = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
// FIXME: only a single domain
|
||||
export const GET_DOMAINS = gql`
|
||||
query Domains {
|
||||
domains @rest(type: "Domain", path: "domains/") {
|
||||
@@ -21,7 +20,7 @@ export const GET_DOMAINS = gql`
|
||||
`;
|
||||
|
||||
export const GET_ALIASES = gql`
|
||||
query OrgUnits {
|
||||
query Aliases {
|
||||
aliases @rest(type: "Alias", path: "aliases/") {
|
||||
id
|
||||
source
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import PNotify from 'pnotify/dist/es/PNotify';
|
||||
import PNotifyButtons from 'pnotify/dist/es/PNotifyButtons';
|
||||
import * as queryString from 'query-string';
|
||||
|
||||
export function parseEmails(text) {
|
||||
@@ -31,9 +32,13 @@ export function setQueryString(obj) {
|
||||
window.history.pushState(null, null, `/lists/${qs}`);
|
||||
}
|
||||
|
||||
export function notify(title, text, type) {
|
||||
/* types: info, success, error */
|
||||
return new PNotify({
|
||||
/**
|
||||
* @param {string} title
|
||||
* @param {string} text
|
||||
* @param {('info'|'success'|'error')} type
|
||||
* */
|
||||
export function notify(title, text, type = 'success') {
|
||||
PNotify.alert({
|
||||
title,
|
||||
text,
|
||||
type,
|
||||
@@ -42,3 +47,19 @@ export function notify(title, text, type) {
|
||||
styling: 'bootstrap4'
|
||||
});
|
||||
}
|
||||
|
||||
export function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim();
|
||||
// Does this cookie string begin with the name we want?
|
||||
if (cookie.substring(0, name.length + 1) === `${name}=`) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
+8
-6
@@ -1,11 +1,12 @@
|
||||
from django.conf.urls import url
|
||||
from django.urls import path, re_path
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from mailadmin.views import MyUserViewSet, MyOrgUnitViewSet, OrgUnitViewSet, AliasesView, get_domains, index, lists
|
||||
from mailadmin.views import MyUserViewSet, MyOrgUnitViewSet, OrgUnitViewSet, AliasesView, get_domains, index, lists, \
|
||||
AliasesDeleteView
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', index, name='index'),
|
||||
url(r'^lists/', lists, name='lists'),
|
||||
re_path(r'^$', index, name='index'),
|
||||
path('lists/', lists, name='lists'),
|
||||
]
|
||||
|
||||
# API
|
||||
@@ -16,6 +17,7 @@ router.register(r'api/orgunits/all', OrgUnitViewSet, basename='allous')
|
||||
urlpatterns += router.urls
|
||||
|
||||
urlpatterns += [
|
||||
url(r'^api/aliases/', AliasesView.as_view(), name='aliases'),
|
||||
url(r'^api/domains/', get_domains, name='email-domains'),
|
||||
path('api/aliases/', AliasesView.as_view(), name='aliases'),
|
||||
path('api/aliases/delete/', AliasesDeleteView.as_view(), name='aliases-delete'),
|
||||
path('api/domains/', get_domains, name='email-domains'),
|
||||
]
|
||||
+25
-33
@@ -46,66 +46,58 @@ def logout(request):
|
||||
return redirect('index')
|
||||
|
||||
|
||||
class AliasesView(views.APIView):
|
||||
""" List, create and delete aliases from django-postfix-dovecot-api """
|
||||
class BaseAliasesView(views.APIView):
|
||||
_api = DjangoPostfixDovecotAPI()
|
||||
permission_classes = (IsAuthenticated, SourcePrefixOwner,)
|
||||
|
||||
def get_groups(self, request):
|
||||
def _user_groups(self, request):
|
||||
group_filter = request.query_params.get('groups', None)
|
||||
if group_filter is None:
|
||||
my_groups = request.user.groups.all()
|
||||
user_groups = request.user.groups.all()
|
||||
else:
|
||||
# Filter groups
|
||||
if ',' in group_filter:
|
||||
group_filter = group_filter.split(',')
|
||||
else:
|
||||
group_filter = [group_filter]
|
||||
my_groups = request.user.groups.filter(pk__in=group_filter)
|
||||
group_filter = group_filter.split(',') if ',' in group_filter else [group_filter]
|
||||
user_groups = request.user.groups.filter(pk__in=group_filter)
|
||||
|
||||
return my_groups
|
||||
return user_groups
|
||||
|
||||
def get_user_prefixes(self, request):
|
||||
my_groups = self.get_groups(request)
|
||||
def user_prefixes(self, request):
|
||||
"""Look for mailinglist prefixes"""
|
||||
orgunits = OrgUnit.objects.filter(is_active=True)
|
||||
if request.user.is_superuser:
|
||||
return Prefix.objects.filter(orgunit__in=orgunits)
|
||||
|
||||
# Look for mailinglist prefixes
|
||||
orgunits = OrgUnit.objects.exclude(is_active=False).distinct()
|
||||
if not request.user.is_superuser:
|
||||
orgunits = orgunits.filter(admin_groups__in=my_groups)
|
||||
orgunits = orgunits.filter(admin_groups__in=self._user_groups(request)).distinct()
|
||||
return Prefix.objects.filter(orgunit__in=orgunits)
|
||||
|
||||
my_prefixes = Prefix.objects.filter(orgunit__in=orgunits)
|
||||
|
||||
return my_prefixes
|
||||
|
||||
class AliasesView(BaseAliasesView):
|
||||
""" List and create aliases from django-postfix-dovecot-api """
|
||||
def get(self, request):
|
||||
my_prefixes = self.get_user_prefixes(request)
|
||||
user_prefixes = self.user_prefixes(request)
|
||||
|
||||
# Prepare regular expression
|
||||
regex = my_prefixes.as_regex()
|
||||
if len(my_prefixes) == 0:
|
||||
regex = user_prefixes.as_regex()
|
||||
if len(user_prefixes) == 0:
|
||||
return Response({'result': 'No results'}) # Need at least one
|
||||
|
||||
res = self._api.list_aliases_regex(regex)
|
||||
|
||||
return Response(res)
|
||||
return Response(self._api.list_aliases_regex(regex))
|
||||
|
||||
def post(self, request):
|
||||
aliases = AliasSerializer(data=request.data, many=True)
|
||||
if not aliases.is_valid():
|
||||
return Response(aliases.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
res = self._api.create_aliases(aliases.data)
|
||||
return Response(self._api.create_aliases(aliases.data))
|
||||
|
||||
return Response(res)
|
||||
|
||||
def delete(self, request):
|
||||
class AliasesDeleteView(BaseAliasesView):
|
||||
""" Delete aliases (using POST) from django-postfix-dovecot-api """
|
||||
def post(self, request):
|
||||
aliases = AliasSerializer(data=request.data, many=True)
|
||||
if not aliases.is_valid():
|
||||
return Response(aliases.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
res = self._api.delete_aliases(aliases.data)
|
||||
|
||||
return Response(res)
|
||||
return Response(self._api.delete_aliases(aliases.data))
|
||||
|
||||
|
||||
class MyUserViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
@@ -123,7 +115,7 @@ class MyOrgUnitViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
orgunits = OrgUnit.objects.exclude(prefixes__isnull=True).exclude(is_active=False).distinct()
|
||||
orgunits = OrgUnit.objects.filter(prefixes__isnull=False, is_active=True).distinct()
|
||||
if self.request.user.is_superuser:
|
||||
return orgunits
|
||||
|
||||
|
||||
Reference in new issue
Block a user