Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions back/app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
class ApplicationController < ActionController::API
include ActionController::Cookies
include DeviseTokenAuth::Concerns::SetUserByToken
before_action :set_auth_headers_from_cookies

private

def set_auth_headers_from_cookies
request.headers['access-token'] ||= cookies['access-token']
request.headers['client'] ||= cookies['client']
request.headers['uid'] ||= cookies['uid']
request.headers['expiry'] ||= cookies['expiry']
end
end
19 changes: 11 additions & 8 deletions back/app/controllers/auth/omniauth_callbacks_controller.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
class Auth::OmniauthCallbacksController < DeviseTokenAuth::OmniauthCallbacksController

# オーバーライド
# 後でよく確認する
def redirect_callbacks
user = User.from_omniauth(request.env['omniauth.auth'])

if user.persisted?
sign_in(user)
# トークンを生成
client_id = SecureRandom.urlsafe_base64(nil, false)
token = SecureRandom.urlsafe_base64(nil, false)
client_id = SecureRandom.urlsafe_base64(nil, false)
token = SecureRandom.urlsafe_base64(nil, false)
token_hash = BCrypt::Password.create(token)
expiry = (Time.now + DeviseTokenAuth.token_lifespan).to_i
expiry = (Time.now + DeviseTokenAuth.token_lifespan).to_i
github_token = request.env['omniauth.auth']['credentials']['token'].to_s

user.tokens[client_id] = {
Expand All @@ -22,7 +19,13 @@ def redirect_callbacks
user.github_token = Encryptor.encrypt(github_token)

if user.save
redirect_to "#{ENV['FRONT_URL']}/record?uid=#{user.uid}&token=#{token}&client=#{client_id}&expiry=#{expiry}", allow_other_host: true
cookie_options = { httponly: true, secure: Rails.env.production? }
cookies['access-token'] = cookie_options.merge(value: token)
cookies['client'] = cookie_options.merge(value: client_id)
cookies['uid'] = cookie_options.merge(value: user.uid)
cookies['expiry'] = cookie_options.merge(value: expiry.to_s)

redirect_to "#{ENV['FRONT_URL']}/record", allow_other_host: true
else
redirect_to "#{ENV['FRONT_URL']}/?status=error", allow_other_host: true
end
Expand All @@ -31,4 +34,4 @@ def redirect_callbacks
redirect_to "#{ENV['FRONT_URL']}/?status=error", allow_other_host: true
end
end
end
end
4 changes: 4 additions & 0 deletions back/app/controllers/auth/sessions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,9 @@ class Auth::SessionsController < DeviseTokenAuth::SessionsController

def destroy
super
cookies.delete('access-token')
cookies.delete('client')
cookies.delete('uid')
cookies.delete('expiry')
end
end
12 changes: 3 additions & 9 deletions back/config/initializers/cors.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
# Be sure to restart your server when you modify this file.

# Avoid CORS issues when API is called from the frontend app.
# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin Ajax requests.

# Read more: https://github.com/cyu/rack-cors

Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins ENV["FRONT_URL"] || "http://localhost:3000"
origins ENV["FRONT_URL"] || "http://localhost:8000"

resource "*",
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head]
methods: [:get, :post, :put, :patch, :delete, :options, :head],
credentials: true
end
end
19 changes: 5 additions & 14 deletions front/src/__tests__/api/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,10 @@ import { authFetch } from '@/api/auth'

describe('authFetch', () => {
beforeEach(() => {
localStorage.clear()
vi.restoreAllMocks()
})

it('localStorageの値をリクエストヘッダーに付加する', async () => {
localStorage.setItem('access-token', 'test-token')
localStorage.setItem('client', 'test-client')
localStorage.setItem('uid', 'test-uid')
localStorage.setItem('expiry', '9999999999')

it('credentials: include でリクエストを送る', async () => {
const mockFetch = vi.fn().mockResolvedValue({
status: 200,
json: () => Promise.resolve({ success: true }),
Expand All @@ -21,14 +15,11 @@ describe('authFetch', () => {

await authFetch('/me')

const [, options] = mockFetch.mock.calls[0] as [string, RequestInit & { headers: Record<string, string> }]
expect(options.headers['access-token']).toBe('test-token')
expect(options.headers['client']).toBe('test-client')
expect(options.headers['uid']).toBe('test-uid')
expect(options.headers['expiry']).toBe('9999999999')
const [, options] = mockFetch.mock.calls[0] as [string, RequestInit]
expect(options.credentials).toBe('include')
})

it('localStorageが空の場合ヘッダーにaccess-tokenが含まれない', async () => {
it('Content-Type: application/json ヘッダーが付加される', async () => {
const mockFetch = vi.fn().mockResolvedValue({
status: 200,
json: () => Promise.resolve({}),
Expand All @@ -38,6 +29,6 @@ describe('authFetch', () => {
await authFetch('/me')

const [, options] = mockFetch.mock.calls[0] as [string, RequestInit & { headers: Record<string, string> }]
expect(options.headers['access-token']).toBeUndefined()
expect(options.headers['Content-Type']).toBe('application/json')
})
})
90 changes: 7 additions & 83 deletions front/src/api/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,15 @@ interface AuthResponse {
user: { id: number; name: string };
}

function getAuthHeaders(): Record<string, string> {
if (typeof window === "undefined") return {};
const headers: Record<string, string> = {};
const accessToken = localStorage.getItem("access-token");
const client = localStorage.getItem("client");
const uid = localStorage.getItem("uid");
const expiry = localStorage.getItem("expiry");
if (accessToken) headers["access-token"] = accessToken;
if (client) headers["client"] = client;
if (uid) headers["uid"] = uid;
if (expiry) headers["expiry"] = expiry;
return headers;
}

export async function authFetch<T>(
path: string,
options: FetchOptions = {}
): Promise<{ status: number; data: T }> {
const res = await fetch(`${API_URL}${path}`, {
...options,
credentials: "include",
headers: {
"Content-Type": "application/json",
...getAuthHeaders(),
...options.headers,
},
});
Expand All @@ -49,6 +35,7 @@ async function baseFetch<T>(
): Promise<{ status: number; data: T }> {
const res = await fetch(`${BASE_API_URL}${path}`, {
...options,
credentials: "include",
headers: {
"Content-Type": "application/json",
...options.headers,
Expand All @@ -67,81 +54,18 @@ export const useAuth = () => {

async function autoLogin(): Promise<boolean> {
const res = await authFetch<AuthResponse>("/me");
if (res.status !== 200) {
clearStorage();
return false;
}
if (!res.data.success) {
clearStorage();
return false;
}
setUser({ id: res.data.user.id, name: res.data.user.name });
return true;
}

async function currentUser({
uid = "",
client = "",
token = "",
expiry = "",
}: {
uid?: string;
client?: string;
token?: string;
expiry?: string;
} = {}): Promise<boolean> {
if (uid && client && token && expiry) {
setStorage({ accessToken: token, client, uid, expiry });
}
const res = await authFetch<AuthResponse>("/me");
if (res.status !== 200) {
clearStorage();
return false;
}
if (!res.data.success) {
clearStorage();
return false;
}
if (res.status !== 200) return false;
if (!res.data.success) return false;
setUser({ id: res.data.user.id, name: res.data.user.name });
return true;
}

async function logout(): Promise<boolean> {
const res = await baseFetch<null>("/auth/sign_out", {
method: "DELETE",
headers: {
"access-token": localStorage.getItem("access-token") ?? "",
client: localStorage.getItem("client") ?? "",
uid: localStorage.getItem("uid") ?? "",
expiry: localStorage.getItem("expiry") ?? "",
},
});
if (res.status !== 200) {
return false;
}
clearStorage();
const res = await baseFetch<null>("/auth/sign_out", { method: "DELETE" });
if (res.status !== 200) return false;
setUser({ id: null, name: "" });
return true;
}

const setStorage = (data: {
accessToken: string;
client: string;
uid: string;
expiry: string;
}) => {
localStorage.setItem("access-token", data.accessToken);
localStorage.setItem("client", data.client);
localStorage.setItem("uid", data.uid);
localStorage.setItem("expiry", data.expiry);
};

const clearStorage = () => {
localStorage.removeItem("access-token");
localStorage.removeItem("client");
localStorage.removeItem("uid");
localStorage.removeItem("expiry");
};

return { login, currentUser, logout, autoLogin };
return { login, autoLogin, logout };
};
24 changes: 3 additions & 21 deletions front/src/app/record/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,16 @@ import * as Shadcn from "@/components/shadcn";

export default function UserPage() {
const user = useUserState();
const { currentUser, autoLogin } = useAuth();
const { autoLogin } = useAuth();
const router = useRouter();
const [records, setRecords] = useRecordsState();
const [isLoading, setIsLoading] = useState(true);

const fetchUserData = useCallback(async () => {
if (user.id) return;

const queryParams = new URLSearchParams(window.location.search);
const uid = queryParams.get("uid");
const client = queryParams.get("client");
const token = queryParams.get("token");
const expiry = queryParams.get("expiry");
let logged = false;
try {
if (uid && client && token && expiry) {
await currentUser({ uid, client, token, expiry }).then(
(res) => (logged = res)
);
if (logged) {
router.push("/record");
}
} else {
await autoLogin().then((res) => (logged = res));
}
if (!logged) {
router.push("/");
}
const logged = await autoLogin();
if (!logged) router.push("/");
} catch (e) {
alert("エラーが発生しました");
router.push("/");
Expand Down
Loading