Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* PublicSignupTokens * bug fix * bug fixes and test * bug fixes and test * bug fixes and test * Add feature flag * tests * tests * Update 20220908093515-add-public-signup-tokens.js Bug Fix * task: use swc instead of ts-jest (#2042) * Add a counter for total number of environments (#1964) * add groupId to gradual rollout template (#2045) * add groupId to gradual rollout template * FMT * Improve tabs UI on smaller devices (#2014) * Improve tabs UI on smaller devices * Improve tabs UI on smaller devices * bug fix * add proper scrollable tabs * removed centered from Tabs (conflicts with scrollable) * PR comments * 4.15.0-beta.10 * Fix broken doc links (#2046) ## What This PR fixes some broken links that have been hanging around in the docs for what seems like a very long time. ## Why As discovered by the link check in #1912, there are a fair few broken links in the docs. Everyone hates broken links because it makes it harder to understand what they were supposed to be pointing at. ## How There are 3 types of links that have been fixed: - Links that should have been internal but were absolute. E.g. `https://docs.getunleash.io/path/article` that should have been `./article.md` - External links that have changed, such as Slack's API description - GitHub links to files that either no longer exist or that have been moved. These links generally pointed to `master`/`main`, meaning they are subject to change. They have been replaced with permalinks pointing to specific commits. ----- * docs: fix slack api doc link * docs: update links in migration guide * docs: fix broken link to ancient feature schema validation * docs: update links to v3 auth hooks * docs: update broken link in the go sdk article * Fix: use permalink for GitHub link * docs: fix wrong google auth link * 4.15.0 * 4.15.1 * docs: update link for symfony sdk (#2048) The doc link appears to have pointed at an address that is no longer reachable. Instead, let's point to the equivalent GitHub link Relates to and closes #2047 * docs: test broken links in website (#1912) The action triggers manually as a first step to test this functionality. In the near future, we might schedule it * Schedule link checker action (#2050) Runs at 12:30 UTC on Mon, Tue, Wed, Thu and Fri * fix: add env and project labels to feature updated metrics. (#2043) * Revert workflow (#2051) * update snapshot * PR comments * Added Events and tests * Throw error if token not found Co-authored-by: Christopher Kolstad <chriswk@getunleash.ai> Co-authored-by: Thomas Heartman <thomas@getunleash.ai> Co-authored-by: Gastón Fournier <gaston@getunleash.ai> Co-authored-by: Ivar Conradi Østhus <ivarconr@gmail.com> Co-authored-by: sjaanus <sellinjaanus@gmail.com>
- Loading branch information
1 parent
ce3db75
commit 6778d34
Showing
25 changed files
with
1,531 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
import { EventEmitter } from 'events'; | ||
import { Knex } from 'knex'; | ||
import metricsHelper from '../util/metrics-helper'; | ||
import { DB_TIME } from '../metric-events'; | ||
import { Logger, LogProvider } from '../logger'; | ||
import NotFoundError from '../error/notfound-error'; | ||
import { PublicSignupTokenSchema } from '../openapi/spec/public-signup-token-schema'; | ||
import { IPublicSignupTokenStore } from '../types/stores/public-signup-token-store'; | ||
import { UserSchema } from '../openapi/spec/user-schema'; | ||
import { IPublicSignupTokenCreate } from '../types/models/public-signup-token'; | ||
|
||
const TABLE = 'public_signup_tokens'; | ||
const TOKEN_USERS_TABLE = 'public_signup_tokens_user'; | ||
|
||
interface ITokenInsert { | ||
secret: string; | ||
name: string; | ||
expires_at: Date; | ||
created_at: Date; | ||
created_by?: string; | ||
role_id: number; | ||
} | ||
|
||
interface ITokenRow extends ITokenInsert { | ||
users: UserSchema[]; | ||
} | ||
|
||
interface ITokenUserRow { | ||
secret: string; | ||
user_id: number; | ||
created_at: Date; | ||
} | ||
const tokenRowReducer = (acc, tokenRow) => { | ||
const { userId, name, ...token } = tokenRow; | ||
if (!acc[tokenRow.secret]) { | ||
acc[tokenRow.secret] = { | ||
secret: token.secret, | ||
name: token.name, | ||
expiresAt: token.expires_at, | ||
createdAt: token.created_at, | ||
createdBy: token.created_by, | ||
roleId: token.role_id, | ||
users: [], | ||
}; | ||
} | ||
const currentToken = acc[tokenRow.secret]; | ||
if (userId) { | ||
currentToken.users.push({ userId, name }); | ||
} | ||
return acc; | ||
}; | ||
|
||
const toRow = (newToken: IPublicSignupTokenCreate) => { | ||
if (!newToken) return; | ||
return { | ||
secret: newToken.secret, | ||
name: newToken.name, | ||
expires_at: newToken.expiresAt, | ||
created_by: newToken.createdBy || null, | ||
role_id: newToken.roleId, | ||
}; | ||
}; | ||
|
||
const toTokens = (rows: any[]): PublicSignupTokenSchema[] => { | ||
const tokens = rows.reduce(tokenRowReducer, {}); | ||
return Object.values(tokens); | ||
}; | ||
|
||
export class PublicSignupTokenStore implements IPublicSignupTokenStore { | ||
private logger: Logger; | ||
|
||
private timer: Function; | ||
|
||
private db: Knex; | ||
|
||
constructor(db: Knex, eventBus: EventEmitter, getLogger: LogProvider) { | ||
this.db = db; | ||
this.logger = getLogger('public-signup-tokens.js'); | ||
this.timer = (action: string) => | ||
metricsHelper.wrapTimer(eventBus, DB_TIME, { | ||
store: 'public-signup-tokens', | ||
action, | ||
}); | ||
} | ||
|
||
count(): Promise<number> { | ||
return this.db(TABLE) | ||
.count('*') | ||
.then((res) => Number(res[0].count)); | ||
} | ||
|
||
private makeTokenUsersQuery() { | ||
return this.db<ITokenRow>(`${TABLE} as tokens`) | ||
.leftJoin( | ||
`${TOKEN_USERS_TABLE} as token_project_users`, | ||
'tokens.secret', | ||
'token_project_users.secret', | ||
) | ||
.leftJoin(`users`, 'token_project_users.user_id', 'users.id') | ||
.select( | ||
'tokens.secret', | ||
'tokens.name', | ||
'tokens.expires_at', | ||
'tokens.created_at', | ||
'tokens.created_by', | ||
'tokens.role_id', | ||
'token_project_users.user_id', | ||
'users.name', | ||
); | ||
} | ||
|
||
async getAll(): Promise<PublicSignupTokenSchema[]> { | ||
const stopTimer = this.timer('getAll'); | ||
const rows = await this.makeTokenUsersQuery(); | ||
stopTimer(); | ||
return toTokens(rows); | ||
} | ||
|
||
async getAllActive(): Promise<PublicSignupTokenSchema[]> { | ||
const stopTimer = this.timer('getAllActive'); | ||
const rows = await this.makeTokenUsersQuery() | ||
.where('expires_at', 'IS', null) | ||
.orWhere('expires_at', '>', 'now()'); | ||
stopTimer(); | ||
return toTokens(rows); | ||
} | ||
|
||
async addTokenUser(secret: string, userId: number): Promise<void> { | ||
await this.db<ITokenUserRow>(TOKEN_USERS_TABLE).insert( | ||
{ user_id: userId, secret }, | ||
['created_at'], | ||
); | ||
} | ||
|
||
async insert( | ||
newToken: IPublicSignupTokenCreate, | ||
): Promise<PublicSignupTokenSchema> { | ||
const response = await this.db<ITokenRow>(TABLE).insert( | ||
toRow(newToken), | ||
['created_at'], | ||
); | ||
return toTokens([response])[0]; | ||
} | ||
|
||
async isValid(secret: string): Promise<boolean> { | ||
const result = await this.db.raw( | ||
`SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE secret = ? AND expires_at::date > ?) AS valid`, | ||
[secret, new Date()], | ||
); | ||
const { valid } = result.rows[0]; | ||
return valid; | ||
} | ||
|
||
destroy(): void {} | ||
|
||
async exists(secret: string): Promise<boolean> { | ||
const result = await this.db.raw( | ||
`SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE secret = ?) AS present`, | ||
[secret], | ||
); | ||
const { present } = result.rows[0]; | ||
return present; | ||
} | ||
|
||
async get(key: string): Promise<PublicSignupTokenSchema> { | ||
const row = await this.makeTokenUsersQuery() | ||
.where('secret', key) | ||
.first(); | ||
|
||
if (!row) | ||
throw new NotFoundError('Could not find a token with that key'); | ||
|
||
return toTokens([row])[0]; | ||
} | ||
|
||
async delete(secret: string): Promise<void> { | ||
return this.db<ITokenInsert>(TABLE).where({ secret }).del(); | ||
} | ||
|
||
async deleteAll(): Promise<void> { | ||
return this.db<ITokenInsert>(TABLE).del(); | ||
} | ||
|
||
async setExpiry( | ||
secret: string, | ||
expiresAt: Date, | ||
): Promise<PublicSignupTokenSchema> { | ||
const rows = await this.makeTokenUsersQuery() | ||
.update({ expires_at: expiresAt }) | ||
.where('secret', secret) | ||
.returning('*'); | ||
if (rows.length > 0) { | ||
return toTokens(rows)[0]; | ||
} | ||
throw new NotFoundError('Could not find public signup token.'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { validateSchema } from '../validate'; | ||
import { PublicSignupTokenSchema } from './public-signup-token-schema'; | ||
|
||
test('publicSignupTokenSchema', () => { | ||
const data: PublicSignupTokenSchema = { | ||
name: 'Default', | ||
secret: 'some-secret', | ||
expiresAt: new Date().toISOString(), | ||
users: [], | ||
role: { name: 'Viewer ', type: 'type', id: 1 }, | ||
createdAt: new Date().toISOString(), | ||
createdBy: 'someone', | ||
}; | ||
|
||
expect( | ||
validateSchema('#/components/schemas/publicSignupTokenSchema', {}), | ||
).not.toBeUndefined(); | ||
|
||
expect( | ||
validateSchema('#/components/schemas/publicSignupTokenSchema', data), | ||
).toBeUndefined(); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { FromSchema } from 'json-schema-to-ts'; | ||
|
||
export const publicSignupTokenCreateSchema = { | ||
$id: '#/components/schemas/publicSignupTokenCreateSchema', | ||
type: 'object', | ||
additionalProperties: false, | ||
required: ['name', 'expiresAt'], | ||
properties: { | ||
name: { | ||
type: 'string', | ||
}, | ||
expiresAt: { | ||
type: 'string', | ||
format: 'date-time', | ||
}, | ||
}, | ||
components: {}, | ||
} as const; | ||
|
||
export type PublicSignupTokenCreateSchema = FromSchema< | ||
typeof publicSignupTokenCreateSchema | ||
>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { FromSchema } from 'json-schema-to-ts'; | ||
import { userSchema } from './user-schema'; | ||
import { roleSchema } from './role-schema'; | ||
|
||
export const publicSignupTokenSchema = { | ||
$id: '#/components/schemas/publicSignupTokenSchema', | ||
type: 'object', | ||
additionalProperties: false, | ||
required: ['secret', 'name', 'expiresAt', 'createdAt', 'createdBy', 'role'], | ||
properties: { | ||
secret: { | ||
type: 'string', | ||
}, | ||
name: { | ||
type: 'string', | ||
}, | ||
expiresAt: { | ||
type: 'string', | ||
format: 'date-time', | ||
}, | ||
createdAt: { | ||
type: 'string', | ||
format: 'date-time', | ||
}, | ||
createdBy: { | ||
type: 'string', | ||
nullable: true, | ||
}, | ||
users: { | ||
type: 'array', | ||
items: { | ||
$ref: '#/components/schemas/userSchema', | ||
}, | ||
nullable: true, | ||
}, | ||
role: { | ||
$ref: '#/components/schemas/roleSchema', | ||
}, | ||
}, | ||
components: { | ||
schemas: { | ||
userSchema, | ||
roleSchema, | ||
}, | ||
}, | ||
} as const; | ||
|
||
export type PublicSignupTokenSchema = FromSchema< | ||
typeof publicSignupTokenSchema | ||
>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { FromSchema } from 'json-schema-to-ts'; | ||
|
||
export const publicSignupTokenUpdateSchema = { | ||
$id: '#/components/schemas/publicSignupTokenUpdateSchema', | ||
type: 'object', | ||
additionalProperties: false, | ||
required: ['expiresAt'], | ||
properties: { | ||
expiresAt: { | ||
type: 'string', | ||
format: 'date-time', | ||
}, | ||
}, | ||
components: {}, | ||
} as const; | ||
|
||
export type PublicSignupTokenUpdateSchema = FromSchema< | ||
typeof publicSignupTokenUpdateSchema | ||
>; |
Oops, something went wrong.