mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
api: register with username / password
This commit is contained in:
parent
b2c59b4e8e
commit
916130b5fc
6 changed files with 103 additions and 11 deletions
|
@ -13,7 +13,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@logto/essentials": "^1.0.5",
|
||||
"@logto/schemas": "^1.0.3",
|
||||
"@logto/schemas": "^1.0.5",
|
||||
"dayjs": "^1.10.5",
|
||||
"dotenv": "^10.0.0",
|
||||
"got": "^11.8.2",
|
||||
|
@ -24,6 +24,7 @@
|
|||
"koa-proxies": "^0.12.1",
|
||||
"koa-router": "^10.0.0",
|
||||
"module-alias": "^2.2.2",
|
||||
"nanoid": "^3.1.23",
|
||||
"oidc-provider": "^7.4.1",
|
||||
"slonik": "^23.8.1",
|
||||
"slonik-interceptor-preset": "^1.2.10",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { IdentifierSqlTokenType, sql } from 'slonik';
|
||||
import { IdentifierSqlTokenType, sql, ValueExpressionType } from 'slonik';
|
||||
|
||||
type Table = { table: string; fields: Record<string, string> };
|
||||
type FieldIdentifiers<Key extends string | number | symbol> = {
|
||||
|
@ -19,3 +19,15 @@ export const convertToIdentifiers = <T extends Table>(
|
|||
{} as FieldIdentifiers<keyof T['fields']>
|
||||
),
|
||||
});
|
||||
|
||||
export const insertInto = <T extends string>(
|
||||
table: IdentifierSqlTokenType,
|
||||
fields: FieldIdentifiers<T>,
|
||||
fieldKeys: readonly T[],
|
||||
value: { [key in T]?: ValueExpressionType }
|
||||
) => sql`
|
||||
insert into ${table} (${sql.join(Object.values(fields), sql`, `)})
|
||||
values (${sql.join(
|
||||
fieldKeys.map((key) => value[key] ?? null),
|
||||
sql`, `
|
||||
)})`;
|
||||
|
|
|
@ -3,11 +3,13 @@ import Router from 'koa-router';
|
|||
import { Provider } from 'oidc-provider';
|
||||
import createSignInRoutes from '@/routes/sign-in';
|
||||
import createUIProxy from '@/proxies/ui';
|
||||
import createRegisterRoutes from '@/routes/register';
|
||||
|
||||
const createRouter = (provider: Provider): Router => {
|
||||
const router = new Router();
|
||||
const router = new Router({ prefix: '/api' });
|
||||
|
||||
router.use('/api', createSignInRoutes(provider));
|
||||
router.use(createSignInRoutes(provider));
|
||||
router.use(createRegisterRoutes());
|
||||
|
||||
return router;
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { UserDBEntry, Users } from '@logto/schemas';
|
||||
import { sql } from 'slonik';
|
||||
import pool from '@/database/pool';
|
||||
import { convertToIdentifiers } from '@/database/utils';
|
||||
import { convertToIdentifiers, insertInto } from '@/database/utils';
|
||||
|
||||
const { table, fields } = convertToIdentifiers(Users);
|
||||
|
||||
|
@ -9,5 +9,22 @@ export const findUserById = async (id: string) =>
|
|||
pool.one<UserDBEntry>(sql`
|
||||
select ${sql.join(Object.values(fields), sql`,`)}
|
||||
from ${table}
|
||||
where id=${id}
|
||||
where ${fields.id}=${id}
|
||||
`);
|
||||
|
||||
export const hasUser = async (username: string) =>
|
||||
pool.exists(sql`
|
||||
select ${fields.id}
|
||||
from ${table}
|
||||
where ${fields.username}=${username}
|
||||
`);
|
||||
|
||||
export const hasUserWithId = async (id: string) =>
|
||||
pool.exists(sql`
|
||||
select ${fields.id}
|
||||
from ${table}
|
||||
where ${fields.id}=${id}
|
||||
`);
|
||||
|
||||
export const insertUser = async (user: UserDBEntry) =>
|
||||
pool.query(insertInto(table, fields, Users.fieldKeys, user));
|
||||
|
|
60
packages/core/src/routes/register.ts
Normal file
60
packages/core/src/routes/register.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
import Router from 'koa-router';
|
||||
import koaBody from 'koa-body';
|
||||
import { object, string } from 'zod';
|
||||
import { encryptPassword } from '@/utils/password';
|
||||
import { hasUser, hasUserWithId, insertUser } from '@/queries/user';
|
||||
import { customAlphabet, nanoid } from 'nanoid';
|
||||
import { PasswordEncryptionMethod } from '@logto/schemas';
|
||||
|
||||
const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
||||
const userId = customAlphabet(alphabet, 12);
|
||||
|
||||
const generateUserId = async (maxRetries = 500) => {
|
||||
for (let i = 0; i < maxRetries; ++i) {
|
||||
const id = userId();
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
if (!(await hasUserWithId(id))) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Cannot generate user ID in reasonable retries');
|
||||
};
|
||||
|
||||
export default function createRegisterRoutes() {
|
||||
const router = new Router();
|
||||
|
||||
router.post('/register', koaBody(), async (ctx) => {
|
||||
const RegisterBody = object({
|
||||
username: string().min(3),
|
||||
password: string().min(6),
|
||||
});
|
||||
const { username, password } = RegisterBody.parse(ctx.request.body);
|
||||
|
||||
if (await hasUser(username)) {
|
||||
throw new Error('Username already exists');
|
||||
}
|
||||
|
||||
const id = await generateUserId();
|
||||
const passwordEncryptionSalt = nanoid();
|
||||
const passwordEncryptionMethod = PasswordEncryptionMethod.SaltAndPepper;
|
||||
const passwordEncrypted = encryptPassword(
|
||||
id,
|
||||
password,
|
||||
passwordEncryptionSalt,
|
||||
passwordEncryptionMethod
|
||||
);
|
||||
|
||||
await insertUser({
|
||||
id,
|
||||
username,
|
||||
passwordEncrypted,
|
||||
passwordEncryptionMethod,
|
||||
passwordEncryptionSalt,
|
||||
});
|
||||
|
||||
ctx.body = { id };
|
||||
});
|
||||
|
||||
return router.routes();
|
||||
}
|
|
@ -396,10 +396,10 @@
|
|||
lodash.orderby "^4.6.0"
|
||||
lodash.pick "^4.4.0"
|
||||
|
||||
"@logto/schemas@^1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@logto/schemas/-/schemas-1.0.3.tgz#6a596bb9d1b99460857eaec5d643040ae384e037"
|
||||
integrity sha512-inCo/PQUQl9OZFkjehjcB/4PBzYBW9FVn/3nZ2CPr/+3dJKDF3B0qxu8gyQICGJuWYx1BZPygc/NvV0F1klR3g==
|
||||
"@logto/schemas@^1.0.5":
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@logto/schemas/-/schemas-1.0.5.tgz#7784ca8f58569bfe1feafb4ad5ca6f098b1a557e"
|
||||
integrity sha512-JxA9uXNz2tt0NqAp8Gl0hp0L/dLhZakG5sm41JrJKfhtse2Yy2K1X7gBYkH1C9P8P+NcHhv/K4pEgY66yXMOGQ==
|
||||
|
||||
"@mrmlnc/readdir-enhanced@^2.2.1":
|
||||
version "2.2.1"
|
||||
|
@ -3870,7 +3870,7 @@ multimap@^1.1.0:
|
|||
resolved "https://registry.yarnpkg.com/multimap/-/multimap-1.1.0.tgz#5263febc085a1791c33b59bb3afc6a76a2a10ca8"
|
||||
integrity sha512-0ZIR9PasPxGXmRsEF8jsDzndzHDj7tIav+JUmvIFB/WHswliFnquxECT/De7GR4yg99ky/NlRKJT82G1y271bw==
|
||||
|
||||
nanoid@^3.1.15:
|
||||
nanoid@^3.1.15, nanoid@^3.1.23:
|
||||
version "3.1.23"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81"
|
||||
integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==
|
||||
|
|
Loading…
Reference in a new issue