added collections back to playground
Summary: Ref <T2485> Reviewers: #reviewers, Etoo Subscribers: kuba-orlik, jenkins-user Maniphest Tasks: T2485 Differential Revision: https://hub.sealcode.org/D1217master
parent
942c9b443d
commit
9b9f1df7a8
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,6 @@
|
||||
describe("collections", () => {
|
||||
require("./password-reset-intents.subtest");
|
||||
require("./registration-intents.subtest");
|
||||
require("./user-roles.subtest");
|
||||
require("./users.subtest");
|
||||
});
|
@ -0,0 +1,89 @@
|
||||
import axios from "axios";
|
||||
import assert from "assert";
|
||||
import TheApp from "../app";
|
||||
import { withProdApp } from "../test_utils/with-prod-app";
|
||||
|
||||
describe("password-reset-intents", () => {
|
||||
async function createAUser(app: TheApp) {
|
||||
await app.collections.users.suCreate({
|
||||
username: "user",
|
||||
email: "user@example.com",
|
||||
password: "password",
|
||||
roles: [],
|
||||
});
|
||||
}
|
||||
|
||||
it("tells you if the email address doesn't exist", async () =>
|
||||
withProdApp(async ({ app, base_url }) => {
|
||||
const email = "fake@example.com";
|
||||
try {
|
||||
await axios.post(
|
||||
`${base_url}/api/v1/collections/password-reset-intents`,
|
||||
{
|
||||
email: email,
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
assert.equal(
|
||||
e.response.data.data.field_messages.email.message,
|
||||
app.i18n("invalid_existing_value", ["users", "email", email])
|
||||
);
|
||||
return;
|
||||
}
|
||||
throw new Error("it didn't throw");
|
||||
}));
|
||||
|
||||
it("allows anyone to create an intent, if the email exists", async () =>
|
||||
withProdApp(async ({ app, base_url }) => {
|
||||
await createAUser(app);
|
||||
const { email, token } = (
|
||||
await axios.post(
|
||||
`${base_url}/api/v1/collections/password-reset-intents`,
|
||||
{
|
||||
email: "user@example.com",
|
||||
}
|
||||
)
|
||||
).data;
|
||||
assert.deepEqual(
|
||||
{ email, token },
|
||||
{
|
||||
email: "user@example.com",
|
||||
token: "it's a secret to everybody",
|
||||
}
|
||||
);
|
||||
}));
|
||||
|
||||
it("tells you if the email address is malformed", async () =>
|
||||
withProdApp(async ({ app, base_url }) => {
|
||||
const email = "incorrect-address";
|
||||
try {
|
||||
await axios.post(
|
||||
`${base_url}/api/v1/collections/password-reset-intents`,
|
||||
{
|
||||
email: email,
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
assert.equal(
|
||||
e.response.data.data.field_messages.email.message,
|
||||
app.i18n("invalid_email", [email])
|
||||
);
|
||||
return;
|
||||
}
|
||||
throw new Error("it didn't throw");
|
||||
}));
|
||||
|
||||
it("sends an email with the reset password link", async () =>
|
||||
withProdApp(async ({ app, base_url, mail_api }) => {
|
||||
await createAUser(app);
|
||||
await axios.post(`${base_url}/api/v1/collections/password-reset-intents`, {
|
||||
email: "user@example.com",
|
||||
});
|
||||
const messages = (await mail_api.getMessages()).filter(
|
||||
(message) => message.recipients[0] == "<user@example.com>"
|
||||
);
|
||||
assert.equal(messages.length, 1);
|
||||
assert.equal(messages[0].recipients.length, 1);
|
||||
assert.equal(messages[0].recipients[0], "<user@example.com>");
|
||||
}));
|
||||
});
|
@ -0,0 +1,39 @@
|
||||
import { App, Collection, CollectionItem, Context, FieldTypes, Policies } from "sealious";
|
||||
import PasswordResetTemplate from "../email-templates/password-reset";
|
||||
import TheApp from "../app";
|
||||
|
||||
export default class PasswordResetIntents extends Collection {
|
||||
name = "password-reset-intents";
|
||||
fields = {
|
||||
email: new FieldTypes.ValueExistingInCollection({
|
||||
field: "email",
|
||||
collection: "users",
|
||||
include_forbidden: true,
|
||||
}),
|
||||
token: new FieldTypes.SecretToken(),
|
||||
};
|
||||
policies = {
|
||||
create: new Policies.Public(),
|
||||
edit: new Policies.Noone(),
|
||||
};
|
||||
defaultPolicy: Policies.Super;
|
||||
async init(app: App, name: string) {
|
||||
const theApp = app as TheApp;
|
||||
await super.init(app, name);
|
||||
app.collections["password-reset-intents"].on(
|
||||
"after:create",
|
||||
async ([context, intent]: [
|
||||
Context,
|
||||
CollectionItem<PasswordResetIntents>,
|
||||
any
|
||||
]) => {
|
||||
const intent_as_super = await intent.fetchAs(new app.SuperContext());
|
||||
const message = await PasswordResetTemplate(theApp, {
|
||||
email_address: intent.get("email") as string,
|
||||
token: intent_as_super.get("token") as string,
|
||||
});
|
||||
await message.send(app);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
import axios from "axios";
|
||||
import assert from "assert";
|
||||
import { TestUtils, Policies } from "sealious";
|
||||
import { withProdApp } from "../test_utils/with-prod-app";
|
||||
|
||||
describe("registration-intents", () => {
|
||||
it("doesn't allow setting a role for registration intention when the user in context can't create user-roles", async () =>
|
||||
withProdApp(async ({ app, base_url }) => {
|
||||
app.collections["user-roles"].setPolicy("create", new Policies.Noone());
|
||||
await TestUtils.assertThrowsAsync(
|
||||
() =>
|
||||
axios.post(`${base_url}/api/v1/collections/registration-intents`, {
|
||||
email: "cunning@fox.com",
|
||||
role: "admin",
|
||||
}),
|
||||
(e: any) => {
|
||||
assert.equal(
|
||||
e.response.data.data.field_messages.role.message,
|
||||
app.i18n("policy_users_who_can_deny", [
|
||||
"create",
|
||||
"user-roles",
|
||||
app.i18n("policy_noone_deny"),
|
||||
])
|
||||
);
|
||||
}
|
||||
);
|
||||
}));
|
||||
|
||||
it("allows setting a role for registration intention when the user in context can create user-roles", async () =>
|
||||
withProdApp(async ({ app, base_url }) => {
|
||||
app.collections["user-roles"].setPolicy("create", new Policies.Public());
|
||||
const intent = (
|
||||
await axios.post(`${base_url}/api/v1/collections/registration-intents`, {
|
||||
email: "genuine@fox.com",
|
||||
role: "admin",
|
||||
})
|
||||
).data;
|
||||
assert.equal(intent.role, "admin");
|
||||
|
||||
const role = (
|
||||
await app.collections["registration-intents"].suGetByID(
|
||||
intent.id as string
|
||||
)
|
||||
).get("role");
|
||||
|
||||
assert.equal(role, "admin");
|
||||
}));
|
||||
});
|
@ -0,0 +1,43 @@
|
||||
import { App, Collection, FieldTypes, Policies } from "sealious";
|
||||
import RegistrationIntentTemplate from "../email-templates/registration-intent";
|
||||
|
||||
export default class RegistrationIntents extends Collection {
|
||||
fields = {
|
||||
email: new FieldTypes.ValueNotExistingInCollection({
|
||||
collection: "users",
|
||||
field: "email",
|
||||
include_forbidden: true,
|
||||
}),
|
||||
token: new FieldTypes.SecretToken(),
|
||||
role: new FieldTypes.SettableBy(
|
||||
new FieldTypes.Enum((app: App) => app.ConfigManager.get("roles")),
|
||||
new Policies.UsersWhoCan(["create", "user-roles"])
|
||||
),
|
||||
};
|
||||
|
||||
policies = {
|
||||
create: new Policies.Public(),
|
||||
edit: new Policies.Noone(),
|
||||
};
|
||||
|
||||
defaultPolicy = new Policies.Super();
|
||||
|
||||
async init(app: App, name: string) {
|
||||
await super.init(app, name);
|
||||
this.on("after:create", async ([context, intent]) => {
|
||||
await intent.decode(context);
|
||||
const {
|
||||
items: [item],
|
||||
} = await app.collections["registration-intents"]
|
||||
.suList()
|
||||
.ids([intent.id])
|
||||
.fetch();
|
||||
const token = item.get("token") as string;
|
||||
const message = await RegistrationIntentTemplate(app, {
|
||||
email_address: intent.get("email") as string,
|
||||
token,
|
||||
});
|
||||
await message.send(app);
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
import { Collection, FieldTypes } from "sealious";
|
||||
import { Roles } from "../policy-types/roles";
|
||||
|
||||
/* For testing the Roles policy */
|
||||
export class Secrets extends Collection {
|
||||
fields = {
|
||||
content: new FieldTypes.Text(),
|
||||
};
|
||||
defaultPolicy = new Roles(["admin"]);
|
||||
}
|
||||
|
||||
export default new Secrets();
|
@ -0,0 +1,71 @@
|
||||
import assert from "assert";
|
||||
import axios from "axios";
|
||||
import { CollectionItem, TestUtils } from "sealious";
|
||||
import { Users } from "./users";
|
||||
import TheApp from "../app";
|
||||
import { withProdApp } from "../test_utils/with-prod-app";
|
||||
|
||||
function createAUser(app: TheApp, username: string) {
|
||||
return app.collections.users.suCreate({
|
||||
username,
|
||||
email: `${username}@example.com`,
|
||||
password: "password",
|
||||
roles: [],
|
||||
});
|
||||
}
|
||||
|
||||
type Unpromisify<T> = T extends Promise<infer R> ? R : T;
|
||||
|
||||
async function createAdmin(
|
||||
app: TheApp,
|
||||
rest_api: TestUtils.MockRestApi
|
||||
): Promise<[CollectionItem<Users>, Unpromisify<ReturnType<typeof rest_api.login>>]> {
|
||||
const user = await createAUser(app, "super_user");
|
||||
await app.collections["user-roles"].suCreate({
|
||||
user: user.id,
|
||||
role: "admin",
|
||||
});
|
||||
const session = await rest_api.login({
|
||||
username: "super_user",
|
||||
password: "password",
|
||||
});
|
||||
return [user, session];
|
||||
}
|
||||
|
||||
describe("user-roles", () => {
|
||||
it("rejects when given an empty role", async () =>
|
||||
withProdApp(async ({ app, rest_api }) => {
|
||||
const [user, session] = await createAdmin(app, rest_api);
|
||||
await TestUtils.assertThrowsAsync(
|
||||
async () => {
|
||||
return rest_api.post(
|
||||
`/api/v1/collections/user-roles`,
|
||||
{
|
||||
user: user.id,
|
||||
},
|
||||
session
|
||||
);
|
||||
},
|
||||
(e: any) => {
|
||||
assert.equal(
|
||||
e?.response.data.data.field_messages.role?.message,
|
||||
"Missing value for field 'role'."
|
||||
);
|
||||
}
|
||||
);
|
||||
}));
|
||||
|
||||
it("accepts correct dataset", async () =>
|
||||
withProdApp(async ({ app, base_url, rest_api }) => {
|
||||
const [user, session] = await createAdmin(app, rest_api);
|
||||
const response = await axios.post(
|
||||
`${base_url}/api/v1/collections/user-roles`,
|
||||
{
|
||||
user: user.id,
|
||||
role: "admin",
|
||||
},
|
||||
session
|
||||
);
|
||||
assert.equal(response.status, 201);
|
||||
}));
|
||||
});
|
@ -0,0 +1,37 @@
|
||||
import { ActionName, App, Collection, FieldTypes, Policies, Policy } from "sealious";
|
||||
import { Roles } from "../policy-types/roles";
|
||||
|
||||
export class UserRoles extends Collection {
|
||||
name = "user-roles";
|
||||
fields = {
|
||||
role: new FieldTypes.Enum((app: App) =>
|
||||
app.ConfigManager.get("roles")
|
||||
).setRequired(true),
|
||||
user: new FieldTypes.SingleReference("users"),
|
||||
};
|
||||
|
||||
policies = {
|
||||
create: new Roles(["admin"]),
|
||||
delete: new Policies.Public(),
|
||||
show: new Policies.UserReferencedInField("user"),
|
||||
edit: new Policies.Noone(),
|
||||
} as { [policy: string]: Policy }; // this `as` statement allows the policies to be overwritten;
|
||||
|
||||
async init(app: App, collection_name: string) {
|
||||
await super.init(app, collection_name);
|
||||
app.on("started", async () => {
|
||||
const roles = app.collections["user-roles"];
|
||||
for (const action of ["create", "delete"] as ActionName[]) {
|
||||
const policy = roles.getPolicy(action);
|
||||
if (policy instanceof Policies.Public) {
|
||||
app.Logger.warn(
|
||||
"USER POLICY",
|
||||
`<user-roles> collection is using <public> access strategy for ${action} action. Anyone can change anyone elses role. This is the default behavior and you should overwrite it with <set_policy>`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new UserRoles();
|
@ -0,0 +1,20 @@
|
||||
import assert from "assert";
|
||||
import { withProdApp } from "../test_utils/with-prod-app";
|
||||
|
||||
describe("users", () => {
|
||||
it.skip("should properly handle route to account creation", async () =>
|
||||
withProdApp(async ({ app, rest_api }) => {
|
||||
const sealious_response = await app.collections["registration-intents"]
|
||||
.suList()
|
||||
.filter({ email: app.manifest.admin_email })
|
||||
.fetch();
|
||||
|
||||
const { email, token } = sealious_response.items[0].serializeBody();
|
||||
const response = await rest_api.get(
|
||||
`/account-creation-details?token=${token as string}&email=${
|
||||
email as string
|
||||
}`
|
||||
);
|
||||
assert(response.includes("Please fill in the details of your account"));
|
||||
}));
|
||||
});
|
@ -0,0 +1,52 @@
|
||||
import { App, Collections, FieldTypes } from "sealious";
|
||||
import TheApp from "../app";
|
||||
|
||||
export class Users extends Collections.users {
|
||||
fields = {
|
||||
...App.BaseCollections.users.fields,
|
||||
email: new FieldTypes.Email().setRequired(true),
|
||||
roles: new FieldTypes.ReverseSingleReference({
|
||||
referencing_collection: "user-roles",
|
||||
referencing_field: "user",
|
||||
}),
|
||||
};
|
||||
|
||||
async init(app: TheApp, name: string) {
|
||||
await super.init(app, name);
|
||||
app.on("started", async () => {
|
||||
const users = await app.collections.users
|
||||
.suList()
|
||||
.filter({ email: app.manifest.admin_email })
|
||||
.fetch();
|
||||
if (users.empty) {
|
||||
app.Logger.warn(
|
||||
"ADMIN",
|
||||
`Creating an admin account for ${app.manifest.admin_email}`
|
||||
);
|
||||
await app.collections["registration-intents"].suCreate({
|
||||
email: app.manifest.admin_email,
|
||||
role: "admin",
|
||||
token: "",
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async populate(): Promise<void> {
|
||||
if (await this.app.Metadata.get("my_collection_populated")) {
|
||||
return;
|
||||
}
|
||||
const app = this.app as TheApp;
|
||||
|
||||
await app.collections.users.suCreate({
|
||||
email: "admin@example.com",
|
||||
roles: [],
|
||||
username: "admin",
|
||||
password: "password",
|
||||
});
|
||||
|
||||
await this.app.Metadata.set("my_collection_populated", "true");
|
||||
}
|
||||
}
|
||||
|
||||
export default new Users();
|
@ -0,0 +1,31 @@
|
||||
import { App, EmailTemplates, Errors } from "sealious";
|
||||
import TheApp from "../app";
|
||||
|
||||
export default async function PasswordResetTemplate(
|
||||
app: TheApp,
|
||||
{ email_address, token }: { email_address: string; token: string }
|
||||
) {
|
||||
const matching_users = await app.collections["users"]
|
||||
.suList()
|
||||
.filter({ email: email_address })
|
||||
.fetch();
|
||||
|
||||
if (!matching_users.items.length) {
|
||||
throw new Errors.NotFound("No user with that email");
|
||||
}
|
||||
|
||||
const username = matching_users.items[0].get("username");
|
||||
|
||||
return EmailTemplates.Simple(app, {
|
||||
subject: app.i18n("password_reset_email_subject", [app.manifest.name]),
|
||||
to: `${username}<${email_address}>`,
|
||||
text: `
|
||||
${app.i18n("password_reset_email_text", [app.manifest.name, username])}`,
|
||||
buttons: [
|
||||
{
|
||||
text: app.i18n("password_reset_cta"),
|
||||
href: `${app.manifest.base_url}/confirm-password-reset?token=${token}&email=${email_address}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
import { App, EmailTemplates } from "sealious";
|
||||
|
||||
export default async function RegistrationIntentTemplate(
|
||||
app: App,
|
||||
{ email_address, token }: { email_address: string; token: string }
|
||||
) {
|
||||
return EmailTemplates.Simple(app, {
|
||||
subject: app.i18n("registration_intent_email_subject", [app.manifest.name]),
|
||||
to: email_address,
|
||||
text: `
|
||||
${app.i18n("registration_intent_email_text", [app.manifest.name])}`,
|
||||
buttons: [
|
||||
{
|
||||
text: app.i18n("registration_intent_cta"),
|
||||
href: `${app.manifest.base_url}/account/confirm-registration-email?token=${token}&email=${email_address}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
describe("policy-types", () => {
|
||||
require("./roles.subtest");
|
||||
});
|
@ -0,0 +1,58 @@
|
||||
import assert from "assert";
|
||||
import { TestUtils } from "sealious";
|
||||
import { withProdApp } from "../test_utils/with-prod-app";
|
||||
|
||||
const ALLOWED_ROLES = ["admin"];
|
||||
|
||||
describe("roles", () => {
|
||||
it("allows access to users with designated role and denies access to users without it", async () =>
|
||||
withProdApp(async ({ app, rest_api }) => {
|
||||
await app.collections.users.suCreate({
|
||||
username: "regular-user",
|
||||
password: "password",
|
||||
email: "regular@example.com",
|
||||
roles: [],
|
||||
});
|
||||
|
||||
const admin = await app.collections.users.suCreate({
|
||||
username: "admin",
|
||||
password: "admin-password",
|
||||
email: "admin@example.com",
|
||||
roles: [],
|
||||
});
|
||||
|
||||
await app.collections["user-roles"].suCreate({
|
||||
user: admin.id,
|
||||
role: "admin",
|
||||
});
|
||||
|
||||
await app.collections.secrets.suCreate({
|
||||
content: "It's a secret to everybody",
|
||||
});
|
||||
|
||||
const admin_session = await rest_api.login({
|
||||
username: "admin",
|
||||
password: "admin-password",
|
||||
});
|
||||
|
||||
const { items: admin_response } = await rest_api.get(
|
||||
"/api/v1/collections/secrets",
|
||||
admin_session
|
||||
);
|
||||
assert.equal(admin_response.length, 1);
|
||||
|
||||
const user_session = await rest_api.login({
|
||||
username: "regular-user",
|
||||
password: "password",
|
||||
});
|
||||
await TestUtils.assertThrowsAsync(
|
||||
() => rest_api.get("/api/v1/collections/secrets", user_session),
|
||||
(error) => {
|
||||
assert.equal(
|
||||
error.response.data.message,
|
||||
app.i18n("policy_roles_deny", [ALLOWED_ROLES.join(", ")])
|
||||
);
|
||||
}
|
||||
);
|
||||
}));
|
||||
});
|
@ -0,0 +1,55 @@
|
||||
import { Context, Policy, QueryTypes } from "sealious";
|
||||
|
||||
export class Roles extends Policy {
|
||||
static type_name = "roles";
|
||||
allowed_roles: string[];
|
||||
constructor(allowed_roles: string[]) {
|
||||
super(allowed_roles);
|
||||
this.allowed_roles = allowed_roles;
|
||||
}
|
||||
|
||||
async countMatchingRoles(context: Context) {
|
||||
const user_id = context.user_id;
|
||||
context.app.Logger.debug2("ROLES", "Checking the roles for user", user_id);
|
||||
const user_roles = await context.app.collections["user-roles"]
|
||||
.list(context)
|
||||
.filter({ user: user_id })
|
||||
.fetch();
|
||||
const roles = user_roles.items.map((user_role) => user_role.get("role"));
|
||||
|
||||
return this.allowed_roles.filter((allowed_role) => roles.includes(allowed_role))
|
||||
.length;
|
||||
}
|
||||
|
||||
async _getRestrictingQuery(context: Context) {
|
||||
if (context.is_super) {
|
||||
return new QueryTypes.AllowAll();
|
||||
}
|
||||
if (context.user_id === null) {
|
||||
return new QueryTypes.DenyAll();
|
||||
}
|
||||
|
||||
const matching_roles_count = await this.countMatchingRoles(context);
|
||||
|
||||
return matching_roles_count > 0
|
||||
? new QueryTypes.AllowAll()
|
||||
: new QueryTypes.DenyAll();
|
||||
}
|
||||
|
||||
async checkerFunction(context: Context) {
|
||||
if (context.user_id === null) {
|
||||
return Policy.deny(context.app.i18n("policy_logged_in_deny"));
|
||||
}
|
||||
const matching_roles_count = await this.countMatchingRoles(context);
|
||||
|
||||
return matching_roles_count > 0
|
||||
? Policy.allow(
|
||||
context.app.i18n("policy_roles_allow", [
|
||||
this.allowed_roles.join(", "),
|
||||
])
|
||||
)
|
||||
: Policy.deny(
|
||||
context.app.i18n("policy_roles_deny", [this.allowed_roles.join(", ")])
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import Router from "@koa/router";
|
||||
import { Middlewares } from "sealious";
|
||||
import finalizePasswordReset from "./finalize-password-reset";
|
||||
import confirmPasswordReset from "./confirm-password-reset";
|
||||
import finalizeRegistrationIntent from "./finalize-registration-intent";
|
||||
import createRouter from "./create/create.routes";
|
||||
import { confirmRegistrationRouter } from "./confirm-registration-email/confirm-registration-email.routes";
|
||||
|
||||
export const accountsRouter = (router: Router): void => {
|
||||
router.post(
|
||||
"/account/finalize-registration-intent",
|
||||
Middlewares.parseBody,
|
||||
finalizeRegistrationIntent
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/account/finalize-password-reset",
|
||||
Middlewares.parseBody,
|
||||
finalizePasswordReset
|
||||
);
|
||||
|
||||
router.get("/account/confirm-password-reset", confirmPasswordReset);
|
||||
createRouter(router);
|
||||
confirmRegistrationRouter(router);
|
||||
};
|
@ -0,0 +1,6 @@
|
||||
describe("routes", () => {
|
||||
// require("./finalize-registration-intent.subtest");
|
||||
// require("./finalize-password-reset.subtest");
|
||||
require("./confirm-password-reset.subtest");
|
||||
require("./account-creation-details.subtest");
|
||||
});
|
@ -0,0 +1,11 @@
|
||||
import axios from "axios";
|
||||
import { withProdApp } from "../../test_utils/with-prod-app";
|
||||
|
||||
describe("confirm-password-reset", () => {
|
||||
it("displays an html form", async () =>
|
||||
withProdApp(async ({ base_url }) => {
|
||||
await axios.get(
|
||||
`${base_url}/confirm-password-reset?token=kupcia&email=dupcia`
|
||||
);
|
||||
}));
|
||||
});
|
@ -0,0 +1,80 @@
|
||||
import { Middleware } from "@koa/router";
|
||||
import * as assert from "assert";
|
||||
|
||||
import { App } from "sealious";
|
||||
|
||||
const render_form = async (app: App, token: string, email: string) => /* HTML */ `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<style>
|
||||
html {
|
||||
background-color: #edeaea;
|
||||
}
|
||||
body {
|
||||
max-width: 21cm;
|
||||
margin: 1cm auto;
|
||||
font-family: sans-serif;
|
||||
background-color: white;
|
||||
padding: 1cm;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.reveal-button {
|
||||
margin-left: -0.5rem;
|
||||
}
|
||||
</style>
|
||||
<meta charset="utf-8" />
|
||||
<title>${app.i18n("password_reset_cta")}</title>
|
||||
<img src="/api/v1/logo" alt="${app.manifest.name} - logo" />
|
||||
<h1>${app.i18n("password_reset_cta")}</h1>
|
||||
<form method="POST" action="/finalize-password-reset">
|
||||
<input type="hidden" name="token" value="${token}" />
|
||||
<input type="hidden" name="email" value="${email}" />
|
||||
<fieldset>
|
||||
<legend>${app.i18n("password_reset_input_cta", [email])}</legend>
|
||||
<input id="pwd" name="password" type="password" size="32" />
|
||||
<button
|
||||
id="reveal"
|
||||
class="reveal-button"
|
||||
onclick="toggle(event)"
|
||||
title="${app.i18n("reveal_password")}"
|
||||
>
|
||||
🙈
|
||||
</button>
|
||||
<br />
|
||||
<input type="submit" value="${app.i18n("password_reset_cta")}" />
|
||||
</fieldset>
|
||||
</form>
|
||||
<script>
|
||||
function toggle(event) {
|
||||
event.preventDefault();
|
||||
if (pwd.type == "password") {
|
||||
pwd.type = "text";
|
||||
reveal.textContent = "👀";
|
||||
} else {
|
||||
pwd.type = "password";
|
||||
reveal.textContent = "🙈";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
</script>
|
||||
</html>
|
||||
`;
|
||||
|
||||
const confirmPasswordReset: Middleware = async (ctx) => {
|
||||
assert.ok(ctx.request.query.token);
|
||||
assert.ok(ctx.request.query.email);
|
||||
|
||||
if (typeof ctx.request.query.token !== "string") {
|
||||
throw new Error("Token isn't a string or is missing");
|
||||
}
|
||||
if (typeof ctx.request.query.email !== "string") {
|
||||
throw new Error("Email isn't a string or is missing");
|
||||
}
|
||||
ctx.body = await render_form(
|
||||
ctx.$app,
|
||||
ctx.request.query.token,
|
||||
ctx.request.query.email
|
||||
);
|
||||
};
|
||||
|
||||
export default confirmPasswordReset;
|
@ -0,0 +1,68 @@
|
||||
import Router from "@koa/router";
|
||||
import { Errors, Middlewares } from "sealious";
|
||||
import { formHasAllFields, formHasSomeFields } from "../../common/form";
|
||||
import { accountCreationDetailsForm } from "./confirm-registration-email.views";
|
||||
|
||||
export const confirmRegistrationRouter = (router: Router): void => {
|
||||
router.get(
|
||||
"/account/confirm-registration-email",
|
||||
Middlewares.extractContext(),
|
||||
async (ctx) => {
|
||||
if (!formHasAllFields(ctx, <const>["email", "token"], ctx.query)) return;
|
||||
ctx.body = await accountCreationDetailsForm(ctx, { values: ctx.query });
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/account/confirm-registration-email",
|
||||
Middlewares.extractContext(),
|
||||
Middlewares.parseBody(),
|
||||
async (ctx) => {
|
||||
if (
|
||||
!formHasSomeFields(ctx, <const>["username", "password"], ctx.$body) ||
|
||||
!formHasAllFields(ctx, <const>["token", "email"], ctx.$body)
|
||||
)
|
||||
return;
|
||||
try {
|
||||
const { items: matching_intents } = await ctx.$app.collections[
|
||||
"registration-intents"
|
||||
]
|
||||
.suList()
|
||||
.filter({ token: ctx.$body.token })
|
||||
.fetch();
|
||||
if (matching_intents.length !== 1) {
|
||||
ctx.status = 403;
|
||||
return;
|
||||
}
|
||||
await ctx.$app.collections.users.suCreateUnsafe({
|
||||
username: ctx.$body.username,
|
||||
email: matching_intents[0].get("email"),
|
||||
|
||||
password: ctx.$body.password,
|
||||
});
|
||||
await (
|
||||
await ctx.$app.collections["registration-intents"].getByID(
|
||||
new ctx.$app.SuperContext(),
|
||||
matching_intents[0].id
|
||||
)
|
||||
).delete(new ctx.$app.SuperContext());
|
||||
ctx.status = 303;
|
||||
ctx.redirect("account-created");
|
||||
} catch (e) {
|
||||
console.log("error", e);
|
||||
if (Errors.FieldsError.isFieldsError(ctx.$app.collections.users, e)) {
|
||||
ctx.status = 422;
|
||||
ctx.body = await accountCreationDetailsForm(ctx, {
|
||||
values: {
|
||||
username: ctx.$body.username,
|
||||
email: ctx.$body.email,
|
||||
token: ctx.$body.token,
|
||||
},
|
||||
errors: e.getSimpleMessages(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
@ -0,0 +1,24 @@
|
||||
import axios from "axios";
|
||||
import assert from "assert";
|
||||
import { TestUtils } from "sealious";
|
||||
import { withProdApp } from "../../../test_utils/with-prod-app";
|
||||
|
||||
describe("account-creation-details", () => {
|
||||
it("throws when no token/email is present", () =>
|
||||
withProdApp(({ base_url }) =>
|
||||
TestUtils.assertThrowsAsync(
|
||||
async () => {
|
||||
await axios.get(`${base_url}/account-creation-details`);
|
||||
},
|
||||
async function () {}
|
||||
)
|
||||
));
|
||||
it("displays an html form after the positive flow", () =>
|
||||
withProdApp(async ({ base_url }) => {
|
||||
const resp = await axios.get(
|
||||
`${base_url}/account-creation-details?token=oieajgoiea&email=ababab@ok.pl`
|
||||
);
|
||||
assert.deepEqual(resp.status, 200);
|
||||
assert(resp.data.length);
|
||||
}));
|
||||
});
|
@ -0,0 +1,55 @@
|
||||
import { BaseContext } from "koa";
|
||||
import html from "../../../html";
|
||||
import navbar from "../../common/navbar";
|
||||
import input from "../../common/ui/input";
|
||||
|
||||
export async function accountCreationDetailsForm(
|
||||
ctx: BaseContext,
|
||||
{
|
||||
values,
|
||||
errors,
|
||||
}: {
|
||||
values: { token: string; email: string; username?: string };
|
||||
errors?: { email?: string; username?: string; password?: string };
|
||||
}
|
||||
) {
|
||||
errors = errors || {};
|
||||
return html(
|
||||
ctx,
|
||||
/* HTML */ `
|
||||
${navbar(ctx)}
|
||||
<h1>${ctx.$app.i18n("registration_intent_cta")}</h1>
|
||||
<form method="POST" id="form" action="/account/confirm-registration-email">
|
||||
<input type="hidden" name="token" value="${values.token || ""}" />
|
||||
<fieldset>
|
||||
<legend>
|
||||
${ctx.$app.i18n("registration_intent_form_description")}
|
||||
</legend>
|
||||
${input({
|
||||
name: "email",
|
||||
type: "email",
|
||||
value: values.email || "",
|
||||
readonly: true,
|
||||
error: "",
|
||||
})}
|
||||
${input({
|
||||
name: "username",
|
||||
value: values.username,
|
||||
error: errors.username || "",
|
||||
type: "text",
|
||||
})}
|
||||
${input({
|
||||
name: "password",
|
||||
value: "",
|
||||
error: errors.password || "",
|
||||
type: "password",
|
||||
})}
|
||||
<input
|
||||
type="submit"
|
||||
value="${ctx.$app.i18n("registration_intent_cta")}"
|
||||
/>
|
||||
</fieldset>
|
||||
</form>
|
||||
`
|
||||
);
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
import Router from "@koa/router";
|
||||
import { Errors, Middlewares } from "sealious";
|
||||
import html from "../../../html";
|
||||
import { formHasSomeFields } from "../../common/form";
|
||||
import { createAccountForm } from "./create.views";
|
||||
|
||||
export default function createRouter(router: Router) {
|
||||
router.use("/account/create", Middlewares.extractContext());
|
||||
|
||||
router.get("/account/create", (ctx) => {
|
||||
console.log({ ctx });
|
||||
ctx.body = createAccountForm(ctx);
|
||||
});
|
||||
|
||||
router.get(
|
||||
"/account/create/email-sent",
|
||||
(ctx) => (ctx.body = html(ctx, `Registration email sent`))
|
||||
);
|
||||
|
||||
router.post("/account/create", Middlewares.parseBody(), async (ctx) => {
|
||||
const registrationIntents = ctx.$app.collections["registration-intents"];
|
||||
// the line below enables typescript to deduce the type of ctx.$body and
|
||||
// avoid type assertions
|
||||
if (!formHasSomeFields(ctx, <const>["email"], ctx.$body)) return;
|
||||
try {
|
||||
await registrationIntents.create(ctx.$context, ctx.$body);
|
||||
ctx.status = 303;
|
||||
ctx.redirect("/account/create/email-sent");
|
||||
} catch (e) {
|
||||
if (Errors.FieldsError.isFieldsError(registrationIntents, e)) {
|
||||
ctx.status = 422;
|
||||
ctx.body = createAccountForm(ctx, {
|
||||
values: { email: ctx.$body.email },
|
||||
errors: e,
|
||||
});
|
||||
} else {
|
||||
ctx.body = "error";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
import { BaseContext } from "koa";
|
||||
import { Errors } from "sealious";
|
||||
import RegistrationIntents from "../../../collections/registration-intents";
|
||||
import html from "../../../html";
|
||||
import { CollectionTiedFormData } from "../../common/form";
|
||||
import navbar from "../../common/navbar";
|
||||
import input from "../../common/ui/input";
|
||||
|
||||
export function createAccountForm(
|
||||
ctx: BaseContext,
|
||||
{ values, errors }: CollectionTiedFormData<RegistrationIntents> = {
|
||||
values: {},
|
||||
}
|
||||
) {
|
||||
errors =
|
||||
errors ||
|
||||
new Errors.FieldsError(ctx.$app.collections["registration-intents"], {}); // empty error;
|
||||
return html(
|
||||
ctx,
|
||||
/* HTML */ `<title>Sign up</title>${navbar(ctx)}
|
||||
<h1>Register</h1>
|
||||
<form action="/account/create" method="POST">
|
||||
${input({
|
||||
name: "email",
|
||||
value: values.email,
|
||||
type: "email",
|
||||
error: errors.getErrorForField("email"),
|
||||
})}
|
||||
<input type="submit" value="register" />
|
||||
</form>`
|
||||
);
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
import assert from "assert";
|
||||
import { AxiosError } from "axios";
|
||||
import { TestUtils } from "sealious";
|
||||
import TheApp from "../../app";
|
||||
import { withProdApp } from "../../test_utils/with-prod-app";
|
||||
|
||||
describe.only("finalize password reset", () => {
|
||||
async function createAUser(app: TheApp) {
|
||||
await app.collections.users.suCreate({
|
||||
username: "user",
|
||||
email: "user@example.com",
|
||||
password: "password",
|
||||
roles: [],
|
||||
});
|
||||
}
|
||||
|
||||
it("allows to change a password (entire flow)", async () =>
|
||||
withProdApp(async ({ app, mail_api, rest_api }) => {
|
||||
await createAUser(app);
|
||||
|
||||
const options = await rest_api.login({
|
||||
username: "user",
|
||||
password: "password",
|
||||
});
|
||||
await rest_api.delete("/api/v1/collections/sessions/current", options);
|
||||
await rest_api.post("/api/v1/collections/password-reset-intents", {
|
||||
email: "user@example.com",
|
||||
});
|
||||
|
||||
const message_metadata = (await mail_api.getMessages()).filter(
|
||||
(message) => message.recipients[0] == "<user@example.com>"
|
||||
)[0];
|
||||
assert(message_metadata.subject);
|
||||
|
||||
const message = await mail_api.getMessageById(message_metadata.id);
|
||||
|
||||
const matches = /token=([^?&]+)/.exec(message);
|
||||
if (!matches) {
|
||||
throw new Error("token not found in the message");
|
||||
}
|
||||
const token = matches[1];
|
||||
await rest_api.post("/finalize-password-reset", {
|
||||
email: "user@example.com",
|
||||
token,
|
||||
password: "new-password",
|
||||
});
|
||||
await rest_api.post(
|
||||
"/api/v1/sessions",
|
||||
{ username: "user", password: "new-password" },
|
||||
options
|
||||
);
|
||||
|
||||
await TestUtils.assertThrowsAsync(
|
||||
async () =>
|
||||
rest_api.post("/finalize-password-reset", {
|
||||
email: "user@example.com",
|
||||
token,
|
||||
password: "using the same token twice hehehehhee",
|
||||
}),
|
||||
(e: AxiosError) => {
|
||||
assert.strictEqual(e?.response?.data?.message, "Incorrect token");
|
||||
}
|
||||
);
|
||||
}));
|
||||
});
|
@ -0,0 +1,52 @@
|
||||
import { Middleware } from "@koa/router";
|
||||
import { URL } from "url";
|
||||
import { Errors } from "sealious";
|
||||
import { hasShape, predicates } from "@sealcode/ts-predicates";
|
||||
|
||||
const finalizePasswordReset: Middleware = async (ctx) => {
|
||||
if (
|
||||
!hasShape(
|
||||
{
|
||||
redirect: predicates.or(predicates.string, predicates.undefined),
|
||||
token: predicates.string,
|
||||
password: predicates.string,
|
||||
},
|
||||
ctx.$body
|
||||
)
|
||||
) {
|
||||
throw new Error("Wrong parameters. Needed: token, password. Optional: redirect.");
|
||||
}
|
||||
|
||||
const intent_response = await ctx.$app.collections["password-reset-intents"]
|
||||
.suList()
|
||||
.filter({ token: ctx.$body.token })
|
||||
.fetch();
|
||||
|
||||
if (intent_response.empty) {
|
||||
throw new Errors.BadContext("Incorrect token");
|
||||
}
|
||||
|
||||
const intent = intent_response.items[0];
|
||||
|
||||
const user_response = await ctx.$app.collections.users
|
||||
.suList()
|
||||
.filter({ email: intent.get("email") as string })
|
||||
.fetch();
|
||||
if (user_response.empty) {
|
||||
throw new Error("No user with this email address.");
|
||||
}
|
||||
user_response.items[0].set("password", ctx.$body.password);
|
||||
await user_response.items[0].save(new ctx.$app.SuperContext());
|
||||
await intent.remove(new ctx.$app.SuperContext());
|
||||
|
||||
if (
|
||||
ctx.$body.redirect &&
|
||||
new URL(ctx.$app.manifest.base_url).origin == new URL(ctx.$body.redirect).origin
|
||||
) {
|
||||
ctx.redirect(ctx.$body.redirect);
|
||||
} else {
|
||||
ctx.body = "Password reset successful";
|
||||
}
|
||||
};
|
||||
|
||||
export default finalizePasswordReset;
|
@ -0,0 +1,43 @@
|
||||
import * as assert from "assert";
|
||||
import { withProdApp } from "../../test_utils/with-prod-app";
|
||||
|
||||
describe("finalize registration", () => {
|
||||
it("allows to register an account (entire flow)", async () =>
|
||||
withProdApp(async ({ app, mail_api, rest_api }) => {
|
||||
app.ConfigManager.set("roles", ["admin"]);
|
||||
await rest_api.post("/api/v1/collections/registration-intents", {
|
||||
email: "user@example.com",
|
||||
role: "admin",
|
||||
});
|
||||
const message_metadata = (await mail_api.getMessages()).filter(
|
||||
(message) => message.recipients[0] == "<user@example.com>"
|
||||
)[0];
|
||||
assert.ok(message_metadata?.subject);
|
||||
|
||||
const message = await mail_api.getMessageById(message_metadata.id);
|
||||
const match_result = /token=([^?&]+)/.exec(message);
|
||||
if (!match_result) {
|
||||
throw new Error("Didn't find a token");
|
||||
}
|
||||
const token = match_result[1];
|
||||
|
||||
await rest_api.post("/finalize-registration-intent", {
|
||||
email: "user@example.com",
|
||||
token,
|
||||
password: "password",
|
||||
username: "user",
|
||||
});
|
||||
|
||||
const options = await rest_api.login({
|
||||
username: "user",
|
||||
password: "password",
|
||||
});
|
||||
|
||||
const response = await rest_api.get(
|
||||
"/api/v1/collections/users/me?attachments[roles]=true",
|
||||
options
|
||||
);
|
||||
assert.equal(response.items[0].roles.length, 1);
|
||||
assert.equal(response.attachments[response.items[0].roles[0]].role, "admin");
|
||||
}));
|
||||
});
|
@ -0,0 +1,53 @@
|
||||
import { Middleware } from "@koa/router";
|
||||
import { hasShape, predicates } from "@sealcode/ts-predicates";
|
||||
import assert from "assert";
|
||||
|
||||
const finalizeRegistrationIntent: Middleware = async (ctx) => {
|
||||
if (
|
||||
!hasShape(
|
||||
{
|
||||
token: predicates.string,
|
||||
username: predicates.string,
|
||||
password: predicates.string,
|
||||
},
|
||||
ctx.$body
|
||||
)
|
||||
) {
|
||||
throw new Error("Missing attributes. Required: token, username, password");
|
||||
}
|
||||
const intents = await ctx.$app.collections["registration-intents"]
|
||||
.suList()
|
||||
.filter({ token: ctx.$body.token })
|
||||
.fetch();
|
||||
if (intents.empty) {
|
||||
throw new Error("Incorrect token");
|
||||
}
|
||||
|
||||
const intent = intents.items[0];
|
||||
const user = await ctx.$app.collections.users.suCreate({
|
||||
password: ctx.$body.password,
|
||||
username: ctx.$body.username,
|
||||
email: intent.get("email") as string,
|
||||
roles: [],
|
||||
});
|
||||
if (intent.get("role")) {
|
||||
await ctx.$app.collections["user-roles"].suCreate({
|
||||
user: user.id,
|
||||
role: intent.get("role") as string,
|
||||
});
|
||||
}
|
||||
await intent.remove(new ctx.$app.SuperContext());
|
||||
const target_path = ctx.$app.ConfigManager.get("accout_creation_success_path");
|
||||
if (target_path) {
|
||||
assert.strictEqual(
|
||||
target_path[0],
|
||||
"/",
|
||||
"'accout_creation_success_path' set, but doesn't start with a '/'"
|
||||
);
|
||||
ctx.body = `<meta http-equiv="refresh" content="0; url=${target_path}" />`;
|
||||
}
|
||||
ctx.body = "Account creation successful";
|
||||
ctx.status = 201;
|
||||
};
|
||||
|
||||
export default finalizeRegistrationIntent;
|
@ -0,0 +1,49 @@
|
||||
import { BaseContext } from "koa";
|
||||
import { hasShape, is, predicates } from "@sealcode/ts-predicates";
|
||||
import { Collection, Errors } from "sealious";
|
||||
import { ItemFields } from "sealious/@types/src/chip-types/collection-item-body";
|
||||
|
||||
export interface CollectionTiedFormData<C extends Collection> {
|
||||
values: Partial<{ [field in keyof ItemFields<C>]: string }>;
|
||||
errors?: Errors.FieldsError<C>;
|
||||
}
|
||||
|
||||
export function formHasAllFields<Fields extends readonly string[]>(
|
||||
ctx: BaseContext,
|
||||
fields: Fields,
|
||||
obj: unknown
|
||||
): obj is { [field in Fields[number]]: string } {
|
||||
const valid =
|
||||
is(obj, predicates.object) &&
|
||||
hasShape(
|
||||
Object.fromEntries(fields.map((field) => [field, predicates.string])),
|
||||
obj
|
||||
);
|
||||
if (!valid) {
|
||||
ctx.status = 422;
|
||||
if (is(obj, predicates.object)) {
|
||||
ctx.body = `Missing params: ${fields
|
||||
.filter((field) => !Object.keys(obj).includes(field))
|
||||
.join(", ")}`;
|
||||
}
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
|
||||
export function formHasSomeFields<Fields extends readonly string[]>(
|
||||
ctx: BaseContext,
|
||||
fields: Fields,
|
||||
obj: unknown
|
||||
): obj is Partial<{ [field in Fields[number]]: string }> {
|
||||
const valid =
|
||||
is(obj, predicates.object) &&
|
||||
hasShape(
|
||||
Object.fromEntries(fields.map((field) => [field, predicates.string])),
|
||||
obj
|
||||
);
|
||||
if (!valid) {
|
||||
ctx.status = 422;
|
||||
ctx.body = "Wrong type of params, expected string or undefined";
|
||||
}
|
||||
return valid;
|
||||
}
|
@ -1,14 +1,16 @@
|
||||
import html from "../html";
|
||||
import html from "../../html";
|
||||
import { BaseContext } from "koa";
|
||||
import { Readable } from "stream";
|
||||
import { tempstream } from "tempstream";
|
||||
import { NewTask, TaskList } from "../views/tasks";
|
||||
import navbar from "./navbar";
|
||||
import { NewTask, TaskList } from "../tasks/tasks.views";
|
||||
|
||||
export function MainView(ctx: BaseContext): Readable {
|
||||
return html(
|
||||
ctx,
|
||||
tempstream/* HTML */ ` <title>My Own ToDo App</title>
|
||||
<body>
|
||||
${navbar(ctx)}
|
||||
<h1>My ToDo App (with esbuild!)</h1>
|
||||
|
||||
${TaskList(ctx.$context)} ${NewTask()}
|
@ -0,0 +1,18 @@
|
||||
import { BaseContext } from "koa";
|
||||
|
||||
export default function navbar(ctx: BaseContext) {
|
||||
return /* HTML */ ` <nav>
|
||||
<a href="/" style="display: flex; align-items: center">
|
||||
<img
|
||||
src="/assets/logo"
|
||||
alt="${ctx.$app.manifest.name} - logo"
|
||||
width="50"
|
||||
height="50"
|
||||
/>
|
||||
Sealious Playground
|
||||
</a>
|
||||
<ul>
|
||||
<li><a href="/account/create">Register</a></li>
|
||||
</ul>
|
||||
</nav>`;
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
.input {
|
||||
&__error {
|
||||
color: red;
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
export default function input({
|
||||
name,
|
||||
id,
|
||||
label,
|
||||
type,
|
||||
value,
|
||||
placeholder,
|
||||
error,
|
||||
readonly,
|
||||
}: {
|
||||
name: string;
|
||||
id?: string;
|
||||
label?: string;
|
||||
type?: string;
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
readonly?: boolean;
|
||||
error: string;
|
||||
}) {
|
||||
id = id || name;
|
||||
label = label || name;
|
||||
type = type || "text";
|
||||
value = value || "";
|
||||
placeholder = placeholder || type;
|
||||
readonly = readonly || false;
|
||||
return /* HTML */ `<div class="input">
|
||||
<label for="${id}">${label}</label>
|
||||
<input
|
||||
id="${id}"
|
||||
type="${type}"
|
||||
name="${name}"
|
||||
value="${value}"
|
||||
placeholder="${placeholder}"
|
||||
${readonly ? "readonly" : ""}
|
||||
/>
|
||||
${error ? `<div class="input__error">${error}</div>` : ""}
|
||||
</div>`;
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
import TheApp from "../app";
|
||||
import { mainRouter } from "../routes";
|
||||
import _locreq from "locreq";
|
||||
|
||||
const locreq = _locreq(__dirname);
|
||||
import Sealious, { SMTPMailer } from "sealious";
|
||||
import { TestUtils } from "sealious";
|
||||
|
||||
declare module "koa" {
|
||||
interface BaseContext {
|
||||
$context: Sealious.Context;
|
||||
$app: TheApp;
|
||||
$body: Record<string, unknown>;
|
||||
}
|
||||
}
|
||||
|
||||
export async function withProdApp(
|
||||
callback: (args: {
|
||||
app: TheApp;
|
||||
base_url: string;
|
||||
rest_api: TestUtils.MockRestApi;
|
||||
mail_api: TestUtils.MailcatcherAPI;
|
||||
}) => Promise<void>
|
||||
) {
|
||||
const app = new TheApp();
|
||||
const port = 9999;
|
||||
|
||||
app.config["www-server"].port = port;
|
||||
app.config.datastore_mongo = {
|
||||
host: "db",
|
||||
port: 27017,
|
||||
db_name: "sealious-playground-test",
|
||||
};
|
||||
app.config.logger.level = <const>"none";
|
||||
app.mailer = new SMTPMailer({
|
||||
host: "mailcatcher",
|
||||
port: 1025,
|
||||
user: "any",
|
||||
password: "any",
|
||||
});
|
||||
|
||||
await app.start();
|
||||
mainRouter(app.HTTPServer.router);
|
||||
|
||||
app.HTTPServer.addStaticRoute("/", locreq.resolve("public"));
|
||||
|
||||
const base_url = `http://127.0.0.1:${port}`;
|
||||
const mail_api = new TestUtils.MailcatcherAPI("http://mailcatcher:1080", app);
|
||||
await mail_api.deleteAllInstanceEmails();
|
||||
|
||||
async function stop() {
|
||||
await app.removeAllData();
|
||||
await app.stop();
|
||||
}
|
||||
|
||||
try {
|
||||
await callback({
|
||||
app,
|
||||
base_url,
|
||||
rest_api: new TestUtils.MockRestApi(base_url),
|
||||
mail_api,
|
||||
});
|
||||
await stop();
|
||||
} catch (e) {
|
||||
await stop();
|
||||
console.error(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
const mri = require("mri");
|
||||
const { spawn } = require("child_process");
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
const args = mri(argv);
|
||||
|
||||
const bin_dir = "./node_modules/.bin/";
|
||||
|
||||
const mocha = bin_dir + "mocha";
|
||||
|
||||
let mocha_options = [
|
||||
"--recursive",
|
||||
"--timeout=10000",
|
||||
"--require",
|
||||
"source-map-support/register",
|
||||
];
|
||||
|
||||
if (args["test-report"]) {
|
||||
mocha_options = [
|
||||
...mocha_options,
|
||||
// "--require",
|
||||
// "ts-node/register",
|
||||
// "--require",
|
||||
// "./src/http/type-overrides.ts",
|
||||
"--reporter",
|
||||
"xunit",
|
||||
"--reporter-option",
|
||||
"output=.xunit",
|
||||
];
|
||||
}
|
||||
|
||||
const mocha_files = ["dist/**/*.test.js"];
|
||||
|
||||
let command = [mocha, ...mocha_options, ...mocha_files];
|
||||
|
||||
if (args.cover) {
|
||||
const nyc = [
|
||||
bin_dir + "nyc",
|
||||
"-all",
|
||||
"--exclude",
|
||||
"src/front",
|
||||
"--exclude",
|
||||
"dist",
|
||||
"--source-map",
|
||||
"false",
|
||||
];
|
||||
if (args["cover-html"]) {
|
||||
nyc.push("--reporter", "lcov");
|
||||
} else {
|
||||
nyc.push("--reporter", "clover");
|
||||
}
|
||||
command = [...nyc, ...command];
|
||||
}
|
||||
|
||||
if (args.debug) {
|
||||
command = ["node", "inspect", ...command];
|
||||
}
|
||||
|
||||
console.log("spawning mocha...", command);
|
||||
|
||||
const proc = spawn(command[0], command.slice(1), {
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
proc.on("exit", function (code) {
|
||||
if (args["test-report"]) {
|
||||
process.exit(0);
|
||||
} else {
|
||||
process.exit(code);
|
||||
}
|
||||
});
|
Loading…
Reference in New Issue