-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: oauth with github * fix: import paths * fix: handle secrets * fix: fallback to /index.html * fix: origin * fix: strip last / * fix: complete oauth after nick register * chore: relax codecov * fix: relax codecov more * fix: relax codecov even more * fix: add patch thresholds in codecov
- Loading branch information
Showing
25 changed files
with
445 additions
and
77 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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,7 +6,7 @@ on: | |
|
||
jobs: | ||
deploy: | ||
name: Deploy | ||
name: Production | ||
runs-on: ubuntu-latest | ||
environment: Production | ||
|
||
|
@@ -40,7 +40,9 @@ jobs: | |
- name: Generate .env file | ||
uses: SpicyPizza/[email protected] | ||
with: | ||
envkey_DATABASE_URL: ${{ secrets.DATABASE_URL }} | ||
envkey_DATABASE_URL: ${{ env.DATABASE_URL }} | ||
envkey_OAUTH_GITHUB_CLIENT_ID: ${{ secrets.OAUTH_GITHUB_CLIENT_ID }} | ||
envkey_OAUTH_GITHUB_CLIENT_SECRET: ${{ secrets.OAUTH_GITHUB_CLIENT_SECRET }} | ||
|
||
- name: Run migrations | ||
run: bun run kysely -- migrate:latest | ||
|
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 |
---|---|---|
|
@@ -7,7 +7,7 @@ on: | |
|
||
jobs: | ||
cleanup: | ||
name: Cleanup | ||
name: PR | ||
runs-on: ubuntu-latest | ||
environment: Preview | ||
|
||
|
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 |
---|---|---|
@@ -1,4 +1,4 @@ | ||
name: PR | ||
name: Preview | ||
|
||
on: | ||
pull_request: | ||
|
@@ -8,7 +8,7 @@ on: | |
- synchronize | ||
|
||
jobs: | ||
pr: | ||
preview: | ||
name: PR | ||
runs-on: ubuntu-latest | ||
environment: Preview | ||
|
@@ -61,6 +61,8 @@ jobs: | |
uses: SpicyPizza/[email protected] | ||
with: | ||
envkey_DATABASE_URL: ${{ env.DATABASE_URL }} | ||
envkey_OAUTH_GITHUB_CLIENT_ID: ${{ secrets.OAUTH_GITHUB_CLIENT_ID }} | ||
envkey_OAUTH_GITHUB_CLIENT_SECRET: ${{ secrets.OAUTH_GITHUB_CLIENT_SECRET }} | ||
|
||
- name: Run migrations | ||
run: bun run kysely -- migrate:latest | ||
|
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
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 |
---|---|---|
|
@@ -3,38 +3,39 @@ import { hash } from 'jsr:@denorg/[email protected]' | |
import { createCookie, randomHash, timeout } from 'utils' | ||
import { kv } from '../core/app.ts' | ||
import { SALT as salt } from '../core/constants.ts' | ||
import { Context } from '../core/router.ts' | ||
import { Context, RouteError } from '../core/router.ts' | ||
import { sendEmail } from '../core/send-email.ts' | ||
import { sessions } from '../core/sessions.ts' | ||
import { db } from '../db.ts' | ||
import { env } from '../env.ts' | ||
import { actions, RpcError } from '../routes/rpc.ts' | ||
import { User, UserLogin, UserRegister, UserSession } from '../schemas/user.ts' | ||
import { actions } from '../routes/rpc.ts' | ||
import { UserLogin, UserRegister, UserSession } from '../schemas/user.ts' | ||
import { ADMINS } from './admin.ts' | ||
import { User } from '../models.ts' | ||
|
||
// const DEBUG = true | ||
|
||
class LoginError extends RpcError { | ||
class LoginError extends RouteError { | ||
constructor() { super(403, 'Wrong user or password') } | ||
} | ||
|
||
class UserExistsError extends RpcError { | ||
class UserExistsError extends RouteError { | ||
constructor() { super(409, 'User already exists') } | ||
} | ||
|
||
class UnableToRegisterError extends RpcError { | ||
class UnableToRegisterError extends RouteError { | ||
constructor() { super(500, 'Unable to register') } | ||
} | ||
|
||
class UserEmailAlreadyVerified extends RpcError { | ||
class UserEmailAlreadyVerified extends RouteError { | ||
constructor() { super(404, 'Email already verified') } | ||
} | ||
|
||
class UserNotFound extends RpcError { | ||
class UserNotFound extends RouteError { | ||
constructor() { super(404, 'User not found') } | ||
} | ||
|
||
class TokenNotFound extends RpcError { | ||
class TokenNotFound extends RouteError { | ||
constructor() { super(404, 'Token not found') } | ||
} | ||
|
||
|
@@ -58,7 +59,7 @@ export async function getUser(nickOrEmail: string) { | |
return await getUserByNick(nickOrEmail) || await getUserByEmail(nickOrEmail) | ||
} | ||
|
||
async function loginUser(ctx: Context, nick: string) { | ||
export async function loginUser(ctx: Context, nick: string) { | ||
ctx.log('Login:', nick) | ||
|
||
const sessionId = randomHash() | ||
|
@@ -80,6 +81,7 @@ async function loginUser(ctx: Context, nick: string) { | |
'session', | ||
sessionId, | ||
expires, | ||
'Path=/', | ||
'SameSite=Strict', | ||
'Secure', | ||
'HttpOnly', | ||
|
@@ -106,7 +108,7 @@ async function generateEmailVerificationToken(email: string) { | |
actions.post.whoami = whoami | ||
export async function whoami(ctx: Context) { | ||
const session = sessions.get(ctx) | ||
return session | ||
return session ?? null | ||
} | ||
|
||
actions.post.login = login | ||
|
@@ -124,23 +126,66 @@ export async function login(ctx: Context, userLogin: UserLogin) { | |
} | ||
|
||
actions.post.register = register | ||
export async function register(ctx: Context, userRegister: UserRegister) { | ||
const { nick, email, password } = userRegister | ||
|
||
if (await getUserByNick(nick) || await getUserByEmail(email)) { | ||
throw new UserExistsError() | ||
export async function register(ctx: Context, userRegister: UserRegister, oauthField?: 'oauthGithub') { | ||
const { nick, email } = userRegister | ||
|
||
const userByNick = await getUserByNick(nick) | ||
const userByEmail = await getUserByEmail(email) | ||
|
||
// user nick or email exists | ||
if (userByNick || userByEmail) { | ||
// user has registered with password before and is now logging in with oauth | ||
if (oauthField && userByEmail) { | ||
await db | ||
.updateTable('user') | ||
.where('email', '=', email) | ||
.set('emailVerified', true) | ||
.set(oauthField, true) | ||
.executeTakeFirstOrThrow(UnableToRegisterError) | ||
} | ||
// user has logged in with oauth before and is now registering with password | ||
else if (userByEmail?.emailVerified && 'password' in userRegister && !userByEmail.password) { | ||
await db | ||
.updateTable('user') | ||
.where('email', '=', email) | ||
.set('password', hash(userRegister.password, { salt })) | ||
.executeTakeFirstOrThrow(UnableToRegisterError) | ||
} | ||
else { | ||
throw new UserExistsError() | ||
} | ||
} | ||
// user is new | ||
else { | ||
let values: UserRegister | ||
// user registers with password | ||
if ('password' in userRegister) { | ||
values = { | ||
nick, | ||
email, | ||
password: hash(userRegister.password, { salt }) | ||
} | ||
} | ||
// user registers with oauth | ||
else if (oauthField) { | ||
values = { | ||
nick, | ||
email, | ||
emailVerified: true, | ||
// @ts-ignore ts has issues with dynamic keys even though it is typed | ||
[oauthField]: true | ||
} | ||
} | ||
else { | ||
throw new RouteError(400, 'Invalid registration') | ||
} | ||
|
||
await db.insertInto('user') | ||
.values({ | ||
nick, | ||
email, | ||
emailVerified: false, | ||
password: hash(password, { salt }) | ||
}) | ||
.executeTakeFirstOrThrow(UnableToRegisterError) | ||
await db.insertInto('user') | ||
.values(values) | ||
.executeTakeFirstOrThrow(UnableToRegisterError) | ||
|
||
sendVerificationEmail(ctx, email).catch(ctx.log) | ||
if (!oauthField) sendVerificationEmail(ctx, email).catch(ctx.log) | ||
} | ||
|
||
return loginUser(ctx, nick) | ||
} | ||
|
@@ -173,7 +218,7 @@ If you did not register, simply ignore this email.`, | |
}) | ||
|
||
if (!result.ok) { | ||
throw new RpcError(result.error.statusCode, result.error.message) | ||
throw new RouteError(result.error.statusCode, result.error.message) | ||
} | ||
} | ||
|
||
|
@@ -248,7 +293,7 @@ If you did not request a password reset, simply ignore this email.`, | |
}) | ||
|
||
if (!result.ok) { | ||
throw new RpcError(result.error.statusCode, result.error.message) | ||
throw new RouteError(result.error.statusCode, result.error.message) | ||
} | ||
} | ||
|
||
|
@@ -259,9 +304,8 @@ export async function getResetPasswordUser(_ctx: Context, token: string) { | |
if (result.value) { | ||
const user = await getUserByNick(result.value) | ||
if (user) { | ||
// @ts-ignore remove password before returning | ||
delete user.password | ||
return user as Omit<User, 'password'> | ||
user.password = null | ||
return user | ||
} | ||
} | ||
|
||
|
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,31 @@ | ||
import { kv } from '../core/app.ts' | ||
import { Context, RouteError } from '../core/router.ts' | ||
import { OAuthLogin } from '../routes/oauth/github.ts' | ||
import { actions } from '../routes/rpc.ts' | ||
import * as loginRegisterActions from './login-register.ts' | ||
|
||
function pascalCase(s: string) { | ||
return s.replace(/(^|-)([a-z])/g, (_, __, c) => c.toUpperCase()) | ||
} | ||
|
||
actions.get.getLoginSession = getLoginSession | ||
export async function getLoginSession(_ctx: Context, id: string) { | ||
const entry = await kv.get(['oauth', id]) | ||
if (!entry.value) throw new RouteError(404, 'OAuth session not found') | ||
const session = OAuthLogin.parse(entry.value) | ||
return { login: session.login } | ||
} | ||
|
||
actions.post.registerOAuth = registerOAuth | ||
export async function registerOAuth(ctx: Context, id: string, nick: string) { | ||
const entry = await kv.get(['oauth', id]) | ||
if (!entry.value) throw new RouteError(404, 'OAuth session not found') | ||
const target = 'oauth' + pascalCase(id.split('-')[0]) as 'oauthGithub' | ||
const session = OAuthLogin.parse(entry.value) | ||
return await loginRegisterActions.register(ctx, { | ||
nick, | ||
email: session.email, | ||
// @ts-ignore ts has issues with dynamic keys | ||
[target]: true | ||
}, target) | ||
} |
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,7 @@ | ||
export async function fetchJson(url: string, init: RequestInit) { | ||
const res = await fetch(url, init) | ||
if (!res.ok) { | ||
throw new Error(`Fetch error ${res.status}`) | ||
} | ||
return res.json() | ||
} |
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
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 |
---|---|---|
@@ -1,9 +1,12 @@ | ||
import os from 'https://deno.land/x/[email protected]/src/mod.deno.ts' | ||
import * as path from 'jsr:@std/path' | ||
import * as rpc from '../routes/rpc.ts' | ||
import * as oauthCommon from '../routes/oauth/common.ts' | ||
import * as oauthGitHub from '../routes/oauth/github.ts' | ||
import { app } from './app.ts' | ||
import { IS_DEV } from './constants.ts' | ||
import { cors, files, logger, session, watcher } from './middleware.ts' | ||
import '../actions/oauth.ts' | ||
|
||
const dist = 'dist' | ||
const home = os.home() ?? '~' | ||
|
@@ -21,6 +24,8 @@ app.use(null, [logger]) | |
app.use(null, [cors]) | ||
app.use(null, [session]) | ||
|
||
oauthCommon.mount(app) | ||
oauthGitHub.mount(app) | ||
rpc.mount(app) | ||
|
||
IS_DEV && app.log('Listening: https://devito.test:8000') | ||
|
Oops, something went wrong.