Merge pull request #39 from linux-odyssey/feat-pick-username

feat: pick username
This commit is contained in:
Lin, Hung-Shin authored and GitHub committed 2023-10-14 15:14:11 +08:00
commit 784ae29ed0
28 files changed
+489 -278

No files matched your search

+22
View File
@@ -0,0 +1,22 @@
name: Code Review GPT
on:
pull_request:
branches: [main]
jobs:
run_code_review:
permissions:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Code Review GPT
uses: mattzcarey/code-review-gpt@v0.1.4-alpha
with:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
MODEL: 'gpt-3.5-turbo'
GITHUB_TOKEN: ${{ github.token }}
+2
View File
@@ -4,10 +4,12 @@ WORKDIR /app
COPY package.json yarn.lock ./
COPY app/package.json ./app/package.json
COPY packages/file-graph/package.json ./packages/file-graph/package.json
COPY packages/utils/package.json ./packages/utils/package.json
RUN yarn install --frozen-lockfile
COPY app ./app
COPY packages/file-graph ./packages/file-graph
COPY packages/utils ./packages/utils
RUN yarn build
# Production stage
+1
View File
@@ -4,6 +4,7 @@ WORKDIR /app
COPY package.json yarn.lock ./
COPY app/package.json ./app/package.json
COPY packages/file-graph/package.json ./packages/file-graph/package.json
COPY packages/utils/package.json ./packages/utils/package.json
RUN yarn install --frozen-lockfile
WORKDIR /app/app
+1
View File
@@ -12,6 +12,7 @@
},
"dependencies": {
"@linux-odyssey/file-graph": "*",
"@linux-odyssey/utils": "*",
"@fortawesome/fontawesome-svg-core": "^6.4.0",
"@fortawesome/free-brands-svg-icons": "^6.4.0",
"@fortawesome/free-regular-svg-icons": "^6.4.0",
@@ -1,15 +1,27 @@
<script setup>
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { login, register } from '../utils/auth'
import { RouterLink, useRouter } from 'vue-router'
import api from '../utils/api'
const emit = defineEmits(['onSubmit', 'onChange'])
defineProps({
type: {
type: String,
default: 'login',
},
socialLogin: {
type: Boolean,
default: true,
},
title: {
type: String,
default: 'Linux Odyssey',
},
})
const router = useRouter()
const isRegister = ref(false)
const username = ref('')
const usernameInput = ref(null)
const email = ref('')
const emailInput = ref(null)
const password = ref('')
const errorMessage = ref('')
@@ -30,89 +42,45 @@ const success = () => {
router.push({ name: 'game' })
}
const handleLogin = async () => {
try {
const isSuccess = await login(username.value, password.value)
if (isSuccess) success()
else errorMessage.value = 'Invalid username or password.'
} catch (err) {
console.error(err)
errorMessage.value = 'Something went wrong.'
}
}
async function check() {
const user = username.value.trim()
try {
const res = await api.get('/auth/check-username', {
params: { username: user },
})
// user not exists, try register
const { type } = res.data
if (type === 'email') {
email.value = user
username.value = ''
}
isRegister.value = true
usernameInput.value.focus()
} catch (err) {
if (err.response?.status === 409) {
// user already exists, try login
await handleLogin()
return
}
if (err.response?.status === 400) {
errorMessage.value = 'Invalid username or email.'
return
}
errorMessage.value = 'Something went wrong.'
console.error(err)
}
}
function handleRegister() {
register(username.value, email.value, password.value)
.then((isSuccess) => {
if (isSuccess) {
success()
} else {
errorMessage.value = 'Something went wrong.'
}
})
.catch((err) => {
if (err.response?.status === 409) {
// Handle username or email already exists error here
if (err.response.data.type === 'username') {
errorMessage.value = 'Username already exists.'
} else if (err.response.data.type === 'email') {
errorMessage.value = 'Email already exists.'
} else {
errorMessage.value = 'Something went wrong.'
}
} else if (err.response?.status === 400) {
errorMessage.value = 'Invalid username or email.'
} else {
errorMessage.value = 'Something went wrong.'
console.error(err)
}
})
}
async function handleSubmit() {
if (!isRegister.value) {
check()
return
}
// register
handleRegister()
const error = (err) => {
console.error(err)
errorMessage.value = err
}
const clearError = () => {
errorMessage.value = ''
}
const goBack = () => {
isRegister.value = false
const handleSubmit = () => {
if (errorMessage.value) return
try {
emit('onSubmit', {
username: username.value,
email: email.value,
password: password.value,
success,
error,
})
} catch (err) {
console.error(err)
errorMessage.value = 'Something went wrong'
}
}
const handleChange = () => {
clearError()
try {
emit('onChange', {
username: username.value,
email: email.value,
password: password.value,
success,
error,
})
} catch (err) {
console.error(err)
errorMessage.value = 'Something went wrong'
}
}
</script>
<template>
@@ -120,37 +88,41 @@ const goBack = () => {
<div
class="bg-background border-8 border-background-primary flex flex-1 flex-col items-center justify-center rounded-3xl p-10"
>
<h1 class="text-text-primary text-3xl font-black mb-8">Linux Odyssey</h1>
<form @submit.prevent="handleSubmit" class="w-full">
<h1 class="text-text-primary text-3xl font-black mb-8">{{ title }}</h1>
<form @submit.prevent="handleSubmit()" class="w-full">
<div class="mb-6">
<label for="username" class="text-sm font-semibold text-text">{{
isRegister ? 'Username' : 'Email / Username'
type === 'login' ? 'Email / Username' : 'Username'
}}</label
><input
type="text"
id="username"
ref="usernameInput"
class="my-4 bg-background-primary text-text-primary bg- rounded-md block w-full px-3 h-10 shadow-sm focus:outline-none placeholder:text-text-line focus:ring-2 focus:ring-text-primary ring-1 ring-background-secondary"
placeholder="Enter your email or username"
:placeholder="
type === 'login'
? 'Enter your email or username'
: 'Enter your username'
"
v-model="username"
@input="clearError"
@input="handleChange()"
autocomplete="username"
/>
</div>
<div class="mb-6" v-if="isRegister">
<div class="mb-6" v-if="type === 'register'">
<label for="email" class="text-sm font-semibold text-text">Email</label
><input
type="email"
id="email"
ref="emailInput"
class="my-4 bg-background-primary text-text-primary bg- rounded-md block w-full px-3 h-10 shadow-sm focus:outline-none placeholder:text-text-line focus:ring-2 focus:ring-text-primary ring-1 ring-background-secondary"
placeholder="Enter your email or username"
placeholder="Enter your email"
v-model="email"
@input="clearError"
@input="handleChange()"
autocomplete="email"
/>
</div>
<div class="mb-6">
<div class="mb-6" v-if="type === 'login' || type === 'register'">
<label for="password" class="text-sm font-semibold leading-6 text-text"
>Password</label
><input
@@ -159,7 +131,7 @@ const goBack = () => {
class="my-4 bg-background-primary text-text-primary bg- rounded-md block w-full px-3 h-10 shadow-sm focus:outline-none placeholder:text-text-line focus:ring-2 focus:ring-text-primary ring-1 ring-background-secondary"
placeholder="Enter your password"
v-model="password"
@input="clearError"
@input="handleChange()"
/>
</div>
<p class="text-red-500 flex justify-center" v-if="errorMessage">
@@ -169,17 +141,26 @@ const goBack = () => {
class="inline-flex justify-center rounded-lg text-sm font-black py-2 mt-3 bg-text-primary text-background w-full"
type="submit"
>
<span v-if="isRegister">Sign up</span>
<span v-else>Log in / Sign up</span>
<span v-if="type === 'login'">Log in</span>
<span v-else>Register</span>
</button>
<button
class="inline-flex justify-center rounded-lg text-sm font-black py-2 mt-3 bg-text-primary text-background w-full"
v-if="isRegister"
@click="goBack"
>
<span>Back to Login</span>
</button>
<div v-if="hasSocialLogins">
<p class="text-text flex justify-center mt-3">
<span v-if="type === 'register'"
>Already have an account?
<RouterLink class="text-text-primary" to="/login"
>Log in</RouterLink
></span
>
<span v-else-if="type === 'login'"
>Don't have an account?
<RouterLink class="text-text-primary" to="/register"
>Register</RouterLink
></span
>
</p>
<div v-if="socialLogin && hasSocialLogins">
<hr class="my-8" />
<p class="flex justify-center font-semibold text-text text-base">
Log in with social account
+1 -1
View File
@@ -6,7 +6,7 @@ const router = useRouter()
const handleLogout = async () => {
await logout()
router.push({ name: 'auth' })
router.push({ name: 'login' })
}
</script>
+18 -4
View File
@@ -1,6 +1,8 @@
import { createRouter, createWebHistory } from 'vue-router'
import GamePage from './views/GamePage.vue'
import AuthPage from './views/AuthPage.vue'
import LoginPage from './views/LoginPage.vue'
import RegisterPage from './views/RegisterPage.vue'
import ChooseUsernamePage from './views/ChooseUsernamePage.vue'
import { isLoggedIn } from './utils/auth'
const routes = [
@@ -10,8 +12,14 @@ const routes = [
},
{
path: '/login',
name: 'auth',
component: AuthPage,
name: 'login',
component: LoginPage,
meta: { requiresGuest: true },
},
{
path: '/register',
name: 'register',
component: RegisterPage,
meta: { requiresGuest: true },
},
{
@@ -20,6 +28,12 @@ const routes = [
component: GamePage,
meta: { requiresAuth: true },
},
{
path: '/choose-username',
name: 'choose-username',
component: ChooseUsernamePage,
meta: { requiresGuest: true },
},
]
const router = createRouter({
@@ -34,7 +48,7 @@ router.beforeEach(async (to, from, next) => {
// Check if the user isn't authenticated
// Redirect to the login page
next({
name: 'auth',
name: 'register',
query: { redirect: to.fullPath }, // Optionally pass a redirect parameter
})
} else if (to.meta.requiresGuest && loggedIn) {
-16
View File
@@ -1,16 +0,0 @@
<script setup>
import AuthenticationPart from '../components/AuthenticationPart.vue'
import Background from '../components/DynamicBackground.vue'
</script>
<template>
<div class="relative w-screen h-screen">
<Background class="w-full h-full" />
<div
class="h-screen w-screen absolute top-0 left-0 flex flex-wrap justify-center content-center"
>
<div class="h-2/3 w-1/3">
<AuthenticationPart />
</div>
</div>
</div>
</template>
+61
View File
@@ -0,0 +1,61 @@
<script setup>
import { isValidUsername } from '@linux-odyssey/utils'
import AuthForm from '../components/AuthForm.vue'
import Background from '../components/DynamicBackground.vue'
import api from '../utils/api'
async function checkUsername({ username, error }) {
if (username && !isValidUsername(username)) {
error('Invalid username.')
return
}
if (username.length > 3) {
try {
const { data } = await api.get('/auth/check-username', {
params: { username },
})
const { available } = data
if (!available) {
error('Username is already taken.')
}
} catch {
error('Invalid username.')
}
}
}
function handleSubmit({ username, success, error }) {
api
.post('/auth/register-from-session', {
username,
})
.then(success)
.catch((err) => {
if (err.response?.status === 409) {
error('Username is already taken.')
} else {
console.error(err.message)
error('Something went wrong.')
}
})
}
</script>
<template>
<div class="relative w-screen h-screen">
<Background class="w-full h-full" />
<div
class="h-screen w-screen absolute top-0 left-0 flex flex-wrap justify-center content-center"
>
<div class="h-2/3 w-1/3">
<AuthForm
@onSubmit="handleSubmit"
@onChange="checkUsername"
type="username"
:socialLogin="false"
/>
</div>
</div>
</div>
</template>
+28
View File
@@ -0,0 +1,28 @@
<script setup>
import AuthForm from '../components/AuthForm.vue'
import Background from '../components/DynamicBackground.vue'
import { login } from '../utils/auth'
const handleLogin = async ({ username, password, success, error }) => {
try {
const isSuccess = await login(username, password)
if (isSuccess) success()
else error('Wrong username or password.')
} catch (err) {
console.error(err)
}
}
</script>
<template>
<div class="relative w-screen h-screen">
<Background class="w-full h-full" />
<div
class="h-screen w-screen absolute top-0 left-0 flex flex-wrap justify-center content-center"
>
<div class="h-2/3 w-1/3">
<AuthForm @onSubmit="handleLogin" type="login" />
</div>
</div>
</div>
</template>
+75
View File
@@ -0,0 +1,75 @@
<script setup>
import { isValidEmail, isValidUsername } from '@linux-odyssey/utils'
import AuthForm from '../components/AuthForm.vue'
import Background from '../components/DynamicBackground.vue'
import api from '../utils/api'
import { register } from '../utils/auth'
function handleRegister({ username, email, password, success, error }) {
register(username, email, password)
.then(success)
.catch((err) => {
if (err.response?.status === 409) {
// Handle username or email already exists error here
if (err.response.data.type === 'username') {
error('Username already exists.')
} else if (err.response.data.type === 'email') {
error('Email already exists.')
} else {
error('Something went wrong.')
}
} else if (err.response?.status === 400) {
error('Invalid username or email.')
} else {
console.error(err)
}
})
}
async function check({ username, email, error }) {
if (username && !isValidUsername(username)) {
error('Invalid username.')
return
}
if (email && !isValidEmail(email)) {
error('Invalid email.')
return
}
if (!username && !email) {
return
}
try {
const res = await api.get('/auth/check-username', {
params: { username },
})
const { type, available } = res.data
if (!available) {
error(`${type} already exists.`)
}
} catch (err) {
if (err.response?.status === 400) {
error('Invalid username or email.')
return
}
console.error(err)
error('Something went wrong.')
}
}
</script>
<template>
<div class="relative w-screen h-screen">
<Background class="w-full h-full" />
<div
class="h-screen w-screen absolute top-0 left-0 flex flex-wrap justify-center content-center"
>
<div class="h-2/3 w-1/3">
<AuthForm
@onSubmit="handleRegister"
@onChange="check"
type="register"
/>
</div>
</div>
</div>
</template>
+1 -1
View File
@@ -10,7 +10,7 @@ services:
- internal
volumes:
- ./app:/app/app
- ./packages/file-graph:/app/packages/file-graph
- ./packages:/app/packages
backend:
build:
context: .
+38
View File
@@ -0,0 +1,38 @@
export function isValidUsername(username) {
// Define a regular expression pattern for a valid Linux username.
const usernamePattern = /^[a-z][a-z0-9_-]*$/
// Test the username against the pattern.
if (!usernamePattern.test(username)) return false
// check if the username is between 1 and 32 characters
if (username.length < 4 || username.length > 32) {
return false
}
const reservedWords = [
'root',
'admin',
'administrator',
'sysadmin',
'system',
'daemon',
'bin',
'nobody',
'nogroup',
'guest',
'user',
]
if (reservedWords.includes(username)) {
return false
}
return true
}
export function isValidEmail(email) {
// Define a regular expression pattern for a valid email address.
const emailPattern = /^[\w-.+]+@([\w][\w-]*\.)+[\w-]{2,}$/
// Test the email against the pattern.
return emailPattern.test(email)
}
@@ -1,4 +1,4 @@
import { isValidEmail, isValidUsername } from '../../src/utils/auth'
import { isValidEmail, isValidUsername } from './auth.js'
test('check valid username', () => {
const valid = ['alex', 'alex123', 'john_doe', 'john-doe', 'rudeus']
+1
View File
@@ -0,0 +1 @@
export { isValidEmail, isValidUsername } from './auth.js'
+8
View File
@@ -0,0 +1,8 @@
{
"name": "@linux-odyssey/utils",
"version": "1.0.0",
"main": "index.js",
"type": "module",
"exports": "./index.js",
"license": "MIT"
}
+2
View File
@@ -4,10 +4,12 @@ WORKDIR /app
COPY ./package.json yarn.lock ./
COPY server/package.json ./server/package.json
COPY packages/file-graph/package.json ./packages/file-graph/package.json
COPY packages/utils/package.json ./packages/utils/package.json
RUN yarn install --frozen-lockfile --production
COPY ./server ./server
COPY packages/file-graph ./packages/file-graph
COPY packages/utils ./packages/utils
COPY quests ./quests
WORKDIR /app/server
+1
View File
@@ -4,6 +4,7 @@ WORKDIR /app
COPY package.json yarn.lock ./
COPY server/package.json ./server/package.json
COPY packages/file-graph/package.json ./packages/file-graph/package.json
COPY packages/utils/package.json ./packages/utils/package.json
RUN yarn install --frozen-lockfile
COPY server/nodemon.json ./server/nodemon.json
+1
View File
@@ -11,6 +11,7 @@
},
"dependencies": {
"@linux-odyssey/file-graph": "*",
"@linux-odyssey/utils": "*",
"bcrypt": "^5.1.1",
"commander": "^11.0.0",
"connect-mongo": "^5.0.0",
+68 -29
View File
@@ -1,10 +1,6 @@
import { isValidEmail, isValidUsername } from '@linux-odyssey/utils'
import User from '../../models/user.js'
import {
genJWT,
hashPassword,
isValidEmail,
isValidUsername,
} from '../../utils/auth.js'
import { genJWT, hashPassword } from '../../utils/auth.js'
export async function issueToken(req, res) {
const { user } = req
@@ -16,6 +12,14 @@ export async function issueToken(req, res) {
res.json({ token })
}
async function invalidUsername(username) {
return !isValidUsername(username) || User.exists({ username })
}
async function invalidEmail(email) {
return !isValidEmail(email) || User.exists({ email })
}
export async function checkUsername(req, res) {
const { username, email } = req.query
const cred = username || email
@@ -27,30 +31,16 @@ export async function checkUsername(req, res) {
}
if (isValidEmail(cred)) {
if (await User.exists({ email: cred })) {
res.status(409).json({
type: 'email',
message: `"${cred}" already exists`,
})
return
}
res.status(200).json({
res.json({
type: 'email',
message: `"${cred}" is available`,
available: !(await User.exists({ email: cred })),
})
return
}
if (isValidUsername(cred)) {
if (await User.exists({ username: cred })) {
res.status(409).json({
type: 'username',
message: `"${cred}" already exists`,
})
return
}
res.status(200).json({
res.json({
type: 'username',
message: `"${cred}" is available`,
available: !(await User.exists({ username: cred })),
})
return
}
@@ -68,18 +58,18 @@ export async function register(req, res, next) {
return
}
if (await User.exists({ username })) {
if (await invalidUsername(username)) {
res.status(409).json({
type: 'username',
message: 'username already exists',
message: 'Invalid username',
})
return
}
if (await User.exists({ email })) {
if (await invalidEmail(email)) {
res.status(409).json({
type: 'email',
message: 'email already exists',
message: 'Invalid email',
})
return
}
@@ -126,6 +116,55 @@ export function logout(req, res) {
export function checkSession(req, res) {
res.json({
loggedIn: req.isAuthenticated(),
loggedIn: req.isAuthenticated() && !req.session.newUser,
})
}
export async function socialLogin(req, res) {
const { newUser } = req.user
if (newUser) {
req.session.newUser = newUser
res.redirect('/choose-username')
return
}
res.redirect('/')
}
export async function registerFromSession(req, res) {
const { username } = req.body
const { newUser } = req.session
if (!newUser) {
res.status(400).json({
message: 'no new user found',
})
return
}
if (await invalidUsername(username)) {
res.status(409).json({
type: 'username',
message: 'Invalid username',
})
return
}
newUser.username = username
const user = new User(newUser)
await user.save()
req.login(user, (err) => {
if (err) {
console.error(err)
res.status(500).json({
message: 'error logging in',
})
return
}
delete req.session.newUser
res.status(201).json({
message: 'user created',
})
})
}
+6 -14
View File
@@ -8,6 +8,8 @@ import {
checkUsername,
checkSession,
logout,
socialLogin,
registerFromSession,
} from '../controllers/authController.js'
const router = Router()
@@ -34,24 +36,14 @@ router.get('/available-methods', (req, res) => {
if (enabledMethods.google) {
router.get('/google', passport.authenticate('google'))
router.get(
'/google/callback',
passport.authenticate('google', {
successRedirect: '/',
failureRedirect: '/login',
})
)
router.get('/google/callback', passport.authenticate('google'), socialLogin)
}
if (enabledMethods.github) {
router.get('/github', passport.authenticate('github'))
router.get(
'/github/callback',
passport.authenticate('github', {
successRedirect: '/',
failureRedirect: '/login',
})
)
router.get('/github/callback', passport.authenticate('github'), socialLogin)
}
router.post('/register-from-session', registerFromSession)
export default router
+2 -33
View File
@@ -1,41 +1,10 @@
import GitHubStrategy from 'passport-github2'
import config from '../config.js'
import User from '../models/user.js'
import oauthVerify from './oauthVerify.js'
function verify(accessToken, refreshToken, profile, cb) {
process.nextTick(async () => {
console.log(accessToken, refreshToken, profile)
// cb('todo')
const email = profile.emails[0]?.value
if (!email) {
cb(new Error('No email found'))
return
}
let user = await User.findOne({ 'github.id': profile.id })
if (!user) {
user = await User.findOne({ email })
const github = {
id: profile.id,
displayName: profile.displayName,
}
if (!user) {
user = new User({
email,
github,
})
} else if (user.github?.id) {
cb(new Error('Email already used and binded to another account'))
return
} else {
user.github = github
}
await user.save()
}
cb(null, user)
})
oauthVerify('github', profile, { 'github.id': profile.id }, cb)
}
const enabled = config.github.clientID && config.github.clientSecret
+2 -31
View File
@@ -1,39 +1,10 @@
import GoogleStrategy from 'passport-google-oidc'
import config from '../config.js'
import User from '../models/user.js'
import oauthVerify from './oauthVerify.js'
function verify(issuer, profile, cb) {
process.nextTick(async () => {
const email = profile.emails[0]?.value
if (!email) {
cb(new Error('No email found'))
return
}
let user = await User.findOne({ 'google.id': profile.id })
if (!user) {
user = await User.findOne({ email })
const google = {
id: profile.id,
displayName: profile.displayName,
}
if (!user) {
user = new User({
email,
google,
})
} else if (user.google?.id) {
cb(new Error('Email already used and binded to another account'))
return
} else {
user.google = google
}
await user.save()
}
cb(null, user)
})
oauthVerify('google', profile, { 'google.id': profile.id }, cb)
}
const enabled = config.google.clientID && config.google.clientSecret
+24
View File
@@ -0,0 +1,24 @@
import User from '../models/user.js'
export default async function oauthVerify(provider, profile, query, cb) {
process.nextTick(async () => {
const email = profile.emails[0]?.value
if (!email) {
return cb(new Error('No email found'))
}
let user = await User.findOne(query)
if (!user) {
user = await User.findOne({ email })
if (!user) {
const newUser = { email }
newUser[provider] = profile
return cb(null, { newUser })
}
user[provider] = profile
await user.save()
}
return cb(null, user)
})
}
+1 -1
View File
@@ -1,5 +1,5 @@
export default function authRequired(req, res, next) {
if (req.isAuthenticated()) {
if (req.isAuthenticated() && !req.session.newUser) {
next()
return
}
+1 -2
View File
@@ -4,8 +4,7 @@ const User = model('User', {
username: {
type: String,
unique: true,
required: false, // should be required,
// but need to let social login users to pick one after login
required: true,
},
email: {
type: String,
-39
View File
@@ -47,42 +47,3 @@ export function verifyJWT(token) {
})
})
}
export function isValidUsername(username) {
// Define a regular expression pattern for a valid Linux username.
const usernamePattern = /^[a-z][a-z0-9_-]*$/
// Test the username against the pattern.
if (!usernamePattern.test(username)) return false
// check if the username is between 1 and 32 characters
if (username.length < 1 || username.length > 32) {
return false
}
const reservedWords = [
'root',
'admin',
'administrator',
'sysadmin',
'system',
'daemon',
'bin',
'nobody',
'nogroup',
'guest',
'user',
]
if (reservedWords.includes(username)) {
return false
}
return true
}
export function isValidEmail(email) {
// Define a regular expression pattern for a valid email address.
const emailPattern = /^[\w-.+]+@([\w][\w-]*\.)+[\w-]{2,}$/
// Test the email against the pattern.
return emailPattern.test(email)
}
+40 -4
View File
@@ -211,6 +211,31 @@ paths:
type: string
example: User already exists
/auth/register-from-session:
post:
summary: Register a new user from a session, for social login to pick a username
security:
- cookieAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
username:
type: string
format: username
example: alex
responses:
'201':
description: User created
headers:
Set-Cookie:
schema:
type: string
example: connect.sid=s%3Aq1Z; Path=/; HttpOnly; SameSite=Strict; Secure
/auth/login:
post:
summary: Log in a user
@@ -297,19 +322,30 @@ paths:
type: string
format: username
example: alex
- in: query
name: email
description: email to check
required: true
schema:
type: string
format: email
example: alex@example.com
responses:
'200':
description: Username is available
description: Username / Email checking result
content:
application/json:
schema:
type: object
properties:
message:
type:
type: string
example: Username is available
'409':
description: Username is already taken
available:
type: boolean
example: true
'400':
description: Bad Request
content:
application/json:
schema: