2024-10-24 18:48:11 +02:00
import { apiRoute , applyConfig , auth , jsonOrForm } from "@/api" ;
2024-05-29 02:59:49 +02:00
import { tempmailDomains } from "@/tempmail" ;
2024-08-27 20:14:10 +02:00
import { createRoute } from "@hono/zod-openapi" ;
2024-11-01 20:57:16 +01:00
import { User } from "@versia/kit/db" ;
2024-11-01 21:05:54 +01:00
import { Users } from "@versia/kit/tables" ;
2024-06-14 11:05:04 +02:00
import { and , eq , isNull } from "drizzle-orm" ;
2024-04-07 07:30:49 +02:00
import ISO6391 from "iso-639-1" ;
2024-04-14 12:36:25 +02:00
import { z } from "zod" ;
2024-05-29 02:59:49 +02:00
import { config } from "~/packages/config-manager" ;
2023-10-16 05:51:29 +02:00
export const meta = applyConfig ( {
2024-04-07 07:30:49 +02:00
route : "/api/v1/accounts" ,
ratelimits : {
max : 2 ,
duration : 60 ,
} ,
auth : {
required : false ,
oauthPermissions : [ "write:accounts" ] ,
} ,
2024-06-14 10:03:51 +02:00
challenge : {
required : true ,
} ,
2023-10-16 05:51:29 +02:00
} ) ;
2023-09-13 07:53:58 +02:00
2024-05-06 09:16:33 +02:00
export const schemas = {
2024-07-11 12:56:28 +02:00
json : z.object ( {
2024-11-21 09:26:03 +01:00
username : z.string ( ) ,
2024-05-09 00:54:44 +02:00
email : z.string ( ) . toLowerCase ( ) ,
2024-06-14 11:05:04 +02:00
password : z.string ( ) . optional ( ) ,
2024-05-06 09:16:33 +02:00
agreement : z
. string ( )
2024-05-09 00:37:28 +02:00
. transform ( ( v ) = > [ "true" , "1" , "on" ] . includes ( v . toLowerCase ( ) ) )
. or ( z . boolean ( ) ) ,
2024-05-06 09:16:33 +02:00
locale : z.string ( ) ,
reason : z.string ( ) ,
} ) ,
} ;
2024-08-27 20:14:10 +02:00
const route = createRoute ( {
method : "post" ,
path : "/api/v1/accounts" ,
summary : "Create account" ,
description : "Register a new account" ,
2024-10-24 18:48:11 +02:00
middleware : [
auth ( meta . auth , meta . permissions , meta . challenge ) ,
jsonOrForm ( ) ,
] ,
2024-08-27 20:14:10 +02:00
request : {
body : {
content : {
"application/json" : {
schema : schemas.json ,
} ,
"multipart/form-data" : {
schema : schemas.json ,
} ,
"application/x-www-form-urlencoded" : {
schema : schemas.json ,
} ,
} ,
} ,
} ,
responses : {
200 : {
description : "Account created" ,
} ,
422 : {
description : "Validation failed" ,
content : {
"application/json" : {
schema : z.object ( {
error : z.string ( ) ,
details : z.object ( {
username : z.array (
z . object ( {
error : z.enum ( [
"ERR_BLANK" ,
"ERR_INVALID" ,
"ERR_TOO_LONG" ,
"ERR_TOO_SHORT" ,
"ERR_BLOCKED" ,
"ERR_TAKEN" ,
"ERR_RESERVED" ,
"ERR_ACCEPTED" ,
"ERR_INCLUSION" ,
] ) ,
description : z.string ( ) ,
} ) ,
) ,
email : z.array (
z . object ( {
error : z.enum ( [
"ERR_BLANK" ,
"ERR_INVALID" ,
"ERR_BLOCKED" ,
"ERR_TAKEN" ,
] ) ,
description : z.string ( ) ,
} ) ,
) ,
password : z.array (
z . object ( {
error : z.enum ( [
"ERR_BLANK" ,
"ERR_INVALID" ,
"ERR_TOO_LONG" ,
"ERR_TOO_SHORT" ,
] ) ,
description : z.string ( ) ,
} ) ,
) ,
agreement : z.array (
z . object ( {
error : z.enum ( [ "ERR_ACCEPTED" ] ) ,
description : z.string ( ) ,
} ) ,
) ,
locale : z.array (
z . object ( {
error : z.enum ( [ "ERR_BLANK" , "ERR_INVALID" ] ) ,
description : z.string ( ) ,
} ) ,
) ,
reason : z.array (
z . object ( {
error : z.enum ( [ "ERR_BLANK" ] ) ,
description : z.string ( ) ,
} ) ,
) ,
} ) ,
} ) ,
} ,
} ,
} ,
} ,
} ) ;
2024-05-06 09:16:33 +02:00
2024-08-27 20:14:10 +02:00
export default apiRoute ( ( app ) = >
app . openapi ( route , async ( context ) = > {
const form = context . req . valid ( "json" ) ;
const { username , email , password , agreement , locale } =
context . req . valid ( "json" ) ;
2024-04-14 12:36:25 +02:00
2024-08-27 20:14:10 +02:00
if ( ! config . signups . registration ) {
return context . json (
{
error : "Registration is disabled" ,
2024-05-06 09:16:33 +02:00
} ,
2024-08-27 20:14:10 +02:00
422 ,
) ;
}
2024-05-06 09:16:33 +02:00
2024-08-27 20:14:10 +02:00
const errors : {
details : Record <
string ,
{
error :
| "ERR_BLANK"
| "ERR_INVALID"
| "ERR_TOO_LONG"
| "ERR_TOO_SHORT"
| "ERR_BLOCKED"
| "ERR_TAKEN"
| "ERR_RESERVED"
| "ERR_ACCEPTED"
| "ERR_INCLUSION" ;
description : string ;
} [ ]
> ;
} = {
details : {
password : [ ] ,
username : [ ] ,
email : [ ] ,
agreement : [ ] ,
locale : [ ] ,
reason : [ ] ,
} ,
} ;
2024-04-07 07:30:49 +02:00
2024-08-27 20:14:10 +02:00
// Check if fields are blank
for ( const value of [
"username" ,
"email" ,
"password" ,
"agreement" ,
"locale" ,
"reason" ,
] ) {
// @ts-expect-error We don't care about the type here
if ( ! form [ value ] ) {
errors . details [ value ] . push ( {
error : "ERR_BLANK" ,
2024-10-03 10:27:41 +02:00
description : "can't be blank" ,
2024-05-06 09:16:33 +02:00
} ) ;
2024-06-13 04:26:43 +02:00
}
2024-08-27 20:14:10 +02:00
}
2024-04-07 07:30:49 +02:00
2024-08-27 20:14:10 +02:00
// Check if username is valid
if ( ! username ? . match ( /^[a-z0-9_]+$/ ) ) {
errors . details . username . push ( {
error : "ERR_INVALID" ,
description :
"must only contain lowercase letters, numbers, and underscores" ,
} ) ;
}
2024-04-07 07:30:49 +02:00
2024-08-27 20:14:10 +02:00
// Check if username doesnt match filters
if ( config . filters . username . some ( ( filter ) = > username ? . match ( filter ) ) ) {
errors . details . username . push ( {
error : "ERR_INVALID" ,
description : "contains blocked words" ,
} ) ;
}
2024-04-07 07:30:49 +02:00
2024-08-27 20:14:10 +02:00
// Check if username is too long
if ( ( username ? . length ? ? 0 ) > config . validation . max_username_size ) {
errors . details . username . push ( {
error : "ERR_TOO_LONG" ,
description : ` is too long (maximum is ${ config . validation . max_username_size } characters) ` ,
} ) ;
}
2024-04-07 07:30:49 +02:00
2024-08-27 20:14:10 +02:00
// Check if username is too short
if ( ( username ? . length ? ? 0 ) < 3 ) {
errors . details . username . push ( {
error : "ERR_TOO_SHORT" ,
description : "is too short (minimum is 3 characters)" ,
} ) ;
}
2024-04-07 07:30:49 +02:00
2024-08-27 20:14:10 +02:00
// Check if username is reserved
if ( config . validation . username_blacklist . includes ( username ? ? "" ) ) {
errors . details . username . push ( {
error : "ERR_RESERVED" ,
description : "is reserved" ,
} ) ;
}
2024-04-07 07:30:49 +02:00
2024-08-27 20:14:10 +02:00
// Check if username is taken
if (
await User . fromSql (
and ( eq ( Users . username , username ) ) ,
isNull ( Users . instanceId ) ,
)
) {
errors . details . username . push ( {
error : "ERR_TAKEN" ,
description : "is already taken" ,
} ) ;
}
2024-04-07 07:30:49 +02:00
2024-08-27 20:14:10 +02:00
// Check if email is valid
if (
! email ? . match (
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ ,
)
) {
errors . details . email . push ( {
error : "ERR_INVALID" ,
description : "must be a valid email address" ,
} ) ;
}
2024-04-07 07:30:49 +02:00
2024-08-27 20:14:10 +02:00
// Check if email is blocked
if (
config . validation . email_blacklist . includes ( email ) ||
( config . validation . blacklist_tempmail &&
tempmailDomains . domains . includes ( ( email ? ? "" ) . split ( "@" ) [ 1 ] ) )
) {
errors . details . email . push ( {
error : "ERR_BLOCKED" ,
description : "is from a blocked email provider" ,
} ) ;
}
2024-04-29 02:09:34 +02:00
2024-08-27 20:14:10 +02:00
// Check if email is taken
if ( await User . fromSql ( eq ( Users . email , email ) ) ) {
errors . details . email . push ( {
error : "ERR_TAKEN" ,
description : "is already taken" ,
} ) ;
}
2024-04-07 07:30:49 +02:00
2024-08-27 20:14:10 +02:00
// Check if agreement is accepted
if ( ! agreement ) {
errors . details . agreement . push ( {
error : "ERR_ACCEPTED" ,
description : "must be accepted" ,
} ) ;
}
2024-04-07 07:30:49 +02:00
2024-08-27 20:14:10 +02:00
if ( ! locale ) {
errors . details . locale . push ( {
error : "ERR_BLANK" ,
2024-10-03 10:27:41 +02:00
description : "can't be blank" ,
2024-08-27 20:14:10 +02:00
} ) ;
}
2024-04-07 07:30:49 +02:00
2024-08-27 20:14:10 +02:00
if ( ! ISO6391 . validate ( locale ? ? "" ) ) {
errors . details . locale . push ( {
error : "ERR_INVALID" ,
description : "must be a valid ISO 639-1 code" ,
} ) ;
}
2024-05-06 09:16:33 +02:00
2024-08-27 20:14:10 +02:00
// If any errors are present, return them
if ( Object . values ( errors . details ) . some ( ( value ) = > value . length > 0 ) ) {
// Error is something like "Validation failed: Password can't be blank, Username must contain only letters, numbers and underscores, Agreement must be accepted"
const errorsText = Object . entries ( errors . details )
. filter ( ( [ _ , errors ] ) = > errors . length > 0 )
. map (
( [ name , errors ] ) = >
` ${ name } ${ errors
. map ( ( error ) = > error . description )
. join ( ", " ) } ` ,
)
. join ( ", " ) ;
return context . json (
{
error : ` Validation failed: ${ errorsText } ` ,
details : Object.fromEntries (
Object . entries ( errors . details ) . filter (
( [ _ , errors ] ) = > errors . length > 0 ,
2024-04-29 02:15:33 +02:00
) ,
2024-08-27 20:14:10 +02:00
) ,
} ,
422 ,
) ;
}
2024-04-07 07:30:49 +02:00
2024-08-27 20:14:10 +02:00
await User . fromDataLocal ( {
username : username ? ? "" ,
password : password ? ? "" ,
email : email ? ? "" ,
} ) ;
2024-04-07 07:30:49 +02:00
2024-08-27 20:14:10 +02:00
return context . newResponse ( null , 200 ) ;
} ) ,
2024-08-19 20:06:38 +02:00
) ;