@social-mail/social-mail-web-server 1.7.60 → 1.7.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/content/json/bad-names.json +591 -0
  2. package/dist/common/globalEnv.d.ts +1 -0
  3. package/dist/common/globalEnv.d.ts.map +1 -1
  4. package/dist/common/globalEnv.js +1 -0
  5. package/dist/common/globalEnv.js.map +1 -1
  6. package/dist/server/model/entities/User.d.ts +2 -0
  7. package/dist/server/model/entities/User.d.ts.map +1 -1
  8. package/dist/server/model/entities/User.js +13 -0
  9. package/dist/server/model/entities/User.js.map +1 -1
  10. package/dist/server/model/events/DomainEvents.d.ts +1 -0
  11. package/dist/server/model/events/DomainEvents.d.ts.map +1 -1
  12. package/dist/server/model/events/DomainEvents.js +22 -0
  13. package/dist/server/model/events/DomainEvents.js.map +1 -1
  14. package/dist/server/model/events/HostedDomainEvents.d.ts.map +1 -1
  15. package/dist/server/model/events/HostedDomainEvents.js.map +1 -1
  16. package/dist/server/model/events/UserEvents.d.ts.map +1 -1
  17. package/dist/server/model/events/UserEvents.js +5 -0
  18. package/dist/server/model/events/UserEvents.js.map +1 -1
  19. package/dist/server/seed/ui/seed-ui.js +1 -1
  20. package/dist/server/services/EmailAddressService.d.ts +1 -0
  21. package/dist/server/services/EmailAddressService.d.ts.map +1 -1
  22. package/dist/server/services/EmailAddressService.js +81 -58
  23. package/dist/server/services/EmailAddressService.js.map +1 -1
  24. package/dist/server/services/ui/renderUIClient.d.ts.map +1 -1
  25. package/dist/server/services/ui/renderUIClient.js +1 -0
  26. package/dist/server/services/ui/renderUIClient.js.map +1 -1
  27. package/dist/tsconfig.tsbuildinfo +1 -1
  28. package/dist/wwwroot/routes/social-mail/user/password/reset/post.d.ts +24 -0
  29. package/dist/wwwroot/routes/social-mail/user/password/reset/post.d.ts.map +1 -0
  30. package/dist/wwwroot/routes/social-mail/user/password/reset/post.js +109 -0
  31. package/dist/wwwroot/routes/social-mail/user/password/reset/post.js.map +1 -0
  32. package/dist/wwwroot/routes/social-mail/user/signup/get.d.ts +5 -0
  33. package/dist/wwwroot/routes/social-mail/user/signup/get.d.ts.map +1 -0
  34. package/dist/wwwroot/routes/social-mail/user/signup/get.js +8 -0
  35. package/dist/wwwroot/routes/social-mail/user/signup/get.js.map +1 -0
  36. package/dist/wwwroot/routes/social-mail/user/signup/post.d.ts +4 -12
  37. package/dist/wwwroot/routes/social-mail/user/signup/post.d.ts.map +1 -1
  38. package/dist/wwwroot/routes/social-mail/user/signup/post.js +86 -97
  39. package/dist/wwwroot/routes/social-mail/user/signup/post.js.map +1 -1
  40. package/package.json +2 -2
  41. package/src/common/globalEnv.ts +1 -0
  42. package/src/server/model/entities/User.ts +13 -0
  43. package/src/server/model/events/DomainEvents.ts +10 -1
  44. package/src/server/model/events/HostedDomainEvents.ts +1 -0
  45. package/src/server/model/events/UserEvents.ts +5 -0
  46. package/src/server/seed/ui/seed-ui.ts +1 -1
  47. package/src/server/services/EmailAddressService.ts +9 -0
  48. package/src/server/services/ui/renderUIClient.tsx +1 -0
  49. package/src/wwwroot/routes/social-mail/user/password/reset/post.tsx +128 -0
  50. package/src/wwwroot/routes/social-mail/user/signup/get.tsx +9 -0
  51. package/src/wwwroot/routes/social-mail/user/signup/post.tsx +88 -103
@@ -3,7 +3,7 @@ import semver from "semver";
3
3
 
4
4
  export default async function seedUI(config: DBConfig) {
5
5
 
6
- const version = "1.4.79";
6
+ const version = "1.4.80";
7
7
 
8
8
  const existing = await config.getValue(UIPackageConfig, false);
9
9
  if (existing) {
@@ -15,6 +15,15 @@ export default class EmailAddressService implements ICacheContainer<EmailAddress
15
15
 
16
16
  cache: ObjectCache<EmailAddressService>;
17
17
 
18
+ @cacheFor()
19
+ public async getHostedDomain(name: string) {
20
+ using scope = globalServices.createScope();
21
+ const db = scope.resolve(SocialMailContext);
22
+ return await db.hostedDomains.where({ name }, (p) => (x) => x.domain.name === p.name)
23
+ .include((x) => x.domain)
24
+ .first();
25
+ }
26
+
18
27
  @cacheFor()
19
28
  public async getOrCreateDomain(name: string) {
20
29
  name = name.toLocaleLowerCase().trim();
@@ -45,6 +45,7 @@ export const renderUIClient = async (page: Page, view?: string, addScript?: stri
45
45
  path: globalEnv.path,
46
46
  name: globalEnv.name,
47
47
  cdn: globalEnv.cdn,
48
+ signupEnabled: globalEnv.signupEnabled,
48
49
  externalLogin: globalEnv.externalLogin,
49
50
  dnsEnabled: globalEnv.dns?.enabled
50
51
  })};
@@ -0,0 +1,128 @@
1
+ import EntityAccessError from "@entity-access/entity-access/dist/common/EntityAccessError.js";
2
+ import Inject from "@entity-access/entity-access/dist/di/di.js";
3
+ import Page from "@entity-access/server-pages/dist/Page.js";
4
+ import { Prepare } from "@entity-access/server-pages/dist/decorators/Prepare.js";
5
+ import SocialMailContext from "../../../../../../server/model/SocialMailContext.js";
6
+ import CryptoService from "../../../../../../server/services/CryptoService.js";
7
+ import EmailAddressService from "../../../../../../server/services/EmailAddressService.js";
8
+ import CaptchaService from "../../../../../../server/services/captcha/CaptchaService.js";
9
+ import { IVerification } from "../../../../../../server/services/captcha/IVerification.js";
10
+ import ExternalOtpInService from "../../../../../../server/services/external-otp-in/ExternalOtpInService.js";
11
+
12
+ export interface ISignup {
13
+ username: string;
14
+ userID: number;
15
+ phoneNumber: string;
16
+ captcha: IVerification;
17
+ phone: IVerification;
18
+ password: string;
19
+ }
20
+
21
+ @Prepare.parseJsonBody
22
+ export default class extends Page {
23
+
24
+ @Inject
25
+ captchaService: CaptchaService;
26
+
27
+ @Inject
28
+ cryptoService: CryptoService;
29
+
30
+ @Inject
31
+ emailAddressService: EmailAddressService;
32
+
33
+ @Inject
34
+ ois: ExternalOtpInService;
35
+
36
+
37
+ @Inject
38
+ db: SocialMailContext;
39
+
40
+ async run() {
41
+
42
+ const { db } = this;
43
+
44
+ db.verifyFilters = false;
45
+ db.raiseEvents = false;
46
+
47
+ const model = this.body as ISignup;
48
+
49
+ model.username = model.username?.trim();
50
+ if (!model.username) {
51
+ throw new EntityAccessError("Username is required");
52
+ }
53
+ const [username, domain] = model.username.split("@");
54
+
55
+ model.username = username?.trim()?.toLocaleLowerCase()?.replace(/[\.\-]/g, "") + "@" + domain;
56
+
57
+ model.phoneNumber = `+${model.phoneNumber.replace(/[^0-9]/g, "")}`;
58
+
59
+ // console.log(model);
60
+
61
+ // verify captcha...
62
+ await this.captchaService.verify(model.captcha as any);
63
+
64
+ const user = await db.users.where(model, (p) => (x) => x.userName === p.username)
65
+ .first();
66
+
67
+ if (!user) {
68
+ throw new EntityAccessError(`Username ${model.username} not found, make sure it is complete email address.`);
69
+ }
70
+
71
+ if (!model.phone.id) {
72
+
73
+
74
+ // check if phone exists...
75
+ const phone = await db.userAuthFactors
76
+ .where(model, (p) => (x) =>
77
+ x.user.userName === p.username
78
+ && x.phoneNumber === p.phoneNumber
79
+ )
80
+ .first();
81
+
82
+ if (!phone) {
83
+ throw new EntityAccessError(`No phone number is associated with this user, please contact us at support@neurospeech.com .`);
84
+ }
85
+
86
+ if (!model.phone.id) {
87
+ const v = await this.ois.create([model.phoneNumber]);
88
+ model.phone.id = v.verificationID as any;
89
+ model.phone.code = v.accessCode;
90
+ model.phone.smsConfig = v.externalConfig;
91
+ return this.json(model);
92
+ }
93
+
94
+ }
95
+
96
+ if(model.phone.id) {
97
+ // verify...
98
+ const v = await db.verificationTokens.statements.select({} , { verificationID: model.phone.id as any });
99
+ if (!v.verified) {
100
+ return this.json(model);
101
+ }
102
+ model.phone.verified = "true";
103
+ }
104
+
105
+ if (!model.password) {
106
+ return this.json(model);
107
+ }
108
+
109
+ if (model.password.length < 7) {
110
+ throw new EntityAccessError("Password is cannot be less than 7 characters");
111
+ }
112
+
113
+ if (/\s/i.test(model.password)) {
114
+ throw new EntityAccessError("Password must not any whitespace characters");
115
+ }
116
+
117
+ const passwordSalt = this.cryptoService.generateSalt();
118
+ const passwordHash = this.cryptoService.hash(passwordSalt, model.password);
119
+ model.userID = user.userID;
120
+ await db.users.where(user, (p) => (x) => x.userID === p.userID)
121
+ .update({ passwordSalt, passwordHash}, (p) => (x) => ({
122
+ passwordHash: p.passwordHash,
123
+ passwordSalt: p.passwordSalt
124
+ }));
125
+ return this.json(model);
126
+ }
127
+
128
+ }
@@ -0,0 +1,9 @@
1
+ import Page from "@entity-access/server-pages/dist/Page.js";
2
+ import { renderUIClient } from "../../../../../server/services/ui/renderUIClient.js";
3
+
4
+ export default class extends Page {
5
+
6
+ async run() {
7
+ return renderUIClient(this, "dist/public/users/signup/SignupPage");
8
+ }
9
+ }
@@ -1,42 +1,45 @@
1
- import EntityAccessError from "@entity-access/entity-access/dist/common/EntityAccessError.js";
2
1
  import Inject from "@entity-access/entity-access/dist/di/di.js";
3
- import DateTime from "@entity-access/entity-access/dist/types/DateTime.js";
4
2
  import Page from "@entity-access/server-pages/dist/Page.js";
5
3
  import { Prepare } from "@entity-access/server-pages/dist/decorators/Prepare.js";
4
+ import EntityAccessError from "@entity-access/entity-access/dist/common/EntityAccessError.js";
5
+ import DateTime from "@entity-access/entity-access/dist/types/DateTime.js";
6
+ import { readFileSync } from "fs";
6
7
  import SocialMailContext from "../../../../../server/model/SocialMailContext.js";
8
+ import VerificationToken from "../../../../../server/model/entities/VerificationToken.js";
7
9
  import CryptoService from "../../../../../server/services/CryptoService.js";
8
10
  import EmailAddressService from "../../../../../server/services/EmailAddressService.js";
9
11
  import CaptchaService from "../../../../../server/services/captcha/CaptchaService.js";
10
- import { IVerification } from "../../../../../server/services/captcha/IVerification.js";
11
12
  import ExternalOtpInService from "../../../../../server/services/external-otp-in/ExternalOtpInService.js";
12
- import { randomUUID } from "crypto";
13
- import { ContactAddress } from "../../../../../server/model/entities/Contact.js";
14
- import WebSiteService from "../../../../../server/services/files/WebSiteService.js";
15
- import { StoreAccountAddress } from "../../../../../server/model/entities/StoreAddress.js";
16
- import VerificationToken from "../../../../../server/model/entities/VerificationToken.js";
17
13
 
18
14
  export interface ISignup {
19
15
  userID: number;
16
+ username: string;
17
+ emailAddress: string;
20
18
  password: string;
21
19
  name: string;
22
20
  phoneNumber: string;
23
- emailAddress: string;
24
21
  isdCode?: string;
25
22
  country: string;
26
23
  captcha: Partial<VerificationToken>;
27
24
  phone: Partial<VerificationToken>;
28
- email: Partial<VerificationToken>;
29
- billing?: Partial<StoreAccountAddress>;
30
- shipping?: Partial<StoreAccountAddress>;
31
25
  }
32
26
 
33
- /**
34
- * External Signup is used for signing up for Blog or Store.
35
- */
27
+ const ensureUsername = (name: string) => {
28
+
29
+ const json = JSON.parse(readFileSync("./content/json/bad-names.json","utf8")) as { reserved: string[], partial: string[] };
30
+
31
+ if(json.reserved.includes(name)) {
32
+ throw new EntityAccessError("Username is reserved.");
33
+ }
34
+ for (const iterator of json.partial) {
35
+ if (name.includes(iterator)) {
36
+ throw new EntityAccessError("Username is not acceptable.");
37
+ }
38
+ }
39
+ };
36
40
 
37
41
  @Prepare.parseJsonBody
38
- @Prepare.authorize
39
- export default class ExternalSignup extends Page {
42
+ export default class extends Page {
40
43
 
41
44
  @Inject
42
45
  captchaService: CaptchaService;
@@ -53,9 +56,6 @@ export default class ExternalSignup extends Page {
53
56
  @Inject
54
57
  db: SocialMailContext;
55
58
 
56
- @Inject
57
- websiteService: WebSiteService;
58
-
59
59
  async run() {
60
60
 
61
61
  const { db } = this;
@@ -65,24 +65,40 @@ export default class ExternalSignup extends Page {
65
65
 
66
66
  const model = this.body as ISignup;
67
67
 
68
- model.emailAddress = model.email.verifyAddresses;
69
- model.phoneNumber = model.phone.verifyAddresses;
68
+ model.emailAddress = model.emailAddress.trim().toLowerCase();
69
+
70
+ const [username, domain] = model.emailAddress.split("@", 2);
71
+ model.username = username;
70
72
 
71
- const w = await this.websiteService.get(this.request.hostName);
72
- const storeID = w.folderID;
73
- if (!storeID) {
74
- throw new EntityAccessError(`Signup not allowed`);
73
+ if (!/\p{Letter}[\p{Letter}\.\-0-9]{0,50}/iu.test(username)) {
74
+ throw new EntityAccessError(`Invalid email address`);
75
75
  }
76
76
 
77
77
  model.password = model.password?.trim();
78
- if (!model.emailAddress) {
79
- throw new EntityAccessError("Email is required");
78
+ model.username = model.username?.trim()?.toLowerCase();
79
+ if (!model.username) {
80
+ throw new EntityAccessError("Username is required");
81
+ }
82
+ ensureUsername(model.username);
83
+ if (!model.password) {
84
+ throw new EntityAccessError("Password is required");
85
+ }
86
+
87
+ if (model.password.length < 7) {
88
+ throw new EntityAccessError("Password is cannot be less than 7 characters");
89
+ }
90
+
91
+ if (/\s/i.test(model.password)) {
92
+ throw new EntityAccessError("Password must not any whitespace characters");
80
93
  }
81
94
 
82
95
  model.phoneNumber = `+${model.phoneNumber.replace(/[^0-9]/g, "")}`;
83
96
 
97
+ // console.log(model);
98
+
84
99
  // check if phone number already exists...
85
- for(const v1 of [model.captcha, model.email, model.phone]) {
100
+
101
+ for(const v1 of [model.captcha, model.phone]) {
86
102
  const { verificationID , accessCode } = v1;
87
103
  const vr = await db.verificationTokens.statements.select({}, { verificationID, accessCode });
88
104
  if (!vr.verified) {
@@ -90,27 +106,26 @@ export default class ExternalSignup extends Page {
90
106
  }
91
107
  }
92
108
 
93
- const { emailAddress, phoneNumber } = model;
94
- const emailExists = await db.storeAccountEmails.where({ emailAddress, storeID}, (p) => (x) =>
95
- x.emailAddress.emailAddress === p.emailAddress
96
- && x.storeAccount.storeID === p.storeID
97
- )
109
+ const hostedDomain = await this.emailAddressService.getHostedDomain(domain);
110
+ if(!hostedDomain || !hostedDomain.allowSignup) {
111
+ throw new EntityAccessError(`Signup for email address ${model.emailAddress} not allowed for domain ${domain}`);
112
+ }
113
+ const emailAddress = model.emailAddress;
114
+ const user = await db.mailboxes.where({ emailAddress}, (p) => (x) => x.emailAddress.emailAddress === p.emailAddress)
98
115
  .some();
99
- if (emailExists) {
116
+ if (user) {
100
117
  throw new EntityAccessError(`Email address already exists.`);
101
118
  }
102
119
 
103
- // check if phone exists...
104
- const exists = await db.storeAccountPhones
105
- .where({ phoneNumber, storeID }, (p) => (x) => x.phoneNumberE64 === p.phoneNumber
106
- && x.storeAccount.storeID === p.storeID
107
- ).some();
108
120
 
121
+ // check if phone exists...
122
+ const exists = await db.users
123
+ .where(model, (p) => (x) => x.uniquePhoneNumber === p.phoneNumber)
124
+ .some();
109
125
  if (exists) {
110
126
  throw new EntityAccessError(`Phone number is already registered.`);
111
127
  }
112
128
 
113
- model.password = randomUUID();
114
129
  const passwordSalt = this.cryptoService.generateSalt();
115
130
  const passwordHash = this.cryptoService.hash(passwordSalt, model.password);
116
131
 
@@ -118,30 +133,23 @@ export default class ExternalSignup extends Page {
118
133
  const phoneNumberE64 = model.phoneNumber;
119
134
  const name = model.name;
120
135
 
121
- let billingAddress: StoreAccountAddress;
122
- let shippingAddress: StoreAccountAddress;
123
- const addresses = [];
124
- if (model.billing?.title) {
125
- billingAddress = db.storeAccountAddresses.add(model.billing);
126
- addresses.push(billingAddress);
127
- }
128
- if (model.shipping?.title) {
129
- shippingAddress = db.storeAccountAddresses.add(model.shipping);
130
- addresses.push(shippingAddress);
131
- }
132
-
133
- const emailAddressID = await this.emailAddressService.getId(model.emailAddress, model.name);
136
+ const mailboxID = await this.emailAddressService.getId(emailAddress, name);
137
+ const { domainID } = hostedDomain;
134
138
 
135
139
  // create user...
136
140
  const account = db.users.add({
137
- /** This is to make customer user unique per store */
138
- userName: storeID + ":" + model.emailAddress ,
141
+ userName: model.emailAddress ,
139
142
  displayName: name,
140
143
  passwordSalt,
141
144
  passwordHash,
142
145
  multiFactor: false,
143
146
  dateUpdated: dateCreated,
144
- isExternal: true,
147
+ uniquePhoneNumber: phoneNumberE64,
148
+ roles: [
149
+ db.userRoles.add({
150
+ roleName: "User"
151
+ })
152
+ ],
145
153
  authFactors: [
146
154
  db.userAuthFactors.add({
147
155
  phoneNumber: phoneNumberE64,
@@ -149,66 +157,43 @@ export default class ExternalSignup extends Page {
149
157
  method: "otp-in",
150
158
  dateUpdated: dateCreated
151
159
  })
152
- ]
153
- });
154
-
155
- const contact = db.storeAccounts.add({
156
- dateCreated,
157
- user: account,
158
- storeID,
159
- /** This is important as name must be unique in the ledger */
160
- name: `${model.name}(${emailAddressID})`,
161
- head: "Customer",
162
- emails: [
163
- db.storeAccountEmails.add({
164
- emailAddressID
165
- })
166
160
  ],
167
- phones: [
168
- db.storeAccountPhones.add({
169
- phoneNumberE64
161
+ userMailboxes:[
162
+ db.userMailboxes.add({
163
+ mailbox: db.mailboxes.add({
164
+ mailboxID,
165
+ domainID,
166
+ uniqueAddress: model.username.replace(/[\-\.]/g, "") + "@" + hostedDomain.domain.name,
167
+ maxStorage: 1024*1024*1024
168
+ })
170
169
  })
171
- ],
172
- addresses
170
+ ]
173
171
  });
174
172
 
175
173
  await db.saveChanges();
176
174
 
177
- if (billingAddress) {
178
- contact.billingAddressID = billingAddress.addressID;
179
- }
180
- if (shippingAddress) {
181
- contact.shippingAddressID = shippingAddress.addressID;
182
- }
183
- if (addresses.length) {
184
- await db.saveChanges();
185
- }
175
+ // todo pending delete
176
+ // await this.captchaService.delete(model.captcha as any);
186
177
 
187
- // Todo: delete captcha
178
+ model.userID = account.userID;
188
179
 
189
- const userID = model.userID = account.userID;
180
+ const start = DateTime.now;
190
181
 
191
- // lets login here...
182
+ // add login cookie..
183
+ const expiry = start.addDays(30);
192
184
 
193
- const dateUpdated = DateTime.now;
194
- const start = dateUpdated;
195
- const expiry = dateUpdated.addDays(30);
196
-
197
- // create login session...
198
- const loginSession = await db.loginSessions.statements.insert({
199
- userID,
200
- dateUpdated,
185
+ model.userID = account.userID;
186
+ const session = await db.loginSessions.statements.insert({
187
+ userID: account.userID,
201
188
  start,
202
189
  expiry,
203
- invalid: false,
204
- verificationID: model.phone.verificationID as any,
205
- status: "created"
190
+ status: "created",
191
+ invalid: false
206
192
  });
207
-
208
193
  await this.sessionUser.setAuthCookie({
209
- userID: loginSession.userID,
210
- id: loginSession.sessionID,
211
- expiry: expiry.asJSDate,
194
+ userID: session.userID,
195
+ id: session.sessionID,
196
+ expiry: session.expiry.asJSDate,
212
197
  version: "1.1",
213
198
  active: true
214
199
  });