mirror of
https://github.com/AtHeartEngineering/bandada.git
synced 2026-01-09 19:38:07 -05:00
@@ -4,8 +4,8 @@ API_URL=http://localhost:3000
|
||||
DASHBOARD_URL=http://localhost:3001
|
||||
ETHEREUM_NETWORK=localhost
|
||||
|
||||
# Jwt
|
||||
JWT_SECRET_KEY="bandada_jwt_secret"
|
||||
# Iron session
|
||||
IRON_SESSION_PASSWORD="JJ1EnoEPyesNnpdcDVD4ujVG2XKXJLQx"
|
||||
|
||||
# Github Passport
|
||||
GITHUB_CLIENT_ID=""
|
||||
|
||||
@@ -12,24 +12,16 @@
|
||||
"@ethersproject/hash": "^5.7.0",
|
||||
"@nestjs/common": "^9.0.0",
|
||||
"@nestjs/core": "^9.0.0",
|
||||
"@nestjs/jwt": "^10.0.2",
|
||||
"@nestjs/mapped-types": "^1.2.2",
|
||||
"@nestjs/passport": "^9.0.3",
|
||||
"@nestjs/platform-express": "^9.0.0",
|
||||
"@nestjs/schedule": "^2.2.0",
|
||||
"@nestjs/typeorm": "^9.0.1",
|
||||
"@semaphore-protocol/group": "3.4.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"dotenv": "^16.0.3",
|
||||
"express": "^4.18.2",
|
||||
"express-session": "^1.17.3",
|
||||
"passport": "^0.6.0",
|
||||
"passport-github": "^1.1.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-oauth2": "^1.6.1",
|
||||
"passport-twitter": "^1.0.4",
|
||||
"iron-session": "^6.3.1",
|
||||
"pg": "^8.8.0",
|
||||
"querystring": "^0.2.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
@@ -43,14 +35,8 @@
|
||||
"@nestjs/cli": "^9.0.0",
|
||||
"@nestjs/schematics": "^9.0.0",
|
||||
"@nestjs/testing": "^9.0.0",
|
||||
"@types/cookie-parser": "^1.4.3",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/express-session": "^1.17.7",
|
||||
"@types/node": "18.11.18",
|
||||
"@types/passport": "^1.0.12",
|
||||
"@types/passport-github": "^1.1.7",
|
||||
"@types/passport-jwt": "^3.0.8",
|
||||
"@types/passport-twitter": "^1.0.37",
|
||||
"ethers": "5.5.1",
|
||||
"ts-node": "^10.0.0",
|
||||
"typescript": "^4.7.4"
|
||||
|
||||
13
apps/api/src/app/admins/admins.module.ts
Normal file
13
apps/api/src/app/admins/admins.module.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Global, Module } from "@nestjs/common"
|
||||
import { TypeOrmModule } from "@nestjs/typeorm"
|
||||
import { Admin } from "./entities/admin.entity"
|
||||
import { AdminService } from "./admins.service"
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Admin])],
|
||||
exports: [AdminService],
|
||||
providers: [AdminService],
|
||||
controllers: []
|
||||
})
|
||||
export class AdminsModule {}
|
||||
@@ -3,20 +3,20 @@ import { id } from "@ethersproject/hash"
|
||||
import { Injectable } from "@nestjs/common"
|
||||
import { InjectRepository } from "@nestjs/typeorm"
|
||||
import { FindOptionsWhere, Repository } from "typeorm"
|
||||
import { CreateUserDTO } from "./dto/create-user.dto"
|
||||
import { User } from "./entities/user.entity"
|
||||
import { CreateAdminDTO } from "./dto/create-admin.dto"
|
||||
import { Admin } from "./entities/admin.entity"
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
export class AdminService {
|
||||
constructor(
|
||||
@InjectRepository(User)
|
||||
private readonly userRepository: Repository<User>
|
||||
@InjectRepository(Admin)
|
||||
private readonly adminRepository: Repository<Admin>
|
||||
) {}
|
||||
|
||||
public async create(payload: CreateUserDTO): Promise<User> {
|
||||
public async create(payload: CreateAdminDTO): Promise<Admin> {
|
||||
const username = payload.username || payload.address.slice(-5)
|
||||
|
||||
return this.userRepository.save({
|
||||
return this.adminRepository.save({
|
||||
id: id(payload.id),
|
||||
address: payload.address,
|
||||
username,
|
||||
@@ -25,8 +25,8 @@ export class UserService {
|
||||
}
|
||||
|
||||
public async findOne(
|
||||
payload: FindOptionsWhere<User> | FindOptionsWhere<User>[]
|
||||
): Promise<User> {
|
||||
return this.userRepository.findOneBy(payload)
|
||||
payload: FindOptionsWhere<Admin> | FindOptionsWhere<Admin>[]
|
||||
): Promise<Admin> {
|
||||
return this.adminRepository.findOneBy(payload)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IsOptional, IsString } from "class-validator"
|
||||
|
||||
export class CreateUserDTO {
|
||||
export class CreateAdminDTO {
|
||||
@IsString()
|
||||
id: string
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Column, CreateDateColumn, Entity, PrimaryColumn } from "typeorm"
|
||||
|
||||
@Entity("users")
|
||||
export class User {
|
||||
@Entity("admins")
|
||||
export class Admin {
|
||||
@PrimaryColumn({ unique: true })
|
||||
id: string
|
||||
|
||||
// Wallet address of the user when using SIWE
|
||||
// Wallet address of the admin when using SIWE
|
||||
@Column({ unique: true })
|
||||
address: string
|
||||
|
||||
@@ -6,7 +6,7 @@ dotenvConfig({ path: resolve(process.cwd(), ".env") })
|
||||
|
||||
import { Module } from "@nestjs/common"
|
||||
import { TypeOrmModule } from "@nestjs/typeorm"
|
||||
import { UsersModule } from "./users/users.module"
|
||||
import { AdminsModule } from "./admins/admins.module"
|
||||
import { AuthModule } from "./auth/auth.module"
|
||||
import { GroupsModule } from "./groups/groups.module"
|
||||
import { InvitesModule } from "./invites/invites.module"
|
||||
@@ -16,7 +16,7 @@ type DB_TYPE = "mysql" | "sqlite" | "postgres"
|
||||
@Module({
|
||||
imports: [
|
||||
AuthModule,
|
||||
UsersModule,
|
||||
AdminsModule,
|
||||
InvitesModule,
|
||||
GroupsModule,
|
||||
TypeOrmModule.forRoot({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Body, Controller, Delete, Post, Req, Res } from "@nestjs/common"
|
||||
import { Request, Response } from "express"
|
||||
import { Body, Controller, Delete, Get, Post, Req } from "@nestjs/common"
|
||||
import { Request } from "express"
|
||||
import { generateNonce } from "siwe"
|
||||
import { AuthService } from "./auth.service"
|
||||
import { SignInWithEthereumDTO } from "./dto/siwe-dto"
|
||||
|
||||
@@ -7,29 +8,34 @@ import { SignInWithEthereumDTO } from "./dto/siwe-dto"
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@Get("nonce")
|
||||
async nonce(@Req() req: Request) {
|
||||
req.session.nonce = generateNonce()
|
||||
|
||||
await req.session.save()
|
||||
|
||||
return req.session.nonce
|
||||
}
|
||||
|
||||
@Post("")
|
||||
async signIn(@Body() body: SignInWithEthereumDTO, @Res() res: Response) {
|
||||
const { token, user } = await this.authService.signIn({
|
||||
message: body.message,
|
||||
signature: body.signature
|
||||
})
|
||||
async signIn(@Body() body: SignInWithEthereumDTO, @Req() req: Request) {
|
||||
const { admin } = await this.authService.signIn(
|
||||
{
|
||||
message: body.message,
|
||||
signature: body.signature
|
||||
},
|
||||
req.session.nonce
|
||||
)
|
||||
|
||||
res.cookie("token", token, {
|
||||
httpOnly: true,
|
||||
expires: new Date()
|
||||
})
|
||||
req.session.adminId = admin.id
|
||||
|
||||
res.send(user)
|
||||
await req.session.save()
|
||||
|
||||
return admin
|
||||
}
|
||||
|
||||
@Delete("")
|
||||
logOut(@Req() _req: Request, @Res() res: Response) {
|
||||
res.cookie("token", "", {
|
||||
httpOnly: true,
|
||||
expires: new Date()
|
||||
})
|
||||
|
||||
// TODO: Avoid this redirect here and move to client side
|
||||
res.redirect(`${process.env.DASHBOARD_URL}`)
|
||||
logOut(@Req() req: Request) {
|
||||
req.session.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,31 +4,25 @@ import {
|
||||
Injectable,
|
||||
UnauthorizedException
|
||||
} from "@nestjs/common"
|
||||
import { JwtService } from "@nestjs/jwt"
|
||||
import { UserService } from "../users/users.service"
|
||||
import { AdminService } from "../admins/admins.service"
|
||||
|
||||
@Injectable()
|
||||
export class AuthGuard implements CanActivate {
|
||||
constructor(
|
||||
private jwtService: JwtService,
|
||||
private userService: UserService
|
||||
) {}
|
||||
constructor(private adminService: AdminService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest()
|
||||
const { token } = request.cookies
|
||||
if (!token) {
|
||||
const req = context.switchToHttp().getRequest()
|
||||
|
||||
const { adminId } = req.session
|
||||
|
||||
if (!adminId) {
|
||||
throw new UnauthorizedException()
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await this.jwtService.verifyAsync(token, {
|
||||
secret: process.env.JWT_SECRET_KEY
|
||||
})
|
||||
const admin = await this.adminService.findOne({ id: adminId })
|
||||
|
||||
const user = await this.userService.findOne({ id: payload.id })
|
||||
|
||||
request["user"] = user
|
||||
req["admin"] = admin
|
||||
} catch {
|
||||
throw new UnauthorizedException()
|
||||
}
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
import { Global, Module } from "@nestjs/common"
|
||||
import { JwtModule } from "@nestjs/jwt"
|
||||
import { UsersModule } from "../users/users.module"
|
||||
import { CookieSerializer } from "../utils"
|
||||
import { AdminsModule } from "../admins/admins.module"
|
||||
import { AuthController } from "./auth.controller"
|
||||
import { AuthService } from "./auth.service"
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
UsersModule,
|
||||
JwtModule.register({
|
||||
secret: process.env.JWT_SECRET_KEY,
|
||||
signOptions: { expiresIn: "300s" }
|
||||
})
|
||||
],
|
||||
providers: [AuthService, CookieSerializer],
|
||||
imports: [AdminsModule],
|
||||
providers: [AuthService],
|
||||
controllers: [AuthController],
|
||||
exports: [JwtModule, AuthService]
|
||||
exports: [AuthService]
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Test } from "@nestjs/testing"
|
||||
import { TypeOrmModule } from "@nestjs/typeorm"
|
||||
import { SiweMessage } from "siwe"
|
||||
import { ethers } from "ethers"
|
||||
import { JwtModule } from "@nestjs/jwt"
|
||||
import { SiweMessage } from "siwe"
|
||||
import { Admin } from "../admins/entities/admin.entity"
|
||||
import { AdminService } from "../admins/admins.service"
|
||||
import { AuthService } from "./auth.service"
|
||||
import { User } from "../users/entities/user.entity"
|
||||
import { UserService } from "../users/users.service"
|
||||
|
||||
jest.mock("@bandada/utils", () => ({
|
||||
__esModule: true,
|
||||
@@ -28,10 +27,11 @@ const account2 = new ethers.Wallet(
|
||||
|
||||
const mockDashboardUrl = new URL("https://bandada.test")
|
||||
|
||||
function createSiweMessage(address, statement?: string) {
|
||||
function createSiweMessage(address: string, statement?: string) {
|
||||
const message = new SiweMessage({
|
||||
domain: mockDashboardUrl.host,
|
||||
address,
|
||||
nonce: "1234",
|
||||
statement:
|
||||
statement ||
|
||||
"You are using your Ethereum Wallet to sign in to Bandada.",
|
||||
@@ -45,7 +45,7 @@ function createSiweMessage(address, statement?: string) {
|
||||
|
||||
describe("AuthService", () => {
|
||||
let authService: AuthService
|
||||
let userService: UserService
|
||||
let adminService: AdminService
|
||||
|
||||
let originalApiUrl: string
|
||||
|
||||
@@ -57,21 +57,17 @@ describe("AuthService", () => {
|
||||
type: "sqlite",
|
||||
database: ":memory:",
|
||||
dropSchema: true,
|
||||
entities: [User],
|
||||
entities: [Admin],
|
||||
synchronize: true
|
||||
})
|
||||
}),
|
||||
TypeOrmModule.forFeature([User]),
|
||||
JwtModule.register({
|
||||
secret: "s3cret",
|
||||
signOptions: { expiresIn: "300s" }
|
||||
})
|
||||
TypeOrmModule.forFeature([Admin])
|
||||
],
|
||||
providers: [AuthService, UserService]
|
||||
providers: [AuthService, AdminService]
|
||||
}).compile()
|
||||
|
||||
authService = await module.resolve(AuthService)
|
||||
userService = await module.resolve(UserService)
|
||||
adminService = await module.resolve(AdminService)
|
||||
|
||||
// Set API_URL so auth service can validate domain
|
||||
originalApiUrl = process.env.DASHBOARD_URL
|
||||
@@ -85,66 +81,76 @@ describe("AuthService", () => {
|
||||
})
|
||||
|
||||
describe("# SIWE", () => {
|
||||
it("Should sign in and generate token for a new user", async () => {
|
||||
const message = await createSiweMessage(account1.address)
|
||||
it("Should sign in and generate token for a new admin", async () => {
|
||||
const message = createSiweMessage(account1.address)
|
||||
const signature = await account1.signMessage(message)
|
||||
|
||||
const { user, token } = await authService.signIn({
|
||||
message,
|
||||
signature
|
||||
})
|
||||
const { admin } = await authService.signIn(
|
||||
{
|
||||
message,
|
||||
signature
|
||||
},
|
||||
"1234"
|
||||
)
|
||||
|
||||
expect(user).toBeTruthy()
|
||||
expect(user.address).toBe(account1.address)
|
||||
expect(token).toBeTruthy()
|
||||
expect(admin).toBeTruthy()
|
||||
expect(admin.address).toBe(account1.address)
|
||||
})
|
||||
|
||||
it("Should sign in and generate token for an existing user", async () => {
|
||||
// Create a user directly
|
||||
const user2 = await userService.create({
|
||||
it("Should sign in and generate token for an existing admin", async () => {
|
||||
// Create a admin directly
|
||||
const admin2 = await adminService.create({
|
||||
id: "account2",
|
||||
address: account2.address
|
||||
})
|
||||
|
||||
// Sign in with same address
|
||||
const message = await createSiweMessage(account2.address)
|
||||
const message = createSiweMessage(account2.address)
|
||||
const signature = await account2.signMessage(message)
|
||||
|
||||
const { user, token } = await authService.signIn({
|
||||
message,
|
||||
signature
|
||||
})
|
||||
const { admin } = await authService.signIn(
|
||||
{
|
||||
message,
|
||||
signature
|
||||
},
|
||||
"1234"
|
||||
)
|
||||
|
||||
expect(user).toBeTruthy()
|
||||
expect(token).toBeTruthy()
|
||||
expect(user.address).toBe(user2.address)
|
||||
expect(user.address).toBe(account2.address)
|
||||
expect(admin).toBeTruthy()
|
||||
expect(admin.address).toBe(admin2.address)
|
||||
expect(admin.address).toBe(account2.address)
|
||||
})
|
||||
|
||||
it("Should throw an error if the signature is invalid", async () => {
|
||||
const message = await createSiweMessage(account1.address)
|
||||
const message = createSiweMessage(account1.address)
|
||||
|
||||
// Sign the message with a different account
|
||||
const signature = await account2.signMessage(message)
|
||||
|
||||
await expect(
|
||||
authService.signIn({
|
||||
message,
|
||||
signature
|
||||
})
|
||||
authService.signIn(
|
||||
{
|
||||
message,
|
||||
signature
|
||||
},
|
||||
"1234"
|
||||
)
|
||||
).rejects.toThrow()
|
||||
})
|
||||
|
||||
it("Should throw an error if the statement is invalid", async () => {
|
||||
// Use a custom message to sign
|
||||
const message = await createSiweMessage(account1.address, "Sign in")
|
||||
const message = createSiweMessage(account1.address, "Sign in")
|
||||
const signature = await account1.signMessage(message)
|
||||
|
||||
await expect(
|
||||
authService.signIn({
|
||||
message: "invalid message",
|
||||
signature
|
||||
})
|
||||
authService.signIn(
|
||||
{
|
||||
message: "invalid message",
|
||||
signature
|
||||
},
|
||||
"1234"
|
||||
)
|
||||
).rejects.toThrow()
|
||||
})
|
||||
|
||||
@@ -152,14 +158,17 @@ describe("AuthService", () => {
|
||||
process.env.DASHBOARD_URL = "https://bandada2.test"
|
||||
|
||||
// Use a custom message to sign
|
||||
const message = await createSiweMessage(account1.address)
|
||||
const message = createSiweMessage(account1.address)
|
||||
const signature = await account1.signMessage(message)
|
||||
|
||||
await expect(
|
||||
authService.signIn({
|
||||
message: "invalid message",
|
||||
signature
|
||||
})
|
||||
authService.signIn(
|
||||
{
|
||||
message: "invalid message",
|
||||
signature
|
||||
},
|
||||
"1234"
|
||||
)
|
||||
).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
/* istanbul ignore file */
|
||||
import { Injectable, UnauthorizedException } from "@nestjs/common"
|
||||
import { JwtService } from "@nestjs/jwt"
|
||||
import {
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
UnprocessableEntityException
|
||||
} from "@nestjs/common"
|
||||
import { SiweMessage } from "siwe"
|
||||
import { v4 } from "uuid"
|
||||
import { UserService } from "../users/users.service"
|
||||
import { AdminService } from "../admins/admins.service"
|
||||
import { SignInWithEthereumDTO } from "./dto/siwe-dto"
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private readonly userService: UserService,
|
||||
private readonly jwtService: JwtService
|
||||
) {}
|
||||
|
||||
async signIn(params: SignInWithEthereumDTO) {
|
||||
const { message, signature } = params
|
||||
constructor(private readonly adminService: AdminService) {}
|
||||
|
||||
async signIn(
|
||||
{ message, signature }: SignInWithEthereumDTO,
|
||||
expectedNonce: string
|
||||
) {
|
||||
const siweMessage = new SiweMessage(message)
|
||||
const { address, statement, domain } = await siweMessage.validate(
|
||||
signature
|
||||
)
|
||||
const { address, statement, domain, nonce } =
|
||||
await siweMessage.validate(signature)
|
||||
|
||||
if (nonce !== expectedNonce) {
|
||||
throw new UnprocessableEntityException("Invalid nonce.")
|
||||
}
|
||||
|
||||
if (statement !== process.env.SIWE_STATEMENT) {
|
||||
throw new UnauthorizedException(
|
||||
@@ -34,21 +38,15 @@ export class AuthService {
|
||||
)
|
||||
}
|
||||
|
||||
let user = await this.userService.findOne({ address })
|
||||
let admin = await this.adminService.findOne({ address })
|
||||
|
||||
if (!user) {
|
||||
user = await this.userService.create({
|
||||
if (!admin) {
|
||||
admin = await this.adminService.create({
|
||||
id: v4(),
|
||||
address
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Use common expiration
|
||||
const token = this.jwtService.sign({
|
||||
id: user.id,
|
||||
username: user.username
|
||||
})
|
||||
|
||||
return { token, user }
|
||||
return { admin, siweMessage }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export type ServiceType = "twitter" | "github" | "reddit"
|
||||
|
||||
export type Payload = {
|
||||
userId: string
|
||||
username: string
|
||||
}
|
||||
@@ -8,16 +8,16 @@ import {
|
||||
Post,
|
||||
Put,
|
||||
Req,
|
||||
Request,
|
||||
UseGuards
|
||||
} from "@nestjs/common"
|
||||
import { Request } from "express"
|
||||
import { AuthGuard } from "../auth/auth.guard"
|
||||
import { stringifyJSON } from "../utils"
|
||||
import { AddMemberDto } from "./dto/add-member.dto"
|
||||
import { CreateGroupDto } from "./dto/create-group.dto"
|
||||
import { UpdateGroupDto } from "./dto/update-group.dto"
|
||||
import { mapGroupToResponseDTO } from "./groups.utils"
|
||||
import { GroupsService } from "./groups.service"
|
||||
import { AuthGuard } from "../auth/auth.guard"
|
||||
import { mapGroupToResponseDTO } from "./groups.utils"
|
||||
|
||||
@Controller("groups")
|
||||
export class GroupsController {
|
||||
@@ -33,7 +33,9 @@ export class GroupsController {
|
||||
@Get("admin-groups")
|
||||
@UseGuards(AuthGuard)
|
||||
async getGroupsByAdmin(@Req() req: Request) {
|
||||
const groups = await this.groupsService.getGroupsByAdmin(req["user"].id)
|
||||
const groups = await this.groupsService.getGroupsByAdmin(
|
||||
req["admin"].id
|
||||
)
|
||||
|
||||
return groups.map((g) => mapGroupToResponseDTO(g))
|
||||
}
|
||||
@@ -46,7 +48,7 @@ export class GroupsController {
|
||||
|
||||
const response: any = mapGroupToResponseDTO(
|
||||
group,
|
||||
req["user"].id.toString() === group.admin
|
||||
req["admin"].id.toString() === group.admin
|
||||
)
|
||||
|
||||
return response
|
||||
@@ -55,10 +57,10 @@ export class GroupsController {
|
||||
@Post()
|
||||
@UseGuards(AuthGuard)
|
||||
async createGroup(@Req() req: Request, @Body() dto: CreateGroupDto) {
|
||||
const group = await this.groupsService.createGroup(dto, req["user"].id)
|
||||
const group = await this.groupsService.createGroup(dto, req["admin"].id)
|
||||
return mapGroupToResponseDTO(
|
||||
group,
|
||||
req["user"].id.toString() === group.admin
|
||||
req["admin"].id.toString() === group.admin
|
||||
)
|
||||
}
|
||||
|
||||
@@ -72,12 +74,12 @@ export class GroupsController {
|
||||
const group = await this.groupsService.updateGroup(
|
||||
groupId,
|
||||
dto,
|
||||
req["user"].id
|
||||
req["admin"].id
|
||||
)
|
||||
|
||||
return mapGroupToResponseDTO(
|
||||
group,
|
||||
req["user"].id.toString() === group.admin
|
||||
req["admin"].id.toString() === group.admin
|
||||
)
|
||||
}
|
||||
|
||||
@@ -149,6 +151,10 @@ export class GroupsController {
|
||||
}
|
||||
|
||||
// Remove as admin
|
||||
await this.groupsService.removeMember(groupId, memberId, req["user"].id)
|
||||
await this.groupsService.removeMember(
|
||||
groupId,
|
||||
memberId,
|
||||
req["admin"].id
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { InjectRepository } from "@nestjs/typeorm"
|
||||
import { Group as CachedGroup } from "@semaphore-protocol/group"
|
||||
import { Repository } from "typeorm"
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import { v4 } from "uuid"
|
||||
import { InvitesService } from "../invites/invites.service"
|
||||
import { CreateGroupDto } from "./dto/create-group.dto"
|
||||
import { UpdateGroupDto } from "./dto/update-group.dto"
|
||||
@@ -115,7 +115,7 @@ export class GroupsService {
|
||||
|
||||
// Generate a new API key if it doesn't exist
|
||||
if (!group.apiKey) {
|
||||
group.apiKey = uuidv4()
|
||||
group.apiKey = v4()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ export class InvitesController {
|
||||
): Promise<string> {
|
||||
const { code } = await this.invitesService.createInvite(
|
||||
dto,
|
||||
req["user"].id
|
||||
req["admin"].id
|
||||
)
|
||||
|
||||
return code
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Global, Module } from "@nestjs/common"
|
||||
import { TypeOrmModule } from "@nestjs/typeorm"
|
||||
import { User } from "./entities/user.entity"
|
||||
import { UserService } from "./users.service"
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([User])],
|
||||
exports: [UserService],
|
||||
providers: [UserService],
|
||||
controllers: []
|
||||
})
|
||||
export class UsersModule {}
|
||||
@@ -1,14 +0,0 @@
|
||||
/* istanbul ignore file */
|
||||
import { Injectable } from "@nestjs/common"
|
||||
import { PassportSerializer } from "@nestjs/passport"
|
||||
|
||||
@Injectable()
|
||||
export default class CookieSerializer extends PassportSerializer {
|
||||
serializeUser(user, done: (a, b) => void) {
|
||||
done(null, user)
|
||||
}
|
||||
|
||||
deserializeUser(payload, done: (a, b) => void) {
|
||||
done(null, payload)
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,4 @@
|
||||
import mapEntity from "./mapEntity"
|
||||
import stringifyJSON from "./stringifyJSON"
|
||||
import CookieSerializer from "./cookie.serializer"
|
||||
import RedditStrategy, { RedditProfile } from "./passportReddit"
|
||||
|
||||
export {
|
||||
mapEntity,
|
||||
stringifyJSON,
|
||||
CookieSerializer,
|
||||
RedditProfile,
|
||||
RedditStrategy
|
||||
}
|
||||
export { mapEntity, stringifyJSON }
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
/* istanbul ignore file */
|
||||
import {
|
||||
InternalOAuthError,
|
||||
Strategy as OAuth2Strategy,
|
||||
StrategyOptions,
|
||||
VerifyFunction
|
||||
} from "passport-oauth2"
|
||||
import { stringify } from "querystring"
|
||||
|
||||
export type RedditProfile = {
|
||||
id: string
|
||||
name: string
|
||||
icon_img: string
|
||||
subreddit: {
|
||||
title: string
|
||||
}
|
||||
}
|
||||
|
||||
export default class Strategy extends OAuth2Strategy {
|
||||
public name = "reddit"
|
||||
|
||||
private _userProfileURL = "https://oauth.reddit.com/api/v1/me"
|
||||
|
||||
constructor(redditOptions: any, verify: VerifyFunction) {
|
||||
const options: StrategyOptions = {
|
||||
authorizationURL: "https://ssl.reddit.com/api/v1/authorize",
|
||||
tokenURL: "https://ssl.reddit.com/api/v1/access_token",
|
||||
...redditOptions
|
||||
}
|
||||
|
||||
if (options.scope) {
|
||||
if (Array.isArray(options.scope)) {
|
||||
options.scope.push("identity")
|
||||
options.scopeSeparator = ","
|
||||
} else {
|
||||
options.scope = options.scope
|
||||
.split(",")
|
||||
.reduce(
|
||||
(previousValue, currentValue) => {
|
||||
if (currentValue !== "")
|
||||
previousValue.push(currentValue)
|
||||
return previousValue
|
||||
},
|
||||
["identity"]
|
||||
)
|
||||
.join(",")
|
||||
}
|
||||
} else {
|
||||
options.scope = "identity"
|
||||
}
|
||||
|
||||
if (
|
||||
typeof options.state === "undefined" &&
|
||||
typeof options.store === "undefined"
|
||||
) {
|
||||
options.state = true
|
||||
}
|
||||
|
||||
super(options, verify)
|
||||
|
||||
this._oauth2.useAuthorizationHeaderforGET(true)
|
||||
|
||||
this._oauth2.getOAuthAccessToken = function getOAuthAccessToken(
|
||||
code: string,
|
||||
params: any,
|
||||
callback: any
|
||||
) {
|
||||
params = params || {}
|
||||
params.type = "web_server"
|
||||
const codeParam =
|
||||
params.grant_type === "refresh_token" ? "refresh_token" : "code"
|
||||
params[codeParam] = code
|
||||
|
||||
const post_data = stringify(params)
|
||||
const authorization = `Basic ${Buffer.from(
|
||||
`${this._clientId}:${this._clientSecret}`,
|
||||
"utf8"
|
||||
).toString("base64")}`
|
||||
const post_headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Authorization: authorization
|
||||
}
|
||||
|
||||
this._request(
|
||||
"POST",
|
||||
this._getAccessTokenUrl(),
|
||||
post_headers,
|
||||
post_data,
|
||||
null,
|
||||
(error: any, data: any) => {
|
||||
if (error) {
|
||||
callback(error)
|
||||
} else {
|
||||
const results = JSON.parse(data)
|
||||
const { access_token } = results
|
||||
const { refresh_token } = results
|
||||
|
||||
delete results.refresh_token
|
||||
|
||||
callback(null, access_token, refresh_token, results)
|
||||
}
|
||||
}
|
||||
)
|
||||
} as any
|
||||
}
|
||||
|
||||
userProfile(
|
||||
accessToken: string,
|
||||
done: (err?: Error | null, profile?: any) => void
|
||||
) {
|
||||
this._oauth2.get(
|
||||
this._userProfileURL,
|
||||
accessToken,
|
||||
(err: { statusCode: number; data?: any }, result: string) => {
|
||||
if (err) {
|
||||
return done(
|
||||
new InternalOAuthError(
|
||||
"Failed to fetch user profile",
|
||||
err
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
done(null, JSON.parse(result))
|
||||
} catch (error) {
|
||||
done(error)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Logger, ValidationPipe } from "@nestjs/common"
|
||||
import { NestFactory } from "@nestjs/core"
|
||||
import * as cookieParser from "cookie-parser"
|
||||
import * as session from "express-session"
|
||||
import { ironSession } from "iron-session/express"
|
||||
import { AppModule } from "./app/app.module"
|
||||
|
||||
async function bootstrap() {
|
||||
@@ -18,14 +17,17 @@ async function bootstrap() {
|
||||
const port = 3000
|
||||
|
||||
app.use(
|
||||
session({
|
||||
secret: process.env.JWT_SECRET_KEY,
|
||||
resave: false,
|
||||
saveUninitialized: false
|
||||
ironSession({
|
||||
cookieName: "bandada_siwe_cookie",
|
||||
password: process.env.IRON_SESSION_PASSWORD,
|
||||
cookieOptions: {
|
||||
// TODO: decide when session should expiry.
|
||||
// expires: ,
|
||||
secure: process.env.NODE_ENV === "production"
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
app.use(cookieParser())
|
||||
app.enableCors({
|
||||
origin: true,
|
||||
credentials: true
|
||||
|
||||
9
apps/api/types/iron-session/index.d.ts
vendored
Normal file
9
apps/api/types/iron-session/index.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
import "iron-session"
|
||||
import { SiweMessage } from "siwe"
|
||||
|
||||
declare module "iron-session" {
|
||||
interface IronSessionData {
|
||||
nonce?: string
|
||||
adminId?: string
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@
|
||||
"react-icons": "^4.8.0",
|
||||
"react-router-dom": "^6.8.1",
|
||||
"siwe": "^1.1.6",
|
||||
"wagmi": "^0.11.7"
|
||||
"wagmi": "^0.12.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.27",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { request } from "@bandada/utils"
|
||||
import { Group } from "../types/groups"
|
||||
import { SiweMessage } from "siwe"
|
||||
import { Group } from "../types/groups"
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL
|
||||
const CLIENT_URL = import.meta.env.VITE_CLIENT_URL
|
||||
@@ -73,58 +73,62 @@ export async function removeMember(groupId: string, memberId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function logOut(): Promise<void | null> {
|
||||
try {
|
||||
// TODO: check if this works properly.
|
||||
await request(`${API_URL}/auth/log-out`, {
|
||||
method: "post"
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function isLoggedIn(): Promise<boolean> {
|
||||
try {
|
||||
return await request(`${import.meta.env.VITE_API_URL}/auth/getUser`)
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateGroup(
|
||||
groupId: string,
|
||||
{ apiEnabled }: { apiEnabled: boolean }
|
||||
) {
|
||||
try {
|
||||
const group = await request(`${API_URL}/groups/${groupId}`, {
|
||||
return (await request(`${API_URL}/groups/${groupId}`, {
|
||||
method: "PUT",
|
||||
data: { apiEnabled }
|
||||
})
|
||||
|
||||
return group as Group
|
||||
})) as Group
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getNonce(): Promise<string | null> {
|
||||
try {
|
||||
return await request(`${API_URL}/auth/nonce`, {
|
||||
method: "GET"
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function signIn({
|
||||
message,
|
||||
signature
|
||||
}: {
|
||||
message: SiweMessage
|
||||
signature: string
|
||||
}) {
|
||||
const response = await request(`${API_URL}/auth`, {
|
||||
method: "POST",
|
||||
data: {
|
||||
message,
|
||||
signature
|
||||
}
|
||||
})
|
||||
}): Promise<any | null> {
|
||||
try {
|
||||
return await request(`${API_URL}/auth`, {
|
||||
method: "POST",
|
||||
data: {
|
||||
message: message.toMessage(),
|
||||
signature
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
const { user } = response.data
|
||||
return { user }
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function logOut(): Promise<void | null> {
|
||||
try {
|
||||
await request(`${API_URL}/auth`, {
|
||||
method: "DELETE"
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useCallback } from "react"
|
||||
import { useNavigate, useSearchParams } from "react-router-dom"
|
||||
import { useAccount, useConnect } from "wagmi"
|
||||
import { logOut as _logOut } from "../api/bandadaAPI"
|
||||
import { deleteAdmin } from "../utils/session"
|
||||
|
||||
export default function NavBar(): JSX.Element {
|
||||
const navigate = useNavigate()
|
||||
@@ -28,6 +29,8 @@ export default function NavBar(): JSX.Element {
|
||||
const logOut = useCallback(async () => {
|
||||
await _logOut()
|
||||
|
||||
deleteAdmin()
|
||||
|
||||
navigate("/")
|
||||
}, [navigate])
|
||||
|
||||
|
||||
@@ -1,29 +1,27 @@
|
||||
import {
|
||||
connectorsForWallets,
|
||||
lightTheme,
|
||||
AuthenticationStatus,
|
||||
connectorsForWallets,
|
||||
createAuthenticationAdapter,
|
||||
lightTheme,
|
||||
RainbowKitAuthenticationProvider,
|
||||
RainbowKitProvider
|
||||
} from "@rainbow-me/rainbowkit"
|
||||
import React, { ReactNode } from "react"
|
||||
import {
|
||||
metaMaskWallet,
|
||||
coinbaseWallet,
|
||||
walletConnectWallet,
|
||||
trustWallet,
|
||||
ledgerWallet
|
||||
injectedWallet,
|
||||
metaMaskWallet,
|
||||
walletConnectWallet
|
||||
} from "@rainbow-me/rainbowkit/wallets"
|
||||
import { configureChains, createClient, WagmiConfig } from "wagmi"
|
||||
import { mainnet } from "wagmi/chains"
|
||||
import { publicProvider } from "wagmi/providers/public"
|
||||
import React, { ReactNode, useMemo, useState } from "react"
|
||||
import { SiweMessage } from "siwe"
|
||||
import { signIn } from "../api/bandadaAPI"
|
||||
import { deleteUser, getUser, saveUser } from "../utils/auth"
|
||||
import { User } from "../types/user"
|
||||
import { configureChains, createClient, WagmiConfig } from "wagmi"
|
||||
import { goerli } from "wagmi/chains"
|
||||
import { publicProvider } from "wagmi/providers/public"
|
||||
import { getNonce, logOut, signIn } from "../api/bandadaAPI"
|
||||
import { deleteAdmin, getAdmin, saveAdmin } from "../utils/session"
|
||||
|
||||
const { chains, provider, webSocketProvider } = configureChains(
|
||||
[mainnet],
|
||||
[goerli],
|
||||
[publicProvider()]
|
||||
)
|
||||
|
||||
@@ -31,11 +29,10 @@ const connectors = connectorsForWallets([
|
||||
{
|
||||
groupName: "Wallets",
|
||||
wallets: [
|
||||
injectedWallet({ chains }),
|
||||
metaMaskWallet({ chains }),
|
||||
coinbaseWallet({ appName: "Bandada", chains }),
|
||||
walletConnectWallet({ chains }),
|
||||
trustWallet({ chains }),
|
||||
ledgerWallet({ chains })
|
||||
walletConnectWallet({ chains })
|
||||
]
|
||||
}
|
||||
])
|
||||
@@ -47,42 +44,34 @@ const wagmiClient = createClient({
|
||||
webSocketProvider
|
||||
})
|
||||
|
||||
type AuthContextParams = {
|
||||
user: User | null
|
||||
}
|
||||
|
||||
export const AuthContext = React.createContext<AuthContextParams>({
|
||||
user: getUser()
|
||||
})
|
||||
|
||||
const customTheme = lightTheme()
|
||||
customTheme.radii.modal = "10px"
|
||||
|
||||
export function AuthContextProvider(props: { children: ReactNode }) {
|
||||
const { children } = props
|
||||
const verifyingRef = React.useRef(false)
|
||||
|
||||
const [user, setUser] = React.useState<User | null>(getUser())
|
||||
|
||||
const [authStatus, setAuthStatus] =
|
||||
React.useState<AuthenticationStatus>("loading")
|
||||
useState<AuthenticationStatus>("loading")
|
||||
|
||||
React.useEffect(() => {
|
||||
setAuthStatus(getUser() ? "authenticated" : "unauthenticated")
|
||||
setAuthStatus(getAdmin() ? "authenticated" : "unauthenticated")
|
||||
}, [])
|
||||
|
||||
const authAdapter = React.useMemo(
|
||||
const authAdapter = useMemo(
|
||||
() =>
|
||||
createAuthenticationAdapter({
|
||||
async getNonce() {
|
||||
return Math.random().toString(36).substring(7)
|
||||
const nonce = await getNonce()
|
||||
|
||||
return nonce ?? ""
|
||||
},
|
||||
|
||||
createMessage: ({ nonce, address, chainId }) =>
|
||||
new SiweMessage({
|
||||
domain: window.location.host,
|
||||
address,
|
||||
statement: "Sign in with Ethereum to the app.",
|
||||
statement:
|
||||
"You are using your Ethereum Wallet to sign in to Bandada.",
|
||||
uri: window.location.origin,
|
||||
version: "1",
|
||||
chainId,
|
||||
@@ -92,62 +81,54 @@ export function AuthContextProvider(props: { children: ReactNode }) {
|
||||
getMessageBody: ({ message }) => message.prepareMessage(),
|
||||
|
||||
verify: async ({ message, signature }) => {
|
||||
verifyingRef.current = true
|
||||
const user = await signIn({
|
||||
message,
|
||||
signature
|
||||
})
|
||||
|
||||
try {
|
||||
const { user: _user } = await signIn({
|
||||
message,
|
||||
signature
|
||||
})
|
||||
if (user) {
|
||||
setAuthStatus("authenticated")
|
||||
saveAdmin(user.address)
|
||||
|
||||
saveUser(_user)
|
||||
|
||||
setAuthStatus(
|
||||
_user ? "authenticated" : "unauthenticated"
|
||||
)
|
||||
setUser(_user)
|
||||
window.location.reload()
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return false
|
||||
} finally {
|
||||
verifyingRef.current = false
|
||||
}
|
||||
|
||||
setAuthStatus("unauthenticated")
|
||||
|
||||
return false
|
||||
},
|
||||
|
||||
signOut: async () => {
|
||||
await logOut()
|
||||
|
||||
deleteAdmin()
|
||||
|
||||
setAuthStatus("unauthenticated")
|
||||
deleteUser()
|
||||
}
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
user
|
||||
}}
|
||||
>
|
||||
<WagmiConfig client={wagmiClient}>
|
||||
<RainbowKitAuthenticationProvider
|
||||
adapter={authAdapter}
|
||||
status={authStatus}
|
||||
<WagmiConfig client={wagmiClient}>
|
||||
<RainbowKitAuthenticationProvider
|
||||
adapter={authAdapter}
|
||||
status={authStatus}
|
||||
>
|
||||
<RainbowKitProvider
|
||||
chains={chains}
|
||||
modalSize="compact"
|
||||
theme={customTheme}
|
||||
appInfo={{
|
||||
appName: "Bandada"
|
||||
}}
|
||||
showRecentTransactions={false}
|
||||
>
|
||||
<RainbowKitProvider
|
||||
chains={chains}
|
||||
modalSize="compact"
|
||||
theme={customTheme}
|
||||
appInfo={{
|
||||
appName: "Bandada"
|
||||
}}
|
||||
showRecentTransactions={false}
|
||||
>
|
||||
{children}
|
||||
</RainbowKitProvider>
|
||||
</RainbowKitAuthenticationProvider>
|
||||
</WagmiConfig>
|
||||
</AuthContext.Provider>
|
||||
{children}
|
||||
</RainbowKitProvider>
|
||||
</RainbowKitAuthenticationProvider>
|
||||
</WagmiConfig>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,14 +4,14 @@ import "@rainbow-me/rainbowkit/styles.css"
|
||||
import { StrictMode } from "react"
|
||||
import * as ReactDOM from "react-dom/client"
|
||||
import { createBrowserRouter, redirect, RouterProvider } from "react-router-dom"
|
||||
import { isLoggedIn } from "./api/bandadaAPI"
|
||||
import { AuthContextProvider } from "./context/auth-context"
|
||||
import NotFoundPage from "./pages/404"
|
||||
import Home from "./pages/home"
|
||||
import Manage from "./pages/manage"
|
||||
import MyGroups from "./pages/my-groups"
|
||||
import SSO from "./pages/siwe"
|
||||
import SIWE from "./pages/siwe"
|
||||
import theme from "./styles"
|
||||
import { AuthContextProvider } from "./context/auth-context"
|
||||
import { getAdmin } from "./utils/session"
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
@@ -20,14 +20,15 @@ const router = createBrowserRouter([
|
||||
async loader({ request }) {
|
||||
const { pathname } = new URL(request.url)
|
||||
|
||||
const _loggedIn = await isLoggedIn()
|
||||
if (["/login", "/sign-up"].includes(pathname) && getAdmin()) {
|
||||
throw redirect("/my-groups")
|
||||
}
|
||||
|
||||
if (["/login", "/sign-up"].includes(pathname)) {
|
||||
if (_loggedIn) {
|
||||
throw redirect("/my-groups")
|
||||
}
|
||||
} else if (!_loggedIn) {
|
||||
throw redirect("/login")
|
||||
if (
|
||||
!["/", "/login", "/sign-up"].includes(pathname) &&
|
||||
!getAdmin()
|
||||
) {
|
||||
throw redirect("/")
|
||||
}
|
||||
|
||||
return null
|
||||
@@ -35,11 +36,11 @@ const router = createBrowserRouter([
|
||||
children: [
|
||||
{
|
||||
path: "login",
|
||||
element: <SSO />
|
||||
element: <SIWE />
|
||||
},
|
||||
{
|
||||
path: "sign-up",
|
||||
element: <SSO />
|
||||
element: <SIWE />
|
||||
},
|
||||
{
|
||||
path: "my-groups",
|
||||
|
||||
@@ -9,27 +9,14 @@ import {
|
||||
Text,
|
||||
VStack
|
||||
} from "@chakra-ui/react"
|
||||
import { useEffect } from "react"
|
||||
import { FaEthereum } from "react-icons/fa"
|
||||
import { useLocation, useNavigate } from "react-router-dom"
|
||||
import { goerli, useAccount, useConnect, useSwitchNetwork } from "wagmi"
|
||||
import logoUrl from "../assets/logo.svg"
|
||||
import { useConnectModal } from "@rainbow-me/rainbowkit"
|
||||
import { FaEthereum } from "react-icons/fa"
|
||||
import { useLocation } from "react-router-dom"
|
||||
import logoUrl from "../assets/logo.svg"
|
||||
|
||||
export default function SSO(): JSX.Element {
|
||||
const navigate = useNavigate()
|
||||
export default function SIWE(): JSX.Element {
|
||||
const { openConnectModal } = useConnectModal()
|
||||
const { switchNetwork } = useSwitchNetwork()
|
||||
const { pathname } = useLocation()
|
||||
const { isConnected } = useAccount()
|
||||
|
||||
useEffect(() => {
|
||||
if (isConnected) {
|
||||
switchNetwork?.(goerli.id)
|
||||
|
||||
navigate("/my-groups?on-chain")
|
||||
}
|
||||
}, [isConnected, navigate, switchNetwork])
|
||||
|
||||
return (
|
||||
<Container maxW="container.xl" pt="20" pb="20" px="6">
|
||||
@@ -63,9 +50,7 @@ export default function SSO(): JSX.Element {
|
||||
border="1px solid #D0D1D2"
|
||||
fontSize="18px"
|
||||
w="500px"
|
||||
onClick={() =>
|
||||
openConnectModal()
|
||||
}
|
||||
onClick={openConnectModal}
|
||||
>
|
||||
<Icon as={FaEthereum} mr="13px" />
|
||||
Connect Wallet
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export type User = {
|
||||
id: string;
|
||||
address: string;
|
||||
username: string;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { User } from "../types/user";
|
||||
|
||||
export function saveUser(user: User) {
|
||||
// Update token in storage
|
||||
window.localStorage.setItem('user', JSON.stringify(user));
|
||||
}
|
||||
|
||||
export function getUser() {
|
||||
const user = window.localStorage.getItem('user');
|
||||
if (user) {
|
||||
return JSON.parse(user);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function deleteUser() {
|
||||
window.localStorage.removeItem('user');
|
||||
}
|
||||
16
apps/dashboard/src/utils/session.ts
Normal file
16
apps/dashboard/src/utils/session.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// iron-session handles sessions with encoded cookies.
|
||||
// Since browsers cannot access it, it is necessary
|
||||
// to store admins' sessions manually.
|
||||
// TODO: explore other solutions.
|
||||
|
||||
export function saveAdmin(address: string) {
|
||||
localStorage.setItem("admin", address)
|
||||
}
|
||||
|
||||
export function getAdmin() {
|
||||
return localStorage.getItem("admin")
|
||||
}
|
||||
|
||||
export function deleteAdmin() {
|
||||
localStorage.removeItem("admin")
|
||||
}
|
||||
Reference in New Issue
Block a user