Add mailboxes
This commit is contained in:
parent
aa9c5c2f53
commit
b5acd3feb1
|
@ -35,6 +35,8 @@ import FixUserMainEmailRelation from "wms-core/auth/migrations/FixUserMainEmailR
|
|||
import DropNameFromUsers from "wms-core/auth/migrations/DropNameFromUsers";
|
||||
import MagicLink from "wms-core/auth/models/MagicLink";
|
||||
import AddNameToUsers from "./migrations/AddNameToUsers";
|
||||
import CreateMailTables from "./migrations/CreateMailTables";
|
||||
import MailboxBackendController from "./controllers/MailboxBackendController";
|
||||
|
||||
export default class App extends Application {
|
||||
private readonly port: number;
|
||||
|
@ -56,6 +58,7 @@ export default class App extends Application {
|
|||
FixUserMainEmailRelation,
|
||||
DropNameFromUsers,
|
||||
AddNameToUsers,
|
||||
CreateMailTables,
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -123,6 +126,7 @@ export default class App extends Application {
|
|||
this.use(new AccountController());
|
||||
this.use(new MagicLinkController(this.magicLinkWebSocketListener!))
|
||||
this.use(new BackendController());
|
||||
this.use(new MailboxBackendController());
|
||||
this.use(new AuthController());
|
||||
|
||||
// Core functionality
|
||||
|
|
|
@ -8,18 +8,47 @@ import MagicLinkController from "./MagicLinkController";
|
|||
import {MagicLinkActionType} from "./MagicLinkActionType";
|
||||
import UserEmail from "wms-core/auth/models/UserEmail";
|
||||
import {BadRequestError, ForbiddenHttpError, NotFoundHttpError} from "wms-core/HttpError";
|
||||
import MailDomain from "../models/MailDomain";
|
||||
import UserMailIdentityComponent from "../models/UserMailIdentityComponent";
|
||||
import MailIdentity from "../models/MailIdentity";
|
||||
import UserNameComponent from "../models/UserNameComponent";
|
||||
import {WhereOperator, WhereTest} from "wms-core/db/ModelQuery";
|
||||
|
||||
export default class AccountController extends Controller {
|
||||
public getRoutesPrefix(): string {
|
||||
return '/account';
|
||||
}
|
||||
|
||||
routes(): void {
|
||||
this.get('/account', this.getAccount, 'account', REQUIRE_AUTH_MIDDLEWARE);
|
||||
this.get('/', this.getAccount, 'account', REQUIRE_AUTH_MIDDLEWARE);
|
||||
this.post('/add-recovery-email', this.addRecoveryEmail, 'add-recovery-email', REQUIRE_AUTH_MIDDLEWARE);
|
||||
this.post('/set-main-email', this.postSetMainEmail, 'set-main-email', REQUIRE_AUTH_MIDDLEWARE);
|
||||
this.post('/remove-email', this.postRemoveEmail, 'remove-email', REQUIRE_AUTH_MIDDLEWARE);
|
||||
this.post('/set-main-email', this.postSetMainRecoveryEmail, 'set-main-recovery-email', REQUIRE_AUTH_MIDDLEWARE);
|
||||
this.post('/remove-email', this.postRemoveRecoveryEmail, 'remove-recovery-email', REQUIRE_AUTH_MIDDLEWARE);
|
||||
|
||||
this.post('/create-mail-identity', this.postCreateMailIdentity, 'create-mail-identity', REQUIRE_AUTH_MIDDLEWARE);
|
||||
this.post('/delete-mail-identity', this.postDeleteMailIdentity, 'delete-mail-identity', REQUIRE_AUTH_MIDDLEWARE);
|
||||
}
|
||||
|
||||
protected async getAccount(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
const user = req.models.user!;
|
||||
const userMailIdentity = user.as(UserMailIdentityComponent);
|
||||
|
||||
res.render('account', {
|
||||
emails: await req.models.user!.emails.get(),
|
||||
emails: await user.emails.get(),
|
||||
mailboxIdentity: await (await userMailIdentity.mainMailIdentity.get())?.toEmail(),
|
||||
identities: await Promise.all((await userMailIdentity.mailIdentities.get()).map(async identity => ({
|
||||
id: identity.id,
|
||||
email: await identity.toEmail(),
|
||||
}))),
|
||||
domains: (await MailDomain.select()
|
||||
.where('user_id', user.id!)
|
||||
.where('user_id', null, WhereTest.EQ, WhereOperator.OR)
|
||||
.sortBy('user_id', 'DESC')
|
||||
.get())
|
||||
.map(d => ({
|
||||
value: d.id,
|
||||
display: d.name,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -53,7 +82,7 @@ export default class AccountController extends Controller {
|
|||
}));
|
||||
}
|
||||
|
||||
protected async postSetMainEmail(req: Request, res: Response): Promise<void> {
|
||||
protected async postSetMainRecoveryEmail(req: Request, res: Response): Promise<void> {
|
||||
if (!req.body.id)
|
||||
throw new BadRequestError('Missing id field', 'Check form parameters.', req.url);
|
||||
|
||||
|
@ -69,10 +98,10 @@ export default class AccountController extends Controller {
|
|||
await req.models.user!.save();
|
||||
|
||||
req.flash('success', 'This email was successfully set as your main address.');
|
||||
res.redirect(Controller.route('account'));
|
||||
res.redirectBack();
|
||||
}
|
||||
|
||||
protected async postRemoveEmail(req: Request, res: Response): Promise<void> {
|
||||
protected async postRemoveRecoveryEmail(req: Request, res: Response): Promise<void> {
|
||||
if (!req.body.id)
|
||||
throw new BadRequestError('Missing id field', 'Check form parameters.', req.url);
|
||||
|
||||
|
@ -87,6 +116,64 @@ export default class AccountController extends Controller {
|
|||
await userEmail.delete();
|
||||
|
||||
req.flash('success', 'This email was successfully removed from your account.');
|
||||
res.redirect(Controller.route('account'));
|
||||
res.redirectBack();
|
||||
}
|
||||
|
||||
protected async postCreateMailIdentity(req: Request, res: Response): Promise<void> {
|
||||
const domain = await MailDomain.getById(req.body.mail_domain_id);
|
||||
if (!domain) throw new NotFoundHttpError('domain', req.url);
|
||||
|
||||
const user = req.models.user!;
|
||||
const mailIdentityComponent = user.as(UserMailIdentityComponent);
|
||||
|
||||
const identity = MailIdentity.create({
|
||||
user_id: user.id,
|
||||
name: req.body.name,
|
||||
mail_domain_id: req.body.mail_domain_id,
|
||||
});
|
||||
|
||||
// Check whether this identity can be created by this user
|
||||
if (domain.isPublic()) {
|
||||
await this.validate({
|
||||
name: new Validator<string>().defined().equals(user.as(UserNameComponent).name),
|
||||
}, req.body);
|
||||
if ((await mailIdentityComponent.getPublicAddressesCount()) >= mailIdentityComponent.getMaxPublicAddressesCount()) {
|
||||
req.flash('error', 'You have reached maximum public email addresses.');
|
||||
res.redirectBack();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!domain.canCreateAddresses(user)) {
|
||||
throw new ForbiddenHttpError('domain', req.url);
|
||||
}
|
||||
}
|
||||
|
||||
// Save identity
|
||||
await identity.save();
|
||||
|
||||
// Set main mail identity if not already set
|
||||
if (!mailIdentityComponent.main_mail_identity_id) {
|
||||
mailIdentityComponent.main_mail_identity_id = identity.id;
|
||||
await user.save();
|
||||
req.flash('info', 'Congratulations! You just created your mailbox.');
|
||||
}
|
||||
|
||||
req.flash('success', 'Mail identity ' + (await identity.toEmail()) + ' successfully created.')
|
||||
res.redirectBack();
|
||||
}
|
||||
|
||||
protected async postDeleteMailIdentity(req: Request, res: Response): Promise<void> {
|
||||
const identity = await MailIdentity.getById(req.body.id);
|
||||
if (!identity) throw new NotFoundHttpError('Mail identity', req.url);
|
||||
if (identity.user_id !== req.models.user!.id) throw new ForbiddenHttpError('mail identity', req.url);
|
||||
if (req.models.user!.as(UserMailIdentityComponent).main_mail_identity_id === identity.id) {
|
||||
req.flash('error', 'Cannot delete your mailbox identity.');
|
||||
res.redirectBack();
|
||||
return;
|
||||
}
|
||||
|
||||
await identity.delete();
|
||||
req.flash('success', 'Identity ' + (await identity.toEmail()) + ' successfully deleted.');
|
||||
res.redirectBack();
|
||||
}
|
||||
}
|
|
@ -6,7 +6,6 @@ import {MagicLinkActionType} from "./MagicLinkActionType";
|
|||
import Controller from "wms-core/Controller";
|
||||
import {BadOwnerMagicLink} from "wms-core/auth/magic_link/MagicLinkAuthController";
|
||||
import UserEmail from "wms-core/auth/models/UserEmail";
|
||||
import ModelFactory from "wms-core/db/ModelFactory";
|
||||
|
||||
export default class MagicLinkController extends _MagicLinkController {
|
||||
constructor(magicLinkWebSocketListener: MagicLinkWebSocketListener) {
|
||||
|
@ -32,7 +31,7 @@ export default class MagicLinkController extends _MagicLinkController {
|
|||
break;
|
||||
}
|
||||
|
||||
const userEmail = ModelFactory.get(UserEmail).make({
|
||||
const userEmail = UserEmail.create({
|
||||
user_id: user.id,
|
||||
email: email,
|
||||
main: false,
|
||||
|
|
|
@ -0,0 +1,176 @@
|
|||
import Controller from "wms-core/Controller";
|
||||
import {REQUIRE_ADMIN_MIDDLEWARE, REQUIRE_AUTH_MIDDLEWARE} from "wms-core/auth/AuthComponent";
|
||||
import {Request, Response} from "express";
|
||||
import User from "wms-core/auth/models/User";
|
||||
import {WhereTest} from "wms-core/db/ModelQuery";
|
||||
import UserNameComponent from "../models/UserNameComponent";
|
||||
import UserMailIdentityComponent from "../models/UserMailIdentityComponent";
|
||||
import {NotFoundHttpError} from "wms-core/HttpError";
|
||||
import MailDomain from "../models/MailDomain";
|
||||
import BackendController from "wms-core/helpers/BackendController";
|
||||
import MailIdentity from "../models/MailIdentity";
|
||||
|
||||
export default class MailboxBackendController extends Controller {
|
||||
public constructor() {
|
||||
super();
|
||||
BackendController.registerMenuElement({
|
||||
getLink: async () => Controller.route('backend-mailboxes'),
|
||||
getDisplayString: async () => 'Mailboxes',
|
||||
getDisplayIcon: async () => 'mail',
|
||||
});
|
||||
}
|
||||
|
||||
public getRoutesPrefix(): string {
|
||||
return '/backend/mailboxes';
|
||||
}
|
||||
|
||||
public routes(): void {
|
||||
this.get('/', this.getMailboxesBackend, 'backend-mailboxes', REQUIRE_AUTH_MIDDLEWARE, REQUIRE_ADMIN_MIDDLEWARE);
|
||||
this.get('/:id', this.getMailboxBackend, 'backend-mailbox', REQUIRE_AUTH_MIDDLEWARE, REQUIRE_ADMIN_MIDDLEWARE);
|
||||
|
||||
this.post('/add-domain', this.postAddDomain, 'backend-add-domain', REQUIRE_AUTH_MIDDLEWARE, REQUIRE_ADMIN_MIDDLEWARE)
|
||||
this.post('/remove-domain', this.postRemoveDomain, 'backend-remove-domain', REQUIRE_AUTH_MIDDLEWARE, REQUIRE_ADMIN_MIDDLEWARE)
|
||||
|
||||
this.post('/:id/create-mail-identity', this.postCreateMailIdentity, 'backend-create-mail-identity', REQUIRE_AUTH_MIDDLEWARE, REQUIRE_ADMIN_MIDDLEWARE);
|
||||
this.post('/delete-mail-identity', this.postDeleteMailIdentity, 'backend-delete-mail-identity', REQUIRE_AUTH_MIDDLEWARE, REQUIRE_ADMIN_MIDDLEWARE);
|
||||
}
|
||||
|
||||
protected async getMailboxesBackend(req: Request, res: Response): Promise<void> {
|
||||
const mailDomains = await MailDomain.select()
|
||||
.with('owner')
|
||||
.with('identities')
|
||||
.get();
|
||||
|
||||
const users = await User.select()
|
||||
.where('main_mail_identity_id', null, WhereTest.NE)
|
||||
.with('mainMailIdentity')
|
||||
.with('mailIdentities')
|
||||
.get();
|
||||
|
||||
res.render('backend/mailboxes', {
|
||||
domains: await Promise.all(mailDomains.map(async domain => ({
|
||||
id: domain.id,
|
||||
name: domain.name,
|
||||
owner_name: (await domain.owner.get())?.as(UserNameComponent).name,
|
||||
identity_count: (await domain.identities.get()).length,
|
||||
}))),
|
||||
users: [{
|
||||
value: 0,
|
||||
display: 'Public',
|
||||
}, ...(await User.select().get()).map(u => ({
|
||||
value: u.id,
|
||||
display: u.name,
|
||||
}))],
|
||||
mailboxes: await Promise.all(users.map(async user => ({
|
||||
id: user.id,
|
||||
username: user.as(UserNameComponent).name,
|
||||
name: await (await user.as(UserMailIdentityComponent).mainMailIdentity.get())?.toEmail(),
|
||||
identity_count: (await user.as(UserMailIdentityComponent).mailIdentities.get()).length,
|
||||
domain_count: (await user.as(UserMailIdentityComponent).mailDomains.get()).length,
|
||||
}))),
|
||||
});
|
||||
}
|
||||
|
||||
protected async getMailboxBackend(req: Request, res: Response): Promise<void> {
|
||||
const user = await User.select()
|
||||
.where('id', req.params.id)
|
||||
.with('mailIdentities')
|
||||
.first();
|
||||
if (!user) throw new NotFoundHttpError('User', req.url);
|
||||
|
||||
res.render('backend/mailbox', {
|
||||
mailbox: {
|
||||
id: user.id,
|
||||
name: await (await user.as(UserMailIdentityComponent).mainMailIdentity.get())?.toEmail() || 'Not created.',
|
||||
},
|
||||
domains: (await MailDomain.select().get()).map(d => ({
|
||||
display: d.name,
|
||||
value: d.id,
|
||||
})),
|
||||
identities: await Promise.all((await user.as(UserMailIdentityComponent).mailIdentities.get()).map(async i => ({
|
||||
id: i.id,
|
||||
email: await i.toEmail(),
|
||||
}))),
|
||||
});
|
||||
}
|
||||
|
||||
protected async postAddDomain(req: Request, res: Response): Promise<void> {
|
||||
const domain = MailDomain.create({
|
||||
name: req.body.name,
|
||||
user_id: req.body.user_id,
|
||||
});
|
||||
await domain.save();
|
||||
req.flash('success', `Domain ${domain.name} successfully added with owner ${(await domain.owner.get())?.name}`);
|
||||
res.redirectBack();
|
||||
}
|
||||
|
||||
protected async postRemoveDomain(req: Request, res: Response): Promise<void> {
|
||||
const domain = await MailDomain.select()
|
||||
.where('id', req.body.id)
|
||||
.with('identities')
|
||||
.first();
|
||||
if (!domain) throw new NotFoundHttpError('Domain', req.url);
|
||||
|
||||
// Don't delete that domain if it still has identities
|
||||
if ((await domain.identities.get()).length > 0) {
|
||||
req.flash('error', `This domain still has identities. Please remove all of these first (don't forget to rename mailboxes).`);
|
||||
res.redirectBack();
|
||||
return;
|
||||
}
|
||||
|
||||
await domain.delete();
|
||||
|
||||
req.flash('success', `Domain ${domain.name} successfully deleted.`);
|
||||
res.redirectBack();
|
||||
}
|
||||
|
||||
protected async postCreateMailIdentity(req: Request, res: Response): Promise<void> {
|
||||
const user = await User.select()
|
||||
.where('id', req.params.id)
|
||||
.first();
|
||||
if (!user) throw new NotFoundHttpError('User', req.url);
|
||||
|
||||
const domain = await MailDomain.getById(req.body.mail_domain_id);
|
||||
if (!domain) throw new NotFoundHttpError('domain', req.url);
|
||||
|
||||
const mailIdentityComponent = user.as(UserMailIdentityComponent);
|
||||
|
||||
const identity = MailIdentity.create({
|
||||
user_id: user.id,
|
||||
name: req.body.name,
|
||||
mail_domain_id: req.body.mail_domain_id,
|
||||
});
|
||||
|
||||
// Save identity
|
||||
await identity.save();
|
||||
|
||||
// Set main mail identity if not already set
|
||||
if (!mailIdentityComponent.main_mail_identity_id) {
|
||||
mailIdentityComponent.main_mail_identity_id = identity.id;
|
||||
await user.save();
|
||||
req.flash('info', 'Mailbox created.');
|
||||
}
|
||||
|
||||
req.flash('success', 'Mail identity ' + (await identity.toEmail()) + ' successfully created.')
|
||||
res.redirectBack();
|
||||
}
|
||||
|
||||
protected async postDeleteMailIdentity(req: Request, res: Response): Promise<void> {
|
||||
const identity = await MailIdentity.select()
|
||||
.where('id', req.body.id)
|
||||
.with('user')
|
||||
.first();
|
||||
if (!identity) throw new NotFoundHttpError('Mail identity', req.url);
|
||||
|
||||
const user = await identity.user.get();
|
||||
if (user?.as(UserMailIdentityComponent).main_mail_identity_id === identity.id) {
|
||||
req.flash('error', `Cannot delete this user's mailbox identity.`);
|
||||
res.redirectBack();
|
||||
return;
|
||||
}
|
||||
|
||||
await identity.delete();
|
||||
req.flash('success', 'Identity ' + (await identity.toEmail()) + ' successfully deleted.');
|
||||
res.redirectBack();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import Migration from "wms-core/db/Migration";
|
||||
import {Connection} from "mysql";
|
||||
import ModelFactory from "wms-core/db/ModelFactory";
|
||||
import MailDomain from "../models/MailDomain";
|
||||
import MailIdentity from "../models/MailIdentity";
|
||||
import User from "wms-core/auth/models/User";
|
||||
import UserMailIdentityComponent from "../models/UserMailIdentityComponent";
|
||||
|
||||
export default class CreateMailTables extends Migration {
|
||||
public async install(connection: Connection): Promise<void> {
|
||||
await this.query(`CREATE TABLE mail_domains
|
||||
(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
name VARCHAR(252) UNIQUE NOT NULL,
|
||||
user_id INT,
|
||||
PRIMARY KEY (id),
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL
|
||||
)`, connection);
|
||||
await this.query(`CREATE TABLE mail_identities
|
||||
(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
user_id INT NOT NULL,
|
||||
mail_domain_id INT NOT NULL,
|
||||
name VARCHAR(64) NOT NULL,
|
||||
redirects_to VARCHAR(254),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE (mail_domain_id, name),
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (mail_domain_id) REFERENCES mail_domains (id) ON DELETE CASCADE
|
||||
)`, connection);
|
||||
await this.query(`ALTER TABLE users
|
||||
ADD COLUMN main_mail_identity_id INT,
|
||||
ADD FOREIGN KEY main_mail_identity_fk (main_mail_identity_id) REFERENCES mail_identities (id)`, connection);
|
||||
}
|
||||
|
||||
public async rollback(connection: Connection): Promise<void> {
|
||||
await this.query(`ALTER TABLE users
|
||||
DROP FOREIGN KEY main_mail_identity_fk,
|
||||
DROP COLUMN main_mail_identity_id`, connection);
|
||||
await this.query(`DROP TABLE IF EXISTS mail_identities, mail_domains`, connection);
|
||||
}
|
||||
|
||||
public registerModels(): void {
|
||||
ModelFactory.register(MailDomain);
|
||||
ModelFactory.register(MailIdentity);
|
||||
ModelFactory.get(User).addComponent(UserMailIdentityComponent);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import Model from "wms-core/db/Model";
|
||||
import User from "wms-core/auth/models/User";
|
||||
import {ManyModelRelation, OneModelRelation} from "wms-core/db/ModelRelation";
|
||||
import ModelFactory from "wms-core/db/ModelFactory";
|
||||
import MailIdentity from "./MailIdentity";
|
||||
|
||||
export default class MailDomain extends Model {
|
||||
public id?: number = undefined;
|
||||
public name?: string = undefined;
|
||||
public user_id?: number = undefined;
|
||||
|
||||
public readonly owner: OneModelRelation<MailDomain, User> = new OneModelRelation(this, ModelFactory.get(User), {
|
||||
localKey: 'user_id',
|
||||
foreignKey: 'id',
|
||||
});
|
||||
|
||||
public readonly identities: ManyModelRelation<MailDomain, MailIdentity> = new ManyModelRelation(this, ModelFactory.get(MailIdentity), {
|
||||
localKey: 'id',
|
||||
foreignKey: 'mail_domain_id',
|
||||
});
|
||||
|
||||
|
||||
public updateWithData(data: any) {
|
||||
super.updateWithData(data);
|
||||
if (typeof this.user_id !== 'undefined' && this.user_id <= 0) {
|
||||
this.user_id = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected init(): void {
|
||||
this.setValidation('name').defined().maxLength(252).regexp(/^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/)
|
||||
this.setValidation('user_id').acceptUndefined().exists(User, 'id');
|
||||
}
|
||||
|
||||
public isPublic(): boolean {
|
||||
return !this.user_id;
|
||||
}
|
||||
|
||||
public canCreateAddresses(user: User): boolean {
|
||||
return this.user_id === user.id || this.isPublic();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
import Model, {EMAIL_REGEX} from "wms-core/db/Model";
|
||||
import User from "wms-core/auth/models/User";
|
||||
import MailDomain from "./MailDomain";
|
||||
import {OneModelRelation} from "wms-core/db/ModelRelation";
|
||||
import ModelFactory from "wms-core/db/ModelFactory";
|
||||
|
||||
export default class MailIdentity extends Model {
|
||||
public static get table(): string {
|
||||
return 'mail_identities';
|
||||
}
|
||||
|
||||
public id?: number = undefined;
|
||||
public user_id?: number = undefined;
|
||||
public mail_domain_id?: number = undefined;
|
||||
public name?: string = undefined;
|
||||
public redirects_to?: string = undefined;
|
||||
|
||||
public readonly user: OneModelRelation<MailIdentity, User> = new OneModelRelation(this, ModelFactory.get(User), {
|
||||
foreignKey: 'id',
|
||||
localKey: 'user_id',
|
||||
});
|
||||
public readonly domain: OneModelRelation<MailIdentity, MailDomain> = new OneModelRelation(this, ModelFactory.get(MailDomain), {
|
||||
foreignKey: 'id',
|
||||
localKey: 'mail_domain_id',
|
||||
});
|
||||
|
||||
protected init(): void {
|
||||
this.setValidation('user_id').defined().exists(User, 'id');
|
||||
this.setValidation('mail_domain_id').defined().exists(MailDomain, 'id');
|
||||
this.setValidation('name').defined().maxLength(64).regexp(/^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+$/)
|
||||
.unique(this, 'name', () => MailIdentity.select().where('mail_domain_id', this.mail_domain_id));
|
||||
this.setValidation('redirects_to').acceptUndefined().maxLength(254).regexp(EMAIL_REGEX);
|
||||
}
|
||||
|
||||
public async toEmail(): Promise<string> {
|
||||
return this.name + '@' + (await this.domain.get())!.name;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
import ModelComponent from "wms-core/db/ModelComponent";
|
||||
import User from "wms-core/auth/models/User";
|
||||
import {ManyModelRelation, OneModelRelation} from "wms-core/db/ModelRelation";
|
||||
import MailIdentity from "./MailIdentity";
|
||||
import ModelFactory from "wms-core/db/ModelFactory";
|
||||
import MailDomain from "./MailDomain";
|
||||
|
||||
export default class UserMailIdentityComponent extends ModelComponent<User> {
|
||||
public main_mail_identity_id?: number = undefined;
|
||||
|
||||
public readonly mailIdentities: ManyModelRelation<User, MailIdentity> = new ManyModelRelation(this._model, ModelFactory.get(MailIdentity), {
|
||||
localKey: 'id',
|
||||
foreignKey: 'user_id',
|
||||
});
|
||||
public readonly publicMailIdentities: ManyModelRelation<User, MailIdentity> = this.mailIdentities.clone()
|
||||
.filter(async model => (await model.domain.get())!.isPublic());
|
||||
|
||||
public readonly mailDomains: ManyModelRelation<User, MailDomain> = new ManyModelRelation(this._model, ModelFactory.get(MailDomain), {
|
||||
localKey: 'id',
|
||||
foreignKey: 'user_id',
|
||||
});
|
||||
|
||||
public readonly mainMailIdentity: OneModelRelation<User, MailIdentity> = new OneModelRelation(this._model, ModelFactory.get(MailIdentity), {
|
||||
foreignKey: 'id',
|
||||
localKey: 'main_mail_identity_id',
|
||||
});
|
||||
|
||||
protected init(): void {
|
||||
}
|
||||
|
||||
public getMaxPublicAddressesCount(): number {
|
||||
return 1;
|
||||
}
|
||||
|
||||
public async getPublicAddressesCount(): Promise<number> {
|
||||
return (await this.publicMailIdentities.get()).length;
|
||||
}
|
||||
}
|
|
@ -7,7 +7,6 @@ export default class UserNameComponent extends ModelComponent<User> {
|
|||
public name?: string = undefined;
|
||||
|
||||
public init(): void {
|
||||
super.init();
|
||||
this.setValidation('name').defined().between(3, 64).regexp(USERNAME_REGEXP).unique(this._model);
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,6 @@ export default class UserPasswordComponent extends ModelComponent<User> {
|
|||
}
|
||||
|
||||
public init(): void {
|
||||
super.init();
|
||||
this.setValidation('password').acceptUndefined().maxLength(128);
|
||||
}
|
||||
|
||||
|
|
|
@ -3,74 +3,136 @@
|
|||
{% set title = 'ALDAP - Welcome to the toot.party auth center!' %}
|
||||
|
||||
{% block body %}
|
||||
<div class="container">
|
||||
<main class="panel">
|
||||
<main class="container">
|
||||
<div class="panel">
|
||||
<i data-feather="user"></i>
|
||||
<h1>My account</h1>
|
||||
|
||||
<p>Name: {{ user.name }}</p>
|
||||
</div>
|
||||
|
||||
<section class="sub-panel">
|
||||
<h2>Email addresses</h2>
|
||||
<section class="panel">
|
||||
<h2><i data-feather="shield"></i> Recovery email addresses</h2>
|
||||
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Address</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for email in emails %}
|
||||
{% if email.id == user.main_email_id %}
|
||||
<tr>
|
||||
<td>Main</td>
|
||||
<td>{{ email.email }}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for email in emails %}
|
||||
{% if email.id != user.main_email_id %}
|
||||
<tr>
|
||||
<td>Secondary</td>
|
||||
<td>{{ email.email }}</td>
|
||||
<td class="actions">
|
||||
<form action="{{ route('set-main-recovery-email') }}" method="POST">
|
||||
<input type="hidden" name="id" value="{{ email.id }}">
|
||||
|
||||
<button class="warning"
|
||||
onclick="return confirm('Are you sure you want to set {{ email.email }} as your main address?');">
|
||||
<i data-feather="refresh-ccw"></i> Set as main address
|
||||
</button>
|
||||
{{ macros.csrf(getCSRFToken) }}
|
||||
</form>
|
||||
|
||||
<form action="{{ route('remove-recovery-email') }}" method="POST">
|
||||
<input type="hidden" name="id" value="{{ email.id }}">
|
||||
|
||||
<button class="danger"
|
||||
onclick="return confirm('Are you sure you want to delete {{ email.email }}?');">
|
||||
<i data-feather="trash"></i> Remove
|
||||
</button>
|
||||
{{ macros.csrf(getCSRFToken) }}
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<form action="{{ route('add-recovery-email') }}" method="POST" class="sub-panel">
|
||||
<h3>Add a recovery email address:</h3>
|
||||
|
||||
{{ macros.field(_locals, 'email', 'email', null, 'Choose a safe email address', 'An email we can use to identify you in case you lose access to your account', 'required') }}
|
||||
|
||||
<button><i data-feather="plus"></i> Add recovery email</button>
|
||||
|
||||
{{ macros.csrf(getCSRFToken) }}
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2><i data-feather="mail"></i> Mailbox</h2>
|
||||
|
||||
<p class="center">{% if mailboxIdentity == null %}(Not created yet){% else %}{{ mailboxIdentity }}{% endif %}</p>
|
||||
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Email</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for identity in identities %}
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Address</th>
|
||||
<th>Actions</th>
|
||||
<td>{{ identity.id }}</td>
|
||||
<td>{{ identity.email }}</td>
|
||||
<td class="actions">
|
||||
<form action="{{ route('delete-mail-identity') }}" method="POST">
|
||||
<input type="hidden" name="id" value="{{ identity.id }}">
|
||||
|
||||
<button class="danger"
|
||||
onclick="return confirm('Are you sure you want to delete {{ identity.email }}?')">
|
||||
<i data-feather="trash"></i> Delete
|
||||
</button>
|
||||
|
||||
{{ macros.csrf(getCSRFToken) }}
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="3"><i data-feather="shield-off"></i> No recovery email address.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<tbody>
|
||||
{% for email in emails %}
|
||||
{% if email.id == user.main_email_id %}
|
||||
<tr>
|
||||
<td>Main</td>
|
||||
<td>{{ email.email }}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for email in emails %}
|
||||
{% if email.id != user.main_email_id %}
|
||||
<tr>
|
||||
<td>Secondary</td>
|
||||
<td>{{ email.email }}</td>
|
||||
<td class="actions">
|
||||
<form action="{{ route('set-main-email') }}" method="POST">
|
||||
<input type="hidden" name="id" value="{{ email.id }}">
|
||||
<form action="{{ route('create-mail-identity') }}" method="POST" class="sub-panel">
|
||||
<h3>{% if mailboxIdentity == null %}Create your mailbox{% else %}Create a new mail identity{% endif %}</h3>
|
||||
|
||||
<button class="warning"
|
||||
onclick="return confirm('Are you sure you want to set {{ email.email }} as your main address?');">
|
||||
<i data-feather="refresh-ccw"></i> Set as main address
|
||||
</button>
|
||||
{{ macros.csrf(getCSRFToken) }}
|
||||
</form>
|
||||
<div class="inline-fields">
|
||||
{{ macros.field(_locals, 'text', 'name', user.name, 'Email name', null, 'required') }}
|
||||
<span>@</span>
|
||||
{{ macros.field(_locals, 'select', 'mail_domain_id', null, 'Choose the email domain', null, 'required', domains) }}
|
||||
</div>
|
||||
<div class="hint">
|
||||
<i data-feather="info"></i>
|
||||
If using a "public" domain, can only be set to your username.
|
||||
</div>
|
||||
|
||||
<form action="{{ route('remove-email') }}" method="POST">
|
||||
<input type="hidden" name="id" value="{{ email.id }}">
|
||||
<button><i data-feather="plus"></i> Create</button>
|
||||
|
||||
<button class="danger"
|
||||
onclick="return confirm('Are you sure you want to delete {{ email.email }}?');">
|
||||
<i data-feather="trash"></i> Remove
|
||||
</button>
|
||||
{{ macros.csrf(getCSRFToken) }}
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<form action="{{ route('add-recovery-email') }}" method="POST">
|
||||
{{ macros.field(_locals, 'email', 'email', null, 'Choose a safe email address', 'An email we can use to identify you in case you lose access to your account', 'required') }}
|
||||
|
||||
<button type="submit">Add recovery email</button>
|
||||
|
||||
{{ macros.csrf(getCSRFToken) }}
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
{{ macros.csrf(getCSRFToken) }}
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
{% endblock %}
|
|
@ -0,0 +1,57 @@
|
|||
{% extends 'layouts/base.njk' %}
|
||||
|
||||
{% set title = app.name + ' - Backend' %}
|
||||
|
||||
{% block body %}
|
||||
<div class="container">
|
||||
<main class="panel">
|
||||
<h1><i data-feather="mail"></i> Mailbox</h1>
|
||||
|
||||
<p class="center">{{ mailbox.name }}</p>
|
||||
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Email</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for identity in identities %}
|
||||
<tr>
|
||||
<td>{{ identity.id }}</td>
|
||||
<td>{{ identity.email }}</td>
|
||||
<td class="actions">
|
||||
<form action="{{ route('backend-delete-mail-identity') }}" method="POST">
|
||||
<input type="hidden" name="id" value="{{ identity.id }}">
|
||||
|
||||
<button class="danger" onclick="return confirm('Are you sure you want to delete {{ identity.email }}?')">
|
||||
<i data-feather="trash"></i> Delete
|
||||
</button>
|
||||
|
||||
{{ macros.csrf(getCSRFToken) }}
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<form action="{{ route('backend-create-mail-identity', id) }}" method="POST" class="sub-panel">
|
||||
<h3>{% if mailboxIdentity == null %}Create a mailbox{% else %}Create a new mail identity{% endif %}</h3>
|
||||
|
||||
<div class="inline-fields">
|
||||
{{ macros.field(_locals, 'text', 'name', user.name, 'Email name', null, 'required') }}
|
||||
<span>@</span>
|
||||
{{ macros.field(_locals, 'select', 'mail_domain_id', null, 'Choose the email domain', null, 'required', domains) }}
|
||||
</div>
|
||||
|
||||
<button><i data-feather="plus"></i> Create</button>
|
||||
|
||||
{{ macros.csrf(getCSRFToken) }}
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -0,0 +1,86 @@
|
|||
{% extends 'layouts/base.njk' %}
|
||||
|
||||
{% set title = app.name + ' - Backend' %}
|
||||
|
||||
{% block body %}
|
||||
<h1>Mailbox manager</h1>
|
||||
|
||||
<div class="container">
|
||||
<section class="panel">
|
||||
<h2>Domains</h2>
|
||||
|
||||
<form action="{{ route('backend-add-domain') }}" method="POST" class="sub-panel">
|
||||
<h3>Add domain</h3>
|
||||
{{ macros.field(_locals, 'text', 'name', null, 'Domain name', null, 'required') }}
|
||||
|
||||
{{ macros.field(_locals, 'select', 'user_id', undefined, 'Owner', null, 'required', users) }}
|
||||
|
||||
<button><i data-feather="plus"></i> Add domain</button>
|
||||
|
||||
{{ macros.csrf(getCSRFToken) }}
|
||||
</form>
|
||||
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Owner</th>
|
||||
<th>Identities</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for domain in domains %}
|
||||
<tr>
|
||||
<td>{{ domain.id }}</td>
|
||||
<td>{{ domain.name }}</td>
|
||||
<td>{{ domain.owner_name | default('Public') }}</td>
|
||||
<td>{{ domain.identity_count }}</td>
|
||||
<td class="actions">
|
||||
<form action="{{ route('backend-remove-domain') }}" method="POST">
|
||||
<input type="hidden" name="id" value="{{ domain.id }}">
|
||||
|
||||
<button class="danger" onclick="return confirm('Are you sure you want to delete {{ domain.name }}?')">
|
||||
<i data-feather="trash"></i> Remove
|
||||
</button>
|
||||
|
||||
{{ macros.csrf(getCSRFToken) }}
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Mailboxes</h2>
|
||||
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>User name</th>
|
||||
<th>Name</th>
|
||||
<th>Identities</th>
|
||||
<th>Owned domains</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for box in mailboxes %}
|
||||
<tr>
|
||||
<td>{{ box.id }}</td>
|
||||
<td>{{ box.username }}</td>
|
||||
<td><a href="{{ route('backend-mailbox', box.id) }}">{{ box.name }}</a></td>
|
||||
<td>{{ box.identity_count }}</td>
|
||||
<td>{{ box.domain_count }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -9891,9 +9891,9 @@ widest-line@^3.1.0:
|
|||
string-width "^4.0.0"
|
||||
|
||||
wms-core@^0:
|
||||
version "0.19.14"
|
||||
resolved "https://registry.toot.party/wms-core/-/wms-core-0.19.14.tgz#9c64954f3ccfd0b1287cdbf3f5c646725db55319"
|
||||
integrity sha512-fwVjNNoKO779ZCVVGinhfOwVMPpg48DoJcz+g+QLhJgqmYqrsdULT1oZiSntE+1cgII1pWouahyGRywRJyOtQA==
|
||||
version "0.19.31"
|
||||
resolved "https://registry.toot.party/wms-core/-/wms-core-0.19.31.tgz#6305cf0d742ac5758a11a8ad8ca5316eaf99d4e7"
|
||||
integrity sha512-tbyxn84c9xEdPwsw6OySwstdjc6dSNvWpbEDY/bq3cmJ87Q3WrIEweBRTvVuaxoR0ZISyPAL9vk4ZXnB71NCTg==
|
||||
dependencies:
|
||||
argon2 "^0.26.2"
|
||||
compression "^1.7.4"
|
||||
|
|
Loading…
Reference in New Issue