12
\$\begingroup\$

Working on my first "big" project using the mighty Xamarin. Likewise my first venture in XAML so I suppose most focus will be going towards these new areas. Performance- and user-experience related remarks are welcome as well though. Or you know, just any issue you can see.

The solution consists of 3 actively used projects (the 4th - MoviePicker.Android - doesn't contain any platform-specific code). There's the shared Xamarin.Forms project, a ASP.NET Web Api 2 project and a PCL to share models between the API and the app. I will omit the PCL since it's just a few models with JSON annotations.

Web API

WebApiConfig

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // Web API configuration and services

        // Web API routes
        config.MapHttpAttributeRoutes();

        config.Routes.MapHttpRoute(
            name: "test",
            routeTemplate: "api/{controller}/{action}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new {id = RouteParameter.Optional}
        );
    }
}

BaseController

public class BaseController : ApiController
{
    protected const string ApiKey = "Will_Move_To_AppSettings";
    protected TMDbClient Client = new TMDbClient(ApiKey);
}

MovieController

namespace MoviePickerApi.Controllers
{
    [RoutePrefix("api/movies")]
    public class MovieController : BaseController
    {
        public MovieController()
        {
            Client.GetConfig();
        }

        [Route("~/api/genres")]
        [HttpGet]
        [ResponseType(typeof(IEnumerable<Genre>))]
        public IHttpActionResult GetGenres()
        {
            return Ok(Client.GetGenres());
        }

        [Route("~/api/genres/{id:int}")]
        [HttpGet]
        [ResponseType(typeof(IEnumerable<MovieResult>))]
        public IHttpActionResult GetMoviesForGenre(int id)
        {
            var results = Client.GetGenreMovies(id).Results;

            foreach (var genre in results)
            {
                genre.PosterPath = Client.GetImageUrl("original", genre.PosterPath).ToString();
            }
            return Ok(results);
        }

        [Route("{id:int}")]
        [HttpGet]
        [ResponseType(typeof(Movie))]
        public IHttpActionResult GetMovieDetails(int id)
        {
            var movie = Client.GetMovie(id);
            movie.PosterPath = Client.GetImageUrl("original", movie.PosterPath).ToString();
            return Ok(movie);
        }
    }
}

UserController

namespace MoviePickerApi.Controllers
{
    [RoutePrefix("api/users")]
    public class UserController : BaseController
    {
        private readonly MoviePickerContext _db = new MoviePickerContext();

        [Route("{id:int}")]
        [HttpGet]
        [ResponseType(typeof(User))]
        public async Task<IHttpActionResult> GetUser(int? id)
        {
            var user = await _db.Users.FindAsync(id);
            if (user == null)
            {
                return NotFound();
            }

            return Ok(user);
        }

        [Route("create")]
        [HttpPost]
        [ResponseType(typeof(User))]
        public async Task<IHttpActionResult> CreateUser([FromBody] User user)
        {
            if (UserExists(user.Email))
            {
                return Conflict();
            }

            _db.Users.Add(user);
            await _db.SaveChangesAsync();

            user.Password = string.Empty;
            return Ok(user);
        }

        [Route("login")]
        [HttpPost]
        [ResponseType(typeof(User))]
        public async Task<IHttpActionResult> Login([FromBody] User user)
        {
            var dbUser = await _db.Users.FirstOrDefaultAsync(x => x.Email == user.Email);
            if (dbUser == null || dbUser.Password != user.Password)
            {
                return NotFound();
            }

            return Ok(dbUser);
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                _db.Dispose();
            }
            base.Dispose(disposing);
        }

        private bool UserExists(string email)
        {
            return _db.Users.FirstOrDefault(e => e.Email == email) != null;
        }
    }
}

Xamarin.Forms

DataSource

public class DataSource
{
    private readonly HttpClient _client;
    private const string BaseUrl = "http://mpdev.azurewebsites.net/api";

    public DataSource()
    {
        _client = new HttpClient();
        _client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
    }

    public async Task<IEnumerable<Genre>> GetGenresAsync()
    {
        var data = await _client.GetStringAsync(BaseUrl + "/genres");
        return JsonConvert.DeserializeObject<List<Genre>>(data);
    }

    public async Task<IEnumerable<MovieSearchResult>> GetMoviesForGenreAsync(int genreId)
    {
        var data = await _client.GetStringAsync(BaseUrl + "/genres/" + genreId);
        return JsonConvert.DeserializeObject<List<MovieSearchResult>>(data);
    }

    public async Task<Movie> GetMovieAsync(int movieId)
    {
        var data = await _client.GetStringAsync(BaseUrl + "/movies/" + movieId);
        return JsonConvert.DeserializeObject<Movie>(data); 
    }

    public async Task<HttpResponseMessage> RegisterAsync(string email, string password, string firstname, string lastname)
    {
        var content = new FormUrlEncodedContent(new List<KeyValuePair<string, string>>
                {
                    new KeyValuePair<string, string>("email", email),
                    new KeyValuePair<string, string>("password", password),
                    new KeyValuePair<string, string>("firstname", firstname),
                    new KeyValuePair<string, string>("lastname", lastname),
                });

        return await _client.PostAsync(BaseUrl + "/users/create", content);
    }
}

Session

public class Session
{ 
    public User User { get; set; }

    private readonly DataSource _dataSource = new DataSource();
    public DataSource DataSource
    {
        get { return _dataSource; }
    }

    public string DatabasePath
    {
        get { return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "database.db3"); }
    }

    public bool IsLoggedIn
    {
        get { return User != null; }
    }
}

App

public class App
{
    public static Page GetMainPage()
    {
        var session = new Session();
        return new NavigationPage(new WelcomePage(session));
    }
}

WelcomePage

<ScrollView>
    <StackLayout Orientation="Vertical">
        <Label Text="Welcome!" HorizontalOptions="CenterAndExpand" Font="Bold, Small" />
        <StackLayout Orientation="Vertical">
            <Button Text="Login" HorizontalOptions="CenterAndExpand" VerticalOptions="Center" Clicked="OnLoginButtonClicked" />
            <Button Text="Register" HorizontalOptions="CenterAndExpand" VerticalOptions="Center" Clicked="OnRegisterButtonClicked" />
        </StackLayout>
        <ActivityIndicator x:Name="_activityIndicator" Color="Aqua" IsRunning="false" HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand" />
    </StackLayout>
</ScrollView>

public partial class WelcomePage : ContentPage
{   
    private readonly Session _session;

    public WelcomePage(Session session)
    {
        _session = session;
        InitializeComponent ();
    }

    private async void OnLoginButtonClicked(object sender, EventArgs e)
    {
        _activityIndicator.IsRunning = true;
        await Navigation.PushAsync(new LoginPage(_session));
        _activityIndicator.IsRunning = false;
    }

    private async void OnRegisterButtonClicked(object sender, EventArgs e)
    {
        _activityIndicator.IsRunning = true;
        await Navigation.PushAsync(new RegistrationPage(_session));
        _activityIndicator.IsRunning = false;
    }
}

LoginPage

<ScrollView>
    <StackLayout Orientation="Vertical">
        <!--- Menu -->
        <Label Text="Welcome!" HorizontalOptions="Center" Font="Bold, 30" />
        <Entry x:Name="_emailEntry" Keyboard="Email" Placeholder="Enter your email address" HorizontalOptions="CenterAndExpand" VerticalOptions="Center" />
        <Entry x:Name="_passwordEntry" Keyboard="Text" Placeholder="Enter your password" IsPassword="true" VerticalOptions="Center" HorizontalOptions="CenterAndExpand" />
        <Button Text="Log me in" HorizontalOptions="CenterAndExpand" VerticalOptions="Center" Clicked="OnLoginButtonClicked" />

        <ActivityIndicator x:Name="_activityIndicator" Color="Aqua" IsRunning="false" HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand" />
    </StackLayout>
</ScrollView>   

public partial class LoginPage : ContentPage
{   
    private readonly Session _session;

    public LoginPage(Session session)
    {
        _session = session;
        InitializeComponent();
    }

    private async void OnLoginButtonClicked(object sender, EventArgs args)
    {
        _activityIndicator.IsRunning = true;
        var isValidTask = IsValidLogin();
        var isValid = await isValidTask;

        if (isValid)
        {
            var genres = await _session.DataSource.GetGenresAsync();
            await Navigation.PushAsync(new GenrePage(_session, genres));
        }
        else
        {
            var alert = new AlertConfig
            {
                Message = "The entered credentials are invalid",
                Title = "Invalid login",
                OkText = "Okay then.."
            };
            var dialogService = new UserDialogService();
            await dialogService.AlertAsync(alert);
        }

        if (isValidTask.IsCompleted)
        {
            _activityIndicator.IsRunning = false;
        }
    }

    private async Task<bool> IsValidLogin()
    {
        var email = _emailEntry.Text;
        var password = _passwordEntry.Text;

        using (var client = new HttpClient())
        {
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            var content = new FormUrlEncodedContent(new List<KeyValuePair<string, string>>
                {
                    new KeyValuePair<string, string>("email", email),
                    new KeyValuePair<string, string>("password", password)
                });

            HttpResponseMessage response;
            try
            {
                response = await client.PostAsync("http://mpdev.azurewebsites.net/api/users/login", content);

                var json = await response.Content.ReadAsStringAsync();
                var obj = JsonConvert.DeserializeObject<User>(json);
                _session.User = obj;
            }
            catch (Exception e)
            {
                //TODO: log
                var alert = new AlertConfig
                {
                    Message = "Something went wrong with your request, try again later.",
                    Title = "Uh oh",
                    OkText = "Fk dis app"
                };
                var dialogService = new UserDialogService();
                dialogService.AlertAsync(alert);

                return false;
            }

            return response.IsSuccessStatusCode;
        }
    }
}

RegistrationPage

<ScrollView>
    <StackLayout Orientation="Vertical">
        <Label Text="Registration" HorizontalOptions="CenterAndExpand" Font="Bold, Small" />
        <Entry x:Name="_email" Keyboard="Email" Placeholder="Enter your email address" HorizontalOptions="CenterAndExpand" VerticalOptions="Center" />
        <Entry x:Name="_password" Keyboard="Text" Placeholder="Enter your password" IsPassword="true" VerticalOptions="Center" HorizontalOptions="CenterAndExpand" />
        <Entry x:Name="_firstname" Keyboard="Text" Placeholder="Enter your first name" HorizontalOptions="CenterAndExpand" VerticalOptions="Center" />
        <Entry x:Name="_lastname" Keyboard="Text" Placeholder="Enter your last name" HorizontalOptions="CenterAndExpand" VerticalOptions="Center" />
        <Button Text="Register me" HorizontalOptions="CenterAndExpand" VerticalOptions="Center" Clicked="OnRegistrationButtonClicked" />
        <ActivityIndicator x:Name="_activityIndicator" Color="Aqua" IsRunning="false" HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand" />
    </StackLayout>
</ScrollView>

public partial class RegistrationPage : ContentPage
{   
    private readonly Session _session;

    public RegistrationPage (Session session)
    {
        _session = session;
        InitializeComponent();
    }

    private async void OnRegistrationButtonClicked(object sender, EventArgs e)
    {
        _activityIndicator.IsRunning = true;

        var response = await _session.DataSource.RegisterAsync(_email.Text, _password.Text, _firstname.Text, _lastname.Text);
        if (response.IsSuccessStatusCode)
        {
            var json = await response.Content.ReadAsStringAsync();
            var obj = JsonConvert.DeserializeObject<User>(json);
            _session.User = obj;

            var genres = await _session.DataSource.GetGenresAsync();
            await Navigation.PushAsync(new GenrePage(_session, genres));
        }
        else
        {
            if (response.StatusCode == HttpStatusCode.Conflict)
            {
                var alert = await DisplayAlert("User exists",
                    "A user with this email address already exists",
                    "Go to login",
                    "Cancel");

                // User clicked on "Go to login"
                if (alert)
                {
                    await Navigation.PushAsync(new LoginPage(_session));
                }
            }
            else
            {
                var alert = new AlertConfig
                {
                    Message = "Something went wrong with your request, try again later.",
                    Title = "Uh oh",
                    OkText = "Fk dis app"
                };
                new UserDialogService().AlertAsync(alert);
            }
        }

        _activityIndicator.IsRunning = false;  
    }
}

GenrePage

<StackLayout Orientation="Vertical">
    <Label Text="Select a genre" Font="30" HorizontalOptions="Center" />

    <TableView Intent="Menu">
        <TableRoot>
            <TableSection x:Name="_genreCells">
                <!-- Fill with custom cells in code -->
            </TableSection>
        </TableRoot>
    </TableView>
</StackLayout>

public partial class GenrePage : ContentPage
{   
    private readonly IEnumerable<Genre> _genres; 
    private readonly Session _session;

    public GenrePage(Session session, IEnumerable<Genre> genres)
    {
        _session = session;
        _genres = genres;
        InitializeComponent ();

        foreach (var genre in _genres) 
        {
            _genreCells.Add(new GenreCell(genre, _session, Navigation));
        }
    }
}

public class GenreCell : TextCell
{
    private readonly Session _session;
    private readonly INavigation _navigation;
    public Genre Genre { get; set; }

    public GenreCell(Genre genre, Session session, INavigation navigation)
    {
        Genre = genre;
        _session = session;
        _navigation = navigation;
        Text = genre.Name;
        Tapped += TappedInternal;
    }

    private async void TappedInternal(object sender, EventArgs e)
    {
        var movie = (await _session.DataSource.GetMoviesForGenreAsync(Genre.Id)).Take(1).FirstOrDefault();
        var movieDetails = await _session.DataSource.GetMovieAsync(movie.Id);
        await _navigation.PushAsync(new MoviePage(movieDetails));
    }
}

MoviePage

<ScrollView>
    <StackLayout VerticalOptions="CenterAndExpand" HorizontalOptions="CenterAndExpand">
        <Label x:Name="_title" Font="Bold, 30" />
        <Image x:Name="_image" WidthRequest="300" HeightRequest="250" />
        <Label x:Name="_description" Font="15" />

        <Button Text="More information" HorizontalOptions="EndAndExpand" Clicked="OnMoreInformationClicked" />
        <StackLayout Orientation="Horizontal" HorizontalOptions="CenterAndExpand" >
            <Button Text="Decline" HorizontalOptions="StartAndExpand" Clicked="OnDeclineClicked" />
            <Button Text="Maybe later" HorizontalOptions="CenterAndExpand" Clicked="OnMaybeClicked" />
            <Button Text="Watched!" HorizontalOptions="EndAndExpand" Clicked="OnWatchedClicked" />
        </StackLayout>
    </StackLayout>
</ScrollView>

public partial class MoviePage : ContentPage
{   
    private readonly Movie _movie;

    public MoviePage (Movie movie)
    {
        _movie = movie;
        InitializeComponent();

        _title.Text = string.Format ("{0}({1})", _movie.Title, _movie.ReleaseDate.HasValue ? _movie.ReleaseDate.Value.Year.ToString () : "Unknown");
        _image.Source = new UriImageSource {
            CachingEnabled = true,
            CacheValidity = new TimeSpan(5, 0, 0, 0),
            Uri = new Uri(_movie.PosterPath)
        };
        _description.Text = _movie.Overview;

        // Main layout: Padding = new Thickness(0, 0, 0, 0)
    }

    private void OnMoreInformationClicked(object sender, EventArgs e) { }
    private void OnDeclineClicked(object sender, EventArgs e) { }
    private void OnMaybeClicked(object sender, EventArgs e) { }
    private void OnWatchedClicked(object sender, EventArgs e) { }
}
\$\endgroup\$
4
  • \$\begingroup\$ Unrelated, but when this app will be done, how will I be able to find it on the market? ;) \$\endgroup\$
    – IEatBagels
    Commented Sep 5, 2014 at 12:41
  • 4
    \$\begingroup\$ I'll probably rent a few big billboards in my hometown so if you ever pass through there, you'll see it! Or, well, I'll announce it in the CodeReview chatroom. \$\endgroup\$ Commented Sep 5, 2014 at 12:42
  • \$\begingroup\$ if (dbUser == null || dbUser.Password != user.Password) -- are you storing your passwords in the clear? \$\endgroup\$
    – mjolka
    Commented Sep 9, 2014 at 2:23
  • \$\begingroup\$ @mjolka: yes. If it helps, I left a //TODO: encrypt note somewhere. \$\endgroup\$ Commented Sep 9, 2014 at 8:25

1 Answer 1

5
+50
\$\begingroup\$

I'm just going to comment on one line:

if (dbUser == null || dbUser.Password != user.Password)

As you confirmed, you're currently storing passwords in the clear. Encrypting passwords is not the right solution either.

Cryptographic Right Answers (2009, Colin Percival, author of scrypt)

Password handling: As soon as you receive a password, hash it using scrypt or PBKDF2 and erase the plaintext password from memory.

Do NOT store users' passwords. Do NOT hash them with MD5. Use a real key derivation algorithm. PBKDF2 is the most official standard; but scrypt is stronger. Please keep in mind that even if YOUR application isn't particularly sensitive, your users are probably re-using passwords which they have used on other, more sensitive, websites -- so if you screw up how you store your users' passwords, you might end up doing them a lot of harm.

How To Safely Store A Password (2010)

Use bcrypt

Use bcrypt. Use bcrypt. Use bcrypt. Use bcrypt. Use bcrypt. Use bcrypt. Use bcrypt. Use bcrypt. Use bcrypt.

How to securely hash passwords? (2013)

Conclusion

Use bcrypt. PBKDF2 is not bad either. If you use scrypt you will be a "slightly early adopter" with the risks that are implied by this expression; but it would be a good move for scientific progress ("crash dummy" is a very honourable profession).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.