mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 11:42:30 +03:00
Expire invite link (#2141)
Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
This commit is contained in:
parent
d935e9b963
commit
7499ec85d2
@ -24,6 +24,10 @@ HR:
|
||||
|
||||
- Leaves schedule
|
||||
|
||||
Core:
|
||||
|
||||
- Invite link expire from 1 hour
|
||||
|
||||
## 0.6.28
|
||||
|
||||
Core:
|
||||
|
@ -24,6 +24,7 @@
|
||||
"Copied": "Copied",
|
||||
"Close": "Close",
|
||||
"InviteDescription": "Share this link to invite other users",
|
||||
"InviteNote": "Link is valid for 1 hour",
|
||||
"WantAnotherWorkspace": "Want to create another workspace?",
|
||||
"ChangeAccount": "Change account",
|
||||
"NotSeeingWorkspace": "Not seeing your workspace?"
|
||||
|
@ -24,6 +24,7 @@
|
||||
"Copied": "Скопировано",
|
||||
"Close": "Закрыть",
|
||||
"InviteDescription": "Поделитесь ссылкой чтобы пригласить других участников",
|
||||
"InviteNote": "Ссылка действительна 1 час",
|
||||
"WantAnotherWorkspace": "Хотите создать другое рабочее пространство?",
|
||||
"ChangeAccount": "Сменить пользователя",
|
||||
"NotSeeingWorkspace": "Не видите ваше рабочее пространство?"
|
||||
|
@ -1,6 +1,5 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021, 2022 Hardcore Engineering Inc.
|
||||
// Copyright © 2022 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the 'License');
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
@ -16,7 +15,7 @@
|
||||
<script lang="ts">
|
||||
import { Timestamp } from '@anticrm/core'
|
||||
import { Button, getCurrentLocation, Label, locationToUrl, ticker } from '@anticrm/ui'
|
||||
import { getWorkspaceHash } from '../utils'
|
||||
import { getInviteLink } from '../utils'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import login from '../plugin'
|
||||
import InviteWorkspace from './icons/InviteWorkspace.svelte'
|
||||
@ -24,13 +23,13 @@
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
async function getLink (): Promise<string> {
|
||||
const hash = await getWorkspaceHash()
|
||||
const inviteId = await getInviteLink()
|
||||
const loc = getCurrentLocation()
|
||||
loc.path[0] = login.component.LoginApp
|
||||
loc.path[1] = 'join'
|
||||
loc.path.length = 2
|
||||
loc.query = {
|
||||
workspace: hash
|
||||
inviteId
|
||||
}
|
||||
loc.fragment = undefined
|
||||
|
||||
@ -59,6 +58,9 @@
|
||||
<Label label={login.string.InviteDescription} />
|
||||
<InviteWorkspace size="large" />
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<Label label={login.string.InviteNote} />
|
||||
</div>
|
||||
{#await getLink() then link}
|
||||
<div class="over-underline link" on:click={() => copy(link)}>{link}</div>
|
||||
<div class="buttons flex">
|
||||
|
@ -1,6 +1,5 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021, 2022 Hardcore Engineering Inc.
|
||||
// Copyright © 2022 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
@ -62,13 +61,13 @@
|
||||
|
||||
const [loginStatus, result] =
|
||||
page === 'login'
|
||||
? await join(object.username, object.password, location.query?.workspace ?? '')
|
||||
? await join(object.username, object.password, location.query?.inviteId ?? '')
|
||||
: await signUpJoin(
|
||||
object.username,
|
||||
object.password,
|
||||
object.first,
|
||||
object.last,
|
||||
location.query?.workspace ?? ''
|
||||
location.query?.inviteId ?? ''
|
||||
)
|
||||
status = loginStatus
|
||||
|
||||
@ -76,7 +75,7 @@
|
||||
setMetadataLocalStorage(login.metadata.LoginToken, result.token)
|
||||
setMetadataLocalStorage(login.metadata.LoginEndpoint, result.endpoint)
|
||||
setMetadataLocalStorage(login.metadata.LoginEmail, result.email)
|
||||
setMetadataLocalStorage(login.metadata.CurrentWorkspace, location.query?.workspace ?? '')
|
||||
setMetadataLocalStorage(login.metadata.CurrentWorkspace, result.workspace)
|
||||
navigate({ path: [workbench.component.WorkbenchApp] })
|
||||
}
|
||||
}
|
||||
|
@ -47,6 +47,7 @@ export default mergeIds(loginId, login, {
|
||||
InviteDescription: '' as IntlString,
|
||||
WantAnotherWorkspace: '' as IntlString,
|
||||
NotSeeingWorkspace: '' as IntlString,
|
||||
ChangeAccount: '' as IntlString
|
||||
ChangeAccount: '' as IntlString,
|
||||
InviteNote: '' as IntlString
|
||||
}
|
||||
})
|
||||
|
@ -1,5 +1,5 @@
|
||||
//
|
||||
// Copyright © 2020 Anticrm Platform Contributors.
|
||||
// Copyright © 2022 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
@ -13,12 +13,15 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Status, OK, unknownError, getMetadata, serialize, unknownStatus } from '@anticrm/platform'
|
||||
import type { Request, Response } from '@anticrm/platform'
|
||||
|
||||
import { getMetadata, OK, serialize, Status, unknownError, unknownStatus } from '@anticrm/platform'
|
||||
import login from '@anticrm/login'
|
||||
import { fetchMetadataLocalStorage, getCurrentLocation, navigate } from '@anticrm/ui'
|
||||
|
||||
export interface WorkspaceLoginInfo extends LoginInfo {
|
||||
workspace: string
|
||||
}
|
||||
|
||||
export interface LoginInfo {
|
||||
token: string
|
||||
endpoint: string
|
||||
@ -29,6 +32,8 @@ export interface Workspace {
|
||||
workspace: string
|
||||
}
|
||||
|
||||
const DEV_WORKSPACE = 'DEV WORKSPACE'
|
||||
|
||||
/**
|
||||
* Perform a login operation to required workspace with user credentials.
|
||||
*/
|
||||
@ -168,7 +173,7 @@ export async function getWorkspaces (): Promise<Workspace[]> {
|
||||
if (endpoint !== undefined) {
|
||||
return [
|
||||
{
|
||||
workspace: 'DEV WORKSPACE'
|
||||
workspace: DEV_WORKSPACE
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -205,7 +210,7 @@ export async function getWorkspaces (): Promise<Workspace[]> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function selectWorkspace (workspace: string): Promise<[Status, LoginInfo | undefined]> {
|
||||
export async function selectWorkspace (workspace: string): Promise<[Status, WorkspaceLoginInfo | undefined]> {
|
||||
const accountsUrl = getMetadata(login.metadata.AccountsUrl)
|
||||
|
||||
if (accountsUrl === undefined) {
|
||||
@ -217,7 +222,7 @@ export async function selectWorkspace (workspace: string): Promise<[Status, Logi
|
||||
if (overrideToken !== undefined) {
|
||||
const endpoint = getMetadata(login.metadata.OverrideEndpoint)
|
||||
if (endpoint !== undefined) {
|
||||
return [OK, { token: overrideToken, endpoint, email }]
|
||||
return [OK, { token: overrideToken, endpoint, email, workspace }]
|
||||
}
|
||||
}
|
||||
|
||||
@ -251,9 +256,13 @@ export async function selectWorkspace (workspace: string): Promise<[Status, Logi
|
||||
}
|
||||
}
|
||||
|
||||
export async function getWorkspaceHash (): Promise<string> {
|
||||
export async function getInviteLink (): Promise<string> {
|
||||
const accountsUrl = getMetadata(login.metadata.AccountsUrl)
|
||||
|
||||
const exp = 1000 * 60 * 60
|
||||
const emailMask = ''
|
||||
const limit = -1
|
||||
|
||||
if (accountsUrl === undefined) {
|
||||
throw new Error('accounts url not specified')
|
||||
}
|
||||
@ -267,9 +276,9 @@ export async function getWorkspaceHash (): Promise<string> {
|
||||
return ''
|
||||
}
|
||||
|
||||
const request: Request<[]> = {
|
||||
method: 'getWorkspaceHash',
|
||||
params: []
|
||||
const request: Request<[number, string, number]> = {
|
||||
method: 'getInviteLink',
|
||||
params: [exp, emailMask, limit]
|
||||
}
|
||||
|
||||
const response = await fetch(accountsUrl, {
|
||||
@ -287,8 +296,8 @@ export async function getWorkspaceHash (): Promise<string> {
|
||||
export async function join (
|
||||
email: string,
|
||||
password: string,
|
||||
workspaceHash: string
|
||||
): Promise<[Status, LoginInfo | undefined]> {
|
||||
inviteId: string
|
||||
): Promise<[Status, WorkspaceLoginInfo | undefined]> {
|
||||
const accountsUrl = getMetadata(login.metadata.AccountsUrl)
|
||||
|
||||
if (accountsUrl === undefined) {
|
||||
@ -299,13 +308,13 @@ export async function join (
|
||||
if (token !== undefined) {
|
||||
const endpoint = getMetadata(login.metadata.OverrideEndpoint)
|
||||
if (endpoint !== undefined) {
|
||||
return [OK, { token, endpoint, email }]
|
||||
return [OK, { token, endpoint, email, workspace: DEV_WORKSPACE }]
|
||||
}
|
||||
}
|
||||
|
||||
const request: Request<[string, string, string]> = {
|
||||
method: 'join',
|
||||
params: [email, password, workspaceHash]
|
||||
params: [email, password, inviteId]
|
||||
}
|
||||
|
||||
try {
|
||||
@ -328,8 +337,8 @@ export async function signUpJoin (
|
||||
password: string,
|
||||
first: string,
|
||||
last: string,
|
||||
workspaceHash: string
|
||||
): Promise<[Status, LoginInfo | undefined]> {
|
||||
inviteId: string
|
||||
): Promise<[Status, WorkspaceLoginInfo | undefined]> {
|
||||
const accountsUrl = getMetadata(login.metadata.AccountsUrl)
|
||||
|
||||
if (accountsUrl === undefined) {
|
||||
@ -340,13 +349,13 @@ export async function signUpJoin (
|
||||
if (token !== undefined) {
|
||||
const endpoint = getMetadata(login.metadata.OverrideEndpoint)
|
||||
if (endpoint !== undefined) {
|
||||
return [OK, { token, endpoint, email }]
|
||||
return [OK, { token, endpoint, email, workspace: DEV_WORKSPACE }]
|
||||
}
|
||||
}
|
||||
|
||||
const request: Request<[string, string, string, string, string]> = {
|
||||
method: 'signUpJoin',
|
||||
params: [email, password, first, last, workspaceHash]
|
||||
params: [email, password, first, last, inviteId]
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -13,7 +13,7 @@
|
||||
"docker:build": "docker build -t hardcoreeng/account .",
|
||||
"docker:staging": "../../common/scripts/docker_tag.sh hardcoreeng/account staging",
|
||||
"docker:push": "../../common/scripts/docker_tag.sh hardcoreeng/account",
|
||||
"run-local": "cross-env MONGO_URL=mongodb://localhost:27017 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost TRANSACTOR_URL=ws:/localhost:3333 ts-node src/index.ts",
|
||||
"run-local": "cross-env MONGO_URL=mongodb://localhost:27017 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost SERVER_SECRET='secret' TRANSACTOR_URL=ws:/localhost:3333 ts-node src/index.ts",
|
||||
"lint": "eslint src",
|
||||
"format": "prettier --write src && eslint --fix src"
|
||||
},
|
||||
|
@ -1,6 +1,5 @@
|
||||
//
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 Hardcore Engineering Inc.
|
||||
// Copyright © 2022 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
@ -27,13 +26,14 @@ import platform, {
|
||||
Status,
|
||||
StatusCode
|
||||
} from '@anticrm/platform'
|
||||
import toolPlugin, { connect, initModel, upgradeModel, version } from '@anticrm/server-tool'
|
||||
import { decodeToken, generateToken } from '@anticrm/server-token'
|
||||
import toolPlugin, { connect, initModel, upgradeModel, version } from '@anticrm/server-tool'
|
||||
import { pbkdf2Sync, randomBytes } from 'crypto'
|
||||
import { Binary, Db, ObjectId } from 'mongodb'
|
||||
|
||||
const WORKSPACE_COLLECTION = 'workspace'
|
||||
const ACCOUNT_COLLECTION = 'account'
|
||||
const INVITE_COLLECTION = 'invite'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -106,6 +106,24 @@ export interface LoginInfo {
|
||||
endpoint: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface WorkspaceLoginInfo extends LoginInfo {
|
||||
workspace: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface Invite {
|
||||
_id: ObjectId
|
||||
workspace: string
|
||||
exp: number
|
||||
emailMask: string
|
||||
limit: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -176,7 +194,7 @@ export async function login (db: Db, email: string, password: string): Promise<L
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function selectWorkspace (db: Db, token: string, workspace: string): Promise<LoginInfo> {
|
||||
export async function selectWorkspace (db: Db, token: string, workspace: string): Promise<WorkspaceLoginInfo> {
|
||||
const { email } = decodeToken(token)
|
||||
const accountInfo = await getAccount(db, email)
|
||||
if (accountInfo === null) {
|
||||
@ -192,7 +210,8 @@ export async function selectWorkspace (db: Db, token: string, workspace: string)
|
||||
const result = {
|
||||
endpoint: getEndpoint(),
|
||||
email,
|
||||
token: generateToken(email, workspace)
|
||||
token: generateToken(email, workspace),
|
||||
workspace
|
||||
}
|
||||
return result
|
||||
}
|
||||
@ -205,12 +224,46 @@ export async function selectWorkspace (db: Db, token: string, workspace: string)
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function join (db: Db, email: string, password: string, workspaceHash: string): Promise<LoginInfo> {
|
||||
const { workspace } = decodeToken(workspaceHash)
|
||||
const token = (await login(db, email, password)).token
|
||||
export async function getInvite (db: Db, inviteId: ObjectId): Promise<Invite | null> {
|
||||
return await db.collection(INVITE_COLLECTION).findOne<Invite>({ _id: new ObjectId(inviteId) })
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function checkInvite (invite: Invite | null, email: string): Promise<string> {
|
||||
if (invite === null || invite.limit === 0) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
||||
}
|
||||
if (invite.exp < new Date().getTime()) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
||||
}
|
||||
if (!new RegExp(invite.emailMask).test(email)) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
||||
}
|
||||
|
||||
return invite.workspace
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function useInvite (db: Db, inviteId: ObjectId): Promise<void> {
|
||||
await db.collection(INVITE_COLLECTION).updateOne({ _id: inviteId }, { $inc: { limit: -1 } })
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function join (db: Db, email: string, password: string, inviteId: ObjectId): Promise<WorkspaceLoginInfo> {
|
||||
const invite = await getInvite(db, inviteId)
|
||||
const workspace = await checkInvite(invite, email)
|
||||
await assignWorkspace(db, email, workspace)
|
||||
|
||||
return await selectWorkspace(db, token, workspace)
|
||||
const token = (await login(db, email, password)).token
|
||||
const result = await selectWorkspace(db, token, workspace)
|
||||
await useInvite(db, inviteId)
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
@ -222,14 +275,17 @@ export async function signUpJoin (
|
||||
password: string,
|
||||
first: string,
|
||||
last: string,
|
||||
workspaceHash: string
|
||||
): Promise<LoginInfo> {
|
||||
inviteId: ObjectId
|
||||
): Promise<WorkspaceLoginInfo> {
|
||||
const invite = await getInvite(db, inviteId)
|
||||
const workspace = await checkInvite(invite, email)
|
||||
await createAccount(db, email, password, first, last)
|
||||
const { workspace } = decodeToken(workspaceHash)
|
||||
await assignWorkspace(db, email, workspace)
|
||||
|
||||
const token = (await login(db, email, password)).token
|
||||
return await selectWorkspace(db, token, workspace)
|
||||
const result = await selectWorkspace(db, token, workspace)
|
||||
await useInvite(db, inviteId)
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
@ -335,13 +391,25 @@ export async function createUserWorkspace (db: Db, token: string, workspace: str
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function getWorkspaceHash (db: Db, token: string): Promise<string> {
|
||||
export async function getInviteLink (
|
||||
db: Db,
|
||||
token: string,
|
||||
exp: number,
|
||||
emailMask: string,
|
||||
limit: number
|
||||
): Promise<ObjectId> {
|
||||
const { workspace } = decodeToken(token)
|
||||
const wsPromise = await getWorkspace(db, workspace)
|
||||
if (wsPromise === null) {
|
||||
throw new PlatformError(new Status(Severity.ERROR, accountPlugin.status.WorkspaceNotFound, { workspace }))
|
||||
}
|
||||
return generateToken('', workspace)
|
||||
const result = await db.collection(INVITE_COLLECTION).insertOne({
|
||||
workspace,
|
||||
exp: new Date().getTime() + exp,
|
||||
emailMask,
|
||||
limit
|
||||
})
|
||||
return result.insertedId
|
||||
}
|
||||
|
||||
/**
|
||||
@ -560,7 +628,7 @@ export const methods = {
|
||||
signUpJoin: wrap(signUpJoin),
|
||||
selectWorkspace: wrap(selectWorkspace),
|
||||
getUserWorkspaces: wrap(getUserWorkspaces),
|
||||
getWorkspaceHash: wrap(getWorkspaceHash),
|
||||
getInviteLink: wrap(getInviteLink),
|
||||
getAccountInfo: wrap(getAccountInfo),
|
||||
createAccount: wrap(createAccount),
|
||||
createWorkspace: wrap(createUserWorkspace),
|
||||
|
Loading…
Reference in New Issue
Block a user